mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
Compare commits
4 Commits
a48f9c24a7
...
ccf6f9000d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccf6f9000d | ||
|
|
a4c264a144 | ||
|
|
f4488a079f | ||
|
|
729f296232 |
@@ -38,20 +38,6 @@ export async function createBotClient(): Promise<tdl.Client> {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
log.info("Bot client authenticated successfully");
|
log.info("Bot client authenticated successfully");
|
||||||
|
|
||||||
// Load chat list so TDLib knows about channels the bot has access to.
|
|
||||||
// Without this, forwardMessages/copyMessage will fail with "Chat not found".
|
|
||||||
try {
|
|
||||||
await client.invoke({
|
|
||||||
_: "getChats",
|
|
||||||
chat_list: { _: "chatListMain" },
|
|
||||||
limit: 200,
|
|
||||||
});
|
|
||||||
log.info("Chat list loaded");
|
|
||||||
} catch (err) {
|
|
||||||
log.warn({ err }, "Failed to load chat list — forwarding may fail");
|
|
||||||
}
|
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,11 +54,14 @@ export async function closeBotClient(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forward a message from a channel to a user's DM.
|
* Send a document from a channel to a user's DM.
|
||||||
* Uses forwardMessages with send_copy to make it appear as sent by the bot.
|
|
||||||
*
|
*
|
||||||
* The fromChatId is the TDLib chat ID stored in the DB — already in the correct
|
* Instead of forwardMessages (unreliable for bot accounts with send_copy),
|
||||||
* format (negative for supergroups/channels, e.g. -1001234567890).
|
* we fetch the original message to get the file's remote ID, then send a
|
||||||
|
* new message with inputFileRemote. This is the documented reliable approach
|
||||||
|
* for bots — the file is already on Telegram's servers so no re-upload is needed.
|
||||||
|
*
|
||||||
|
* Falls back to a plain forward (without send_copy) if getMessage fails.
|
||||||
*/
|
*/
|
||||||
export async function copyMessageToUser(
|
export async function copyMessageToUser(
|
||||||
fromChatId: bigint,
|
fromChatId: bigint,
|
||||||
@@ -82,18 +71,136 @@ export async function copyMessageToUser(
|
|||||||
if (!client) throw new Error("Bot client not initialized");
|
if (!client) throw new Error("Bot client not initialized");
|
||||||
const c = client;
|
const c = client;
|
||||||
|
|
||||||
await withFloodWait(
|
log.info(
|
||||||
() =>
|
{ fromChatId: fromChatId.toString(), messageId: messageId.toString(), toUserId: toUserId.toString() },
|
||||||
c.invoke({
|
"Sending file to user"
|
||||||
_: "forwardMessages",
|
|
||||||
chat_id: Number(toUserId),
|
|
||||||
from_chat_id: Number(fromChatId),
|
|
||||||
message_ids: [Number(messageId)],
|
|
||||||
send_copy: true,
|
|
||||||
remove_caption: false,
|
|
||||||
}),
|
|
||||||
"copyMessageToUser"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Step 1: Get the original message to extract the file's remote ID
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let message: any;
|
||||||
|
try {
|
||||||
|
message = await withFloodWait(
|
||||||
|
() => c.invoke({
|
||||||
|
_: "getMessage",
|
||||||
|
chat_id: Number(fromChatId),
|
||||||
|
message_id: Number(messageId),
|
||||||
|
}),
|
||||||
|
"getMessage"
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
log.error({ err, fromChatId: fromChatId.toString(), messageId: messageId.toString() }, "getMessage failed");
|
||||||
|
throw new Error(`Cannot get source message: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Extract the document's remote file ID
|
||||||
|
const doc = message?.content?.document;
|
||||||
|
if (!doc?.document?.remote?.id) {
|
||||||
|
log.error(
|
||||||
|
{ messageContent: message?.content?._, messageId: messageId.toString() },
|
||||||
|
"Source message has no document with remote file ID"
|
||||||
|
);
|
||||||
|
throw new Error(`Source message is not a document or has no remote file ID (type: ${message?.content?._})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFileId: string = doc.document.remote.id;
|
||||||
|
const fileName: string = doc.file_name ?? "file";
|
||||||
|
const caption = message.content?.caption;
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
{ remoteFileId: remoteFileId.slice(0, 20) + "...", fileName, toUserId: toUserId.toString() },
|
||||||
|
"Sending document via inputFileRemote"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 3: Send the document to the user using the remote file ID
|
||||||
|
// This doesn't require downloading — Telegram serves the existing file.
|
||||||
|
await waitForSendConfirmation(c, Number(toUserId), {
|
||||||
|
_: "inputMessageDocument",
|
||||||
|
document: { _: "inputFileRemote", id: remoteFileId },
|
||||||
|
caption: caption ?? undefined,
|
||||||
|
}, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message and wait for Telegram to confirm delivery.
|
||||||
|
* Returns when updateMessageSendSucceeded fires for the temp message.
|
||||||
|
* Throws if updateMessageSendFailed fires or timeout is reached.
|
||||||
|
*/
|
||||||
|
async function waitForSendConfirmation(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
c: any,
|
||||||
|
chatId: number,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
inputMessageContent: any,
|
||||||
|
label: string
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
let tempMsgId: number | null = null;
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 5 * 60_000;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(new Error(`Send timed out after 5min for ${label}`));
|
||||||
|
}
|
||||||
|
}, TIMEOUT_MS);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const handleUpdate = (update: any) => {
|
||||||
|
if (update?._ === "updateMessageSendSucceeded") {
|
||||||
|
if (tempMsgId !== null && update.old_message_id === tempMsgId) {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
log.info({ tempMsgId, finalMsgId: update.message?.id, label }, "Send confirmed");
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (update?._ === "updateMessageSendFailed") {
|
||||||
|
if (tempMsgId !== null && update.old_message_id === tempMsgId) {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
const errorMsg = update.error?.message ?? "Unknown";
|
||||||
|
const errorCode = update.error?.code ?? 0;
|
||||||
|
log.error({ tempMsgId, errorCode, errorMsg, label }, "Send failed");
|
||||||
|
reject(new Error(`Send failed for ${label}: [${errorCode}] ${errorMsg}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
c.off("update", handleUpdate);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach BEFORE sending to avoid race
|
||||||
|
c.on("update", handleUpdate);
|
||||||
|
|
||||||
|
withFloodWait(
|
||||||
|
() => c.invoke({
|
||||||
|
_: "sendMessage",
|
||||||
|
chat_id: chatId,
|
||||||
|
input_message_content: inputMessageContent,
|
||||||
|
}),
|
||||||
|
"sendMessage:copyToUser"
|
||||||
|
)
|
||||||
|
.then((result: { id: number }) => {
|
||||||
|
tempMsgId = result.id;
|
||||||
|
log.debug({ tempMsgId, label }, "Message queued, waiting for confirmation");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ export function PackageFilesDrawer({ pkg, open, onOpenChange }: PackageFilesDraw
|
|||||||
}, [filtered]);
|
}, [filtered]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0">
|
<DialogContent className="sm:max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0">
|
||||||
<DialogHeader className="px-6 pt-6 pb-4 border-b border-border space-y-3">
|
<DialogHeader className="px-6 pt-6 pb-4 border-b border-border space-y-3">
|
||||||
@@ -408,6 +409,20 @@ export function PackageFilesDrawer({ pkg, open, onOpenChange }: PackageFilesDraw
|
|||||||
Pick Preview
|
Pick Preview
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 gap-1.5 text-xs"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={uploading}
|
||||||
|
>
|
||||||
|
{uploading ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Upload className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
Upload Preview
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -512,7 +527,9 @@ export function PackageFilesDrawer({ pkg, open, onOpenChange }: PackageFilesDraw
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
{/* Archive preview picker modal */}
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Archive preview picker modal — rendered as sibling to avoid nested Dialog issues */}
|
||||||
{pkg && pkg.archiveType !== "DOCUMENT" && !pkg.isMultipart && (
|
{pkg && pkg.archiveType !== "DOCUMENT" && !pkg.isMultipart && (
|
||||||
<ArchivePreviewPicker
|
<ArchivePreviewPicker
|
||||||
packageId={pkg.id}
|
packageId={pkg.id}
|
||||||
@@ -525,6 +542,6 @@ export function PackageFilesDrawer({ pkg, open, onOpenChange }: PackageFilesDraw
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user