274 lines
10 KiB
TypeScript
274 lines
10 KiB
TypeScript
import type { proto } from "@whiskeysockets/baileys";
|
|
import {
|
|
extractMessageContent,
|
|
getContentType,
|
|
normalizeMessageContent,
|
|
} from "@whiskeysockets/baileys";
|
|
import { formatLocationText, type NormalizedLocation } from "../../channels/location.js";
|
|
import { logVerbose } from "../../globals.js";
|
|
import { jidToE164 } from "../../utils.js";
|
|
import { parseVcard } from "../vcard.js";
|
|
|
|
function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | undefined {
|
|
const normalized = normalizeMessageContent(message as proto.IMessage | undefined);
|
|
return normalized as proto.IMessage | undefined;
|
|
}
|
|
|
|
function extractContextInfo(message: proto.IMessage | undefined): proto.IContextInfo | undefined {
|
|
if (!message) return undefined;
|
|
const contentType = getContentType(message);
|
|
const candidate = contentType ? (message as Record<string, unknown>)[contentType] : undefined;
|
|
const contextInfo =
|
|
candidate && typeof candidate === "object" && "contextInfo" in candidate
|
|
? (candidate as { contextInfo?: proto.IContextInfo }).contextInfo
|
|
: undefined;
|
|
if (contextInfo) return contextInfo;
|
|
const fallback =
|
|
message.extendedTextMessage?.contextInfo ??
|
|
message.imageMessage?.contextInfo ??
|
|
message.videoMessage?.contextInfo ??
|
|
message.documentMessage?.contextInfo ??
|
|
message.audioMessage?.contextInfo ??
|
|
message.stickerMessage?.contextInfo ??
|
|
message.buttonsResponseMessage?.contextInfo ??
|
|
message.listResponseMessage?.contextInfo ??
|
|
message.templateButtonReplyMessage?.contextInfo ??
|
|
message.interactiveResponseMessage?.contextInfo ??
|
|
message.buttonsMessage?.contextInfo ??
|
|
message.listMessage?.contextInfo;
|
|
if (fallback) return fallback;
|
|
for (const value of Object.values(message)) {
|
|
if (!value || typeof value !== "object") continue;
|
|
if (!("contextInfo" in value)) continue;
|
|
const candidateContext = (value as { contextInfo?: proto.IContextInfo }).contextInfo;
|
|
if (candidateContext) return candidateContext;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function extractMentionedJids(rawMessage: proto.IMessage | undefined): string[] | undefined {
|
|
const message = unwrapMessage(rawMessage);
|
|
if (!message) return undefined;
|
|
|
|
const candidates: Array<string[] | null | undefined> = [
|
|
message.extendedTextMessage?.contextInfo?.mentionedJid,
|
|
message.extendedTextMessage?.contextInfo?.quotedMessage?.extendedTextMessage?.contextInfo
|
|
?.mentionedJid,
|
|
message.imageMessage?.contextInfo?.mentionedJid,
|
|
message.videoMessage?.contextInfo?.mentionedJid,
|
|
message.documentMessage?.contextInfo?.mentionedJid,
|
|
message.audioMessage?.contextInfo?.mentionedJid,
|
|
message.stickerMessage?.contextInfo?.mentionedJid,
|
|
message.buttonsResponseMessage?.contextInfo?.mentionedJid,
|
|
message.listResponseMessage?.contextInfo?.mentionedJid,
|
|
];
|
|
|
|
const flattened = candidates.flatMap((arr) => arr ?? []).filter(Boolean);
|
|
if (flattened.length === 0) return undefined;
|
|
return Array.from(new Set(flattened));
|
|
}
|
|
|
|
export function extractText(rawMessage: proto.IMessage | undefined): string | undefined {
|
|
const message = unwrapMessage(rawMessage);
|
|
if (!message) return undefined;
|
|
const extracted = extractMessageContent(message);
|
|
const candidates = [message, extracted && extracted !== message ? extracted : undefined];
|
|
for (const candidate of candidates) {
|
|
if (!candidate) continue;
|
|
if (typeof candidate.conversation === "string" && candidate.conversation.trim()) {
|
|
return candidate.conversation.trim();
|
|
}
|
|
const extended = candidate.extendedTextMessage?.text;
|
|
if (extended?.trim()) return extended.trim();
|
|
const caption =
|
|
candidate.imageMessage?.caption ??
|
|
candidate.videoMessage?.caption ??
|
|
candidate.documentMessage?.caption;
|
|
if (caption?.trim()) return caption.trim();
|
|
}
|
|
const contactPlaceholder =
|
|
extractContactPlaceholder(message) ??
|
|
(extracted && extracted !== message
|
|
? extractContactPlaceholder(extracted as proto.IMessage | undefined)
|
|
: undefined);
|
|
if (contactPlaceholder) return contactPlaceholder;
|
|
return undefined;
|
|
}
|
|
|
|
export function extractMediaPlaceholder(
|
|
rawMessage: proto.IMessage | undefined,
|
|
): string | undefined {
|
|
const message = unwrapMessage(rawMessage);
|
|
if (!message) return undefined;
|
|
if (message.imageMessage) return "<media:image>";
|
|
if (message.videoMessage) return "<media:video>";
|
|
if (message.audioMessage) return "<media:audio>";
|
|
if (message.documentMessage) return "<media:document>";
|
|
if (message.stickerMessage) return "<media:sticker>";
|
|
return undefined;
|
|
}
|
|
|
|
function extractContactPlaceholder(rawMessage: proto.IMessage | undefined): string | undefined {
|
|
const message = unwrapMessage(rawMessage);
|
|
if (!message) return undefined;
|
|
const contact = message.contactMessage ?? undefined;
|
|
if (contact) {
|
|
const { name, phones } = describeContact({
|
|
displayName: contact.displayName,
|
|
vcard: contact.vcard,
|
|
});
|
|
return formatContactPlaceholder(name, phones);
|
|
}
|
|
const contactsArray = message.contactsArrayMessage?.contacts ?? undefined;
|
|
if (!contactsArray || contactsArray.length === 0) return undefined;
|
|
const labels = contactsArray
|
|
.map((entry) => describeContact({ displayName: entry.displayName, vcard: entry.vcard }))
|
|
.map((entry) => formatContactLabel(entry.name, entry.phones))
|
|
.filter((value): value is string => Boolean(value));
|
|
return formatContactsPlaceholder(labels, contactsArray.length);
|
|
}
|
|
|
|
function describeContact(input: { displayName?: string | null; vcard?: string | null }): {
|
|
name?: string;
|
|
phones: string[];
|
|
} {
|
|
const displayName = (input.displayName ?? "").trim();
|
|
const parsed = parseVcard(input.vcard ?? undefined);
|
|
const name = displayName || parsed.name;
|
|
return { name, phones: parsed.phones };
|
|
}
|
|
|
|
function formatContactPlaceholder(name?: string, phones?: string[]): string {
|
|
const label = formatContactLabel(name, phones);
|
|
if (!label) return "<contact>";
|
|
return `<contact: ${label}>`;
|
|
}
|
|
|
|
function formatContactsPlaceholder(labels: string[], total: number): string {
|
|
const cleaned = labels.map((label) => label.trim()).filter(Boolean);
|
|
if (cleaned.length === 0) {
|
|
const suffix = total === 1 ? "contact" : "contacts";
|
|
return `<contacts: ${total} ${suffix}>`;
|
|
}
|
|
const remaining = Math.max(total - cleaned.length, 0);
|
|
const suffix = remaining > 0 ? ` +${remaining} more` : "";
|
|
return `<contacts: ${cleaned.join(", ")}${suffix}>`;
|
|
}
|
|
|
|
function formatContactLabel(name?: string, phones?: string[]): string | undefined {
|
|
const phoneLabel = formatPhoneList(phones);
|
|
const parts = [name, phoneLabel].filter((value): value is string => Boolean(value));
|
|
if (parts.length === 0) return undefined;
|
|
return parts.join(", ");
|
|
}
|
|
|
|
function formatPhoneList(phones?: string[]): string | undefined {
|
|
const cleaned = phones?.map((phone) => phone.trim()).filter(Boolean) ?? [];
|
|
if (cleaned.length === 0) return undefined;
|
|
const { shown, remaining } = summarizeList(cleaned, cleaned.length, 1);
|
|
const [primary] = shown;
|
|
if (!primary) return undefined;
|
|
if (remaining === 0) return primary;
|
|
return `${primary} (+${remaining} more)`;
|
|
}
|
|
|
|
function summarizeList(
|
|
values: string[],
|
|
total: number,
|
|
maxShown: number,
|
|
): { shown: string[]; remaining: number } {
|
|
const shown = values.slice(0, maxShown);
|
|
const remaining = Math.max(total - shown.length, 0);
|
|
return { shown, remaining };
|
|
}
|
|
|
|
export function extractLocationData(
|
|
rawMessage: proto.IMessage | undefined,
|
|
): NormalizedLocation | null {
|
|
const message = unwrapMessage(rawMessage);
|
|
if (!message) return null;
|
|
|
|
const live = message.liveLocationMessage ?? undefined;
|
|
if (live) {
|
|
const latitudeRaw = live.degreesLatitude;
|
|
const longitudeRaw = live.degreesLongitude;
|
|
if (latitudeRaw != null && longitudeRaw != null) {
|
|
const latitude = Number(latitudeRaw);
|
|
const longitude = Number(longitudeRaw);
|
|
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
|
|
return {
|
|
latitude,
|
|
longitude,
|
|
accuracy: live.accuracyInMeters ?? undefined,
|
|
caption: live.caption ?? undefined,
|
|
source: "live",
|
|
isLive: true,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
const location = message.locationMessage ?? undefined;
|
|
if (location) {
|
|
const latitudeRaw = location.degreesLatitude;
|
|
const longitudeRaw = location.degreesLongitude;
|
|
if (latitudeRaw != null && longitudeRaw != null) {
|
|
const latitude = Number(latitudeRaw);
|
|
const longitude = Number(longitudeRaw);
|
|
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
|
|
const isLive = Boolean(location.isLive);
|
|
return {
|
|
latitude,
|
|
longitude,
|
|
accuracy: location.accuracyInMeters ?? undefined,
|
|
name: location.name ?? undefined,
|
|
address: location.address ?? undefined,
|
|
caption: location.comment ?? undefined,
|
|
source: isLive ? "live" : location.name || location.address ? "place" : "pin",
|
|
isLive,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function describeReplyContext(rawMessage: proto.IMessage | undefined): {
|
|
id?: string;
|
|
body: string;
|
|
sender: string;
|
|
senderJid?: string;
|
|
senderE164?: string;
|
|
} | null {
|
|
const message = unwrapMessage(rawMessage);
|
|
if (!message) return null;
|
|
const contextInfo = extractContextInfo(message);
|
|
const quoted = normalizeMessageContent(
|
|
contextInfo?.quotedMessage as proto.IMessage | undefined,
|
|
) as proto.IMessage | undefined;
|
|
if (!quoted) return null;
|
|
const location = extractLocationData(quoted);
|
|
const locationText = location ? formatLocationText(location) : undefined;
|
|
const text = extractText(quoted);
|
|
let body: string | undefined = [text, locationText].filter(Boolean).join("\n").trim();
|
|
if (!body) body = extractMediaPlaceholder(quoted);
|
|
if (!body) {
|
|
const quotedType = quoted ? getContentType(quoted) : undefined;
|
|
logVerbose(
|
|
`Quoted message missing extractable body${quotedType ? ` (type ${quotedType})` : ""}`,
|
|
);
|
|
return null;
|
|
}
|
|
const senderJid = contextInfo?.participant ?? undefined;
|
|
const senderE164 = senderJid ? (jidToE164(senderJid) ?? senderJid) : undefined;
|
|
const sender = senderE164 ?? "unknown sender";
|
|
return {
|
|
id: contextInfo?.stanzaId ? String(contextInfo.stanzaId) : undefined,
|
|
body,
|
|
sender,
|
|
senderJid,
|
|
senderE164,
|
|
};
|
|
}
|