mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-09-06 07:57:09 +08:00
Import sanitized project structure and GitHub docs
This commit is contained in:
@@ -0,0 +1,728 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import vm from "node:vm";
|
||||
import { Blob } from "node:buffer";
|
||||
|
||||
const SDK_BUNDLES = [
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/lib-polyfill.f81f86eb.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/lib-router.5ab9ff10.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/2105.f8d74876.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/douyin_creator_data_old.2f971672.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/argus-builder-strategy.5a053c46.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/7676.a4cd4900.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/4916.56c33d22.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/8198.b5c0b108.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/4168.b2e72401.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/7771.d27d1891.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/6682.2a991dfb.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/361.4fc40815.js",
|
||||
"https://lf-fe-creator.douyinstatic.com/obj/douyn-creator-scm-cdn/douyin-creator-mono-pc-data/static/js/async/pages-chat.6f823210.js",
|
||||
];
|
||||
|
||||
const CREATOR_CHAT_URL = "https://creator.douyin.com/creator-micro/data/following/chat";
|
||||
const USER_AGENT =
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141 Safari/537.36";
|
||||
|
||||
function noop() {}
|
||||
|
||||
function toCookieString(cookies) {
|
||||
return (cookies || [])
|
||||
.filter((item) => item?.name && item?.value !== undefined)
|
||||
.map((item) => `${item.name}=${item.value}`)
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
function normalizeNickname(value) {
|
||||
return String(value || "").trim();
|
||||
}
|
||||
|
||||
function stableNow() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function toNonNegativeInteger(value, fallback = 0) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed < 0) {
|
||||
return fallback;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeSendStrategy(raw = {}) {
|
||||
const intervalMin = toNonNegativeInteger(raw.messageIntervalSecondsMin, 0);
|
||||
const intervalMax = Math.max(intervalMin, toNonNegativeInteger(raw.messageIntervalSecondsMax, intervalMin));
|
||||
return {
|
||||
messageIntervalSecondsMin: intervalMin,
|
||||
messageIntervalSecondsMax: intervalMax,
|
||||
};
|
||||
}
|
||||
|
||||
function randomBetweenInclusive(min, max) {
|
||||
if (max <= min) {
|
||||
return min;
|
||||
}
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
async function readStdinJson() {
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
||||
if (!raw) {
|
||||
throw new Error("Missing JSON payload on stdin");
|
||||
}
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
async function ensureBundles(cacheDir) {
|
||||
await fs.promises.mkdir(cacheDir, { recursive: true });
|
||||
for (const url of SDK_BUNDLES) {
|
||||
const filename = url.split("/").at(-1);
|
||||
const filePath = path.join(cacheDir, filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
const response = await fetch(url, { headers: { "User-Agent": USER_AGENT } });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download SDK bundle ${url}: ${response.status}`);
|
||||
}
|
||||
const text = await response.text();
|
||||
await fs.promises.writeFile(filePath, text, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
function createWebpackRequire(bundleDir, cookieString) {
|
||||
const modules = {};
|
||||
const cache = {};
|
||||
|
||||
function requireModule(id) {
|
||||
if (cache[id]) {
|
||||
return cache[id].exports;
|
||||
}
|
||||
if (!modules[id]) {
|
||||
throw new Error(`Missing webpack module ${id}`);
|
||||
}
|
||||
const module = { exports: {} };
|
||||
cache[id] = module;
|
||||
modules[id].call(module.exports, module, module.exports, requireModule);
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
requireModule.d = (exports, definition) => {
|
||||
for (const key of Object.keys(definition)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(exports, key)) {
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: definition[key],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
requireModule.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
|
||||
requireModule.r = (exports) => {
|
||||
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
|
||||
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
};
|
||||
requireModule.n = (mod) => {
|
||||
const getter = mod && mod.__esModule ? () => mod.default : () => mod;
|
||||
requireModule.d(getter, { a: getter });
|
||||
return getter;
|
||||
};
|
||||
requireModule.g = globalThis;
|
||||
requireModule.hmd = (module) => module;
|
||||
requireModule.nmd = (module) => module;
|
||||
|
||||
const chunkArray = [];
|
||||
chunkArray.push = (chunk) => Object.assign(modules, chunk[1]);
|
||||
|
||||
const fakeElement = () => ({
|
||||
style: {},
|
||||
setAttribute: noop,
|
||||
appendChild: noop,
|
||||
removeChild: noop,
|
||||
addEventListener: noop,
|
||||
removeEventListener: noop,
|
||||
getContext: () => ({}),
|
||||
});
|
||||
const documentRef = {
|
||||
cookie: cookieString,
|
||||
referrer: CREATOR_CHAT_URL,
|
||||
createElement: fakeElement,
|
||||
getElementsByTagName: () => [],
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
addEventListener: noop,
|
||||
removeEventListener: noop,
|
||||
body: { appendChild: noop, removeChild: noop },
|
||||
head: { appendChild: noop, removeChild: noop },
|
||||
documentElement: { style: {} },
|
||||
};
|
||||
function XMLHttpRequestStub() {
|
||||
this.open = noop;
|
||||
this.setRequestHeader = noop;
|
||||
this.send = noop;
|
||||
}
|
||||
function WebSocketStub() {
|
||||
this.readyState = 1;
|
||||
this.send = noop;
|
||||
this.close = noop;
|
||||
}
|
||||
|
||||
const context = {
|
||||
self: { webpackChunkdouyin_creator_data: chunkArray },
|
||||
window: {},
|
||||
globalThis: null,
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
Buffer,
|
||||
TextDecoder,
|
||||
TextEncoder,
|
||||
Blob,
|
||||
document: documentRef,
|
||||
navigator: {
|
||||
userAgent: USER_AGENT,
|
||||
language: "en-US",
|
||||
cookieEnabled: true,
|
||||
onLine: true,
|
||||
platform: "Linux x86_64",
|
||||
sendBeacon: undefined,
|
||||
appName: "Netscape",
|
||||
},
|
||||
location: {
|
||||
href: CREATOR_CHAT_URL,
|
||||
protocol: "https:",
|
||||
search: "",
|
||||
pathname: "/creator-micro/data/following/chat",
|
||||
hostname: "creator.douyin.com",
|
||||
},
|
||||
localStorage: { getItem: () => null, setItem: noop, removeItem: noop },
|
||||
sessionStorage: { getItem: () => null, setItem: noop, removeItem: noop },
|
||||
performance: { now: () => Date.now() },
|
||||
fetch,
|
||||
XMLHttpRequest: XMLHttpRequestStub,
|
||||
WebSocket: WebSocketStub,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
atob: (value) => Buffer.from(value, "base64").toString("binary"),
|
||||
btoa: (value) => Buffer.from(value, "binary").toString("base64"),
|
||||
crypto,
|
||||
};
|
||||
context.window = context;
|
||||
context.globalThis = context;
|
||||
|
||||
for (const entry of fs.readdirSync(bundleDir).filter((name) => name.endsWith(".js")).sort()) {
|
||||
const code = fs.readFileSync(path.join(bundleDir, entry), "utf8");
|
||||
try {
|
||||
vm.runInNewContext(code, context, { filename: entry });
|
||||
} catch {
|
||||
// Some bundles execute browser-only entrypoints after registering modules.
|
||||
}
|
||||
}
|
||||
|
||||
return requireModule;
|
||||
}
|
||||
|
||||
class ProtocolError extends Error {
|
||||
constructor(message, details = {}) {
|
||||
super(message);
|
||||
this.name = "ProtocolError";
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
function extractCookieMap(cookies) {
|
||||
const items = {};
|
||||
for (const item of cookies || []) {
|
||||
if (item?.name) {
|
||||
items[item.name] = item.value ?? "";
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function buildCreatorHeaders(cookieString, cookieMap, referer = CREATOR_CHAT_URL) {
|
||||
return {
|
||||
"User-Agent": USER_AGENT,
|
||||
Referer: referer,
|
||||
Origin: "https://creator.douyin.com",
|
||||
Accept: "application/json, text/javascript",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: cookieString,
|
||||
"x-tt-passport-csrf-token":
|
||||
cookieMap.passport_csrf_token || cookieMap.passport_csrf_token_default || "",
|
||||
};
|
||||
}
|
||||
|
||||
function buildImHeaders(cookieString) {
|
||||
return {
|
||||
"User-Agent": USER_AGENT,
|
||||
Referer: CREATOR_CHAT_URL,
|
||||
Origin: "https://creator.douyin.com",
|
||||
Cookie: cookieString,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchJson(url, options = {}) {
|
||||
const timeoutMs = options.timeoutMs || 15000;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await response.text();
|
||||
clearTimeout(timer);
|
||||
let data = null;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
return { response, text, data };
|
||||
}
|
||||
|
||||
async function fetchSessionIdentity(cookieString, cookieMap) {
|
||||
const headers = buildCreatorHeaders(cookieString, cookieMap);
|
||||
const params = new URLSearchParams({
|
||||
aid: "2906",
|
||||
app_name: "aweme_creator_platform",
|
||||
device_platform: "web",
|
||||
referer: "",
|
||||
user_agent: USER_AGENT,
|
||||
cookie_enabled: "true",
|
||||
screen_width: "1280",
|
||||
screen_height: "720",
|
||||
browser_language: "en-US@posix",
|
||||
browser_platform: "Linux x86_64",
|
||||
browser_name: "Mozilla",
|
||||
browser_version: USER_AGENT,
|
||||
browser_online: "true",
|
||||
timezone_name: "Asia/Shanghai",
|
||||
});
|
||||
const { response, data, text } = await fetchJson(
|
||||
`https://creator.douyin.com/aweme/v1/creator/im/user_token/?${params.toString()}`,
|
||||
{ headers },
|
||||
);
|
||||
if (!response.ok || data?.status_code !== 0 || !data?.user_id) {
|
||||
throw new ProtocolError("Failed to resolve creator IM session identity", {
|
||||
status: response.status,
|
||||
body: text,
|
||||
});
|
||||
}
|
||||
return {
|
||||
userId: String(data.user_id),
|
||||
sessionToken: String(data.token || ""),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchIdentitySecurityToken(cookieString, cookieMap) {
|
||||
const headers = buildCreatorHeaders(cookieString, cookieMap);
|
||||
const params = new URLSearchParams({
|
||||
scene: "im_send_msg",
|
||||
auto_retry_req: "0",
|
||||
skip_verify: "0",
|
||||
identity_token_force_get_tag: "0",
|
||||
passport_jssdk_version: "5.1.4",
|
||||
passport_jssdk_type: "lite",
|
||||
is_from_ttaccountsdk: "1",
|
||||
aid: "2906",
|
||||
language: "zh",
|
||||
account_app_language: "en-US",
|
||||
id_token_version: "2.1.5",
|
||||
});
|
||||
const { response, data, text } = await fetchJson(
|
||||
`https://creator.douyin.com/passport/safe/get_identity_security_token/?${params.toString()}`,
|
||||
{ headers },
|
||||
);
|
||||
if (!response.ok || data?.message !== "success" || !data?.data?.identity_security_token) {
|
||||
throw new ProtocolError("Failed to resolve identity security token", {
|
||||
status: response.status,
|
||||
body: text,
|
||||
});
|
||||
}
|
||||
return {
|
||||
identitySecurityHeader: JSON.stringify({ token: data.data.identity_security_token }),
|
||||
realDeviceId: String(data.data.device_id || ""),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchProfileNickname(cookieString, secUid) {
|
||||
const url =
|
||||
"https://www.douyin.com/aweme/v1/web/user/profile/other/?" +
|
||||
new URLSearchParams({ sec_user_id: secUid }).toString();
|
||||
const { response, data, text } = await fetchJson(url, {
|
||||
headers: {
|
||||
"User-Agent": USER_AGENT,
|
||||
Referer: `https://www.douyin.com/user/${secUid}`,
|
||||
Cookie: cookieString,
|
||||
Accept: "application/json, text/javascript",
|
||||
},
|
||||
});
|
||||
if (!response.ok || data?.status_code !== 0) {
|
||||
return "";
|
||||
}
|
||||
return normalizeNickname(data?.user?.nickname);
|
||||
}
|
||||
|
||||
function stringifyMaybeLong(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value.toString === "function" && value.toString !== Object.prototype.toString) {
|
||||
return value.toString();
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function selectPeerParticipant(conversation, selfUserId) {
|
||||
const participants = conversation?.firstPageParticipant?.participants || [];
|
||||
for (const participant of participants) {
|
||||
const currentUserId = stringifyMaybeLong(participant?.user_id);
|
||||
if (currentUserId && currentUserId !== selfUserId) {
|
||||
return participant;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createProtocolClient({ bundleDir, cookieString, cookieMap, userId }) {
|
||||
const requireModule = createWebpackRequire(bundleDir, cookieString);
|
||||
const sdk = requireModule(61724);
|
||||
const { BytedIM } = requireModule(26440);
|
||||
|
||||
class AdditionalParamsPlugin extends sdk.BasePlugin {
|
||||
install() {}
|
||||
|
||||
async sendPacket(packet) {
|
||||
packet.device_id = 0;
|
||||
packet.device_platform = "douyin_creator";
|
||||
packet.headers = {
|
||||
...(packet.headers || {}),
|
||||
aid_new: 2906,
|
||||
app_name: "douyin_creator",
|
||||
};
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
class NodeHttpClient extends sdk.IMHttpClient {
|
||||
async send(url, method, body) {
|
||||
const fullUrl = /^https?:/i.test(url)
|
||||
? url
|
||||
: `${String(this.option.apiUrl).replace(/\/$/, "")}/${String(url).replace(/^\//, "")}`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 20000);
|
||||
const response = await fetch(fullUrl, {
|
||||
method,
|
||||
headers: this.headers,
|
||||
body: body ? Buffer.from(body) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
sendByBeacon() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const client = new BytedIM(
|
||||
{
|
||||
appId: 2906,
|
||||
fpId: 9,
|
||||
appKey: "e1bd35ec9db7b8d846de66ed140b1ad9",
|
||||
service: 5,
|
||||
apiUrl: "https://imapi.douyin.com",
|
||||
frontierUrl: "wss://frontier-im.douyin.com/ws/v2",
|
||||
inboxType: 1,
|
||||
token: "",
|
||||
userId,
|
||||
deviceId: userId,
|
||||
authType: sdk.im_proto.AuthType.SESSION_AUTH,
|
||||
devicePlatform: "douyin_pc",
|
||||
timeout: 20000,
|
||||
acceptIncorrectInboxType: true,
|
||||
biz: "douyin_creator",
|
||||
withCredentials: false,
|
||||
httpHeaders: buildImHeaders(cookieString),
|
||||
headers: {},
|
||||
webSocketLevel: sdk.WebSocketLevel.PushOnly,
|
||||
debug: false,
|
||||
http: (ctx) => new NodeHttpClient(ctx),
|
||||
},
|
||||
[AdditionalParamsPlugin],
|
||||
);
|
||||
|
||||
const initResult = await client.init();
|
||||
if (initResult !== sdk.InitResult.Succeeded) {
|
||||
throw new ProtocolError("Protocol IM init did not succeed", { initResult });
|
||||
}
|
||||
|
||||
return { client };
|
||||
}
|
||||
|
||||
async function buildConversationCache({
|
||||
client,
|
||||
selfUserId,
|
||||
cookieString,
|
||||
existingCache = [],
|
||||
targetNames = [],
|
||||
}) {
|
||||
const cachedBySecUid = new Map(
|
||||
(existingCache || []).filter((entry) => entry?.secUid).map((entry) => [entry.secUid, entry]),
|
||||
);
|
||||
const wantedTargets = new Set((targetNames || []).map(normalizeNickname).filter(Boolean));
|
||||
const matchedTargets = new Set();
|
||||
const conversations = await client.getConversationListOnline();
|
||||
const cacheEntries = [];
|
||||
|
||||
for (const conversation of conversations) {
|
||||
if (conversation?.type !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const peer = selectPeerParticipant(conversation, selfUserId);
|
||||
if (!peer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const peerUserId = stringifyMaybeLong(peer.user_id);
|
||||
const secUid = peer.sec_uid || "";
|
||||
if (!peerUserId || !secUid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let nickname = normalizeNickname(cachedBySecUid.get(secUid)?.nickname);
|
||||
if (!nickname) {
|
||||
try {
|
||||
nickname = await fetchProfileNickname(cookieString, secUid);
|
||||
} catch {
|
||||
nickname = "";
|
||||
}
|
||||
}
|
||||
|
||||
cacheEntries.push({
|
||||
nickname,
|
||||
peerUserId,
|
||||
secUid,
|
||||
conversationId: conversation.id,
|
||||
conversationShortId: conversation.shortId,
|
||||
updatedAt: stableNow(),
|
||||
});
|
||||
|
||||
if (nickname && wantedTargets.has(nickname)) {
|
||||
matchedTargets.add(nickname);
|
||||
if (matchedTargets.size === wantedTargets.size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deduped = new Map();
|
||||
for (const entry of existingCache || []) {
|
||||
if (!entry?.nickname || !entry?.secUid) {
|
||||
continue;
|
||||
}
|
||||
deduped.set(entry.secUid, entry);
|
||||
}
|
||||
for (const entry of cacheEntries) {
|
||||
if (!entry.nickname) {
|
||||
continue;
|
||||
}
|
||||
deduped.set(entry.secUid, entry);
|
||||
}
|
||||
return Array.from(deduped.values()).sort((left, right) =>
|
||||
left.nickname.localeCompare(right.nickname, "zh-CN"),
|
||||
);
|
||||
}
|
||||
|
||||
function buildTargetLookup(cacheEntries) {
|
||||
const byNickname = new Map();
|
||||
for (const entry of cacheEntries) {
|
||||
const key = normalizeNickname(entry.nickname);
|
||||
if (key && !byNickname.has(key)) {
|
||||
byNickname.set(key, entry);
|
||||
}
|
||||
}
|
||||
return byNickname;
|
||||
}
|
||||
|
||||
async function sendMessages({
|
||||
client,
|
||||
cacheEntries,
|
||||
messagesByTarget,
|
||||
dryRun,
|
||||
cookieString,
|
||||
cookieMap,
|
||||
sendStrategy,
|
||||
}) {
|
||||
if (!dryRun) {
|
||||
const identity = await fetchIdentitySecurityToken(cookieString, cookieMap);
|
||||
client.updateSendMessageHeaders({
|
||||
identity_security_token: identity.identitySecurityHeader,
|
||||
identity_security_device_id: identity.realDeviceId,
|
||||
identity_security_aid: "2906",
|
||||
});
|
||||
}
|
||||
|
||||
const byNickname = buildTargetLookup(cacheEntries);
|
||||
const resolved = [];
|
||||
const unresolved = [];
|
||||
const sent = [];
|
||||
const normalizedStrategy = normalizeSendStrategy(sendStrategy);
|
||||
|
||||
for (const [target, message] of Object.entries(messagesByTarget)) {
|
||||
const mapping = byNickname.get(normalizeNickname(target));
|
||||
if (!mapping) {
|
||||
unresolved.push({ target, reason: "conversation_not_found" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const conversation = client.getConversation({ conversationId: mapping.conversationId });
|
||||
if (!conversation) {
|
||||
unresolved.push({ target, reason: "conversation_not_loaded", mapping });
|
||||
continue;
|
||||
}
|
||||
|
||||
resolved.push({
|
||||
target,
|
||||
nickname: mapping.nickname,
|
||||
peerUserId: mapping.peerUserId,
|
||||
conversationId: mapping.conversationId,
|
||||
conversationShortId: mapping.conversationShortId,
|
||||
});
|
||||
|
||||
let delayBeforeSendSeconds = 0;
|
||||
if (!dryRun && sent.length > 0 && normalizedStrategy.messageIntervalSecondsMax > 0) {
|
||||
delayBeforeSendSeconds = randomBetweenInclusive(
|
||||
normalizedStrategy.messageIntervalSecondsMin,
|
||||
normalizedStrategy.messageIntervalSecondsMax,
|
||||
);
|
||||
if (delayBeforeSendSeconds > 0) {
|
||||
await sleep(delayBeforeSendSeconds * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({ text: message, aweType: 774 });
|
||||
const messageObject = await client.createMessage({
|
||||
type: 7,
|
||||
content: payload,
|
||||
conversation,
|
||||
insert: false,
|
||||
});
|
||||
|
||||
if (dryRun) {
|
||||
sent.push({
|
||||
target,
|
||||
dryRun: true,
|
||||
message,
|
||||
payload,
|
||||
conversationId: mapping.conversationId,
|
||||
delayBeforeSendSeconds,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const sendResult = await client.sendMessage({ message: messageObject });
|
||||
sent.push({
|
||||
target,
|
||||
dryRun: false,
|
||||
message,
|
||||
success: Boolean(sendResult?.success),
|
||||
statusCode: sendResult?.statusCode ?? null,
|
||||
statusMsg: sendResult?.statusMsg ?? "",
|
||||
conversationId: mapping.conversationId,
|
||||
delayBeforeSendSeconds,
|
||||
sentAt: stableNow(),
|
||||
});
|
||||
}
|
||||
|
||||
return { resolved, unresolved, sent };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const payload = await readStdinJson();
|
||||
const repoRoot = payload.repoRoot || process.cwd();
|
||||
const bundleDir = path.join(repoRoot, ".im_sdk_cache");
|
||||
await ensureBundles(bundleDir);
|
||||
|
||||
const account = payload.account || {};
|
||||
const cookieString = toCookieString(account.cookies);
|
||||
const cookieMap = extractCookieMap(account.cookies);
|
||||
const { userId } = await fetchSessionIdentity(cookieString, cookieMap);
|
||||
const { client } = await createProtocolClient({
|
||||
bundleDir,
|
||||
cookieString,
|
||||
cookieMap,
|
||||
userId,
|
||||
});
|
||||
|
||||
const cacheEntries = await buildConversationCache({
|
||||
client,
|
||||
selfUserId: userId,
|
||||
cookieString,
|
||||
existingCache: account.protocol_targets_cache || [],
|
||||
targetNames: Object.keys(payload.messagesByTarget || {}),
|
||||
});
|
||||
const execution = await sendMessages({
|
||||
client,
|
||||
cacheEntries,
|
||||
messagesByTarget: payload.messagesByTarget || {},
|
||||
dryRun: Boolean(payload.dryRun),
|
||||
cookieString,
|
||||
cookieMap,
|
||||
sendStrategy: payload.sendStrategy || {},
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
username: account.username || "",
|
||||
userId,
|
||||
dryRun: Boolean(payload.dryRun),
|
||||
protocol_targets_cache: cacheEntries,
|
||||
...execution,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await client.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: false,
|
||||
error: error?.message || String(error),
|
||||
details: error?.details || {},
|
||||
stack: error?.stack || "",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user