import {LitElement, html} from './lit-all.min.js'; import * as tfrpc from '/static/tfrpc.js'; class TfSneakerAppElement extends LitElement { static get properties() { return { feeds: {type: Object}, progress: {type: Object}, result: {type: String}, }; } constructor() { super(); this.feeds = []; this.progress = undefined; this.result = undefined; } async search() { let q = this.renderRoot.getElementById('search').value; let result = await tfrpc.rpc.query(` SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name FROM messages_fts(?) JOIN messages ON messages.rowid = messages_fts.rowid WHERE json_extract(messages.content, '$.type') = 'about' AND json_extract(messages.content, '$.about') = messages.author AND json_extract(messages.content, '$.name') IS NOT NULL GROUP BY messages.author HAVING MAX(messages.sequence) ORDER BY COUNT(*) DESC `, [`"${q.replaceAll('"', '""')}"`]); this.feeds = Object.fromEntries(result.map(x => [x.id, x.name])); } format_message(message) { let out = { previous: message.previous ?? null, }; if (message.sequence_before_author) { out.sequence = message.sequence; out.author = message.author; } else { out.author = message.author; out.sequence = message.sequence; } out.timestamp = message.timestamp; out.hash = message.hash; out.content = JSON.parse(message.content); out.signature = message.signature; return {key: message.id, value: out}; } sanitize(value) { return value.replaceAll('/', '_').replaceAll('+', '-'); } guess_ext(data) { function startsWith(prefix) { if (data.length < prefix.length) { return false; } for (let i = 0; i < prefix.length; i++) { if (prefix[i] !== null && data[i] !== prefix[i]) { return false; } } return true; } if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) || startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) || startsWith(data, [0xff, 0xd8, 0xff, 0xee]) || startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) { return '.jpg'; } else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) { return '.png'; } else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) || startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) { return '.gif'; } else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) { return '.webp'; } else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) { return '.svg'; } else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) { return '.mp3'; } else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) || startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) { return '.mp4'; } else { return '.bin'; } } async export(id) { let all_messages = ''; let sequence = -1; let messages_done = 0; let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total; while (true) { let messages = await tfrpc.rpc.query( 'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100', [id, sequence] ); if (messages?.length) { all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n'; sequence = messages[messages.length - 1].sequence; messages_done += messages.length; this.progress = {name: 'messages', value: messages_done, max: messages_max}; } else { break; } } let zip = new JSZip(); zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages); let blobs = await tfrpc.rpc.query( `SELECT blobs.id FROM messages JOIN messages_refs ON messages.id = messages_refs.message JOIN blobs ON messages_refs.ref = blobs.id WHERE messages.author = ?`, [id]); let blobs_done = 0; for (let row of blobs) { this.progress = {name: 'blobs', value: blobs_done, max: blobs.length}; let blob = await tfrpc.rpc.get_blob(row.id); zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob)); blobs_done++; } this.progress = {name: 'saving'}; let blob = await zip.generateAsync({type: 'blob'}); saveAs(blob, `${this.sanitize(id)}.zip`); this.progress = null; } keypress(event) { if (event.key == 'Enter') { this.search(); } } async import(event) { let file = event.target.files[0]; if (!file) { return; } this.progress = {name: 'loading'}; let zip = new JSZip(); file = await zip.loadAsync(file); let messages = []; let blobs = []; file.forEach(function(path, entry) { if (!entry.dir) { if (path.startsWith('message/classic/')) { messages.push(entry); } else { blobs.push(entry); } } }); let success = {messages: 0, blobs: 0}; let progress = 0; let total_messages = 0; for (let entry of messages) { let lines = (await entry.async('string')).split('\n'); total_messages += lines.length; for (let line of lines) { if (!line.length) { continue; } let message = JSON.parse(line); this.progress = {name: 'messages', value: progress++, max: total_messages}; if (await tfrpc.rpc.store_message(message.value)) { success.messages++; } } } progress = 0; for (let blob of blobs) { this.progress = {name: 'blobs', value: progress++, max: blobs.length}; if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) { success.blobs++; } } this.progress = undefined; this.result = `imported ${success.messages} messages and ${success.blobs} blobs`; } render() { let progress; if (this.progress) { if (this.progress.max) { progress = html`
`; } else { progress = html`
${this.progress.name}
`; } } return html`

SSB 👟net

${this.result} ${progress}

Import

Export

`; } } customElements.define('tf-sneaker-app', TfSneakerAppElement);