generated at
Tweetの画像をGyazoにuploadするUserScript
機能
特定のprojectをcrawlし、pbs.twimg.comの画像をgyazoにuploadして置き換える
yesterday?cFQ2f7LRuLYP
機能=昨日takker
スクリプトを実行したprojectで貼り替えられますtakker
自分のとこで実行すれば、自分のとこにあるtwitterの画像がすべてgyazoられる

使い方
requirements
admin以上の権限
1. ここを押して出てきたコードをコピーする
2. gyazoに画像を移動したいprojectをブラウザで開く
3. 開発コンソールを開いて1.のコードを貼り付けて実行する
4. おわり

井戸端には900枚くらいtwitter由来の画像があった
いくつかはリンク切れしてる
とりあえずすべて自分のgyazoアカウントに取り込みます

todo
動画もアップロードする
できなくはないけど、容量を使い切ってしまいそう

2023-07-07 07:53:48 古いページから更新するようにした
2023-07-05 18:55:48 完成した。動かしてます
一時的にDate modifiedが死にますが堪忍ください
2023/07/05#64a4ea9b71fa080000697a28で話されているのを待たずに実行したのはちょっとまずかったかな
続けてuploadしすぎたせいか、CORSエラーが出てしまった
もう一回実行しよう
19:06:14 違う。pbs.twimg.comに画像でない何かが混じってる
リンク切れしている画像があるときにプログラムが止まってしまっていた
直す


元画像を取り出す
これでは取り出せなかった


refererURLにtweetのURLを紐づけたいけど、方法が無いので断念する
代わりにスクボのページへのリンクをつけておく

$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/villagepump/Tweetの画像をGyazoにuploadするUserScript/script.ts
script.ts
import { uploadAndReplace } from "./replace.ts"; import { upload } from "../../takker/scrapbox-file-uploader/mod.ts"; import { parse, Node } from "../../takker/scrapbox-parser/mod.ts"; import { Scrapbox } from "../../takker/scrapbox-jp%2Ftypes/userscript.ts"; import { getPage, exportPages, patch, Socket, makeSocket, disconnect } from "../../takker/scrapbox-userscript-std/mod.ts"; declare const scrapbox: Scrapbox; const getTargetedPages = async (project: string): Promise<string[]> => { const result = await exportPages(scrapbox.Project.name, { metadata: false }); if (!result.ok) { alert(`${result.value.name} ${result.value.message}`); throw Error(result.value.name); } return result.value.pages.flatMap( (page) => page.lines.some((line) => line.includes("pbs.twimg.com/media")) ? [page.title] : [] ); }; const main = async () => { const project = scrapbox.Project.name; // 古いページから更新する const list = (await getTargetedPages(project)).reverse(); let socket: Socket | undefined; try { for (let i = 0; i < list.length; i++) { const title = list[i]; const result = await getPage(project, title); if (!result.ok) continue; const stack: [string, URL[]][] = []; const blocks = parse(result.value.lines.map((line) => line.text).join("\n"), { hasTitle: true }); let lineNo = 0; let total = 0; for (const block of blocks) { switch (block.type) { case "title": lineNo++; break; case "codeBlock": lineNo += block.content.split("\n").length + 1; break; case "table": lineNo += block.cells.length + 1; break; case "line": { const urls = block.nodes.flatMap((node) => getURLs(node)); if (urls.length > 0) { total += urls.length; stack.push([result.value.lines[lineNo].id, urls]); } lineNo++; break; } } } if (total === 0) continue; console.debug(`[${i}/${list.length}] replace ${total} links in "/${project}/${result.value.title}"`); const pairs: [string, string][] = []; let counter = 0; for (const [hash, urls] of stack) { for (const url of urls) { const permalink = await uploadAndReplace(project, result.value.title, hash, url); if (!permalink) continue; console.debug(`[${i}/${list.length}][${counter}/${total}] ${url} => ${permalink}`); pairs.push([`${url}`, permalink]); counter++; } }

本当は外部リンク記法内のURLのみを変換すべきだが、面倒なので文字列置換してしまう
script.ts
socket ??= await makeSocket(); await patch(project, result.value.title, (lines) => lines.map((line) => { let text = line.text; for (const [b, a] of pairs) { text = text.replaceAll(b, a); } return text; }), { socket } ); console.debug(`[${i}/${list.length}] replaced links in "/${project}/${result.value.title}"`); } } finally { if (socket) disconnect(socket); } };

リンクがある行を抽出する
script.ts
const getURLs = (node: Node): URL[] => { switch (node.type) { case "decoration": case "quote": return node.nodes.flatMap((node) => getURLs(node)); case "image": { const url = new URL(node.src); if (url.hostname !== "pbs.twimg.com") return []; if (!url.pathname.startsWith("/media")) return []; return [url]; } default: return []; } }; await main();

replace.ts
import { upload } from "../../takker/deno-gyazo/mod.ts"; import { getGyazoToken } from "../../takker/scrapbox-userscript-std/rest.ts"; import { encodeTitleURI } from "../../takker/scrapbox-userscript-std/text.ts"; declare const GM_fetch: typeof fetch; let token = ""; let checked = false; export const uploadAndReplace = async (project: string, title: string, lineId: string, url: URL): Promise<string | undefined> => { if (url.hostname !== "pbs.twimg.com") return; if (!url.pathname.startsWith("/media")) return; if (!checked) { const result = await getGyazoToken(); checked = true; if (!result.ok) { alert( "You haven't logged in Gyazo yet, so you can only upload images to scrapbox.io.", ); return; } token = result.value || ""; if (!token) { alert( "You haven't connect Gyazo to scrapbox.io yet.", ); return; } } else if (!token) { return; } const res = await GM_fetch(url); if (!res.ok) return; const result = await upload(await res.blob(), { accessToken: token, // 本当は元tweetのURLにしたい refererURL: `https://scrapbox.io/${project}/${encodeTitleURI(title)}#${lineId}`, }); if (!result.ok) throw Error(result.value.name); return result.value.permalink_url; };


テスト:video URLを取得する
2023-07-17 09:36:08 時点で動画を含むページは57 pagesあった
$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/villagepump/Tweetの画像をGyazoにuploadするUserScript/list.ts
list.ts
import { exportPages } from "../../takker/scrapbox-userscript-std/rest.ts"; import { Scrapbox } from "../../takker/scrapbox-jp%2Ftypes/userscript.ts"; declare const scrapbox: Scrapbox; const result = await exportPages(scrapbox.Project.name, { metadata: false }); if (!result.ok) { alert(`${result.value.name} ${result.value.message}`); } else { const titles = result.value.pages.flatMap( (page) => page.lines.some((line) => line.includes("video.twimg.com")) ? [page.title] : [] ); const blob = new Blob([JSON.stringify(titles)], { type: "application/json" }); const url = URL.createObjectURL(blob); open(url); }