Files
douban-login/src/sms/douban-code.ts
2025-10-26 10:24:17 +08:00

183 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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('在设定的时间内未检测到新的豆瓣验证码短信');
}