update at 2025-10-26 10:24:17

This commit is contained in:
douboer
2025-10-26 10:24:17 +08:00
parent bd8da1d56a
commit 06ac359162
14 changed files with 934 additions and 154 deletions

182
src/sms/douban-code.ts Normal file
View File

@@ -0,0 +1,182 @@
import Database from 'better-sqlite3';
import os from 'os';
import path from 'path';
const APPLE_EPOCH_MS = Date.UTC(2001, 0, 1);
const DB_PATH = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
export interface SmsMessage {
id: number;
text: string;
handle: string;
service: string;
isFromMe: boolean;
date: Date;
}
export interface WaitForCodeOptions {
timeoutMs?: number;
pollIntervalMs?: number;
logger?: (message: string) => void;
}
export interface WaitForCodeResult {
code: string;
message: SmsMessage;
}
interface RawMessageRow {
id: number;
text: string | null;
handle: string | null;
service: string | null;
is_from_me: number;
date_raw: number | null;
}
const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
const DEFAULT_POLL_INTERVAL_MS = 2500;
function appleTimestampToDate(raw: number | null): Date {
if (!raw) {
return new Date(0);
}
let ms = raw;
if (raw > 1e15) {
ms = raw / 1_000_000;
} else if (raw > 1e12) {
ms = raw / 1_000;
} else {
ms = raw * 1000;
}
return new Date(APPLE_EPOCH_MS + ms);
}
function openDatabase(): Database.Database {
return new Database(DB_PATH, { readonly: true, fileMustExist: true });
}
function toSmsMessage(row: RawMessageRow | undefined): SmsMessage | null {
if (!row) {
return null;
}
const text = (row.text ?? '').trim();
if (!text) {
return null;
}
return {
id: row.id,
text,
handle: row.handle ?? '',
service: row.service ?? '',
isFromMe: row.is_from_me === 1,
date: appleTimestampToDate(row.date_raw),
};
}
function fetchLatestMessage(db: Database.Database): SmsMessage | null {
const stmt = db.prepare<[], RawMessageRow>(`
SELECT
message.ROWID AS id,
message.text AS text,
handle.id AS handle,
message.service AS service,
message.is_from_me AS is_from_me,
COALESCE(message.date, message.date_delivered, message.date_read) AS date_raw
FROM message
LEFT JOIN handle ON handle.ROWID = message.handle_id
WHERE message.text IS NOT NULL AND message.text != ''
ORDER BY date_raw DESC
LIMIT 1
`);
return toSmsMessage(stmt.get());
}
function fetchLatestDoubanMessage(db: Database.Database): SmsMessage | null {
const stmt = db.prepare<[], RawMessageRow>(`
SELECT
message.ROWID AS id,
message.text AS text,
handle.id AS handle,
message.service AS service,
message.is_from_me AS is_from_me,
COALESCE(message.date, message.date_delivered, message.date_read) AS date_raw
FROM message
LEFT JOIN handle ON handle.ROWID = message.handle_id
WHERE message.is_from_me = 0
AND message.text IS NOT NULL
AND message.text != ''
AND message.text LIKE '%豆瓣%'
AND message.text LIKE '%验证码%'
ORDER BY date_raw DESC
LIMIT 1
`);
return toSmsMessage(stmt.get());
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function parseDoubanSms(text: string | null | undefined): string | null {
if (!text) {
return null;
}
const match = text.match(/验证码[:]\s*([0-9]{4,6})/);
return match ? match[1] : null;
}
export async function waitForDoubanCode(options: WaitForCodeOptions = {}): Promise<WaitForCodeResult> {
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
const deadline = Date.now() + timeoutMs;
const logger = options.logger;
let db: Database.Database | null = null;
try {
db = openDatabase();
const baselineMessage = fetchLatestMessage(db);
const baselineId = baselineMessage?.id ?? 0;
if (logger) {
logger(`已连接 chat.db起始消息 ID: ${baselineId}`);
}
while (Date.now() <= deadline) {
const doubanMessage = fetchLatestDoubanMessage(db);
if (doubanMessage && doubanMessage.id > baselineId) {
const code = parseDoubanSms(doubanMessage.text);
if (code) {
if (logger) {
logger(`捕获验证码短信,消息 ID: ${doubanMessage.id}`);
}
return {
code,
message: doubanMessage,
};
}
}
if (logger) {
logger('未检测到新的豆瓣验证码短信,等待后重试...');
}
await delay(pollIntervalMs);
}
} finally {
if (db) {
db.close();
}
}
throw new Error('在设定的时间内未检测到新的豆瓣验证码短信');
}