Skip to content

Instantly share code, notes, and snippets.

@Zekiah-A
Created June 9, 2024 14:45
Show Gist options
  • Select an option

  • Save Zekiah-A/f4b76e17aea924dc89d4aaf564c8da38 to your computer and use it in GitHub Desktop.

Select an option

Save Zekiah-A/f4b76e17aea924dc89d4aaf564c8da38 to your computer and use it in GitHub Desktop.
A hacky script that moves messages from a telegram chat export to a discord server
import { join } from "path";
import { parseArgs } from "util";
const { values } = parseArgs({
args: Bun.argv,
options: {
folder: {
type: "string",
},
},
strict: true,
allowPositionals: true,
});
const moverConfigPath = "./mover-config.json"
let moverConfigFile = null
try { moverConfigFile = Bun.file(moverConfigPath) } catch { }
if (!await moverConfigFile?.exists()) {
Bun.write(moverConfigPath, JSON.stringify({
sendIntervalMs: 1500,
webhookUrls: [ "ENTER_DISCORD_WEBHOOK_URL_HERE" ],
sendAttempts: 4,
avatarUrls: { "user546432622": "https://cdn.discordapp.com/media/avatar.png" },
nameOverrides: { "user436343422": "Zubigri" },
proxyUrls: [ null ]
}, null, 4))
console.log("Config created! Please update ./mover-config.json before running again.")
process.exit(0)
}
const { sendIntervalMs, webhookUrls, sendAttempts, avatarUrls, nameOverrides, proxyUrls } = await moverConfigFile.json()
let proxyUrlsI = 0
function nextProxyUrl() {
const proxyUrl = proxyUrls[proxyUrlsI++ % proxyUrls.length]
return proxyUrl
}
let webhookUrlsI = 0
function nextWebhookUrl() {
const webhookUrl = webhookUrls[webhookUrlsI++ % webhookUrls.length]
return webhookUrl
}
async function handleRateLimit(response) {
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || (await response.json()).retry_after || 1
console.warn(`Rate limited. Retrying after ${retryAfter} seconds...`)
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
return true
}
return false
}
// Attachments can only be images
async function sendEmbed(username, message, title = username, attachments = null, avatarUrl = null) {
const formData = new FormData();
const embed = {
description: message,
author: { name: title }
}
if (attachments) {
const { data, mimeType, fileName } = attachments;
let blob = new Blob([new Uint8Array(data)], { type: mimeType });
formData.append("file", blob, fileName)
embed.image = {
url: `attachment://${fileName}`
};
}
const payload = {
embeds: [embed],
username: username
};
if (avatarUrl) {
payload.avatar_url = avatarUrl
}
formData.append("payload_json", JSON.stringify(payload));
while (true) {
const response = await fetch(nextWebhookUrl(), {
method: "POST",
body: formData,
proxy: nextProxyUrl()
});
try {
if (!response.ok) {
if (await handleRateLimit(response)) continue;
console.error("Failed to send embed message:", response.status, await response.json());
return false
}
else {
console.log("Embed message sent successfully:", message);
return true
}
}
catch(e) {
console.error("Failed to send embed message", e)
return false
}
}
}
async function sendMessage(username, message, attachment = null, avatarUrl = null) {
const formData = new FormData();
formData.append("content", message);
formData.append("username", username);
if (avatarUrl) {
formData.append("avatar_url", avatarUrl)
}
if (attachment) {
const { data, mimeType, fileName } = attachment;
const blob = new Blob([data], { type: mimeType });
formData.append("file", blob, fileName)
}
const response = await fetch(nextWebhookUrl(), {
method: "POST",
body: formData,
proxy: nextProxyUrl()
});
while (true) {
try {
if (!response.ok) {
if (await handleRateLimit(response)) continue;
console.error("Failed to send message:", response.status, await response.json());
return false
}
else {
console.log("Message sent successfully:", message);
return true
}
}
catch(e) {
console.error("Failed to send mesage", e)
return false
}
}
}
let resultObject = null
let i = parseInt(await (Bun.file("./progress.txt")).text()) || 0
function proceed() {
i++
Bun.write("./progress.txt", i.toString())
}
function mimeToExtension(mimeType) {
const mimeToExt = {
"image/jpeg": "jpg",
"image/png": "png",
"image/gif": "gif",
"image/bmp": "bmp",
"image/webp": "webp",
"video/mp4": "mp4",
"video/mpeg": "mpeg",
"video/ogg": "ogv",
"video/webm": "webm",
"video/quicktime": "mov",
"audio/mpeg": "mp3",
"audio/ogg": "ogg",
"audio/wav": "wav",
"audio/webm": "weba",
"audio/aac": "aac",
"audio/flac": "flac",
"audio/mp4": "m4a",
"application/pdf": "pdf",
"application/zip": "zip",
"application/json": "json",
"text/plain": "txt",
"text/html": "html",
"text/css": "css",
"text/javascript": "js",
"application/x-msdownload": "exe",
"application/vnd.microsoft.portable-executable": "exe",
"application/x-msdos-program": "exe"
}
return mimeToExt[mimeType] || ""
}
async function attemptWithCooldown(sendFnPromise) {
let success = false
for (let i = 0; i < sendAttempts; i++) {
success = await sendFnPromise
await new Promise(resolve => setTimeout(resolve, sendIntervalMs));
if (success) {
return true
}
}
return false
}
function parseMessageText(message) {
let text = ""
if (typeof message.text === "string") {
text = message.text
}
else if (typeof message.text === "object") {
function visitTextNode(data) {
if (Array.isArray(data)) {
let childrenText = ""
for (const node of data) {
childrenText += visitTextNode(node)
}
return childrenText
}
else if (typeof data === "object") {
return data.text
}
else if (typeof data == "string") {
return data
}
else {
throw new Error("Couldn't visit text node - unknown node type")
}
}
text = visitTextNode(message.text)
}
return text
}
function resolveAuthorName(message) {
return nameOverrides[message.from_id] || message.from || "Deleted account"
}
async function startSendLoop() {
while (i < resultObject.messages.length) {
const message = resultObject.messages[i]
let text = parseMessageText(message).replaceAll("@everyone", "@ everyone")
let sender = `[${resultObject.name.slice(0, 12).trim()}] ${resolveAuthorName(message)}`
if (message.type != "message") {
console.log("Ignoring message", message.id, "of type", message.type)
proceed()
continue
}
if (message.reply_to_message_id) {
let targetMessage = null
for (const replyMessage of resultObject.messages) {
if (replyMessage.id == message.reply_to_message_id) {
targetMessage = replyMessage
break
}
}
if (!targetMessage) {
text = `> *Replying to* Deleted Message\n${text}`
}
else {
let targetmessageText = parseMessageText(targetMessage).replaceAll("@everyone", "@ everyone").replaceAll("\\n", "\n> ")
if (targetmessageText.length > 512) {
targetmessageText = targetmessageText.slice(0, 509) + `*...(${targetmessageText.length-512} more)*`
}
text = `> *Replying to* __${resolveAuthorName(targetMessage)}__ ${targetmessageText}\n${text}`
}
}
let file = null
if (message.photo) {
let photoFile = null
try { photoFile = Bun.file(join(values.folder, message.photo)) } catch(e){}
if (!await photoFile?.exists()) {
console.log("Ignoring message", message.id, "photo", message.photo, "(not found)")
}
else {
const buffer = await photoFile.arrayBuffer()
let fileName = /*message.photo.split("@")[1] ||*/ "photo.jpg"
file = { data: buffer, mimeType: "image/jpeg", fileName: fileName }
}
}
else if (message.file) {
let messageFile = null
try { messageFile = Bun.file(join(values.folder, message.file)) } catch(e){}
if (!await messageFile?.exists()) {
console.log("Ignoring message", message.id, "file", message.file, "(not found)")
}
else {
const buffer = await messageFile.arrayBuffer()
let fileName = /*message.file_name ||*/ "attachment"
fileName += "." + mimeToExtension(message.mime_type)
file = { data: buffer, mimeType: message.mime_type, fileName: fileName }
}
}
if (file != null) {
if (!file.fileName.includes(".")) {
file.fileName += "." + file.mimeType.slice("/")[1]
}
if (!text) {
text = "⠀"
}
}
else if (!text) {
console.log("Ignoring message", message.id, "empty text and no attachment")
proceed()
continue
}
else if (text.length > 2000) {
console.log("Truncating message", message.id, "as text is too long")
text = text.slice(0, 1500)
}
const avatarUrl = avatarUrls[message.from_id]
if (message.forwarded_from) {
const forwardTxt = `Forwarded from ${message.forwarded_from}`
if (file) {
if (file.mimeType.startsWith("image/")) {
await attemptWithCooldown(sendEmbed(sender, text, forwardTxt, file, avatarUrl))
}
else {
await attemptWithCooldown(sendEmbed(sender, text, forwardTxt, null, avatarUrl))
await attemptWithCooldown(sendMessage(sender, "> ↴", file, avatarUrl))
}
}
}
else {
await attemptWithCooldown(sendMessage(sender, text, file, avatarUrl))
}
proceed()
}
}
(async function() {
let messagesFile = join(values.folder, "result.json")
console.log("Alright, loading from", messagesFile)
const resultFile = Bun.file(messagesFile)
resultObject = await resultFile.json()
startSendLoop()
})()
@Zekiah-A
Copy link
Author

Zekiah-A commented Jun 9, 2024

More info:

image

  • This script was intended to be used with bun, and it's functionality is not guarenteed with other JS runtimes.
  • This script supports basic replies, handling forwards, attachments, stickers and files. Keep in mind that some things, like system messages or complexly formatted messages are non-trivial to port to discord, and therefore have been omitted for simplicity.
  • The script is not able to automatically handle profile pictures, therefore it is recommended to set up profile pictures manually in mover-config.json. A simple way to do this, is to upload the profile images to discord separately, then reference them in the avatarUrls section of the config, with format user id : profile image url (optionally adding an entry above for their actual name for clarity). For example:
...
"avatarUrls": {
    "Rose": "",
    "user609517172":  "https://media.discordapp.net/attachments/1227920582197116929/1240237669313810462/image.png?ex=6645d4d6&is=66448356&hm=3974f9bd6f971088b8b2e2cd5489b90e27f17b2d56c0c8e2dff78698149fbc86&=&format=webp&quality=lossless&width=381&height=381",
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment