mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 15:48:28 +00:00
fix(line): harden outbound send behavior
This commit is contained in:
@@ -1,11 +1,228 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { createQuickReplyItems } from "./send.js";
|
|
||||||
|
|
||||||
describe("createQuickReplyItems", () => {
|
const {
|
||||||
it("limits items to 13 (LINE maximum)", () => {
|
pushMessageMock,
|
||||||
const labels = Array.from({ length: 20 }, (_, i) => `Option ${i + 1}`);
|
replyMessageMock,
|
||||||
const quickReply = createQuickReplyItems(labels);
|
showLoadingAnimationMock,
|
||||||
|
getProfileMock,
|
||||||
|
MessagingApiClientMock,
|
||||||
|
loadConfigMock,
|
||||||
|
resolveLineAccountMock,
|
||||||
|
resolveLineChannelAccessTokenMock,
|
||||||
|
recordChannelActivityMock,
|
||||||
|
logVerboseMock,
|
||||||
|
} = vi.hoisted(() => {
|
||||||
|
const pushMessageMock = vi.fn();
|
||||||
|
const replyMessageMock = vi.fn();
|
||||||
|
const showLoadingAnimationMock = vi.fn();
|
||||||
|
const getProfileMock = vi.fn();
|
||||||
|
const MessagingApiClientMock = vi.fn(function () {
|
||||||
|
return {
|
||||||
|
pushMessage: pushMessageMock,
|
||||||
|
replyMessage: replyMessageMock,
|
||||||
|
showLoadingAnimation: showLoadingAnimationMock,
|
||||||
|
getProfile: getProfileMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const loadConfigMock = vi.fn(() => ({}));
|
||||||
|
const resolveLineAccountMock = vi.fn(() => ({ accountId: "default" }));
|
||||||
|
const resolveLineChannelAccessTokenMock = vi.fn(() => "line-token");
|
||||||
|
const recordChannelActivityMock = vi.fn();
|
||||||
|
const logVerboseMock = vi.fn();
|
||||||
|
return {
|
||||||
|
pushMessageMock,
|
||||||
|
replyMessageMock,
|
||||||
|
showLoadingAnimationMock,
|
||||||
|
getProfileMock,
|
||||||
|
MessagingApiClientMock,
|
||||||
|
loadConfigMock,
|
||||||
|
resolveLineAccountMock,
|
||||||
|
resolveLineChannelAccessTokenMock,
|
||||||
|
recordChannelActivityMock,
|
||||||
|
logVerboseMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@line/bot-sdk", () => ({
|
||||||
|
messagingApi: { MessagingApiClient: MessagingApiClientMock },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../config/config.js", () => ({
|
||||||
|
loadConfig: loadConfigMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./accounts.js", () => ({
|
||||||
|
resolveLineAccount: resolveLineAccountMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./channel-access-token.js", () => ({
|
||||||
|
resolveLineChannelAccessToken: resolveLineChannelAccessTokenMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../infra/channel-activity.js", () => ({
|
||||||
|
recordChannelActivity: recordChannelActivityMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../globals.js", () => ({
|
||||||
|
logVerbose: logVerboseMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let sendModule: typeof import("./send.js");
|
||||||
|
|
||||||
|
describe("LINE send helpers", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
sendModule = await import("./send.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pushMessageMock.mockReset();
|
||||||
|
replyMessageMock.mockReset();
|
||||||
|
showLoadingAnimationMock.mockReset();
|
||||||
|
getProfileMock.mockReset();
|
||||||
|
MessagingApiClientMock.mockClear();
|
||||||
|
loadConfigMock.mockReset();
|
||||||
|
resolveLineAccountMock.mockReset();
|
||||||
|
resolveLineChannelAccessTokenMock.mockReset();
|
||||||
|
recordChannelActivityMock.mockReset();
|
||||||
|
logVerboseMock.mockReset();
|
||||||
|
|
||||||
|
loadConfigMock.mockReturnValue({});
|
||||||
|
resolveLineAccountMock.mockReturnValue({ accountId: "default" });
|
||||||
|
resolveLineChannelAccessTokenMock.mockReturnValue("line-token");
|
||||||
|
pushMessageMock.mockResolvedValue({});
|
||||||
|
replyMessageMock.mockResolvedValue({});
|
||||||
|
showLoadingAnimationMock.mockResolvedValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("limits quick reply items to 13", () => {
|
||||||
|
const labels = Array.from({ length: 20 }, (_, index) => `Option ${index + 1}`);
|
||||||
|
const quickReply = sendModule.createQuickReplyItems(labels);
|
||||||
|
|
||||||
expect(quickReply.items).toHaveLength(13);
|
expect(quickReply.items).toHaveLength(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pushes images via normalized LINE target", async () => {
|
||||||
|
const result = await sendModule.pushImageMessage(
|
||||||
|
"line:user:U123",
|
||||||
|
"https://example.com/original.jpg",
|
||||||
|
undefined,
|
||||||
|
{ verbose: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pushMessageMock).toHaveBeenCalledWith({
|
||||||
|
to: "U123",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
originalContentUrl: "https://example.com/original.jpg",
|
||||||
|
previewImageUrl: "https://example.com/original.jpg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(recordChannelActivityMock).toHaveBeenCalledWith({
|
||||||
|
channel: "line",
|
||||||
|
accountId: "default",
|
||||||
|
direction: "outbound",
|
||||||
|
});
|
||||||
|
expect(logVerboseMock).toHaveBeenCalledWith("line: pushed image to U123");
|
||||||
|
expect(result).toEqual({ messageId: "push", chatId: "U123" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replies when reply token is provided", async () => {
|
||||||
|
const result = await sendModule.sendMessageLine("line:group:C1", "Hello", {
|
||||||
|
replyToken: "reply-token",
|
||||||
|
mediaUrl: "https://example.com/media.jpg",
|
||||||
|
verbose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replyMessageMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(pushMessageMock).not.toHaveBeenCalled();
|
||||||
|
expect(replyMessageMock).toHaveBeenCalledWith({
|
||||||
|
replyToken: "reply-token",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
originalContentUrl: "https://example.com/media.jpg",
|
||||||
|
previewImageUrl: "https://example.com/media.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Hello",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(logVerboseMock).toHaveBeenCalledWith("line: replied to C1");
|
||||||
|
expect(result).toEqual({ messageId: "reply", chatId: "C1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when push messages are empty", async () => {
|
||||||
|
await expect(sendModule.pushMessagesLine("U123", [])).rejects.toThrow(
|
||||||
|
"Message must be non-empty for LINE sends",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs HTTP body when push fails", async () => {
|
||||||
|
const err = new Error("LINE push failed") as Error & {
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
err.status = 400;
|
||||||
|
err.statusText = "Bad Request";
|
||||||
|
err.body = "invalid flex payload";
|
||||||
|
pushMessageMock.mockRejectedValueOnce(err);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sendModule.pushMessagesLine("U999", [{ type: "text", text: "hello" }]),
|
||||||
|
).rejects.toThrow("LINE push failed");
|
||||||
|
|
||||||
|
expect(logVerboseMock).toHaveBeenCalledWith(
|
||||||
|
"line: push message failed (400 Bad Request): invalid flex payload",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caches profile results by default", async () => {
|
||||||
|
getProfileMock.mockResolvedValue({
|
||||||
|
displayName: "Peter",
|
||||||
|
pictureUrl: "https://example.com/peter.jpg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = await sendModule.getUserProfile("U-cache");
|
||||||
|
const second = await sendModule.getUserProfile("U-cache");
|
||||||
|
|
||||||
|
expect(first).toEqual({
|
||||||
|
displayName: "Peter",
|
||||||
|
pictureUrl: "https://example.com/peter.jpg",
|
||||||
|
});
|
||||||
|
expect(second).toEqual(first);
|
||||||
|
expect(getProfileMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues when loading animation is unsupported", async () => {
|
||||||
|
showLoadingAnimationMock.mockRejectedValueOnce(new Error("unsupported"));
|
||||||
|
|
||||||
|
await expect(sendModule.showLoadingAnimation("line:room:R1")).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(logVerboseMock).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("line: loading animation failed (non-fatal)"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pushes quick-reply text and caps to 13 buttons", async () => {
|
||||||
|
await sendModule.pushTextMessageWithQuickReplies(
|
||||||
|
"U-quick",
|
||||||
|
"Pick one",
|
||||||
|
Array.from({ length: 20 }, (_, index) => `Choice ${index + 1}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pushMessageMock).toHaveBeenCalledTimes(1);
|
||||||
|
const firstCall = pushMessageMock.mock.calls[0] as [
|
||||||
|
{ messages: Array<{ quickReply?: { items: unknown[] } }> },
|
||||||
|
];
|
||||||
|
expect(firstCall[0].messages[0].quickReply?.items).toHaveLength(13);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
337
src/line/send.ts
337
src/line/send.ts
@@ -32,6 +32,18 @@ interface LineSendOpts {
|
|||||||
replyToken?: string;
|
replyToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LineClientOpts = Pick<LineSendOpts, "channelAccessToken" | "accountId">;
|
||||||
|
type LinePushOpts = Pick<LineSendOpts, "channelAccessToken" | "accountId" | "verbose">;
|
||||||
|
|
||||||
|
interface LinePushBehavior {
|
||||||
|
errorContext?: string;
|
||||||
|
verboseMessage?: (chatId: string, messageCount: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LineReplyBehavior {
|
||||||
|
verboseMessage?: (messageCount: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTarget(to: string): string {
|
function normalizeTarget(to: string): string {
|
||||||
const trimmed = to.trim();
|
const trimmed = to.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -52,7 +64,7 @@ function normalizeTarget(to: string): string {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLineMessagingClient(opts: { channelAccessToken?: string; accountId?: string }): {
|
function createLineMessagingClient(opts: LineClientOpts): {
|
||||||
account: ReturnType<typeof resolveLineAccount>;
|
account: ReturnType<typeof resolveLineAccount>;
|
||||||
client: messagingApi.MessagingApiClient;
|
client: messagingApi.MessagingApiClient;
|
||||||
} {
|
} {
|
||||||
@@ -70,7 +82,7 @@ function createLineMessagingClient(opts: { channelAccessToken?: string; accountI
|
|||||||
|
|
||||||
function createLinePushContext(
|
function createLinePushContext(
|
||||||
to: string,
|
to: string,
|
||||||
opts: { channelAccessToken?: string; accountId?: string },
|
opts: LineClientOpts,
|
||||||
): {
|
): {
|
||||||
account: ReturnType<typeof resolveLineAccount>;
|
account: ReturnType<typeof resolveLineAccount>;
|
||||||
client: messagingApi.MessagingApiClient;
|
client: messagingApi.MessagingApiClient;
|
||||||
@@ -126,23 +138,85 @@ function logLineHttpError(err: unknown, context: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recordLineOutboundActivity(accountId: string): void {
|
||||||
|
recordChannelActivity({
|
||||||
|
channel: "line",
|
||||||
|
accountId,
|
||||||
|
direction: "outbound",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushLineMessages(
|
||||||
|
to: string,
|
||||||
|
messages: Message[],
|
||||||
|
opts: LinePushOpts = {},
|
||||||
|
behavior: LinePushBehavior = {},
|
||||||
|
): Promise<LineSendResult> {
|
||||||
|
if (messages.length === 0) {
|
||||||
|
throw new Error("Message must be non-empty for LINE sends");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { account, client, chatId } = createLinePushContext(to, opts);
|
||||||
|
const pushRequest = client.pushMessage({
|
||||||
|
to: chatId,
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (behavior.errorContext) {
|
||||||
|
const errorContext = behavior.errorContext;
|
||||||
|
await pushRequest.catch((err) => {
|
||||||
|
logLineHttpError(err, errorContext);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await pushRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordLineOutboundActivity(account.accountId);
|
||||||
|
|
||||||
|
if (opts.verbose) {
|
||||||
|
const logMessage =
|
||||||
|
behavior.verboseMessage?.(chatId, messages.length) ??
|
||||||
|
`line: pushed ${messages.length} messages to ${chatId}`;
|
||||||
|
logVerbose(logMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageId: "push",
|
||||||
|
chatId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replyLineMessages(
|
||||||
|
replyToken: string,
|
||||||
|
messages: Message[],
|
||||||
|
opts: LinePushOpts = {},
|
||||||
|
behavior: LineReplyBehavior = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const { account, client } = createLineMessagingClient(opts);
|
||||||
|
|
||||||
|
await client.replyMessage({
|
||||||
|
replyToken,
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
recordLineOutboundActivity(account.accountId);
|
||||||
|
|
||||||
|
if (opts.verbose) {
|
||||||
|
logVerbose(
|
||||||
|
behavior.verboseMessage?.(messages.length) ??
|
||||||
|
`line: replied with ${messages.length} messages`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendMessageLine(
|
export async function sendMessageLine(
|
||||||
to: string,
|
to: string,
|
||||||
text: string,
|
text: string,
|
||||||
opts: LineSendOpts = {},
|
opts: LineSendOpts = {},
|
||||||
): Promise<LineSendResult> {
|
): Promise<LineSendResult> {
|
||||||
const cfg = loadConfig();
|
|
||||||
const account = resolveLineAccount({
|
|
||||||
cfg,
|
|
||||||
accountId: opts.accountId,
|
|
||||||
});
|
|
||||||
const token = resolveLineChannelAccessToken(opts.channelAccessToken, account);
|
|
||||||
const chatId = normalizeTarget(to);
|
const chatId = normalizeTarget(to);
|
||||||
|
|
||||||
const client = new messagingApi.MessagingApiClient({
|
|
||||||
channelAccessToken: token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const messages: Message[] = [];
|
const messages: Message[] = [];
|
||||||
|
|
||||||
// Add media if provided
|
// Add media if provided
|
||||||
@@ -161,21 +235,10 @@ export async function sendMessageLine(
|
|||||||
|
|
||||||
// Use reply if we have a reply token, otherwise push
|
// Use reply if we have a reply token, otherwise push
|
||||||
if (opts.replyToken) {
|
if (opts.replyToken) {
|
||||||
await client.replyMessage({
|
await replyLineMessages(opts.replyToken, messages, opts, {
|
||||||
replyToken: opts.replyToken,
|
verboseMessage: () => `line: replied to ${chatId}`,
|
||||||
messages,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: replied to ${chatId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messageId: "reply",
|
messageId: "reply",
|
||||||
chatId,
|
chatId,
|
||||||
@@ -183,25 +246,9 @@ export async function sendMessageLine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Push message (for proactive messaging)
|
// Push message (for proactive messaging)
|
||||||
await client.pushMessage({
|
return pushLineMessages(chatId, messages, opts, {
|
||||||
to: chatId,
|
verboseMessage: (resolvedChatId) => `line: pushed message to ${resolvedChatId}`,
|
||||||
messages,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: pushed message to ${chatId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messageId: "push",
|
|
||||||
chatId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushMessageLine(
|
export async function pushMessageLine(
|
||||||
@@ -216,61 +263,19 @@ export async function pushMessageLine(
|
|||||||
export async function replyMessageLine(
|
export async function replyMessageLine(
|
||||||
replyToken: string,
|
replyToken: string,
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
opts: { channelAccessToken?: string; accountId?: string; verbose?: boolean } = {},
|
opts: LinePushOpts = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { account, client } = createLineMessagingClient(opts);
|
await replyLineMessages(replyToken, messages, opts);
|
||||||
|
|
||||||
await client.replyMessage({
|
|
||||||
replyToken,
|
|
||||||
messages,
|
|
||||||
});
|
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: replied with ${messages.length} messages`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushMessagesLine(
|
export async function pushMessagesLine(
|
||||||
to: string,
|
to: string,
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
opts: { channelAccessToken?: string; accountId?: string; verbose?: boolean } = {},
|
opts: LinePushOpts = {},
|
||||||
): Promise<LineSendResult> {
|
): Promise<LineSendResult> {
|
||||||
if (messages.length === 0) {
|
return pushLineMessages(to, messages, opts, {
|
||||||
throw new Error("Message must be non-empty for LINE sends");
|
errorContext: "push message",
|
||||||
}
|
|
||||||
|
|
||||||
const { account, client, chatId } = createLinePushContext(to, opts);
|
|
||||||
|
|
||||||
await client
|
|
||||||
.pushMessage({
|
|
||||||
to: chatId,
|
|
||||||
messages,
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logLineHttpError(err, "push message");
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: pushed ${messages.length} messages to ${chatId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messageId: "push",
|
|
||||||
chatId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFlexMessage(
|
export function createFlexMessage(
|
||||||
@@ -291,31 +296,11 @@ export async function pushImageMessage(
|
|||||||
to: string,
|
to: string,
|
||||||
originalContentUrl: string,
|
originalContentUrl: string,
|
||||||
previewImageUrl?: string,
|
previewImageUrl?: string,
|
||||||
opts: { channelAccessToken?: string; accountId?: string; verbose?: boolean } = {},
|
opts: LinePushOpts = {},
|
||||||
): Promise<LineSendResult> {
|
): Promise<LineSendResult> {
|
||||||
const { account, client, chatId } = createLinePushContext(to, opts);
|
return pushLineMessages(to, [createImageMessage(originalContentUrl, previewImageUrl)], opts, {
|
||||||
|
verboseMessage: (chatId) => `line: pushed image to ${chatId}`,
|
||||||
const imageMessage = createImageMessage(originalContentUrl, previewImageUrl);
|
|
||||||
|
|
||||||
await client.pushMessage({
|
|
||||||
to: chatId,
|
|
||||||
messages: [imageMessage],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: pushed image to ${chatId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messageId: "push",
|
|
||||||
chatId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -329,31 +314,11 @@ export async function pushLocationMessage(
|
|||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
},
|
},
|
||||||
opts: { channelAccessToken?: string; accountId?: string; verbose?: boolean } = {},
|
opts: LinePushOpts = {},
|
||||||
): Promise<LineSendResult> {
|
): Promise<LineSendResult> {
|
||||||
const { account, client, chatId } = createLinePushContext(to, opts);
|
return pushLineMessages(to, [createLocationMessage(location)], opts, {
|
||||||
|
verboseMessage: (chatId) => `line: pushed location to ${chatId}`,
|
||||||
const locationMessage = createLocationMessage(location);
|
|
||||||
|
|
||||||
await client.pushMessage({
|
|
||||||
to: chatId,
|
|
||||||
messages: [locationMessage],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: pushed location to ${chatId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messageId: "push",
|
|
||||||
chatId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,40 +328,18 @@ export async function pushFlexMessage(
|
|||||||
to: string,
|
to: string,
|
||||||
altText: string,
|
altText: string,
|
||||||
contents: FlexContainer,
|
contents: FlexContainer,
|
||||||
opts: { channelAccessToken?: string; accountId?: string; verbose?: boolean } = {},
|
opts: LinePushOpts = {},
|
||||||
): Promise<LineSendResult> {
|
): Promise<LineSendResult> {
|
||||||
const { account, client, chatId } = createLinePushContext(to, opts);
|
|
||||||
|
|
||||||
const flexMessage: FlexMessage = {
|
const flexMessage: FlexMessage = {
|
||||||
type: "flex",
|
type: "flex",
|
||||||
altText: altText.slice(0, 400), // LINE limit
|
altText: altText.slice(0, 400), // LINE limit
|
||||||
contents,
|
contents,
|
||||||
};
|
};
|
||||||
|
|
||||||
await client
|
return pushLineMessages(to, [flexMessage], opts, {
|
||||||
.pushMessage({
|
errorContext: "push flex message",
|
||||||
to: chatId,
|
verboseMessage: (chatId) => `line: pushed flex message to ${chatId}`,
|
||||||
messages: [flexMessage],
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logLineHttpError(err, "push flex message");
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: pushed flex message to ${chatId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messageId: "push",
|
|
||||||
chatId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -405,29 +348,11 @@ export async function pushFlexMessage(
|
|||||||
export async function pushTemplateMessage(
|
export async function pushTemplateMessage(
|
||||||
to: string,
|
to: string,
|
||||||
template: TemplateMessage,
|
template: TemplateMessage,
|
||||||
opts: { channelAccessToken?: string; accountId?: string; verbose?: boolean } = {},
|
opts: LinePushOpts = {},
|
||||||
): Promise<LineSendResult> {
|
): Promise<LineSendResult> {
|
||||||
const { account, client, chatId } = createLinePushContext(to, opts);
|
return pushLineMessages(to, [template], opts, {
|
||||||
|
verboseMessage: (chatId) => `line: pushed template message to ${chatId}`,
|
||||||
await client.pushMessage({
|
|
||||||
to: chatId,
|
|
||||||
messages: [template],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: pushed template message to ${chatId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messageId: "push",
|
|
||||||
chatId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,31 +362,13 @@ export async function pushTextMessageWithQuickReplies(
|
|||||||
to: string,
|
to: string,
|
||||||
text: string,
|
text: string,
|
||||||
quickReplyLabels: string[],
|
quickReplyLabels: string[],
|
||||||
opts: { channelAccessToken?: string; accountId?: string; verbose?: boolean } = {},
|
opts: LinePushOpts = {},
|
||||||
): Promise<LineSendResult> {
|
): Promise<LineSendResult> {
|
||||||
const { account, client, chatId } = createLinePushContext(to, opts);
|
|
||||||
|
|
||||||
const message = createTextMessageWithQuickReplies(text, quickReplyLabels);
|
const message = createTextMessageWithQuickReplies(text, quickReplyLabels);
|
||||||
|
|
||||||
await client.pushMessage({
|
return pushLineMessages(to, [message], opts, {
|
||||||
to: chatId,
|
verboseMessage: (chatId) => `line: pushed message with quick replies to ${chatId}`,
|
||||||
messages: [message],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
recordChannelActivity({
|
|
||||||
channel: "line",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "outbound",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.verbose) {
|
|
||||||
logVerbose(`line: pushed message with quick replies to ${chatId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messageId: "push",
|
|
||||||
chatId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -500,16 +407,7 @@ export async function showLoadingAnimation(
|
|||||||
chatId: string,
|
chatId: string,
|
||||||
opts: { channelAccessToken?: string; accountId?: string; loadingSeconds?: number } = {},
|
opts: { channelAccessToken?: string; accountId?: string; loadingSeconds?: number } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cfg = loadConfig();
|
const { client } = createLineMessagingClient(opts);
|
||||||
const account = resolveLineAccount({
|
|
||||||
cfg,
|
|
||||||
accountId: opts.accountId,
|
|
||||||
});
|
|
||||||
const token = resolveLineChannelAccessToken(opts.channelAccessToken, account);
|
|
||||||
|
|
||||||
const client = new messagingApi.MessagingApiClient({
|
|
||||||
channelAccessToken: token,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.showLoadingAnimation({
|
await client.showLoadingAnimation({
|
||||||
@@ -540,16 +438,7 @@ export async function getUserProfile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const { client } = createLineMessagingClient(opts);
|
||||||
const account = resolveLineAccount({
|
|
||||||
cfg,
|
|
||||||
accountId: opts.accountId,
|
|
||||||
});
|
|
||||||
const token = resolveLineChannelAccessToken(opts.channelAccessToken, account);
|
|
||||||
|
|
||||||
const client = new messagingApi.MessagingApiClient({
|
|
||||||
channelAccessToken: token,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const profile = await client.getProfile(userId);
|
const profile = await client.getProfile(userId);
|
||||||
|
|||||||
Reference in New Issue
Block a user