Last active
May 22, 2023 07:37
-
-
Save ninjastic/36c14fb2eb1c4b750f40f332d408688f to your computer and use it in GitHub Desktop.
Goes through all your posts looking for imgur.com images (.png|.jpg|.jpeg), reuploads them and edits your posts with a new link.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(async () => { | |
// v1.6 | |
// options | |
const startPage = 1 | |
const provider = 'talkimg' | |
const providers = { | |
talkimg: { | |
url: 'https://proxy.ninjastic.space/?url=https://talkimg.com/api/1/upload', | |
apiKey: 'chv_AiD_124562a509c5fadffba3e15a3a31f8241855c36609c497a325396124b370b138a1d5ecda8061410b4a3478bdf26b51c5589e23d7e277a15dedda70577ca79995', | |
albumId: '', | |
uploadsPerMinute: 20, | |
deleteOnError: true, | |
}, | |
imgbb: { | |
url: 'https://api.imgbb.com/1/upload', | |
apiKey: '', | |
uploadsPerMinute: 10, | |
deleteOnError: false, | |
} | |
} | |
const basePostHistoryUrl = 'https://bitcointalk.org/index.php?action=profile;sa=showPosts' | |
const imageLinkRegex = /https:\/\/i\.imgur\.com\/.*?\.(png|jpg|jpeg|gif)/gi | |
const decoder = new TextDecoder('windows-1252') | |
const parser = new DOMParser() | |
let lastReq | |
if (providers[provider] === undefined) { | |
console.log(`%cERROR: Provider ${provider} does not exists.`, 'color: red; font-weight: bold;') | |
return | |
} | |
if (provider === 'imgbb' && !providers.imgbb.apiKey) { | |
console.log('%cERROR: Missing imgbb API key. Please edit the code and input yours (providers -> imgbb -> apiKey). More info: https://api.imgbb.com', 'color: red; font-weight: bold;') | |
return | |
} | |
let emojiRegex = await fetch('https://proxy.ninjastic.space/?url=https://raw.githubusercontent.com/mathiasbynens/emoji-test-regex-pattern/main/dist/latest/javascript.txt').then(response => response.status === 200 ? response.text() : null) | |
if (!emojiRegex || !'⭐'.match(emojiRegex)) { | |
console.log('%cERROR: Could not fetch Emoji regex', 'color: red; font-weight: bold;') | |
return | |
} | |
const encodeStr = (rawStr) => { | |
return rawStr.replace(new RegExp(`${emojiRegex}|[\u00A0-\u9999<>&]`, 'g'), (i) => `&#${i.codePointAt(0)};`) | |
} | |
const fetchThrottled = async (url, ...rest) => { | |
const timeRemaining = lastReq ? lastReq.getTime() + 1000 * 1 - new Date().getTime() : 0 | |
if (timeRemaining > 0) { | |
await new Promise(resolve => setTimeout(resolve, timeRemaining)) | |
} | |
lastReq = new Date() | |
return await fetch(url, ...rest) | |
} | |
const uploadImage = { | |
talkimg: async (image, description) => { | |
const formData = new FormData() | |
formData.append('type', 'file') | |
formData.append('format', 'json') | |
formData.append('description', String(description)) | |
if (providers.talkimg.albumId) { | |
formData.append('album_id', providers.talkimg.albumId) | |
} | |
formData.append('source', image) | |
const upload = await fetchThrottled(providers.talkimg.url, { | |
method: 'POST', | |
headers: { 'X-API-Key': providers.talkimg.apiKey }, | |
mode: 'cors', | |
body: formData, | |
}) | |
const response = await upload.json() | |
if (response.status_code === 200) { | |
return { url: response.image.url, deleteUrl: response.image.delete_url } | |
} | |
console.log('Could not upload, error:', response?.error?.message ?? response) | |
return undefined | |
}, | |
imgbb: async (image, name) => { | |
const formData = new FormData() | |
formData.append('image', image) | |
formData.append('name', String(name)) | |
const upload = await fetchThrottled(`${providers.imgbb.url}?key=${providers.imgbb.apiKey}`, { | |
method: 'POST', | |
mode: 'cors', | |
body: formData, | |
}) | |
const response = await upload.json() | |
if (response.status === 200) { | |
return { url: response.data.url, deleteUrl: response.data.delete_url } | |
} | |
console.log('Could not upload, error:', response?.error?.message ?? response) | |
return undefined | |
} | |
} | |
const decodeProxyImages = (html) => html.replaceAll(/img.*?src="(.*?)"\s/g, (text, imgUrl) => { | |
const directImgUrl = imgUrl | |
.replace(/https:\/\/ip\.bitcointalk\.org\/\?u=/, '') | |
.replace(/&.*/, '') | |
const decodedUrl = decodeURIComponent(directImgUrl) | |
return text.replace(imgUrl, decodedUrl) | |
}) | |
const checkTopicLocked = async (topicId) => { | |
const url = `https://bitcointalk.org/index.php?action=post;topic=${topicId}` | |
const html = await fetchThrottled(url).then(async response => decoder.decode(await response.arrayBuffer())) | |
const $ = parser.parseFromString(html, 'text/html') | |
if ($.title === 'An Error Has Occurred!') { | |
return $.querySelector('#bodyarea > div:nth-child(1) > table > tbody > tr.windowbg > td')?.textContent.trim() | |
} | |
return undefined | |
} | |
const getSesc = async () => { | |
const html = await fetchThrottled('https://bitcointalk.org/more.php').then(async response => decoder.decode(await response.arrayBuffer())) | |
return html.match(/https\:\/\/bitcointalk\.org\/index\.php\?action=logout;sesc=(.*?)"\>/)?.at(1) | |
} | |
const getQuote = async ({ topicId, postId, sesc }) => { | |
const url = `https://bitcointalk.org/index.php?action=quotefast;xml;quote=${postId};topic=${topicId};sesc=${sesc}` | |
const html = await fetchThrottled(url).then(async response => decoder.decode(await response.arrayBuffer())) | |
const $ = parser.parseFromString(html, 'text/html') | |
const quote = $.querySelector('quote').textContent | |
return quote.replace(/^\[quote.*?\]\n?/, '').replace(/\[\/quote\]$/, '') | |
} | |
const editPost = async ({ topicId, postId, title, message, sesc }) => { | |
const formData = new FormData() | |
formData.append('topic', String(topicId)) | |
formData.append('subject', encodeStr(title)) | |
formData.append('message', encodeStr(message)) | |
formData.append('sc', sesc) | |
formData.append('goback', String(1)) | |
const { redirected } = await fetchThrottled(`https://bitcointalk.org/index.php?action=post2;msg=${postId}`, { method: 'POST', body: formData }) | |
return redirected | |
} | |
const getPosts = async (page) => { | |
const url = `${basePostHistoryUrl};start=${((page ?? 1) - 1) * 20}` | |
const html = await fetchThrottled(url).then(async response => decoder.decode(await response.arrayBuffer())) | |
const decoded = decodeProxyImages(html) | |
const $ = parser.parseFromString(decoded, 'text/html') | |
const postElements = [...$.querySelectorAll('table[width="85%"] table[width="100%"] tbody')] | |
.filter(element => element.querySelector('.post')) | |
const posts = [] | |
for (const postElement of postElements) { | |
const titleElement = postElement.querySelector('tr[class=titlebg2] td:nth-child(2) a:last-child') | |
const title = titleElement.textContent.trim() | |
const [, topicId, postId] = titleElement.getAttribute('href').match(/topic=(\d+)\.msg(\d+)/) | |
const contentElement = postElement.querySelector('.post') | |
const links = [...new Set(contentElement.innerHTML.match(imageLinkRegex))] ?? [] | |
posts.push({ topicId, postId, title, links }) | |
} | |
return posts | |
} | |
const html = await fetchThrottled(basePostHistoryUrl).then(async response => response.text()) | |
const $ = parser.parseFromString(html, 'text/html') | |
const pages = [...$.querySelectorAll('.navPages[href*="sa=showPosts"]')] | |
.filter(element => !['«', '»'].includes(element.textContent)) | |
.reduce((elements, currentElement) => { | |
if(!elements.find(element => element.href === currentElement.getAttribute('href'))) { | |
elements.push({ element: currentElement, page: Number(currentElement.textContent), href: currentElement.getAttribute('href') }) | |
return elements | |
} | |
return elements | |
}, []) | |
const lastPageNum = pages[pages.length - 1].page ?? 1 | |
console.log('%cImgur to TalkImg - automatically fix your broken images', 'color: #fff; font-weight: bold; background-color: blue;') | |
console.log('Provider:', provider) | |
console.log('Number of Pages:', lastPageNum) | |
let numberUploads = 0 | |
if (startPage > lastPageNum) { | |
console.log('%cERROR: startPage is greater than your number of pages, please check.', 'color: red; font-weight: bold;') | |
return | |
} | |
for await (const page of Array.from({ length: lastPageNum - startPage + 1 }).map((_, i) => startPage + i)) { | |
console.log(`--------------------\nGetting posts on page ${page}/${lastPageNum} (${Math.floor(page / lastPageNum * 100)}%)`) | |
const posts = await getPosts(page).then(posts => posts.filter(post => post.links.length > 0)) | |
if (posts.length > 0) { | |
console.log(`Found ${posts.length} posts and ${posts.flatMap(post => post.links).length} images`, posts) | |
} | |
for await (const post of posts) { | |
const topicError = await checkTopicLocked(post.topicId) | |
if (topicError) { | |
console.log(`[${post.postId}] Skipping post because topic returned error:`, topicError) | |
continue | |
} | |
const images = [] | |
for await (const link of post.links) { | |
const blob = await fetchThrottled(link).then(async response => response.blob()) | |
images.push({ link, blob }) | |
} | |
const uploadedImages = [] | |
for await (const image of images) { | |
if (numberUploads >= providers[provider].uploadsPerMinute) { | |
numberUploads = 0 | |
console.log('Upload API limited, waiting 1 minute...') | |
await new Promise(resolve => setTimeout(resolve, 1000 * 60)) | |
} | |
console.log(`[${post.postId}] Uploading image...`) | |
const uploaded = await uploadImage[provider](image.blob, post.postId) | |
if (uploaded?.url) { | |
numberUploads += 1 | |
const oldLink = post.links.find(link => link === image.link) | |
uploadedImages.push({ old: oldLink, new: uploaded.url, deleteUrl: uploaded.deleteUrl }) | |
console.log(`[${post.postId}] Uploaded:`, uploaded.url) | |
} | |
} | |
if (uploadedImages.length > 0) { | |
const sesc = await getSesc() | |
const currPost = await getQuote({ topicId: post.topicId, postId: post.postId, sesc }) | |
let newContent = currPost | |
for (const uploadedImage of uploadedImages) { | |
newContent = newContent.replaceAll(uploadedImage.old, uploadedImage.new) | |
} | |
console.log(`[${post.postId}] Editing post https://bitcointalk.org/index.php?topic=${post.topicId}.msg${post.postId}#msg${post.postId}`) | |
const edited = await editPost({ topicId: post.topicId, postId: post.postId, title: post.title, message: newContent, sesc }) | |
if (!edited && !providers[provider].deleteOnError) { | |
console.log(`[${post.postId}] Could not edit post (maybe locked?)...`) | |
} | |
if (!edited && providers[provider].deleteOnError) { | |
console.log(`[${post.postId}] Could not edit post (maybe locked?), deleting uploaded images...`) | |
for (const uploadedImage of uploadedImages) { | |
await fetchThrottled(uploadedImage.deleteUrl, { redirect: 'manual' }) | |
} | |
} | |
} else { | |
console.log(`[${post.postId}] No images were uploaded, skiping edit...`) | |
} | |
} | |
} | |
console.log('-- Finished! --') | |
console.log('Make sure you donate to joker_josue for taking the time (and money) to create TalkImg!') | |
console.log('bc1qhwnncpdd8gfzqwjkk9n052wf7g9mvks3xaa7qa') | |
console.log('Verify on his website footer:', 'https://www.talkimg.com') | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment