diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 1f7ef42f..130ad1e2 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -3,8 +3,3 @@ useTabs: true semi: true singleQuote: true bracketSpacing: false -# overrides: -# - files: '**/*.json' -# options: -# useTabs: false -# tabWidth: 2 diff --git a/README.md b/README.md index b7c474a2..2ba21b85 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Tilde Friends + Tilde Friends is a tool for making and sharing. A public instance lives at https://www.tildefriends.net/. @@ -7,37 +8,42 @@ It is both a peer-to-peer social network client, participating in Secure Scuttlebutt, as well as a platform for writing and running web applications. ## Goals + 1. Make it easy and fun to run all sorts of web applications. 2. Provide security that is easy to understand and protects your data. 3. Make creating and sharing web applications accessible to anyone with a browser. ## Building -Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for + +Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for all of those host platforms plus mingw64, iOS, and android. -1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies +1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies are kept up to date in the tree. -2. To build, run `make debug` or `make release`. An executable will be +2. To build, run `make debug` or `make release`. An executable will be generated in a subdirectory of `out/`. 3. It's possible to build for Android, iOS, and Windows on Linux, if you have - the right dependencies in the right places. `make windebug winrelease - iosdebug-ipa iosrelease-ipa release-apk`. + the right dependencies in the right places. `make windebug winrelease +iosdebug-ipa iosrelease-ipa release-apk`. 4. To build in docker, `docker build .`. 5. `make format` will normalize formatting to the coding standard. ## Running + By default, running the built `tildefriends` executable will start a web server -at . `tildefriends -h` lists further options. +at . `tildefriends -h` lists further options. The first user to create an account and log in will be granted administrative -privileges. Further administration can be done at +privileges. Further administration can be done at . ## Documentation + Docs are a work in progress: . ## License + All code unless otherwise noted in is provided under the [MIT](https://opensource.org/licenses/MIT) license. diff --git a/apps/admin.json b/apps/admin.json index cfec74e1..a9a2e723 100644 --- a/apps/admin.json +++ b/apps/admin.json @@ -1,4 +1,4 @@ { "type": "tildefriends-app", "emoji": "🎛" -} \ No newline at end of file +} diff --git a/apps/admin/app.js b/apps/admin/app.js index 0a1c35e8..5916f11f 100644 --- a/apps/admin/app.js +++ b/apps/admin/app.js @@ -18,9 +18,13 @@ async function main() { for (let user of await core.users()) { data.users[user] = await core.permissionsForUser(user); } - await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data))); + await app.setDocument( + utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)) + ); } catch { - await app.setDocument('Only an administrator can modify these settings.'); + await app.setDocument( + 'Only an administrator can modify these settings.' + ); } } -main(); \ No newline at end of file +main(); diff --git a/apps/admin/index.html b/apps/admin/index.html index eabbf708..114ab1b9 100644 --- a/apps/admin/index.html +++ b/apps/admin/index.html @@ -1,10 +1,12 @@ - + - +

Tilde Friends Administration

- \ No newline at end of file + diff --git a/apps/admin/script.js b/apps/admin/script.js index 6d43d291..8fabc5a7 100644 --- a/apps/admin/script.js +++ b/apps/admin/script.js @@ -3,25 +3,32 @@ import * as tfrpc from '/static/tfrpc.js'; function delete_user(user) { if (confirm(`Are you sure you want to delete the user "${user}"?`)) { - tfrpc.rpc.delete_user(user).then(function() { - alert(`User "${user}" deleted successfully.`); - }).catch(function(error) { - alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`); - }); + tfrpc.rpc + .delete_user(user) + .then(function () { + alert(`User "${user}" deleted successfully.`); + }) + .catch(function (error) { + alert( + `Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.` + ); + }); } } function global_settings_set(key, value) { - tfrpc.rpc.global_settings_set(key, value).then(function() { - alert(`Set "${key}" to "${value}".`); - }).catch(function(error) { - alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`); - }); + tfrpc.rpc + .global_settings_set(key, value) + .then(function () { + alert(`Set "${key}" to "${value}".`); + }) + .catch(function (error) { + alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`); + }); } -window.addEventListener('load', function() { - const permission_template = (permission) => - html` ${permission}`; +window.addEventListener('load', function () { + const permission_template = (permission) => html` ${permission}`; function input_template(key, description) { if (description.type === 'boolean') { return html` @@ -62,26 +69,24 @@ window.addEventListener('load', function() { } const user_template = (user, permissions) => html`
  • - - ${user}: - ${permissions.map(x => permission_template(x))} + + ${user}: ${permissions.map((x) => permission_template(x))}
  • `; const users_template = (users) => html`

    Users

    -
      - ${Object.entries(users).map(u => user_template(u[0], u[1]))} -
    `; +
      + ${Object.entries(users).map((u) => user_template(u[0], u[1]))} +
    `; const page_template = (data) => html`

    Global Settings

    - ${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)} + ${Object.keys(data.settings) + .sort() + .map((x) => html`${input_template(x, data.settings[x])}`)}
    ${users_template(data.users)} -
    - `; + `; render(page_template(g_data), document.body); -}); \ No newline at end of file +}); diff --git a/apps/api.json b/apps/api.json index 130dcc43..511730eb 100644 --- a/apps/api.json +++ b/apps/api.json @@ -2,4 +2,4 @@ "type": "tildefriends-app", "emoji": "📜", "previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256" -} \ No newline at end of file +} diff --git a/apps/api/docs.js b/apps/api/docs.js index 9e045bbb..037bf240 100644 --- a/apps/api/docs.js +++ b/apps/api/docs.js @@ -219,7 +219,7 @@ Parses an HTTP response. * *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse. `; -docs['sha1Digest()'] =` +docs['sha1Digest()'] = ` Calculates a SHA1 digest. Completes synchronously. @@ -353,4 +353,4 @@ Call a remote function. * **...** Parameters to pass to the function. ### Returns The return value of the called function. -`; \ No newline at end of file +`; diff --git a/apps/apps.json b/apps/apps.json index 8b67c693..03545742 100644 --- a/apps/apps.json +++ b/apps/apps.json @@ -2,4 +2,4 @@ "type": "tildefriends-app", "emoji": "💻", "previous": "&RdVEsVscZm3aWzcMrEZS8mskO5tUmvaEUihex2MMfZQ=.sha256" -} \ No newline at end of file +} diff --git a/apps/apps/app.js b/apps/apps/app.js index d31f1ab7..2cdf9faa 100644 --- a/apps/apps/app.js +++ b/apps/apps/app.js @@ -26,14 +26,15 @@ async function fetch_info(apps) { async function fetch_shared_apps() { let messages = {}; - await ssb.sqlAsync(` + await ssb.sqlAsync( + ` SELECT messages.* FROM messages_fts('"application/tildefriends"') JOIN messages ON messages.rowid = messages_fts.rowid ORDER BY timestamp `, [], - function(row) { + function (row) { let content = JSON.parse(row.content); for (let mention of content.mentions) { if (mention?.type === 'application/tildefriends') { @@ -44,10 +45,13 @@ async function fetch_shared_apps() { }; } } - }); + } + ); let result = {}; - for (let app of Object.values(messages).sort((x, y) => y.message.timestamp - x.message.timestamp)) { + for (let app of Object.values(messages).sort( + (x, y) => y.message.timestamp - x.message.timestamp + )) { let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob))); if (app_object) { app_object.blob_id = app.blob; diff --git a/apps/blog.json b/apps/blog.json index 782fa7bf..d6a298e8 100644 --- a/apps/blog.json +++ b/apps/blog.json @@ -1,5 +1,5 @@ { - "type": "tildefriends-app", - "emoji": "🪵", - "previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256" -} \ No newline at end of file + "type": "tildefriends-app", + "emoji": "🪵", + "previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256" +} diff --git a/apps/blog/app.js b/apps/blog/app.js index 99c0165d..66a800db 100644 --- a/apps/blog/app.js +++ b/apps/blog/app.js @@ -5,4 +5,4 @@ async function main() { await app.setDocument(blog.render_html(blogs)); } -main(); \ No newline at end of file +main(); diff --git a/apps/blog/blog.js b/apps/blog/blog.js index 4a3e0866..d2ba4e01 100644 --- a/apps/blog/blog.js +++ b/apps/blog/blog.js @@ -1,11 +1,19 @@ import * as commonmark from './commonmark.min.js'; function escape(text) { - return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); + return (text ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>'); } function escapeAttribute(text) { - return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); + return (text ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); } export async function get_blog_message(id) { @@ -13,7 +21,7 @@ export async function get_blog_message(id) { await ssb.sqlAsync( 'SELECT author, timestamp, content FROM messages WHERE id = ?', [id], - function(row) { + function (row) { let content = JSON.parse(row.content); message = { author: row.author, @@ -21,7 +29,8 @@ export async function get_blog_message(id) { blog: content?.blog, title: content?.title, }; - }); + } + ); if (message) { await ssb.sqlAsync( ` @@ -34,9 +43,10 @@ export async function get_blog_message(id) { ORDER BY sequence DESC LIMIT 1 `, [message.author], - function(row) { + function (row) { message.name = row.name; - }); + } + ); } return message; } @@ -51,8 +61,12 @@ export function markdown(md) { node = event.node; if (event.entering) { if (node.destination?.startsWith('&')) { - node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal; - } else if (node.destination?.startsWith('@') || node.destination?.startsWith('%')) { + node.destination = + '/' + node.destination + '/view?filename=' + node.firstChild?.literal; + } else if ( + node.destination?.startsWith('@') || + node.destination?.startsWith('%') + ) { node.destination = '/~core/ssb/#' + escape(node.destination); } } @@ -107,7 +121,7 @@ export function render_html(blogs) {

    🪵Tilde Friends Blog

    atom feed
    - ${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')} + ${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')} `; } @@ -135,14 +149,15 @@ export function render_atom(blogs) { ${core.url} ${new Date().toString()} - ${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')} + ${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')} `; } export async function get_posts() { let blogs = []; let ids = await ssb.getIdentities(); - await ssb.sqlAsync(` + await ssb.sqlAsync( + ` WITH blogs AS ( SELECT @@ -182,8 +197,11 @@ export async function get_posts() { JOIN public ON public.author = blogs.author LEFT OUTER JOIN names ON names.author = blogs.author ORDER BY blogs.timestamp DESC LIMIT 20 - `, [JSON.stringify(ids)], function(row) { - blogs.push(row); - }); + `, + [JSON.stringify(ids)], + function (row) { + blogs.push(row); + } + ); return blogs; -} \ No newline at end of file +} diff --git a/apps/blog/handler.js b/apps/blog/handler.js index ef14b62b..32b54bef 100644 --- a/apps/blog/handler.js +++ b/apps/blog/handler.js @@ -2,30 +2,50 @@ import * as blog from './blog.js'; async function main() { if (request.path.startsWith('%') && request.path.endsWith('.sha256')) { - let id = request.path.startsWith('%25') ? '%' + request.path.substring(3) : request.path; + let id = request.path.startsWith('%25') + ? '%' + request.path.substring(3) + : request.path; let message = await blog.get_blog_message(id); if (message) { - respond({data: await blog.render_blog_post_html(message), content_type: 'text/html; charset=utf-8'}); + respond({ + data: await blog.render_blog_post_html(message), + content_type: 'text/html; charset=utf-8', + }); } else { - respond({data: `Message ${id} not found.`, content_type: 'text/html; charset=utf-8'}); + respond({ + data: `Message ${id} not found.`, + content_type: 'text/html; charset=utf-8', + }); } } else if (request.path == 'atom') { let blogs = await blog.get_posts(); - respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'}); + respond({ + data: blog.render_atom(blogs), + content_type: 'application/atom+xml', + }); } else { let blogs = await blog.get_posts(); for (let blog_post of blogs) { let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase(); if (request.path === title) { - respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'}); + respond({ + data: await blog.render_blog_post_html(blog_post), + content_type: 'text/html; charset=utf-8', + }); return; } } - respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'}); + respond({ + data: blog.render_html(blogs), + content_type: 'text/html; charset=utf-8', + }); } } -main().catch(function(error) { - respond({data: ` -
    ${error.message}\n${error.stack}
    `, content_type: 'text/html'}); -}); \ No newline at end of file +main().catch(function (error) { + respond({ + data: ` +
    ${error.message}\n${error.stack}
    `, + content_type: 'text/html', + }); +}); diff --git a/apps/db/app.js b/apps/db/app.js index 6e6c2c41..1f65b890 100644 --- a/apps/db/app.js +++ b/apps/db/app.js @@ -51,7 +51,7 @@ async function key_list(db) { app.setDocument(doc); } -core.register('message', async function(message) { +core.register('message', async function (message) { if (message.event == 'hashChange') { let hash = message.hash.substring(1); if (hash.startsWith(':shared:')) { @@ -67,4 +67,4 @@ core.register('message', async function(message) { } }); -database_list(); \ No newline at end of file +database_list(); diff --git a/apps/follow.json b/apps/follow.json index b9466f16..3cf770d7 100644 --- a/apps/follow.json +++ b/apps/follow.json @@ -1,4 +1,4 @@ { "type": "tildefriends-app", "emoji": "➡️" -} \ No newline at end of file +} diff --git a/apps/follow/app.js b/apps/follow/app.js index 676b7423..098f8f22 100644 --- a/apps/follow/app.js +++ b/apps/follow/app.js @@ -2,7 +2,7 @@ let g_about_cache = {}; async function query(sql, args) { let result = []; - await ssb.sqlAsync(sql, args, function(row) { + await ssb.sqlAsync(sql, args, function (row) { result.push(row); }); return result; @@ -21,7 +21,8 @@ async function contacts_internal(id, last_row_id, following, max_row_id) { json_extract(content, '$.type') = 'contact' ORDER BY sequence `, - [id, last_row_id, max_row_id]); + [id, last_row_id, max_row_id] + ); for (let row of contacts) { let contact = JSON.parse(row.content); if (contact.following === true) { @@ -42,15 +43,34 @@ async function contact(id, last_row_id, following, max_row_id) { return await contacts_internal(id, last_row_id, following, max_row_id); } -async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) { - let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id))); +async function following_deep_internal( + ids, + depth, + blocking, + last_row_id, + following, + max_row_id +) { + let contacts = await Promise.all( + [...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id)) + ); let result = {}; for (let i = 0; i < ids.length; i++) { let id = ids[i]; let contact = contacts[i]; let all_blocking = Object.assign({}, contact.blocking, blocking); - let found = Object.keys(contact.following).filter(y => !all_blocking[y]); - let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : []; + let found = Object.keys(contact.following).filter((y) => !all_blocking[y]); + let deeper = + depth > 1 + ? await following_deep_internal( + found, + depth - 1, + all_blocking, + last_row_id, + following, + max_row_id + ) + : []; result[id] = [id, ...found, ...deeper]; } return [...new Set(Object.values(result).flat())]; @@ -68,10 +88,22 @@ async function following_deep(ids, depth, blocking) { last_row_id: 0, }; } - let max_row_id = (await query(` + let max_row_id = ( + await query( + ` SELECT MAX(rowid) AS max_row_id FROM messages - `, []))[0].max_row_id; - let result = await following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id); + `, + [] + ) + )[0].max_row_id; + let result = await following_deep_internal( + ids, + depth, + blocking, + cache.last_row_id, + cache.following, + max_row_id + ); cache.last_row_id = max_row_id; let store = JSON.stringify(cache); await db.set('following', store); @@ -90,13 +122,15 @@ async function fetch_about(db, ids, users) { }; } let max_row_id = 0; - await ssb.sqlAsync(` + await ssb.sqlAsync( + ` SELECT MAX(rowid) AS max_row_id FROM messages `, [], - function(row) { + function (row) { max_row_id = row.max_row_id; - }); + } + ); for (let id of Object.keys(cache.about)) { if (ids.indexOf(id) == -1) { delete cache.about[id]; @@ -129,17 +163,21 @@ async function fetch_about(db, ids, users) { ORDER BY messages.author, messages.sequence `, [ - JSON.stringify(ids.filter(id => cache.about[id])), - JSON.stringify(ids.filter(id => !cache.about[id])), + JSON.stringify(ids.filter((id) => cache.about[id])), + JSON.stringify(ids.filter((id) => !cache.about[id])), cache.last_row_id, max_row_id, - ]); + ] + ); for (let about of abouts) { let content = JSON.parse(about.content); if (content.about === about.author) { delete content.type; delete content.about; - cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content); + cache.about[about.author] = Object.assign( + cache.about[about.author] || {}, + content + ); } } cache.last_row_id = max_row_id; @@ -155,41 +193,41 @@ async function getAbout(db, id) { if (g_about_cache[id]) { return g_about_cache[id]; } - let o = await db.get(id + ":about"); + let o = await db.get(id + ':about'); const k_version = 4; let f = o ? JSON.parse(o) : o; if (!f || f.version != k_version) { f = {about: {}, sequence: 0, version: k_version}; } await ssb.sqlAsync( - "SELECT "+ - " sequence, "+ - " content "+ - "FROM messages "+ - "WHERE "+ - " author = ?1 AND "+ - " sequence > ?2 AND "+ - " json_extract(content, '$.type') = 'about' AND "+ - " json_extract(content, '$.about') = ?1 "+ - "UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+ - "ORDER BY sequence", + 'SELECT ' + + ' sequence, ' + + ' content ' + + 'FROM messages ' + + 'WHERE ' + + ' author = ?1 AND ' + + ' sequence > ?2 AND ' + + " json_extract(content, '$.type') = 'about' AND " + + " json_extract(content, '$.about') = ?1 " + + 'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' + + 'ORDER BY sequence', [id, f.sequence], - function(row) { + function (row) { f.sequence = row.sequence; if (row.content) { let about = {}; try { about = JSON.parse(row.content); - } catch { - } + } catch {} delete about.about; delete about.type; f.about = Object.assign(f.about, about); } - }); + } + ); let j = JSON.stringify(f); if (o != j) { - await db.set(id + ":about", j); + await db.set(id + ':about', j); } g_about_cache[id] = f.about; return f.about; @@ -198,15 +236,15 @@ async function getAbout(db, id) { async function getSize(db, id) { let size = 0; await ssb.sqlAsync( - "SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1", + 'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1', [id], function (row) { size += row.size; - }); + } + ); return size; } - async function getSizes(ids) { let sizes = {}; await ssb.sqlAsync( @@ -221,7 +259,8 @@ async function getSizes(ids) { [JSON.stringify(ids)], function (row) { sizes[row.author] = row.size; - }); + } + ); return sizes; } @@ -241,7 +280,10 @@ function niceSize(bytes) { } function escape(value) { - return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>'); } async function main() { @@ -249,19 +291,27 @@ async function main() { let db = await database('ssb'); let whoami = await ssb.getIdentities(); let tree = ''; - await app.setDocument(`
    Enumerating followed users...
    `); + await app.setDocument( + `
    Enumerating followed users...
    ` + ); let following = await following_deep(whoami, 2, {}); - await app.setDocument(`
    Getting names and sizes...
    `); + await app.setDocument( + `
    Getting names and sizes...
    ` + ); let [about, sizes] = await Promise.all([ fetch_about(db, following, {}), getSizes(following), ]); await app.setDocument(`
    Finishing...
    `); - following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0))); + following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0)); for (let id of following) { tree += `
  • ${escape(about[id]?.name ?? id)} ${niceSize(sizes[id] ?? 0)}
  • \n`; } - await app.setDocument('\n\n

    Following

    \n
      ' + tree + '
    \n\n'); + await app.setDocument( + '\n\n

    Following

    \n
      ' + + tree + + '
    \n\n' + ); } -main(); \ No newline at end of file +main(); diff --git a/apps/gg.json b/apps/gg.json index b5a6f698..16eba8e0 100644 --- a/apps/gg.json +++ b/apps/gg.json @@ -1,5 +1,5 @@ { - "type": "tildefriends-app", - "emoji": "🗺", - "previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256" -} \ No newline at end of file + "type": "tildefriends-app", + "emoji": "🗺", + "previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256" +} diff --git a/apps/gg/app.js b/apps/gg/app.js index d8ae30db..2384e409 100644 --- a/apps/gg/app.js +++ b/apps/gg/app.js @@ -46,7 +46,7 @@ tfrpc.register(async function query(sql, args) { return result; }); tfrpc.register(async function store_blob(blob) { - if (typeof(blob) == 'string') { + if (typeof blob == 'string') { blob = utf8Encode(blob); } if (Array.isArray(blob)) { @@ -71,10 +71,15 @@ async function main() { let shared_db = await shared_database('state'); attempt = await shared_db.get(core.user.credentials.session.name); } - app.setDocument(utf8Decode(getFile('index.html')).replace('${data}', JSON.stringify({ - attempt: attempt, - state: core.user?.credentials?.session?.name, - }))); + app.setDocument( + utf8Decode(getFile('index.html')).replace( + '${data}', + JSON.stringify({ + attempt: attempt, + state: core.user?.credentials?.session?.name, + }) + ) + ); } -main(); \ No newline at end of file +main(); diff --git a/apps/gg/gpx.js b/apps/gg/gpx.js index 93421d88..56875d17 100644 --- a/apps/gg/gpx.js +++ b/apps/gg/gpx.js @@ -17,7 +17,7 @@ function xml_parse(xml) { let tag = xml.substring(tag_begin, i).trim(); if (tag.startsWith('?') && tag.endsWith('?')) { /* Ignore directives. */ - } else if (tag.startsWith('/')) { + } else if (tag.startsWith('/')) { path.pop(); } else { let parts = tag.split(' '); @@ -63,7 +63,10 @@ export function gpx_parse(xml) { for (let trkseg of xml_each(trk, 'trkseg')) { let segment = []; for (let trkpt of xml_each(trkseg, 'trkpt')) { - segment.push({lat: parseFloat(trkpt.attributes.lat), lon: parseFloat(trkpt.attributes.lon)}); + segment.push({ + lat: parseFloat(trkpt.attributes.lat), + lon: parseFloat(trkpt.attributes.lon), + }); } result.segments.push(segment); } @@ -78,4 +81,4 @@ export function gpx_parse(xml) { } } return result; -} \ No newline at end of file +} diff --git a/apps/gg/handler.js b/apps/gg/handler.js index 647e81cd..4f210ce5 100644 --- a/apps/gg/handler.js +++ b/apps/gg/handler.js @@ -18,4 +18,4 @@ async function main() { status_code: 307, }); } -main(); \ No newline at end of file +main(); diff --git a/apps/gg/index.html b/apps/gg/index.html index 57d98809..66a7f723 100644 --- a/apps/gg/index.html +++ b/apps/gg/index.html @@ -1,14 +1,26 @@ - + - + - + - \ No newline at end of file + diff --git a/apps/gg/polyline.js b/apps/gg/polyline.js index 9f1dff5f..5cea31ce 100644 --- a/apps/gg/polyline.js +++ b/apps/gg/polyline.js @@ -10,24 +10,24 @@ var polyline = {}; function py2_round(value) { - // Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values - return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1); + // Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values + return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1); } function encode(current, previous, factor) { - current = py2_round(current * factor); - previous = py2_round(previous * factor); - var coordinate = (current - previous) * 2; - if (coordinate < 0) { - coordinate = -coordinate - 1 - } - var output = ''; - while (coordinate >= 0x20) { - output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63); - coordinate /= 32; - } - output += String.fromCharCode((coordinate | 0) + 63); - return output; + current = py2_round(current * factor); + previous = py2_round(previous * factor); + var coordinate = (current - previous) * 2; + if (coordinate < 0) { + coordinate = -coordinate - 1; + } + var output = ''; + while (coordinate >= 0x20) { + output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63); + coordinate /= 32; + } + output += String.fromCharCode((coordinate | 0) + 63); + return output; } /** @@ -41,54 +41,53 @@ function encode(current, previous, factor) { * * @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js */ -polyline.decode = function(str, precision) { - var index = 0, - lat = 0, - lng = 0, - coordinates = [], - shift = 0, - result = 0, - byte = null, - latitude_change, - longitude_change, - factor = Math.pow(10, Number.isInteger(precision) ? precision : 5); +polyline.decode = function (str, precision) { + var index = 0, + lat = 0, + lng = 0, + coordinates = [], + shift = 0, + result = 0, + byte = null, + latitude_change, + longitude_change, + factor = Math.pow(10, Number.isInteger(precision) ? precision : 5); - // Coordinates have variable length when encoded, so just keep - // track of whether we've hit the end of the string. In each - // loop iteration, a single coordinate is decoded. - while (index < str.length) { + // Coordinates have variable length when encoded, so just keep + // track of whether we've hit the end of the string. In each + // loop iteration, a single coordinate is decoded. + while (index < str.length) { + // Reset shift, result, and byte + byte = null; + shift = 1; + result = 0; - // Reset shift, result, and byte - byte = null; - shift = 1; - result = 0; + do { + byte = str.charCodeAt(index++) - 63; + result += (byte & 0x1f) * shift; + shift *= 32; + } while (byte >= 0x20); - do { - byte = str.charCodeAt(index++) - 63; - result += (byte & 0x1f) * shift; - shift *= 32; - } while (byte >= 0x20); + latitude_change = result & 1 ? (-result - 1) / 2 : result / 2; - latitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2); + shift = 1; + result = 0; - shift = 1; - result = 0; + do { + byte = str.charCodeAt(index++) - 63; + result += (byte & 0x1f) * shift; + shift *= 32; + } while (byte >= 0x20); - do { - byte = str.charCodeAt(index++) - 63; - result += (byte & 0x1f) * shift; - shift *= 32; - } while (byte >= 0x20); + longitude_change = result & 1 ? (-result - 1) / 2 : result / 2; - longitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2); + lat += latitude_change; + lng += longitude_change; - lat += latitude_change; - lng += longitude_change; + coordinates.push([lat / factor, lng / factor]); + } - coordinates.push([lat / factor, lng / factor]); - } - - return coordinates; + return coordinates; }; /** @@ -98,28 +97,33 @@ polyline.decode = function(str, precision) { * @param {Number} precision * @returns {String} */ -polyline.encode = function(coordinates, precision) { - if (!coordinates.length) { return ''; } +polyline.encode = function (coordinates, precision) { + if (!coordinates.length) { + return ''; + } - var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5), - output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor); + var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5), + output = + encode(coordinates[0][0], 0, factor) + + encode(coordinates[0][1], 0, factor); - for (var i = 1; i < coordinates.length; i++) { - var a = coordinates[i], b = coordinates[i - 1]; - output += encode(a[0], b[0], factor); - output += encode(a[1], b[1], factor); - } + for (var i = 1; i < coordinates.length; i++) { + var a = coordinates[i], + b = coordinates[i - 1]; + output += encode(a[0], b[0], factor); + output += encode(a[1], b[1], factor); + } - return output; + return output; }; function flipped(coords) { - var flipped = []; - for (var i = 0; i < coords.length; i++) { - var coord = coords[i].slice(); - flipped.push([coord[1], coord[0]]); - } - return flipped; + var flipped = []; + for (var i = 0; i < coords.length; i++) { + var coord = coords[i].slice(); + flipped.push([coord[1], coord[0]]); + } + return flipped; } /** @@ -129,14 +133,14 @@ function flipped(coords) { * @param {Number} precision * @returns {String} */ -polyline.fromGeoJSON = function(geojson, precision) { - if (geojson && geojson.type === 'Feature') { - geojson = geojson.geometry; - } - if (!geojson || geojson.type !== 'LineString') { - throw new Error('Input must be a GeoJSON LineString'); - } - return polyline.encode(flipped(geojson.coordinates), precision); +polyline.fromGeoJSON = function (geojson, precision) { + if (geojson && geojson.type === 'Feature') { + geojson = geojson.geometry; + } + if (!geojson || geojson.type !== 'LineString') { + throw new Error('Input must be a GeoJSON LineString'); + } + return polyline.encode(flipped(geojson.coordinates), precision); }; /** @@ -146,13 +150,13 @@ polyline.fromGeoJSON = function(geojson, precision) { * @param {Number} precision * @returns {Object} */ -polyline.toGeoJSON = function(str, precision) { - var coords = polyline.decode(str, precision); - return { - type: 'LineString', - coordinates: flipped(coords) - }; +polyline.toGeoJSON = function (str, precision) { + var coords = polyline.decode(str, precision); + return { + type: 'LineString', + coordinates: flipped(coords), + }; }; let polyline_decode = polyline.decode; -export { polyline_decode as decode }; \ No newline at end of file +export {polyline_decode as decode}; diff --git a/apps/gg/script.js b/apps/gg/script.js index 69e70d04..700dc1f2 100644 --- a/apps/gg/script.js +++ b/apps/gg/script.js @@ -1,4 +1,11 @@ -import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js'; +import { + LitElement, + html, + unsafeHTML, + css, + guard, + until, +} from './lit-all.min.js'; import * as tfrpc from '/static/tfrpc.js'; import * as polyline from './polyline.js'; import {gpx_parse} from './gpx.js'; @@ -56,7 +63,7 @@ class GgAppElement extends LitElement { this.focus = undefined; this.status = undefined; this.tab = 'map'; - this.load().catch(function(e) { + this.load().catch(function (e) { console.log('load error', e); }); this.to_build = '🏠'; @@ -65,9 +72,12 @@ class GgAppElement extends LitElement { async load() { console.log('load'); let emojis = await (await fetch('emojis.json')).json(); - emojis = Object.values(emojis).map(x => Object.values(x)).flat(); + emojis = Object.values(emojis) + .map((x) => Object.values(x)) + .flat(); let today = new Date(); - let date_index = today.getYear() * 356 + today.getMonth() * 31 + today.getDate(); + let date_index = + today.getYear() * 356 + today.getMonth() * 31 + today.getDate(); this.emoji_of_the_day = emojis[(date_index * 123457) % emojis.length]; this.user = await tfrpc.rpc.getUser(); this.url = (await tfrpc.rpc.url()).split('?')[0]; @@ -109,7 +119,8 @@ class GgAppElement extends LitElement { async get_activities_from_ssb() { this.status = {text: 'loading activities'}; this.loaded_activities = []; - let rows = await tfrpc.rpc.query(` + let rows = await tfrpc.rpc.query( + ` SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id FROM messages_fts('"gg-activity"') JOIN messages ON messages.rowid = messages_fts.rowid, @@ -117,10 +128,15 @@ class GgAppElement extends LitElement { WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND json_extract(mention.value, '$.name') = 'activity_data' ORDER BY messages.timestamp DESC - `, []); + `, + [] + ); this.status = {text: 'loading activity data'}; - let authors = rows.map(x => x.author); - let blobs = await this.promise_all(rows.map(x => tfrpc.rpc.get_blob(x.blob_id)), 8); + let authors = rows.map((x) => x.author); + let blobs = await this.promise_all( + rows.map((x) => tfrpc.rpc.get_blob(x.blob_id)), + 8 + ); this.status = {text: 'processing activity data'}; for (let [index, blob] of blobs.entries()) { let activity; @@ -135,13 +151,19 @@ class GgAppElement extends LitElement { } } this.status = {text: 'calculating balance'}; - rows = await tfrpc.rpc.query(` + rows = await tfrpc.rpc.query( + ` SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity' - `, [this.whoami]); + `, + [this.whoami] + ); let currency = rows[0].currency; - rows = await tfrpc.rpc.query(` + rows = await tfrpc.rpc.query( + ` SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place' - `, [this.whoami]); + `, + [this.whoami] + ); let spent = rows[0].cost; this.currency = currency - spent; this.status = {text: 'getting placed emojis'}; @@ -166,8 +188,11 @@ class GgAppElement extends LitElement { } async sync_activities() { - let ids = this.activities.map(x => `https://www.strava.com/activities/${x.id}`); - let missing = await tfrpc.rpc.query(` + let ids = this.activities.map( + (x) => `https://www.strava.com/activities/${x.id}` + ); + let missing = await tfrpc.rpc.query( + ` WITH my_activities AS ( SELECT json_extract(mention.value, '$.link') AS url FROM messages, json_each(messages.content, '$.mentions') AS mention @@ -178,17 +203,26 @@ class GgAppElement extends LitElement { SELECT from_strava.value FROM json_each(?) AS from_strava LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url WHERE my_activities.url IS NULL - `, [this.whoami, JSON.stringify(ids)]); + `, + [this.whoami, JSON.stringify(ids)] + ); console.log('missing = ', missing); for (let [index, row] of missing.entries()) { - this.status = {text: 'syncing from strava', value: index, max: missing.length}; + this.status = { + text: 'syncing from strava', + value: index, + max: missing.length, + }; let url = row.value; let id = url.match(/.*\/(\d+)/)[1]; - let response = await fetch(`https://www.strava.com/api/v3/activities/${id}`, { - headers: { - 'Authorization': `Bearer ${this.strava.access_token}`, - }, - }); + let response = await fetch( + `https://www.strava.com/api/v3/activities/${id}`, + { + headers: { + Authorization: `Bearer ${this.strava.access_token}`, + }, + } + ); let activity = await response.json(); let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity)); let message = { @@ -201,7 +235,7 @@ class GgAppElement extends LitElement { { link: blob_id, name: 'activity_data', - } + }, ], }; await tfrpc.rpc.appendMessage(this.whoami, message); @@ -215,13 +249,20 @@ class GgAppElement extends LitElement { return; } let ids = await tfrpc.rpc.getIdentities(); - let players = ids.length ? (await tfrpc.rpc.query(` + let players = ids.length + ? ( + await tfrpc.rpc.query( + ` SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value WHERE json_extract(messages.content, '$.type') = 'gg-player' AND json_extract(messages.content, '$.active') ORDER BY timestamp DESC limit 1 - `, [JSON.stringify(ids)])).map(row => row.author) : []; + `, + [JSON.stringify(ids)] + ) + ).map((row) => row.author) + : []; if (!players.length) { this.whoami = await tfrpc.rpc.createIdentity(); if (this.whoami) { @@ -246,9 +287,14 @@ class GgAppElement extends LitElement { await tfrpc.rpc.databaseSet('strava', shared); await tfrpc.rpc.sharedDatabaseRemove(name); } - this.strava = JSON.parse(await tfrpc.rpc.databaseGet('strava') || '{}'); + this.strava = JSON.parse((await tfrpc.rpc.databaseGet('strava')) || '{}'); if (new Date().valueOf() / 1000 > this.strava.expires_at) { - console.log('this looks expired', new Date().valueOf() / 1000, '>', this.strava.expires_at); + console.log( + 'this looks expired', + new Date().valueOf() / 1000, + '>', + this.strava.expires_at + ); let x = await tfrpc.rpc.refresh_token(this.strava); if (x) { this.strava = x; @@ -261,13 +307,16 @@ class GgAppElement extends LitElement { async update_activities() { if (this?.strava?.access_token) { - let response = await fetch('https://www.strava.com/api/v3/athlete/activities', { - headers: { - 'Authorization': `Bearer ${this.strava.access_token}`, - }, - }); + let response = await fetch( + 'https://www.strava.com/api/v3/athlete/activities', + { + headers: { + Authorization: `Bearer ${this.strava.access_token}`, + }, + } + ); this.activities = await response.json(); - this.activities.sort((a, b) => (a.id - b.id)); + this.activities.sort((a, b) => a.id - b.id); } } @@ -282,10 +331,12 @@ class GgAppElement extends LitElement { [k_color_default, '🟧'], ]; for (let m of k_map) { - if (m[0][0] == color[0] && + if ( + m[0][0] == color[0] && m[0][1] == color[1] && m[0][2] == color[2] && - m[0][3] == color[3]) { + m[0][3] == color[3] + ) { return m[1]; } } @@ -329,9 +380,11 @@ class GgAppElement extends LitElement { on_click(event) { let popup = L.popup() .setLatLng(event.latlng) - .setContent(` + .setContent( + `
    ${event.latlng.lat}, ${event.latlng.lng}
    - `) + ` + ) .openOn(this.leaflet); } @@ -368,31 +421,43 @@ class GgAppElement extends LitElement { on_marker_click(event) { this.popup = L.popup() .setLatLng(event.latlng) - .setContent(` + .setContent( + ` ${this.to_build} (-${k_store[this.to_build]}) - `) + ` + ) .openOn(this.leaflet); } snap_to_grid(latlng, fudge, zoom) { - let position = this.leaflet.options.crs.latLngToPoint(latlng, zoom ?? this.leaflet.getZoom()); + let position = this.leaflet.options.crs.latLngToPoint( + latlng, + zoom ?? this.leaflet.getZoom() + ); position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0); position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0); - position = this.leaflet.options.crs.pointToLatLng(position, zoom ?? this.leaflet.getZoom()); + position = this.leaflet.options.crs.pointToLatLng( + position, + zoom ?? this.leaflet.getZoom() + ); return position; } on_marker_move(event) { if (!this.no_snap && this.marker) { this.no_snap = true; - this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)); + this.marker.setLatLng( + this.snap_to_grid(this.marker.getLatLng(), k_marker_snap) + ); this.no_snap = false; } } on_zoom(event) { if (this.marker) { - this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)); + this.marker.setLatLng( + this.snap_to_grid(this.marker.getLatLng(), k_marker_snap) + ); } } @@ -403,7 +468,10 @@ class GgAppElement extends LitElement { } if (this.to_build) { - this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {icon: L.divIcon({className: 'build-icon'}), draggable: true}).addTo(this.leaflet); + this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), { + icon: L.divIcon({className: 'build-icon'}), + draggable: true, + }).addTo(this.leaflet); this.marker.on({click: this.on_marker_click.bind(this)}); this.marker.on({drag: this.on_marker_move.bind(this)}); } @@ -417,14 +485,18 @@ class GgAppElement extends LitElement { return; } if (!this.leaflet) { - this.leaflet = L.map(map, {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false}); + this.leaflet = L.map(map, { + attributionControl: false, + maxZoom: 16, + bounceAtZoomLimits: false, + }); this.leaflet.on({contextmenu: this.on_click.bind(this)}); this.leaflet.on({click: this.on_mouse_down.bind(this)}); this.leaflet.on({zoom: this.on_zoom.bind(this)}); } let self = this; let grid_layer = L.GridLayer.extend({ - createTile: function(coords) { + createTile: function (coords) { var tile = L.DomUtil.create('canvas', 'leaflet-tile'); var size = this.getTileSize(); tile.width = size.x; @@ -432,7 +504,7 @@ class GgAppElement extends LitElement { var context = tile.getContext('2d'); context.font = '10pt sans'; let bounds = this._tileCoordsToBounds(coords); - let degrees = 360.0 / (2 ** coords.z); + let degrees = 360.0 / 2 ** coords.z; let ul = bounds.getNorthWest(); let lr = bounds.getSouthEast(); @@ -442,33 +514,53 @@ class GgAppElement extends LitElement { let mini_context = mini.getContext('2d'); let image_data = context.getImageData(0, 0, mini.width, mini.height); for (let activity of self.loaded_activities) { - self.draw_activity_to_tile(image_data, mini.width, mini.height, ul, lr, activity); + self.draw_activity_to_tile( + image_data, + mini.width, + mini.height, + ul, + lr, + activity + ); } context.textAlign = 'left'; context.textBaseline = 'bottom'; for (let x = 0; x < mini.width; x++) { for (let y = 0; y < mini.height; y++) { let start = (y * mini.width + x) * 4; - let pixel = self.color_to_emoji(image_data.data.slice(start, start + 4)); + let pixel = self.color_to_emoji( + image_data.data.slice(start, start + 4) + ); if (pixel) { //context.fillRect(x * size.x / mini.width, y * size.y / mini.height, size.x / mini.width, size.y / mini.height); - context.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height + mini.height); + context.fillText( + pixel, + (x * size.x) / mini.width, + (y * size.y) / mini.height + mini.height + ); } } } for (let placed of self.placed_emojis) { - let position = self.leaflet.options.crs.latLngToPoint(self.snap_to_grid(placed.position, undefined, coords.z), coords.z); + let position = self.leaflet.options.crs.latLngToPoint( + self.snap_to_grid(placed.position, undefined, coords.z), + coords.z + ); let tile_x = Math.floor(position.x / size.x); let tile_y = Math.floor(position.y / size.y); position.x = position.x - tile_x * size.x; position.y = position.y - tile_y * size.y; if (tile_x == coords.x && tile_y == coords.y) { //context.fillRect(position.x, position.y, size.x / mini.width, size.y / mini.height); - context.fillText(placed.emoji, position.x, position.y + mini.height); + context.fillText( + placed.emoji, + position.x, + position.y + mini.height + ); } } return tile; - } + }, }); if (this.grid_layer) { this.grid_layer.redraw(); @@ -484,10 +576,7 @@ class GgAppElement extends LitElement { this.max_lon = Math.max(this.max_lon, bounds.max.lng); } if (this.focus) { - this.leaflet.fitBounds([ - this.focus.min, - this.focus.max, - ]); + this.leaflet.fitBounds([this.focus.min, this.focus.max]); this.focus = undefined; } else { this.leaflet.fitBounds([ @@ -588,7 +677,12 @@ class GgAppElement extends LitElement { let sy = y0 < y1 ? 1 : -1; let error = dx + dy; while (true) { - if (x0 >= 0 && y0 >= 0 && x0 < image_data.width && y0 < image_data.height) { + if ( + x0 >= 0 && + y0 >= 0 && + x0 < image_data.width && + y0 < image_data.height + ) { let base = (y0 * image_data.width + x0) * 4; image_data.data[base + 0] = value[0]; image_data.data[base + 1] = value[1]; @@ -623,8 +717,8 @@ class GgAppElement extends LitElement { let last; for (let pt of polyline.decode(activity.map.polyline)) { let px = [ - Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)), - Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)), + Math.floor((width * (pt[1] - ul.lng)) / (lr.lng - ul.lng)), + Math.floor((height * (pt[0] - ul.lat)) / (lr.lat - ul.lat)), ]; if (last) { this.line(image_data, last[0], last[1], px[0], px[1], color); @@ -637,8 +731,8 @@ class GgAppElement extends LitElement { let last; for (let pt of segment) { let px = [ - Math.floor(width * (pt.lon - ul.lng) / (lr.lng - ul.lng)), - Math.floor(height * (pt.lat - ul.lat) / (lr.lat - ul.lat)), + Math.floor((width * (pt.lon - ul.lng)) / (lr.lng - ul.lng)), + Math.floor((height * (pt.lat - ul.lat)) / (lr.lat - ul.lat)), ]; if (last) { this.line(image_data, last[0], last[1], px[0], px[1], color); @@ -667,7 +761,7 @@ class GgAppElement extends LitElement { { link: blob_id, name: 'activity_data', - } + }, ], }; console.log('id =', this.whoami, 'message = ', message); @@ -693,8 +787,7 @@ class GgAppElement extends LitElement { focus_map(activity) { let bounds = this.activity_bounds(activity); - if (bounds.min.lat < bounds.max.lat && - bounds.min.lng < bounds.max.lng) { + if (bounds.min.lat < bounds.max.lat && bounds.min.lng < bounds.max.lng) { this.tab = 'map'; this.focus = bounds; } @@ -703,9 +796,13 @@ class GgAppElement extends LitElement { render_news() { return html`
      - ${this.loaded_activities.map(x => html` -
    • this.focus_map(x)}>${x.author} ${x.name ?? x.time}
    • - `)} + ${this.loaded_activities.map( + (x) => html` +
    • this.focus_map(x)}> + ${x.author} ${x.name ?? x.time} +
    • + ` + )}
    `; } @@ -714,7 +811,7 @@ class GgAppElement extends LitElement { let [emoji, cost] = item; return html`
    - this.to_build = emoji}> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined} + (this.to_build = emoji)}> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
    `; } @@ -732,7 +829,10 @@ class GgAppElement extends LitElement { render() { let header; if (!this.user?.credentials?.session?.name) { - header = html`
    Please login to Tilde Friends, first.
    `; + header = html`
    + Please login to + Tilde Friends, first. +
    `; } else if (!this.strava?.access_token) { let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`; header = html` @@ -765,10 +865,10 @@ class GgAppElement extends LitElement { } `; @@ -790,13 +890,15 @@ class GgAppElement extends LitElement { return html` - -
    + +
    ${header}
    ${content}
    ${navigation} @@ -804,4 +906,4 @@ class GgAppElement extends LitElement { `; } } -customElements.define('gg-app', GgAppElement); \ No newline at end of file +customElements.define('gg-app', GgAppElement); diff --git a/apps/gg/strava.js b/apps/gg/strava.js index 8c78b412..3955e403 100644 --- a/apps/gg/strava.js +++ b/apps/gg/strava.js @@ -17,4 +17,4 @@ export async function authorization_code(code) { method: 'POST', body: `client_id=${k_client_id}&client_secret=${k_client_secret}&code=${code}&grant_type=authorization_code`, }); -} \ No newline at end of file +} diff --git a/apps/identity.json b/apps/identity.json index 91328bcb..a4c28e41 100644 --- a/apps/identity.json +++ b/apps/identity.json @@ -1,5 +1,5 @@ { - "type": "tildefriends-app", - "emoji": "🪪", - "previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256" -} \ No newline at end of file + "type": "tildefriends-app", + "emoji": "🪪", + "previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256" +} diff --git a/apps/identity/app.js b/apps/identity/app.js index 2aa50363..a56e5572 100644 --- a/apps/identity/app.js +++ b/apps/identity/app.js @@ -18,7 +18,8 @@ tfrpc.register(async function reload() { async function main() { let ids = await ssb.getIdentities(); - await app.setDocument(` + await app.setDocument( + ` + + - \ No newline at end of file + diff --git a/apps/issues/script.js b/apps/issues/script.js index 1c9c2d32..cdb37b91 100644 --- a/apps/issues/script.js +++ b/apps/issues/script.js @@ -31,7 +31,12 @@ class TfIdPickerElement extends LitElement { if (this.ids) { return html` `; } else { @@ -57,13 +62,15 @@ class TfComposeElement extends LitElement { } submit() { - this.dispatchEvent(new CustomEvent('tf-submit', { - bubbles: true, - composed: true, - detail: { - value: this.renderRoot.getElementById('input').value, - }, - })); + this.dispatchEvent( + new CustomEvent('tf-submit', { + bubbles: true, + composed: true, + detail: { + value: this.renderRoot.getElementById('input').value, + }, + }) + ); this.renderRoot.getElementById('input').value = ''; this.input(); } @@ -96,7 +103,8 @@ class TfIssuesAppElement extends LitElement { async load() { let issues = {}; - let messages = await tfrpc.rpc.query(` + let messages = await tfrpc.rpc.query( + ` WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON messages.id = messages_refs.message WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'), @@ -107,7 +115,9 @@ class TfIssuesAppElement extends LitElement { SELECT * FROM issues UNION SELECT * FROM edits ORDER BY timestamp - `, [k_project]); + `, + [k_project] + ); for (let message of messages) { let content = JSON.parse(message.content); switch (content.type) { @@ -123,7 +133,7 @@ class TfIssuesAppElement extends LitElement { break; case 'issue-edit': case 'post': - for (let issue of (content.issues || [])) { + for (let issue of content.issues || []) { if (issues[issue.link]) { if (issue.open !== undefined) { issues[issue.link].open = issue.open; @@ -136,7 +146,9 @@ class TfIssuesAppElement extends LitElement { break; } } - this.issues = Object.values(issues).sort((x, y) => (y.open - x.open) || (y.created - x.created)); + this.issues = Object.values(issues).sort( + (x, y) => y.open - x.open || y.created - x.created + ); if (this.selected) { for (let issue of this.issues) { if (issue.id == this.selected.id) { @@ -150,11 +162,20 @@ class TfIssuesAppElement extends LitElement { return html` ${issue.open ? '☐ open' : '☑ closed'} - ${issue.author} - this.selected = issue}> + + ${issue.author} + + (this.selected = issue)} + > ${issue.text.split('\n')?.[0]} - ${new Date(issue.updated ?? issue.created).toLocaleDateString()} + + ${new Date(issue.updated ?? issue.created).toLocaleDateString()} + `; } @@ -170,13 +191,21 @@ class TfIssuesAppElement extends LitElement {
    ${new Date(update.timestamp).toLocaleString()}
    ${update.author}
    ${message}
    -
    ${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}
    +
    + ${update.open !== undefined + ? update.open + ? 'issue opened' + : 'issue closed' + : undefined} +
    `; } async set_open(id, open) { - if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) { + if ( + confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`) + ) { let whoami = this.shadowRoot.getElementById('picker').selected; await tfrpc.rpc.appendMessage(whoami, { type: 'issue-edit', @@ -207,7 +236,9 @@ class TfIssuesAppElement extends LitElement { type: 'post', text: event.detail.value, root: this.selected.id, - branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id, + branch: this.selected.updates.length + ? this.selected.updates[this.selected.updates.length - 1].id + : this.selected.id, issues: [ { link: this.selected.id, @@ -226,16 +257,18 @@ class TfIssuesAppElement extends LitElement { return html` ${header}
    - this.selected = undefined}> - ${this.selected.open ? - html` this.set_open(this.selected.id, false)}>` : - html` this.set_open(this.selected.id, true)}>`} + (this.selected = undefined)}> + ${ + this.selected.open + ? html` this.set_open(this.selected.id, false)}>` + : html` this.set_open(this.selected.id, true)}>` + }
    ${new Date(this.selected.created).toLocaleString()}
    ${this.selected.author}
    ${this.selected.id}
    ${unsafeHTML(tfutils.markdown(this.selected.text))}
    - ${this.selected.updates.map(x => this.render_update(x))} + ${this.selected.updates.map((x) => this.render_update(x))} `; } else { @@ -250,11 +283,11 @@ class TfIssuesAppElement extends LitElement { Title Date - ${this.issues.map(x => this.render_issue_table_row(x))} + ${this.issues.map((x) => this.render_issue_table_row(x))} `; } } } -customElements.define('tf-issues-app', TfIssuesAppElement); \ No newline at end of file +customElements.define('tf-issues-app', TfIssuesAppElement); diff --git a/apps/issues/tf-utils.js b/apps/issues/tf-utils.js index a1c7666a..444e2a93 100644 --- a/apps/issues/tf-utils.js +++ b/apps/issues/tf-utils.js @@ -1,20 +1,32 @@ import * as linkify from './commonmark-linkify.js'; function image(node, entering) { - if (node.firstChild?.type === 'text' && - node.firstChild.literal.startsWith('video:')) { + if ( + node.firstChild?.type === 'text' && + node.firstChild.literal.startsWith('video:') + ) { if (entering) { - this.lit('