4chan-XZ/src/Archive/Redirect.ts
2023-05-05 14:42:32 +02:00

247 lines
7.3 KiB
TypeScript

import Notice from '../classes/Notice'
import { Conf } from '../globals/globals'
import $ from '../platform/$'
import CrossOrigin from '../platform/CrossOrigin'
import { DAY, dict } from '../platform/helpers'
import archives from './archives.json'
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Redirect = {
archives,
init() {
this.selectArchives()
if (Conf['archiveAutoUpdate']) {
const now = Date.now()
if (now - (2 * DAY) >= Conf['lastarchivecheck'] || Conf['lastarchivecheck'] > now) { return this.update() }
}
},
selectArchives() {
let boardID, boards, data, files
const o = {
thread: dict(),
post: dict(),
file: dict()
}
const archives = dict()
for (data of Conf['archives']) {
let name, software, uid
for (const key of ['boards', 'files']) {
if (!(data[key] instanceof Array)) { data[key] = [] }
}
({ uid, name, boards, files, software } = data)
if (!['fuuka', 'foolfuuka'].includes(software)) { continue }
archives[JSON.stringify(uid ?? name)] = data
for (boardID of boards) {
if (!(boardID in o.thread)) { o.thread[boardID] = data }
if (!(boardID in o.post) && (software === 'foolfuuka')) { o.post[boardID] = data }
if (!(boardID in o.file) && files.includes(boardID)) { o.file[boardID] = data }
}
}
for (boardID in Conf['selectedArchives']) {
const record = Conf['selectedArchives'][boardID]
for (const type in record) {
let archive
const id = record[type]
if ((archive = archives[JSON.stringify(id)]) && $.hasOwn(o, type)) {
boards = type === 'file' ? archive.files : archive.boards
if (boards.includes(boardID)) { o[type][boardID] = archive }
}
}
}
return Redirect.data = o
},
update(cb) {
let url
const urls = []
const responses = []
let nloaded = 0
for (url of Conf['archiveLists'].split('\n')) {
if (url[0] !== '#') {
url = url.trim()
if (url) { urls.push(url) }
}
}
const fail = (url, action, msg) => new Notice('warning', `Error ${action} archive data from\n${url}\n${msg}`, 20)
const load = i => (function () {
if (this.status !== 200) { return fail(urls[i], 'fetching', (this.status ? `Error ${this.statusText} (${this.status})` : 'Connection Error')) }
let { response } = this
if (!(response instanceof Array)) { response = [response] }
responses[i] = response
nloaded++
if (nloaded === urls.length) {
return Redirect.parse(responses, cb)
}
})
if (urls.length) {
for (let i = 0; i < urls.length; i++) {
url = urls[i]
if (['[', '{'].includes(url[0])) {
let response
try {
response = JSON.parse(url)
} catch (err) {
fail(url, 'parsing', err.message)
continue
}
load(i).call({ status: 200, response })
} else {
CrossOrigin.ajax(url,
{ onloadend: load(i) })
}
}
} else {
Redirect.parse([], cb)
}
},
parse(responses, cb) {
const archives = []
const archiveUIDs = dict()
for (const response of responses) {
for (const data of response) {
const uid = JSON.stringify(data.uid ?? data.name)
if (uid in archiveUIDs) {
$.extend(archiveUIDs[uid], data)
} else {
archiveUIDs[uid] = dict.clone(data)
archives.push(data)
}
}
}
const items = { archives, lastarchivecheck: Date.now() }
$.set(items)
$.extend(Conf, items)
Redirect.selectArchives()
return cb
},
to(dest, data) {
const archive = (['search', 'board'].includes(dest) ? Redirect.data.thread : Redirect.data[dest])[data.boardID]
if (!archive) { return '' }
return Redirect[dest](archive, data)
},
protocol(archive) {
let {
protocol
} = location
if (!$.getOwn(archive, protocol.slice(0, -1))) {
protocol = protocol === 'https:' ? 'http:' : 'https:'
}
return `${protocol}//`
},
thread(archive, { boardID, threadID, postID }) {
// Keep the post number only if the location.hash was sent f.e.
let path = threadID ?
`${boardID}/thread/${threadID}`
:
`${boardID}/post/${postID}`
if (archive.software === 'foolfuuka') {
path += '/'
}
if (threadID && postID) {
path += archive.software === 'foolfuuka' ?
`#${postID}`
:
`#p${postID}`
}
return `${Redirect.protocol(archive)}${archive.domain}/${path}`
},
post(archive, { boardID, postID }) {
// For fuuka-based archives:
// https://github.com/eksopl/fuuka/issues/27
const protocol = Redirect.protocol(archive)
const url = `${protocol}${archive.domain}/_/api/chan/post/?board=${boardID}&num=${postID}`
if (!Redirect.securityCheck(url)) { return '' }
return url
},
file(archive, { boardID, filename }) {
if (!filename) { return '' }
if (boardID === 'f') {
filename = encodeURIComponent($.unescape(decodeURIComponent(filename)))
} else {
if (/[sm]\.jpg$/.test(filename)) { return '' }
}
return `${Redirect.protocol(archive)}${archive.domain}/${boardID}/full_image/${filename}`
},
board(archive, { boardID }) {
return `${Redirect.protocol(archive)}${archive.domain}/${boardID}/`
},
search(archive, { boardID, type, value }) {
type = type === 'name' ?
'username'
: type === 'MD5' ?
'image'
:
type
if (type === 'capcode') {
// https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/src/Model/Search.php#L363
value = $.getOwn({
'Developer': 'dev',
'Verified': 'ver'
}, value) || value.toLowerCase()
} else if (type === 'image') {
value = value.replace(/[+/=]/g, c => ({ '+': '-', '/': '_', '=': '' })[c])
}
value = encodeURIComponent(value)
const path = archive.software === 'foolfuuka' ?
`${boardID}/search/${type}/${value}/`
: type === 'image' ?
`${boardID}/image/${value}`
:
`${boardID}/?task=search2&search_${type}=${value}`
return `${Redirect.protocol(archive)}${archive.domain}/${path}`
},
report(boardID) {
const urls = []
for (const archive of Conf['archives']) {
const { software, https, reports, boards, name, domain } = archive
if ((software === 'foolfuuka') && https && reports && boards instanceof Array && boards.includes(boardID)) {
urls.push([name, `https://${domain}/_/api/chan/offsite_report/`])
}
}
return urls
},
securityCheck(url) {
return /^https:\/\//.test(url) ||
(location.protocol === 'http:') ||
Conf['Exempt Archives from Encryption']
},
navigate(dest, data, alternative) {
if (!Redirect.data) { Redirect.init() }
const url = Redirect.to(dest, data)
if (url && (
Redirect.securityCheck(url) ||
confirm(`Redirect to ${url}?\n\nYour connection will not be encrypted.`)
)) {
return location.replace(url)
} else if (alternative) {
return location.replace(alternative)
}
}
}
export default Redirect