Skip to content

Instantly share code, notes, and snippets.

@ninjastic
Last active May 22, 2023 07:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ninjastic/36c14fb2eb1c4b750f40f332d408688f to your computer and use it in GitHub Desktop.
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.
(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