import { JSZip } from "https://deno.land/x/jszip@0.11.0/mod.ts"; export async function fetch(url, options = {}, nothrow) { console.log("fetch", url); options.headers ||= {}; options.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"; for (var i = 3; i; i--) { try { var res = await globalThis.fetch(url, options); break; } catch (error) { console.error(error.stack); if (i <= 1) throw error; } console.log("retry"); } if (!nothrow && !res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); return res; } export var credentials = JSON.parse(Deno.readTextFileSync("credentials.json")); var {client_id, client_secret, username, password, pixiv_cookie, access_token, pleroma_user_id} = credentials; export var known_ids = {}; try { known_ids = Object.fromEntries(Deno.readTextFileSync("known_ids.csv").trim().split("\n").map(line => line.split(","))); } catch (e) {} if (!access_token) { let form = new FormData(); form.append("client_id", client_id); form.append("client_secret", client_secret); form.append("grant_type", "password"); form.append("username", username); form.append("password", password); let data = await fetch("https://pleroma.lamp.wtf/oauth/token", { method: "POST", body: form }).then(res => res.json()); console.log ("logged in", data); credentials.access_token = access_token = data.access_token; Deno.writeTextFileSync("credentials.json", JSON.stringify(credentials, null, '\t')); } export async function getAllStatuses() { var allStatuses = []; await (async function getPage(max_id) { var url = `https://pleroma.lamp.wtf/api/v1/accounts/${pleroma_user_id}/statuses?limit=40`; if (max_id) url += `&max_id=${max_id}`; var statuses = await fetch(url, {headers: {"Authorization": `Bearer ${access_token}`}}).then(res => res.json()); if (!statuses.length) return; allStatuses.push(...statuses); var last_id = statuses.at(-1).id; await getPage(last_id); })(); return allStatuses; } async function uploadFile({data, name}) { var form = new FormData(); form.append("file", data, name); var res = await fetch("https://pleroma.lamp.wtf/api/v1/media", { method: "POST", body: form, headers: {"Authorization": `Bearer ${access_token}`} }); var json = await res.json(); console.log("uploaded file", res.status, json.url); return json; } async function postStatus({status, visibility = "unlisted", content_type = "text/plain", media_ids = [], sensitive, files, edit}) { if (files) { media_ids = (await Promise.all(files.map(file => uploadFile(file)))).map(d => d.id); } var form = new FormData(); form.append("status", status); form.append("visibility", visibility); form.append("source", "bot"); form.append("content_type", content_type); for (let media_id of media_ids) { form.append("media_ids[]", media_id); } if (sensitive) form.append("sensitive", "true"); var res = await fetch("https://pleroma.lamp.wtf/api/v1/statuses" + (edit ? `/${edit}` : ''), { method: edit ? "PUT" : "POST", body: form, headers: {"Authorization": `Bearer ${access_token}`} }); var json = await res.json(); console.log(edit ? "edited" : "posted", res.status, json.uri || json); return json; } async function downloadPixivIllust(illust_id) { var url = `https://www.pixiv.net/ajax/illust/${illust_id}?lang=en`; var res = await fetch(url, {headers: {"Cookie": pixiv_cookie}}, true); if (!res.ok) { console.error(res.status); var res = await fetch(url); } var json = await res.json(); if (json.error) throw new Error(json.message); var illust = json.body; if (illust.illustType == 2) { //ugoira let res = await fetch(`https://www.pixiv.net/ajax/illust/${illust_id}/ugoira_meta`, {headers: {"Cookie": pixiv_cookie}}); let json = await res.json(); if (json.error) throw new Error(json.message); let zip = await fetch(json.body.originalSrc, {headers: {"Referer": "https://www.pixiv.net/"}}).then(res => res.arrayBuffer()); let {data, width, height} = await ugoira2webp(zip, json.body.frames); return {illust, images: [{ name: json.body.originalSrc.split('/').pop() + '.webp', data: new Blob([data], {type: "image/webp"}), type: "image/webp", width, height }]}; } try { let res = await fetch(`https://www.pixiv.net/ajax/illust/${illust_id}/pages`, {headers: {"Cookie": pixiv_cookie}}); let json = await res.json(); var images = json.body.map(x => ({ url: x.urls.original, width: x.width, height: x.height })); } catch (error) { console.error(error.stack); if (!illust.urls.original) { console.error("missing original urls", illust.urls); throw error; } var images = []; for (let i = 0; i < illust.pageCount; i++) { images.push({ url: illust.urls.original.replace('p0', 'p'+i) }); } } for (let image of images) { image.name = image.url.split('/').pop(); image.data = await fetch(image.url, {headers: {"Referer": "https://www.pixiv.net/"}}).then(res => res.blob()); image.type = "image/" + image.url.split('.').pop(); } return {illust, images}; } var decoder = new TextDecoder(); async function ugoira2webp(zip, frames) { console.log("convert ugoira"); var tmpdir = Deno.makeTempDirSync(); var z = new JSZip(); await z.loadAsync(zip); var webpmux_args = []; var width, height; for (let {file, delay} of frames) { let file_data = await z.file(file).async("uint8array"); Deno.writeFileSync(tmpdir + "/" + file, file_data); let output = new Deno.Command("cwebp", { args: [file, "-o", `${file}.webp`], cwd: tmpdir }).outputSync(); let text = decoder.decode(output.stderr); console.debug(text); let match = text.match(/Dimension: (\d+) x (\d+)/); if (match) { width ||= Number(match[1]); height ||= Number(match[2]); } else console.warn("missing dimension match"); webpmux_args.push("-frame", `${file}.webp`, `+${delay}`); } new Deno.Command("webpmux", { args: [...webpmux_args, "-o", "output.webp"], cwd: tmpdir }).outputSync(); return { data: Deno.readFileSync(tmpdir + "/output.webp"), width, height } } export async function pixivToPleroma(illust_id, status_id) { try { var {illust, images} = await downloadPixivIllust(illust_id); var {url} = await postStatus({ status: `https://www.pixiv.net/en/artworks/${illust_id}` + ((images.length > 4) ? `\n⚠ There are ${images.length} attachments` : ''), files: images, visibility: "unlisted", sensitive: Boolean(illust.xRestrict), edit: status_id }); Deno.writeTextFileSync("known_ids.csv", `${illust_id},${url}\n`, {append: true}); var {postPixivIllustToMatrix} = await import("./matrix.js"); //circular dependency -.- await postPixivIllustToMatrix(illust, images, url); } catch (error) { console.error(error.stack); postStatus({ status: `https://www.pixiv.net/en/artworks/${illust_id}\n#error:\n${error.stack}`, edit: status_id }); } }