forked from cory/tildefriends
Sneakernet, here we come.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4292 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
226
apps/sneaker/script.js
Normal file
226
apps/sneaker/script.js
Normal file
@ -0,0 +1,226 @@
|
||||
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`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></div>`;
|
||||
} else {
|
||||
progress = html`<div><span>${this.progress.name}</span></div>`;
|
||||
}
|
||||
}
|
||||
return html`<h1>SSB 👟net</h1>
|
||||
<code>${this.result}</code>
|
||||
${progress}
|
||||
|
||||
<h2>Import</h2>
|
||||
<input type="file" id="import" @change=${this.import}></input>
|
||||
|
||||
<h2>Export</h2>
|
||||
<input type="text" id="search" @keypress=${this.keypress}></input>
|
||||
<input type="button" value="Search Users" @click=${this.search}></input>
|
||||
<ul>
|
||||
${Object.entries(this.feeds).map(([id, name]) => html`
|
||||
<li>
|
||||
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
||||
${name}
|
||||
<code style="color: #ccc">${id}</code>
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('tf-sneaker-app', TfSneakerAppElement);
|
Reference in New Issue
Block a user