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) { const k_flag_sequence_before_author = 1; let out = { previous: message.previous ?? null, }; if (message.flags & k_flag_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 author, id, sequence, timestamp, hash, json(content) AS content, signature, flags 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 messages_refs.ref AS id FROM messages JOIN messages_refs ON messages.id = messages_refs.message WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`, [id] ); let blobs_done = 0; for (let row of blobs) { this.progress = {name: 'blobs', value: blobs_done, max: blobs.length}; let blob; try { blob = await tfrpc.rpc.get_blob(row.id); } catch (e) { console.log(`Failed to get ${row.id}: ${e.message}`); } if (blob) { 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);