Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
529
extensions/line/src/send.ts
Normal file
529
extensions/line/src/send.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
// Line plugin module implements send behavior.
|
||||
import { messagingApi } from "@line/bot-sdk";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { resolveLineAccount } from "./accounts.js";
|
||||
import { messageAction } from "./actions.js";
|
||||
import { resolveLineChannelAccessToken } from "./channel-access-token.js";
|
||||
import { validateLineMediaUrl } from "./outbound-media.js";
|
||||
import { createLineSendReceipt } from "./send-receipt.js";
|
||||
import type { LineSendResult } from "./types.js";
|
||||
|
||||
type Message = messagingApi.Message;
|
||||
type TextMessage = messagingApi.TextMessage;
|
||||
type ImageMessage = messagingApi.ImageMessage;
|
||||
type VideoMessage = messagingApi.VideoMessage & { trackingId?: string };
|
||||
type AudioMessage = messagingApi.AudioMessage;
|
||||
type LocationMessage = messagingApi.LocationMessage;
|
||||
type FlexMessage = messagingApi.FlexMessage;
|
||||
type FlexContainer = messagingApi.FlexContainer;
|
||||
type TemplateMessage = messagingApi.TemplateMessage;
|
||||
type QuickReply = messagingApi.QuickReply;
|
||||
type QuickReplyItem = messagingApi.QuickReplyItem;
|
||||
|
||||
const userProfileCache = new Map<
|
||||
string,
|
||||
{ displayName: string; pictureUrl?: string; fetchedAt: number }
|
||||
>();
|
||||
const PROFILE_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
interface LineSendOpts {
|
||||
cfg: OpenClawConfig;
|
||||
channelAccessToken?: string;
|
||||
accountId?: string;
|
||||
verbose?: boolean;
|
||||
mediaUrl?: string;
|
||||
mediaKind?: "image" | "video" | "audio";
|
||||
previewImageUrl?: string;
|
||||
durationMs?: number;
|
||||
trackingId?: string;
|
||||
replyToken?: string;
|
||||
}
|
||||
|
||||
type LineClientOpts = Pick<LineSendOpts, "cfg" | "channelAccessToken" | "accountId">;
|
||||
type LinePushOpts = Pick<LineSendOpts, "cfg" | "channelAccessToken" | "accountId" | "verbose">;
|
||||
|
||||
interface LinePushBehavior {
|
||||
errorContext?: string;
|
||||
verboseMessage?: (chatId: string, messageCount: number) => string;
|
||||
}
|
||||
|
||||
interface LineReplyBehavior {
|
||||
verboseMessage?: (messageCount: number) => string;
|
||||
}
|
||||
|
||||
function normalizeTarget(to: string): string {
|
||||
const trimmed = to.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("Recipient is required for LINE sends");
|
||||
}
|
||||
|
||||
const normalized = trimmed
|
||||
.replace(/^line:group:/i, "")
|
||||
.replace(/^line:room:/i, "")
|
||||
.replace(/^line:user:/i, "")
|
||||
.replace(/^line:/i, "");
|
||||
|
||||
if (!normalized) {
|
||||
throw new Error("Recipient is required for LINE sends");
|
||||
}
|
||||
|
||||
// Real LINE chat ids are a capital C/U/R followed by 32 lowercase hex chars
|
||||
// (33 chars total) and are case-sensitive — push returns HTTP 400 otherwise.
|
||||
// Reject values that match the LINE id shape but lost their leading capital
|
||||
// so the failure is surfaced as a permanent error (recovery moves the entry
|
||||
// to failed/ immediately instead of silently retrying 5 times). Short test
|
||||
// fixtures (e.g. "U123") are left alone. openclaw/openclaw#81628
|
||||
if (normalized.length >= 33 && !/^[CUR]/.test(normalized)) {
|
||||
throw new Error(
|
||||
`Recipient is not a valid LINE id (case-sensitive; expected leading capital C/U/R): ${normalized.slice(0, 4)}…`,
|
||||
);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function isLineUserChatId(chatId: string): boolean {
|
||||
return /^U/i.test(chatId);
|
||||
}
|
||||
|
||||
function createLineMessagingClient(opts: LineClientOpts): {
|
||||
account: ReturnType<typeof resolveLineAccount>;
|
||||
client: messagingApi.MessagingApiClient;
|
||||
} {
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "LINE send");
|
||||
const account = resolveLineAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const token = resolveLineChannelAccessToken(opts.channelAccessToken, account);
|
||||
const client = new messagingApi.MessagingApiClient({
|
||||
channelAccessToken: token,
|
||||
});
|
||||
return { account, client };
|
||||
}
|
||||
|
||||
function createLinePushContext(
|
||||
to: string,
|
||||
opts: LineClientOpts,
|
||||
): {
|
||||
account: ReturnType<typeof resolveLineAccount>;
|
||||
client: messagingApi.MessagingApiClient;
|
||||
chatId: string;
|
||||
} {
|
||||
const { account, client } = createLineMessagingClient(opts);
|
||||
const chatId = normalizeTarget(to);
|
||||
return { account, client, chatId };
|
||||
}
|
||||
|
||||
function createTextMessage(text: string): TextMessage {
|
||||
return { type: "text", text };
|
||||
}
|
||||
|
||||
export function createImageMessage(
|
||||
originalContentUrl: string,
|
||||
previewImageUrl?: string,
|
||||
): ImageMessage {
|
||||
return {
|
||||
type: "image",
|
||||
originalContentUrl,
|
||||
previewImageUrl: previewImageUrl ?? originalContentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function createVideoMessage(
|
||||
originalContentUrl: string,
|
||||
previewImageUrl: string,
|
||||
trackingId?: string,
|
||||
): VideoMessage {
|
||||
return {
|
||||
type: "video",
|
||||
originalContentUrl,
|
||||
previewImageUrl,
|
||||
...(trackingId ? { trackingId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAudioMessage(originalContentUrl: string, durationMs: number): AudioMessage {
|
||||
return {
|
||||
type: "audio",
|
||||
originalContentUrl,
|
||||
duration: durationMs,
|
||||
};
|
||||
}
|
||||
|
||||
export function createLocationMessage(location: {
|
||||
title: string;
|
||||
address: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}): LocationMessage {
|
||||
return {
|
||||
type: "location",
|
||||
title: truncateUtf16Safe(location.title, 100),
|
||||
address: truncateUtf16Safe(location.address, 100),
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
};
|
||||
}
|
||||
|
||||
function logLineHttpError(err: unknown, context: string): void {
|
||||
if (!err || typeof err !== "object") {
|
||||
return;
|
||||
}
|
||||
const { status, statusText, body } = err as {
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
body?: string;
|
||||
};
|
||||
if (typeof body === "string") {
|
||||
const summary = status ? `${status} ${statusText ?? ""}`.trim() : "unknown status";
|
||||
logVerbose(`line: ${context} failed (${summary}): ${body}`);
|
||||
}
|
||||
}
|
||||
|
||||
function recordLineOutboundActivity(accountId: string): void {
|
||||
recordChannelActivity({
|
||||
channel: "line",
|
||||
accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
}
|
||||
|
||||
function resolveLineReceiptKind(messages: readonly Message[]) {
|
||||
const types = new Set(messages.map((message) => message.type));
|
||||
if (types.has("audio")) {
|
||||
return "voice";
|
||||
}
|
||||
if (types.has("image") || types.has("video")) {
|
||||
return "media";
|
||||
}
|
||||
if (types.has("flex") || types.has("template") || types.has("location")) {
|
||||
return "card";
|
||||
}
|
||||
if (types.has("text")) {
|
||||
return "text";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
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) {
|
||||
await pushRequest.catch((err: unknown) => {
|
||||
logLineHttpError(err, behavior.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,
|
||||
receipt: createLineSendReceipt({
|
||||
messageId: "push",
|
||||
chatId,
|
||||
kind: resolveLineReceiptKind(messages),
|
||||
messageCount: messages.length,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
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(
|
||||
to: string,
|
||||
text: string,
|
||||
opts: LineSendOpts,
|
||||
): Promise<LineSendResult> {
|
||||
const chatId = normalizeTarget(to);
|
||||
const messages: Message[] = [];
|
||||
|
||||
const mediaUrl = opts.mediaUrl?.trim();
|
||||
if (mediaUrl) {
|
||||
await validateLineMediaUrl(mediaUrl);
|
||||
switch (opts.mediaKind) {
|
||||
case "video": {
|
||||
const previewImageUrl = opts.previewImageUrl?.trim();
|
||||
if (!previewImageUrl) {
|
||||
throw new Error("LINE video messages require previewImageUrl to reference an image URL");
|
||||
}
|
||||
await validateLineMediaUrl(previewImageUrl);
|
||||
const trackingId = isLineUserChatId(chatId) ? opts.trackingId : undefined;
|
||||
messages.push(createVideoMessage(mediaUrl, previewImageUrl, trackingId));
|
||||
break;
|
||||
}
|
||||
case "audio":
|
||||
messages.push(createAudioMessage(mediaUrl, opts.durationMs ?? 60000));
|
||||
break;
|
||||
default:
|
||||
// Backward compatibility: keep image as default when media kind is unspecified.
|
||||
{
|
||||
const previewImageUrl = opts.previewImageUrl?.trim() || mediaUrl;
|
||||
await validateLineMediaUrl(previewImageUrl);
|
||||
messages.push(createImageMessage(mediaUrl, previewImageUrl));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (text?.trim()) {
|
||||
messages.push(createTextMessage(text.trim()));
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
throw new Error("Message must be non-empty for LINE sends");
|
||||
}
|
||||
|
||||
if (opts.replyToken) {
|
||||
await replyLineMessages(opts.replyToken, messages, opts, {
|
||||
verboseMessage: () => `line: replied to ${chatId}`,
|
||||
});
|
||||
|
||||
return {
|
||||
messageId: "reply",
|
||||
chatId,
|
||||
receipt: createLineSendReceipt({
|
||||
messageId: "reply",
|
||||
chatId,
|
||||
kind: resolveLineReceiptKind(messages),
|
||||
messageCount: messages.length,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return pushLineMessages(chatId, messages, opts, {
|
||||
verboseMessage: (resolvedChatId) => `line: pushed message to ${resolvedChatId}`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function pushMessageLine(
|
||||
to: string,
|
||||
text: string,
|
||||
opts: LineSendOpts,
|
||||
): Promise<LineSendResult> {
|
||||
return sendMessageLine(to, text, { ...opts, replyToken: undefined });
|
||||
}
|
||||
|
||||
export async function replyMessageLine(
|
||||
replyToken: string,
|
||||
messages: Message[],
|
||||
opts: LinePushOpts,
|
||||
): Promise<void> {
|
||||
await replyLineMessages(replyToken, messages, opts);
|
||||
}
|
||||
|
||||
export async function pushMessagesLine(
|
||||
to: string,
|
||||
messages: Message[],
|
||||
opts: LinePushOpts,
|
||||
): Promise<LineSendResult> {
|
||||
return pushLineMessages(to, messages, opts, {
|
||||
errorContext: "push message",
|
||||
});
|
||||
}
|
||||
|
||||
export function createFlexMessage(
|
||||
altText: string,
|
||||
contents: messagingApi.FlexContainer,
|
||||
): messagingApi.FlexMessage {
|
||||
return {
|
||||
type: "flex",
|
||||
altText,
|
||||
contents,
|
||||
};
|
||||
}
|
||||
|
||||
export async function pushImageMessage(
|
||||
to: string,
|
||||
originalContentUrl: string,
|
||||
previewImageUrl: string | undefined,
|
||||
opts: LinePushOpts,
|
||||
): Promise<LineSendResult> {
|
||||
await validateLineMediaUrl(originalContentUrl);
|
||||
if (previewImageUrl) {
|
||||
await validateLineMediaUrl(previewImageUrl);
|
||||
}
|
||||
return pushLineMessages(to, [createImageMessage(originalContentUrl, previewImageUrl)], opts, {
|
||||
verboseMessage: (chatId) => `line: pushed image to ${chatId}`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function pushLocationMessage(
|
||||
to: string,
|
||||
location: {
|
||||
title: string;
|
||||
address: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
},
|
||||
opts: LinePushOpts,
|
||||
): Promise<LineSendResult> {
|
||||
return pushLineMessages(to, [createLocationMessage(location)], opts, {
|
||||
verboseMessage: (chatId) => `line: pushed location to ${chatId}`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function pushFlexMessage(
|
||||
to: string,
|
||||
altText: string,
|
||||
contents: FlexContainer,
|
||||
opts: LinePushOpts,
|
||||
): Promise<LineSendResult> {
|
||||
const flexMessage: FlexMessage = {
|
||||
type: "flex",
|
||||
altText: truncateUtf16Safe(altText, 400),
|
||||
contents,
|
||||
};
|
||||
|
||||
return pushLineMessages(to, [flexMessage], opts, {
|
||||
errorContext: "push flex message",
|
||||
verboseMessage: (chatId) => `line: pushed flex message to ${chatId}`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function pushTemplateMessage(
|
||||
to: string,
|
||||
template: TemplateMessage,
|
||||
opts: LinePushOpts,
|
||||
): Promise<LineSendResult> {
|
||||
return pushLineMessages(to, [template], opts, {
|
||||
verboseMessage: (chatId) => `line: pushed template message to ${chatId}`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function pushTextMessageWithQuickReplies(
|
||||
to: string,
|
||||
text: string,
|
||||
quickReplyLabels: string[],
|
||||
opts: LinePushOpts,
|
||||
): Promise<LineSendResult> {
|
||||
const message = createTextMessageWithQuickReplies(text, quickReplyLabels);
|
||||
|
||||
return pushLineMessages(to, [message], opts, {
|
||||
verboseMessage: (chatId) => `line: pushed message with quick replies to ${chatId}`,
|
||||
});
|
||||
}
|
||||
|
||||
export function createQuickReplyItems(labels: string[]): QuickReply {
|
||||
const items: QuickReplyItem[] = labels.slice(0, 13).map((label) => ({
|
||||
type: "action",
|
||||
action: messageAction(label, label),
|
||||
}));
|
||||
return { items };
|
||||
}
|
||||
|
||||
export function createTextMessageWithQuickReplies(
|
||||
text: string,
|
||||
quickReplyLabels: string[],
|
||||
): TextMessage & { quickReply: QuickReply } {
|
||||
return {
|
||||
type: "text",
|
||||
text,
|
||||
quickReply: createQuickReplyItems(quickReplyLabels),
|
||||
};
|
||||
}
|
||||
|
||||
export async function showLoadingAnimation(
|
||||
chatId: string,
|
||||
opts: LineClientOpts & { loadingSeconds?: number },
|
||||
): Promise<void> {
|
||||
const { client } = createLineMessagingClient(opts);
|
||||
|
||||
try {
|
||||
await client.showLoadingAnimation({
|
||||
chatId: normalizeTarget(chatId),
|
||||
loadingSeconds: opts.loadingSeconds ?? 20,
|
||||
});
|
||||
logVerbose(`line: showing loading animation to ${chatId}`);
|
||||
} catch (err) {
|
||||
logVerbose(`line: loading animation failed (non-fatal): ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserProfile(
|
||||
userId: string,
|
||||
opts: LineClientOpts & { useCache?: boolean },
|
||||
): Promise<{ displayName: string; pictureUrl?: string } | null> {
|
||||
const useCache = opts.useCache ?? true;
|
||||
|
||||
if (useCache) {
|
||||
const cached = userProfileCache.get(userId);
|
||||
if (cached && Date.now() - cached.fetchedAt < PROFILE_CACHE_TTL_MS) {
|
||||
return { displayName: cached.displayName, pictureUrl: cached.pictureUrl };
|
||||
}
|
||||
}
|
||||
|
||||
const { client } = createLineMessagingClient(opts);
|
||||
|
||||
try {
|
||||
const profile = await client.getProfile(userId);
|
||||
const result = {
|
||||
displayName: profile.displayName,
|
||||
pictureUrl: profile.pictureUrl,
|
||||
};
|
||||
|
||||
userProfileCache.set(userId, {
|
||||
...result,
|
||||
fetchedAt: Date.now(),
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
logVerbose(`line: failed to fetch profile for ${userId}: ${String(err)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserDisplayName(userId: string, opts: LineClientOpts): Promise<string> {
|
||||
const profile = await getUserProfile(userId, opts);
|
||||
return profile?.displayName ?? userId;
|
||||
}
|
||||
Reference in New Issue
Block a user