Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
9b00b41a1e | |||
b1f6ad17e1 | |||
e7979fe9db | |||
7a276adbbc | |||
db4997fdc4 | |||
44ebb841f0 | |||
09ae4e2096 | |||
0b46efe4ea | |||
f1dda43e66 | |||
ce483138d7 | |||
73cc39226d | |||
57257f63dd | |||
88b25790e8 | |||
e01defc4aa | |||
cb50c43e93 | |||
5908d15f91 | |||
f66cfaec12 | |||
259f92c53b | |||
a84f850e91 | |||
5a765e6f07 | |||
791889c659 | |||
5da63faf1f | |||
30d108fc35 | |||
a09fefab5e | |||
f74ca1c236 | |||
30e027092b | |||
fd4ac7c9b9 | |||
4482049b94 | |||
5839380437 | |||
2152470fdc | |||
93b2a81495 | |||
e139e952c0 | |||
cf1c57ccb8 | |||
f7a2138488 |
@ -3,9 +3,9 @@
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
VERSION_CODE := 29
|
||||
VERSION_NUMBER := 0.0.24
|
||||
VERSION_NAME := Honey bunches of boats.
|
||||
VERSION_CODE := 30
|
||||
VERSION_NUMBER := 0.0.25-wip
|
||||
VERSION_NAME := This program kills fascists.
|
||||
|
||||
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470000.zip
|
||||
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🐌",
|
||||
"previous": "&IH3DadMNF785idnMI/LuCpIJQxzvpg1PDp8BI7m1Nx0=.sha256"
|
||||
"previous": "&+UZSxjoucLAl5r/nfRNu5KXx3K/PdutGnizL/Cn2eCU=.sha256"
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class TfElement extends LitElement {
|
||||
}
|
||||
|
||||
set_hash(hash) {
|
||||
this.hash = hash || '#';
|
||||
this.hash = decodeURIComponent(hash || '#');
|
||||
if (this.hash.startsWith('#q=')) {
|
||||
this.tab = 'search';
|
||||
} else if (this.hash === '#connections') {
|
||||
|
@ -289,16 +289,10 @@ class TfProfileElement extends LitElement {
|
||||
typeof profile.image == 'string' ? profile.image : profile.image?.link;
|
||||
image = this.editing?.image ?? image;
|
||||
let description = this.editing?.description ?? profile.description;
|
||||
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
||||
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
||||
<div class="w3-row">
|
||||
<div class="w3-col s1 w3-container w3-right">
|
||||
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
|
||||
</div>
|
||||
<div class="w3-rest w3-container">
|
||||
<input type="text" class="w3-theme-d1" style="width: 100%; vertical-align: middle" readonly value=${this.id}></input>
|
||||
</div>
|
||||
</div>
|
||||
return html`<div class="w3-container" style="box-sizing: border-box; border: 2px solid black; background-color: rgba(255, 255, 255, 0.2)">
|
||||
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
||||
<input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input>
|
||||
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
|
||||
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||
${edit_profile}
|
||||
<div style="flex: 1 0 50%">
|
||||
@ -312,11 +306,11 @@ class TfProfileElement extends LitElement {
|
||||
Blocking ${profile.blocking} identities.
|
||||
Blocked by ${profile.blocked} identities.
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
${edit}
|
||||
${follow}
|
||||
${block}
|
||||
</div>
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ class TfTabConnectionsElement extends LitElement {
|
||||
stored_connections: {type: Array},
|
||||
users: {type: Object},
|
||||
server_identity: {type: String},
|
||||
connect_error: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
@ -163,6 +164,15 @@ class TfTabConnectionsElement extends LitElement {
|
||||
tfrpc.rpc.sync();
|
||||
}
|
||||
|
||||
connect(address) {
|
||||
let self = this;
|
||||
tfrpc.rpc.connect(address).then(function() {
|
||||
self.connect_error = 'connected!';
|
||||
}).catch(function(error) {
|
||||
self.connect_error = error;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
@ -178,10 +188,11 @@ class TfTabConnectionsElement extends LitElement {
|
||||
<button
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() =>
|
||||
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
||||
self.connect(self.renderRoot.getElementById('code').value)}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
<div ?hidden=${this.connect_error === undefined}>${this.connect_error}</div>
|
||||
<h2>Broadcasts</h2>
|
||||
<ul class="w3-ul w3-border">
|
||||
${this.broadcasts
|
||||
|
4
apps/test.json
Normal file
4
apps/test.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📦"
|
||||
}
|
3
apps/test/app.js
Normal file
3
apps/test/app.js
Normal file
@ -0,0 +1,3 @@
|
||||
app.setDocument(
|
||||
'<p style="color: #fff">Maybe one day this app will run tests, but for now there is nothing to see here.</p>'
|
||||
);
|
1
apps/test/hello.txt
Normal file
1
apps/test/hello.txt
Normal file
@ -0,0 +1 @@
|
||||
Hello, world!
|
@ -162,6 +162,7 @@ class TfNavigationElement extends LitElement {
|
||||
class="w3-dropdown-content w3-bar-block w3-card-4"
|
||||
style="max-width: 100%; right: 0"
|
||||
>
|
||||
<div style="position: fixed; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.25); z-index: -100" @click=${self.toggle_id_dropdown}></div>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-border"
|
||||
@click=${() => (window.location.href = '/~core/identity')}
|
||||
@ -1174,7 +1175,7 @@ function api_requestPermission(permission, id) {
|
||||
|
||||
let div = document.createElement('div');
|
||||
div.appendChild(
|
||||
document.createTextNode('This app is requesting the following permission:')
|
||||
document.createTextNode('This app is requesting the following permission: ')
|
||||
);
|
||||
let span = document.createElement('span');
|
||||
span.style = 'font-weight: bold';
|
||||
@ -1190,6 +1191,7 @@ function api_requestPermission(permission, id) {
|
||||
check.classList.add('w3-check');
|
||||
check.classList.add('w3-blue');
|
||||
div.appendChild(check);
|
||||
div.appendChild(document.createTextNode(' '));
|
||||
let label = document.createElement('label');
|
||||
label.htmlFor = check.id;
|
||||
label.appendChild(document.createTextNode('Remember this decision.'));
|
||||
|
364
core/core.js
364
core/core.js
@ -4,89 +4,6 @@ import * as http from './http.js';
|
||||
|
||||
let gProcesses = {};
|
||||
let gStatsTimer = false;
|
||||
|
||||
const k_content_security_policy =
|
||||
'sandbox allow-downloads allow-top-navigation-by-user-activation';
|
||||
|
||||
const k_global_settings = {
|
||||
index: {
|
||||
type: 'string',
|
||||
default_value: '/~core/apps/',
|
||||
description: 'Default path.',
|
||||
},
|
||||
index_map: {
|
||||
type: 'textarea',
|
||||
default_value: undefined,
|
||||
description:
|
||||
'Mappings from hostname to redirect path, one per line, as in: "www.tildefriends.net=/~core/index/"',
|
||||
},
|
||||
room: {
|
||||
type: 'boolean',
|
||||
default_value: true,
|
||||
description: 'Enable peers to tunnel through this instance as a room.',
|
||||
},
|
||||
room_name: {
|
||||
type: 'string',
|
||||
default_value: 'tilde friends tunnel',
|
||||
description: 'Name of the room.',
|
||||
},
|
||||
replicator: {
|
||||
type: 'boolean',
|
||||
default_value: true,
|
||||
description: 'Enable message and blob replication.',
|
||||
},
|
||||
code_of_conduct: {
|
||||
type: 'textarea',
|
||||
default_value: undefined,
|
||||
description: 'Code of conduct presented at sign-in.',
|
||||
},
|
||||
http_redirect: {
|
||||
type: 'string',
|
||||
default_value: undefined,
|
||||
description:
|
||||
'If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "https://example.com")',
|
||||
},
|
||||
fetch_hosts: {
|
||||
type: 'string',
|
||||
default_value: undefined,
|
||||
description:
|
||||
'Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.',
|
||||
},
|
||||
blob_fetch_age_seconds: {
|
||||
type: 'integer',
|
||||
default_value:
|
||||
platform() == 'android' || platform() == 'iphone'
|
||||
? 0.5 * 365 * 24 * 60 * 60
|
||||
: undefined,
|
||||
description:
|
||||
'Only blobs mentioned more recently than this age will be automatically fetched.',
|
||||
},
|
||||
blob_expire_age_seconds: {
|
||||
type: 'integer',
|
||||
default_value:
|
||||
platform() == 'android' || platform() == 'iphone'
|
||||
? 1.0 * 365 * 24 * 60 * 60
|
||||
: undefined,
|
||||
description: 'Blobs older than this will be automatically deleted.',
|
||||
},
|
||||
seeds_host: {
|
||||
type: 'string',
|
||||
default_value: 'seeds.tildefriends.net',
|
||||
description: 'Hostname for seed connections.',
|
||||
},
|
||||
peer_exchange: {
|
||||
type: 'boolean',
|
||||
default_value: false,
|
||||
description:
|
||||
'Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.',
|
||||
},
|
||||
account_registration: {
|
||||
type: 'boolean',
|
||||
default_value: true,
|
||||
description: 'Allow registration of new accounts.',
|
||||
},
|
||||
};
|
||||
|
||||
let kPingInterval = 60 * 1000;
|
||||
|
||||
/**
|
||||
@ -489,7 +406,7 @@ async function getProcessBlob(blobId, key, options) {
|
||||
};
|
||||
if (process.credentials?.permissions?.administration) {
|
||||
imports.core.globalSettingsDescriptions = async function () {
|
||||
let settings = Object.assign({}, k_global_settings);
|
||||
let settings = Object.assign({}, defaultGlobalSettings());
|
||||
for (let [key, value] of Object.entries(await loadSettings())) {
|
||||
if (settings[key]) {
|
||||
settings[key].value = value;
|
||||
@ -812,206 +729,6 @@ async function getProcessBlob(blobId, key, options) {
|
||||
return process;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} response
|
||||
* @param {*} data
|
||||
* @param {*} type
|
||||
* @param {*} headers
|
||||
* @param {*} status_code
|
||||
*/
|
||||
function sendData(response, data, type, headers, status_code) {
|
||||
if (data) {
|
||||
response.writeHead(
|
||||
status_code ?? 200,
|
||||
Object.assign(
|
||||
{
|
||||
'Content-Type':
|
||||
type ||
|
||||
httpd.mime_type_from_magic_bytes(data) ||
|
||||
'application/binary',
|
||||
'Content-Length': data.byteLength,
|
||||
},
|
||||
headers || {}
|
||||
)
|
||||
);
|
||||
response.end(data);
|
||||
} else {
|
||||
response.writeHead(
|
||||
status_code ?? 404,
|
||||
Object.assign(
|
||||
{
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Content-Length': 'File not found'.length,
|
||||
},
|
||||
headers || {}
|
||||
)
|
||||
);
|
||||
response.end('File not found');
|
||||
}
|
||||
}
|
||||
|
||||
let g_handler_index = 0;
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} response
|
||||
* @param {*} handler_blob_id
|
||||
* @param {*} path
|
||||
* @param {*} query
|
||||
* @param {*} headers
|
||||
* @param {*} packageOwner
|
||||
* @param {*} packageName
|
||||
* @returns
|
||||
*/
|
||||
async function useAppHandler(
|
||||
response,
|
||||
handler_blob_id,
|
||||
path,
|
||||
query,
|
||||
headers,
|
||||
packageOwner,
|
||||
packageName
|
||||
) {
|
||||
print('useAppHandler', packageOwner, packageName);
|
||||
let do_resolve;
|
||||
let promise = new Promise(async function (resolve, reject) {
|
||||
do_resolve = resolve;
|
||||
});
|
||||
let process;
|
||||
let result;
|
||||
try {
|
||||
process = await getProcessBlob(
|
||||
handler_blob_id,
|
||||
'handler_' + g_handler_index++,
|
||||
{
|
||||
script: 'handler.js',
|
||||
imports: {
|
||||
request: {
|
||||
path: path,
|
||||
query: query,
|
||||
},
|
||||
respond: do_resolve,
|
||||
},
|
||||
credentials: await httpd.auth_query(headers),
|
||||
packageOwner: packageOwner,
|
||||
packageName: packageName,
|
||||
}
|
||||
);
|
||||
await process.ready;
|
||||
|
||||
result = await promise;
|
||||
} finally {
|
||||
if (process?.task) {
|
||||
await process.task.kill();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} request
|
||||
* @param {*} response
|
||||
* @param {*} blobId
|
||||
* @param {*} uri
|
||||
* @returns
|
||||
*/
|
||||
async function blobHandler(request, response, blobId, uri) {
|
||||
if (!uri) {
|
||||
response.writeHead(303, {
|
||||
Location:
|
||||
(request.client.tls ? 'https://' : 'http://') +
|
||||
(request.headers['x-forwarded-host'] ?? request.headers.host) +
|
||||
blobId +
|
||||
'/',
|
||||
'Content-Length': '0',
|
||||
});
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
let process;
|
||||
let data;
|
||||
let match;
|
||||
let id;
|
||||
let app_id = blobId;
|
||||
let packageOwner;
|
||||
let packageName;
|
||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
||||
packageOwner = match[1];
|
||||
packageName = match[2];
|
||||
let db = new Database(match[1]);
|
||||
app_id = await db.get('path:' + match[2]);
|
||||
}
|
||||
|
||||
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
|
||||
id = app_object?.files[uri.substring(1)];
|
||||
if (!id && app_object?.files['handler.js']) {
|
||||
let answer;
|
||||
try {
|
||||
answer = await useAppHandler(
|
||||
response,
|
||||
app_id,
|
||||
uri.substring(1),
|
||||
request.query ? form.decodeForm(request.query) : undefined,
|
||||
request.headers,
|
||||
packageOwner,
|
||||
packageName
|
||||
);
|
||||
} catch (error) {
|
||||
data = utf8Encode(
|
||||
`Internal Server Error\n\n${error?.message}\n${error?.stack}`
|
||||
);
|
||||
response.writeHead(500, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Content-Length': data.length,
|
||||
});
|
||||
response.end(data);
|
||||
return;
|
||||
}
|
||||
if (answer && typeof answer.data == 'string') {
|
||||
answer.data = utf8Encode(answer.data);
|
||||
}
|
||||
sendData(
|
||||
response,
|
||||
answer?.data,
|
||||
answer?.content_type,
|
||||
Object.assign(answer?.headers ?? {}, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Security-Policy': k_content_security_policy,
|
||||
}),
|
||||
answer.status_code
|
||||
);
|
||||
} else if (id) {
|
||||
if (
|
||||
request.headers['if-none-match'] &&
|
||||
request.headers['if-none-match'] == '"' + id + '"'
|
||||
) {
|
||||
let headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Security-Policy': k_content_security_policy,
|
||||
'Content-Length': '0',
|
||||
};
|
||||
response.writeHead(304, headers);
|
||||
response.end();
|
||||
} else {
|
||||
let headers = {
|
||||
ETag: '"' + id + '"',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Security-Policy': k_content_security_policy,
|
||||
};
|
||||
data = await ssb.blobGet(id);
|
||||
let type =
|
||||
httpd.mime_type_from_extension(uri) ||
|
||||
httpd.mime_type_from_magic_bytes(data);
|
||||
sendData(response, data, type, headers);
|
||||
}
|
||||
} else {
|
||||
sendData(response, data, undefined, {});
|
||||
}
|
||||
}
|
||||
|
||||
ssb.addEventListener('message', function () {
|
||||
broadcastEvent('onMessage', [...arguments]);
|
||||
});
|
||||
@ -1037,7 +754,7 @@ async function loadSettings() {
|
||||
} catch (error) {
|
||||
print('Settings not found in database:', error);
|
||||
}
|
||||
for (let [key, value] of Object.entries(k_global_settings)) {
|
||||
for (let [key, value] of Object.entries(defaultGlobalSettings())) {
|
||||
if (data[key] === undefined) {
|
||||
data[key] = value.default_value;
|
||||
}
|
||||
@ -1063,6 +780,73 @@ function sendStats() {
|
||||
}
|
||||
}
|
||||
|
||||
let g_handler_index = 0;
|
||||
|
||||
exports.callAppHandler = async function callAppHandler(
|
||||
response,
|
||||
app_blob_id,
|
||||
path,
|
||||
query,
|
||||
headers,
|
||||
package_owner,
|
||||
package_name
|
||||
) {
|
||||
let answer;
|
||||
try {
|
||||
let do_resolve;
|
||||
let promise = new Promise(async function (resolve, reject) {
|
||||
do_resolve = resolve;
|
||||
});
|
||||
let process;
|
||||
try {
|
||||
process = await getProcessBlob(
|
||||
app_blob_id,
|
||||
'handler_' + g_handler_index++,
|
||||
{
|
||||
script: 'handler.js',
|
||||
imports: {
|
||||
request: {
|
||||
path: path,
|
||||
query: query,
|
||||
},
|
||||
respond: do_resolve,
|
||||
},
|
||||
credentials: await httpd.auth_query(headers),
|
||||
packageOwner: package_owner,
|
||||
packageName: package_name,
|
||||
}
|
||||
);
|
||||
await process.ready;
|
||||
answer = await promise;
|
||||
} finally {
|
||||
if (process?.task) {
|
||||
await process.task.kill();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
let data = utf8Encode(
|
||||
`Internal Server Error\n\n${error?.message}\n${error?.stack}`
|
||||
);
|
||||
response.writeHead(500, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Content-Length': data.length,
|
||||
});
|
||||
response.end(data);
|
||||
return;
|
||||
}
|
||||
if (typeof answer?.data == 'string') {
|
||||
answer.data = utf8Encode(answer.data);
|
||||
}
|
||||
response.writeHead(answer?.status_code, {
|
||||
'Content-Type': answer?.content_type,
|
||||
'Content-Length': answer?.data?.length,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Security-Policy':
|
||||
'sandbox allow-downloads allow-top-navigation-by-user-activation',
|
||||
});
|
||||
response.end(answer?.data);
|
||||
};
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
*/
|
||||
@ -1072,16 +856,6 @@ loadSettings()
|
||||
httpd.set_http_redirect(settings.http_redirect);
|
||||
}
|
||||
httpd.all('/app/socket', app.socket);
|
||||
httpd.all('', function default_http_handler(request, response) {
|
||||
let match;
|
||||
if ((match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri))) {
|
||||
return blobHandler(request, response, match[1], match[2]);
|
||||
} else if (
|
||||
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
|
||||
) {
|
||||
return blobHandler(request, response, match[1], match[2]);
|
||||
}
|
||||
});
|
||||
let port = httpd.start(tildefriends.http_port);
|
||||
if (tildefriends.args.out_http_port_file) {
|
||||
print('Writing the port file.');
|
||||
|
@ -21,14 +21,14 @@
|
||||
}:
|
||||
pkgs.stdenv.mkDerivation rec {
|
||||
pname = "tildefriends";
|
||||
version = "0.0.23";
|
||||
version = "0.0.24";
|
||||
|
||||
src = pkgs.fetchFromGitea {
|
||||
domain = "dev.tildefriends.net";
|
||||
owner = "cory";
|
||||
repo = "tildefriends";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-ukZpi+BXRTFGbdvd5ApmctTo8bjtPJMHjqFPgVSyBWU=";
|
||||
hash = "sha256-XlmRr08UmScY//qxUEXHzagXHCFqARRYr3q8RK/jKFY=";
|
||||
fetchSubmodules = true;
|
||||
};
|
||||
|
||||
|
2
deps/codemirror/cm6.js
vendored
2
deps/codemirror/cm6.js
vendored
File diff suppressed because one or more lines are too long
186
deps/codemirror_src/package-lock.json
generated
vendored
186
deps/codemirror_src/package-lock.json
generated
vendored
@ -19,9 +19,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz",
|
||||
"integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==",
|
||||
"version": "6.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.2.tgz",
|
||||
"integrity": "sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
@ -129,9 +129,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
|
||||
"integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
|
||||
"version": "6.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.7.tgz",
|
||||
"integrity": "sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
@ -158,9 +158,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz",
|
||||
"integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==",
|
||||
"version": "6.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.2.tgz",
|
||||
"integrity": "sha512-d6n0WFvL970A9Z+l9N2dO+Hk9ev4hDYQzIx+B9tCyBP0W5wPEszi1rhuyFesNSkLZzXbQE5FPH7F/z/TMJfoPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.4.0",
|
||||
@ -370,9 +370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
|
||||
"integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.4.tgz",
|
||||
"integrity": "sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -383,9 +383,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
|
||||
"integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.4.tgz",
|
||||
"integrity": "sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -396,9 +396,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
|
||||
"integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.4.tgz",
|
||||
"integrity": "sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -409,9 +409,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
|
||||
"integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.4.tgz",
|
||||
"integrity": "sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -421,10 +421,36 @@
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.4.tgz",
|
||||
"integrity": "sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.4.tgz",
|
||||
"integrity": "sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
|
||||
"integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.4.tgz",
|
||||
"integrity": "sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -435,9 +461,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
|
||||
"integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.4.tgz",
|
||||
"integrity": "sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -448,9 +474,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
|
||||
"integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.4.tgz",
|
||||
"integrity": "sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -461,9 +487,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
|
||||
"integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.4.tgz",
|
||||
"integrity": "sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -474,9 +500,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
|
||||
"integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.4.tgz",
|
||||
"integrity": "sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -487,9 +513,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
|
||||
"integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.4.tgz",
|
||||
"integrity": "sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -500,9 +526,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
|
||||
"integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.4.tgz",
|
||||
"integrity": "sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -513,9 +539,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
|
||||
"integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz",
|
||||
"integrity": "sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -526,9 +552,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
|
||||
"integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.4.tgz",
|
||||
"integrity": "sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -539,9 +565,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
|
||||
"integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.4.tgz",
|
||||
"integrity": "sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -552,9 +578,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
|
||||
"integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.4.tgz",
|
||||
"integrity": "sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -565,9 +591,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
|
||||
"integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.4.tgz",
|
||||
"integrity": "sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -590,9 +616,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
|
||||
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@ -754,9 +780,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
|
||||
"integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.4.tgz",
|
||||
"integrity": "sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.6"
|
||||
@ -769,22 +795,24 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.24.0",
|
||||
"@rollup/rollup-android-arm64": "4.24.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.24.0",
|
||||
"@rollup/rollup-darwin-x64": "4.24.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.24.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.24.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.24.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.24.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.24.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.24.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.24.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.24.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.24.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.24.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.24.4",
|
||||
"@rollup/rollup-android-arm64": "4.24.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.24.4",
|
||||
"@rollup/rollup-darwin-x64": "4.24.4",
|
||||
"@rollup/rollup-freebsd-arm64": "4.24.4",
|
||||
"@rollup/rollup-freebsd-x64": "4.24.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.24.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.24.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.24.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.24.4",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.24.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.24.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.24.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.24.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.24.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.24.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.24.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.24.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
2
deps/libbacktrace
vendored
2
deps/libbacktrace
vendored
@ -1 +1 @@
|
||||
Subproject commit 531aec7c52b66cd750a28a698f3c060f279b18b0
|
||||
Subproject commit d48f84034ce3e53e501d10593710d025cb1121db
|
@ -12,7 +12,7 @@
|
||||
- nix
|
||||
- comment out the hash in default.nix
|
||||
- update the version
|
||||
- run `nix build`
|
||||
- run `nix-build`
|
||||
- update the hash
|
||||
- bump the versions in GNUmakefile for the next release
|
||||
- make
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.unprompted.tildefriends"
|
||||
android:versionCode="29"
|
||||
android:versionName="0.0.24">
|
||||
android:versionCode="30"
|
||||
android:versionName="0.0.25-wip">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
|
39
src/http.c
39
src/http.c
@ -67,7 +67,6 @@ typedef struct _tf_http_connection_t
|
||||
typedef struct _tf_http_handler_t
|
||||
{
|
||||
const char* pattern;
|
||||
bool is_wildcard;
|
||||
tf_http_callback_t* callback;
|
||||
tf_http_cleanup_t* cleanup;
|
||||
void* user_data;
|
||||
@ -129,9 +128,15 @@ static void _http_allocate_buffer(uv_handle_t* handle, size_t suggested_size, uv
|
||||
*buf = uv_buf_init(connection->incoming, sizeof(connection->incoming));
|
||||
}
|
||||
|
||||
static bool _http_pattern_matches(const char* pattern, const char* path, bool is_wildcard)
|
||||
bool tf_http_pattern_matches(const char* pattern, const char* path)
|
||||
{
|
||||
if (!pattern || !*pattern || (!is_wildcard && strcmp(path, pattern) == 0))
|
||||
if (!*pattern && !*path)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const char* k_word = "{word}";
|
||||
bool is_wildcard = strchr(pattern, '*') || strstr(pattern, k_word);
|
||||
if (!is_wildcard && strcmp(path, pattern) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -140,17 +145,36 @@ static bool _http_pattern_matches(const char* pattern, const char* path, bool is
|
||||
{
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
while (pattern[i] && path[j] && pattern[i] != '*' && pattern[i] == path[j])
|
||||
while (pattern[i] && path[j] && pattern[i] == path[j])
|
||||
{
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
|
||||
if (pattern[i] == '*')
|
||||
size_t k_word_len = strlen(k_word);
|
||||
if (strncmp(pattern + i, k_word, k_word_len) == 0 && ((path[j] >= 'a' && path[j] <= 'z') || (path[j] >= 'A' && path[j] <= 'Z')))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_http_pattern_matches(pattern + i + 1, path + j, strchr(pattern + i + 1, '*') != NULL))
|
||||
if ((path[j] >= 'a' && path[j] <= 'z') || (path[j] >= 'A' && path[j] <= 'Z') || (path[j] >= '0' && path[j] <= '9'))
|
||||
{
|
||||
if (tf_http_pattern_matches(pattern + i + k_word_len, path + j + 1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
}
|
||||
else if (pattern[i] == '*')
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (tf_http_pattern_matches(pattern + i + 1, path + j))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -170,7 +194,7 @@ static bool _http_find_handler(tf_http_t* http, const char* path, tf_http_callba
|
||||
{
|
||||
for (int i = 0; i < http->handlers_count; i++)
|
||||
{
|
||||
if (_http_pattern_matches(http->handlers[i].pattern, path, http->handlers[i].is_wildcard))
|
||||
if (tf_http_pattern_matches(http->handlers[i].pattern, path))
|
||||
{
|
||||
*out_callback = http->handlers[i].callback;
|
||||
*out_trace_name = http->handlers[i].pattern;
|
||||
@ -741,7 +765,6 @@ void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_
|
||||
http->handlers = tf_resize_vec(http->handlers, sizeof(tf_http_handler_t) * (http->handlers_count + 1));
|
||||
http->handlers[http->handlers_count++] = (tf_http_handler_t) {
|
||||
.pattern = tf_strdup(pattern),
|
||||
.is_wildcard = pattern && strchr(pattern, '*') != NULL,
|
||||
.callback = callback,
|
||||
.cleanup = cleanup,
|
||||
.user_data = user_data,
|
||||
|
@ -228,4 +228,12 @@ void tf_http_request_websocket_upgrade(tf_http_request_t* request);
|
||||
*/
|
||||
const char* tf_http_status_text(int status);
|
||||
|
||||
/**
|
||||
** Match URL patterns. "*" matches anything, and "{word}" matches [a-zA-Z][a-zA-Z0-9]*".
|
||||
** @param pattern The pattern to match.
|
||||
** @param path The path to test.
|
||||
** @return true if the path matches the pattern.
|
||||
*/
|
||||
bool tf_http_pattern_matches(const char* pattern, const char* path);
|
||||
|
||||
/** @} */
|
||||
|
286
src/httpd.js.c
286
src/httpd.js.c
@ -10,6 +10,7 @@
|
||||
#include "tlscontext.js.h"
|
||||
#include "trace.h"
|
||||
#include "util.js.h"
|
||||
#include "version.h"
|
||||
|
||||
#include "ow-crypt.h"
|
||||
|
||||
@ -33,6 +34,7 @@
|
||||
|
||||
#define CYAN "\e[1;36m"
|
||||
#define MAGENTA "\e[1;35m"
|
||||
#define YELLOW "\e[1;33m"
|
||||
#define RESET "\e[0m"
|
||||
|
||||
const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
|
||||
@ -222,6 +224,17 @@ static void _httpd_message_callback(tf_http_request_t* request, int op_code, con
|
||||
JS_FreeValue(context, on_message);
|
||||
}
|
||||
|
||||
static JSValue _httpd_make_response_object(JSContext* context, tf_http_request_t* request)
|
||||
{
|
||||
JSValue response_object = JS_NewObjectClass(context, _httpd_request_class_id);
|
||||
JS_SetOpaque(response_object, request);
|
||||
JS_SetPropertyStr(context, response_object, "writeHead", JS_NewCFunction(context, _httpd_response_write_head, "writeHead", 2));
|
||||
JS_SetPropertyStr(context, response_object, "end", JS_NewCFunction(context, _httpd_response_end, "end", 1));
|
||||
JS_SetPropertyStr(context, response_object, "send", JS_NewCFunction(context, _httpd_response_send, "send", 2));
|
||||
JS_SetPropertyStr(context, response_object, "upgrade", JS_NewCFunction(context, _httpd_websocket_upgrade, "upgrade", 2));
|
||||
return response_object;
|
||||
}
|
||||
|
||||
static void _httpd_callback_internal(tf_http_request_t* request, bool is_websocket)
|
||||
{
|
||||
http_handler_data_t* data = request->user_data;
|
||||
@ -248,14 +261,9 @@ static void _httpd_callback_internal(tf_http_request_t* request, bool is_websock
|
||||
JS_SetPropertyStr(context, client, "tls", request->is_tls ? JS_TRUE : JS_FALSE);
|
||||
JS_SetPropertyStr(context, request_object, "client", client);
|
||||
|
||||
JSValue response_object = JS_NewObjectClass(context, _httpd_request_class_id);
|
||||
JSValue response_object = _httpd_make_response_object(context, request);
|
||||
/* The ref is owned by the JS object and will be released by the finalizer. */
|
||||
tf_http_request_ref(request);
|
||||
JS_SetOpaque(response_object, request);
|
||||
JS_SetPropertyStr(context, response_object, "writeHead", JS_NewCFunction(context, _httpd_response_write_head, "writeHead", 2));
|
||||
JS_SetPropertyStr(context, response_object, "end", JS_NewCFunction(context, _httpd_response_end, "end", 1));
|
||||
JS_SetPropertyStr(context, response_object, "send", JS_NewCFunction(context, _httpd_response_send, "send", 2));
|
||||
JS_SetPropertyStr(context, response_object, "upgrade", JS_NewCFunction(context, _httpd_websocket_upgrade, "upgrade", 2));
|
||||
JSValue args[] = {
|
||||
request_object,
|
||||
response_object,
|
||||
@ -423,7 +431,7 @@ static JSValue _httpd_endpoint_start(JSContext* context, JSValueConst this_val,
|
||||
*listener = (httpd_listener_t) { .context = context, .tls = JS_DupValue(context, argv[1]) };
|
||||
tf_tls_context_t* tls = tf_tls_context_get(listener->tls);
|
||||
int assigned_port = tf_http_listen(http, port, tls, _httpd_listener_cleanup, listener);
|
||||
tf_printf(CYAN "~😎 Tilde Friends" RESET " is now up at " MAGENTA "http%s://127.0.0.1:%d/" RESET ".\n", tls ? "s" : "", assigned_port);
|
||||
tf_printf(CYAN "~😎 Tilde Friends" RESET " " YELLOW VERSION_NUMBER RESET " is now up at " MAGENTA "http%s://127.0.0.1:%d/" RESET ".\n", tls ? "s" : "", assigned_port);
|
||||
return JS_NewInt32(context, assigned_port);
|
||||
}
|
||||
|
||||
@ -558,7 +566,7 @@ static bool _magic_bytes_match(const magic_bytes_t* magic, const uint8_t* actual
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char* _httpd_mime_type_from_magic_bytes_internal(const uint8_t* bytes, size_t size)
|
||||
static const char* _httpd_mime_type_from_magic_bytes(const uint8_t* bytes, size_t size)
|
||||
{
|
||||
const char* type = "application/binary";
|
||||
if (bytes)
|
||||
@ -635,13 +643,6 @@ static const char* _httpd_mime_type_from_magic_bytes_internal(const uint8_t* byt
|
||||
return type;
|
||||
}
|
||||
|
||||
static JSValue _httpd_mime_type_from_magic_bytes(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
size_t size = 0;
|
||||
uint8_t* bytes = tf_util_try_get_array_buffer(context, &size, argv[0]);
|
||||
return JS_NewString(context, _httpd_mime_type_from_magic_bytes_internal(bytes, size));
|
||||
}
|
||||
|
||||
static const char* _ext_to_content_type(const char* ext, bool use_fallback)
|
||||
{
|
||||
if (ext)
|
||||
@ -674,14 +675,6 @@ static const char* _ext_to_content_type(const char* ext, bool use_fallback)
|
||||
return use_fallback ? "application/binary" : NULL;
|
||||
}
|
||||
|
||||
static JSValue _httpd_mime_type_from_extension(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
const char* name = JS_ToCString(context, argv[0]);
|
||||
const char* type = _ext_to_content_type(strrchr(name, '.'), false);
|
||||
JS_FreeCString(context, name);
|
||||
return type ? JS_NewString(context, type) : JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static void _httpd_finalizer(JSRuntime* runtime, JSValue value)
|
||||
{
|
||||
tf_http_t* http = JS_GetOpaque(value, _httpd_class_id);
|
||||
@ -966,6 +959,22 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
|
||||
tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
|
||||
}
|
||||
|
||||
static void _httpd_endpoint_add_slash(tf_http_request_t* request)
|
||||
{
|
||||
const char* host = tf_http_request_get_header(request, "x-forwarded-host");
|
||||
if (!host)
|
||||
{
|
||||
host = tf_http_request_get_header(request, "host");
|
||||
}
|
||||
char url[1024];
|
||||
snprintf(url, sizeof(url), "%s%s%s/", request->is_tls ? "https://" : "http://", host, request->path);
|
||||
const char* headers[] = {
|
||||
"Location",
|
||||
url,
|
||||
};
|
||||
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, "", 0);
|
||||
}
|
||||
|
||||
typedef struct _user_app_t
|
||||
{
|
||||
const char* user;
|
||||
@ -1016,12 +1025,204 @@ static user_app_t* _parse_user_app_from_path(const char* path, const char* expec
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct _app_blob_t
|
||||
{
|
||||
tf_http_request_t* request;
|
||||
bool found;
|
||||
bool not_modified;
|
||||
bool use_handler;
|
||||
void* data;
|
||||
size_t size;
|
||||
char app_blob_id[k_blob_id_len];
|
||||
const char* file;
|
||||
user_app_t* user_app;
|
||||
char etag[256];
|
||||
} app_blob_t;
|
||||
|
||||
static void _httpd_endpoint_app_blob_work(tf_ssb_t* ssb, void* user_data)
|
||||
{
|
||||
app_blob_t* data = user_data;
|
||||
tf_http_request_t* request = data->request;
|
||||
if (request->path[0] == '/' && request->path[1] == '~')
|
||||
{
|
||||
const char* last_slash = strchr(request->path + 1, '/');
|
||||
if (last_slash)
|
||||
{
|
||||
last_slash = strchr(last_slash + 1, '/');
|
||||
}
|
||||
data->user_app = last_slash ? _parse_user_app_from_path(request->path, last_slash) : NULL;
|
||||
if (data->user_app)
|
||||
{
|
||||
size_t path_length = strlen("path:") + strlen(data->user_app->app) + 1;
|
||||
char* app_path = tf_malloc(path_length);
|
||||
snprintf(app_path, path_length, "path:%s", data->user_app->app);
|
||||
const char* value = tf_ssb_db_get_property(ssb, data->user_app->user, app_path);
|
||||
snprintf(data->app_blob_id, sizeof(data->app_blob_id), "%s", value);
|
||||
tf_free(app_path);
|
||||
tf_free((void*)value);
|
||||
data->file = last_slash + 1;
|
||||
}
|
||||
}
|
||||
else if (request->path[0] == '/' && request->path[1] == '&')
|
||||
{
|
||||
const char* end = strstr(request->path, ".sha256/");
|
||||
if (end)
|
||||
{
|
||||
snprintf(data->app_blob_id, sizeof(data->app_blob_id), "%.*s", (int)(end + strlen(".sha256") - request->path - 1), request->path + 1);
|
||||
data->file = end + strlen(".sha256/");
|
||||
}
|
||||
}
|
||||
|
||||
char* app_blob = NULL;
|
||||
size_t app_blob_size = 0;
|
||||
if (*data->app_blob_id && tf_ssb_db_blob_get(ssb, data->app_blob_id, (uint8_t**)&app_blob, &app_blob_size))
|
||||
{
|
||||
JSMallocFunctions funcs = { 0 };
|
||||
tf_get_js_malloc_functions(&funcs);
|
||||
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
|
||||
JSContext* context = JS_NewContext(runtime);
|
||||
|
||||
JSValue app_object = JS_ParseJSON(context, app_blob, app_blob_size, NULL);
|
||||
JSValue files = JS_GetPropertyStr(context, app_object, "files");
|
||||
JSValue blob_id = JS_GetPropertyStr(context, files, data->file);
|
||||
if (JS_IsUndefined(blob_id))
|
||||
{
|
||||
blob_id = JS_GetPropertyStr(context, files, "handler.js");
|
||||
if (!JS_IsUndefined(blob_id))
|
||||
{
|
||||
data->use_handler = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const char* blob_id_str = JS_ToCString(context, blob_id);
|
||||
if (blob_id_str)
|
||||
{
|
||||
snprintf(data->etag, sizeof(data->etag), "\"%s\"", blob_id_str);
|
||||
const char* match = tf_http_request_get_header(data->request, "if-none-match");
|
||||
if (match && strcmp(match, data->etag) == 0)
|
||||
{
|
||||
data->not_modified = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
data->found = tf_ssb_db_blob_get(ssb, blob_id_str, (uint8_t**)&data->data, &data->size);
|
||||
}
|
||||
}
|
||||
JS_FreeCString(context, blob_id_str);
|
||||
}
|
||||
JS_FreeValue(context, blob_id);
|
||||
JS_FreeValue(context, files);
|
||||
JS_FreeValue(context, app_object);
|
||||
|
||||
JS_FreeContext(context);
|
||||
JS_FreeRuntime(runtime);
|
||||
tf_free(app_blob);
|
||||
}
|
||||
}
|
||||
|
||||
static void _httpd_call_app_handler(tf_ssb_t* ssb, tf_http_request_t* request, const char* app_blob_id, const char* path, const char* package_owner, const char* app)
|
||||
{
|
||||
JSContext* context = tf_ssb_get_context(ssb);
|
||||
JSValue global = JS_GetGlobalObject(context);
|
||||
JSValue exports = JS_GetPropertyStr(context, global, "exports");
|
||||
JSValue call_app_handler = JS_GetPropertyStr(context, exports, "callAppHandler");
|
||||
|
||||
JSValue response = _httpd_make_response_object(context, request);
|
||||
tf_http_request_ref(request);
|
||||
JSValue handler_blob_id = JS_NewString(context, app_blob_id);
|
||||
JSValue path_value = JS_NewString(context, path);
|
||||
JSValue package_owner_value = JS_NewString(context, package_owner);
|
||||
JSValue app_value = JS_NewString(context, app);
|
||||
JSValue query_value = request->query ? JS_NewString(context, request->query) : JS_UNDEFINED;
|
||||
|
||||
JSValue headers = JS_NewObject(context);
|
||||
for (int i = 0; i < request->headers_count; i++)
|
||||
{
|
||||
char name[256] = "";
|
||||
snprintf(name, sizeof(name), "%.*s", (int)request->headers[i].name_len, request->headers[i].name);
|
||||
JS_SetPropertyStr(context, headers, name, JS_NewStringLen(context, request->headers[i].value, request->headers[i].value_len));
|
||||
}
|
||||
|
||||
JSValue args[] = {
|
||||
response,
|
||||
handler_blob_id,
|
||||
path_value,
|
||||
query_value,
|
||||
headers,
|
||||
package_owner_value,
|
||||
app_value,
|
||||
};
|
||||
|
||||
JSValue result = JS_Call(context, call_app_handler, JS_NULL, tf_countof(args), args);
|
||||
tf_util_report_error(context, result);
|
||||
JS_FreeValue(context, result);
|
||||
|
||||
JS_FreeValue(context, headers);
|
||||
JS_FreeValue(context, query_value);
|
||||
JS_FreeValue(context, app_value);
|
||||
JS_FreeValue(context, package_owner_value);
|
||||
JS_FreeValue(context, handler_blob_id);
|
||||
JS_FreeValue(context, path_value);
|
||||
JS_FreeValue(context, response);
|
||||
JS_FreeValue(context, call_app_handler);
|
||||
JS_FreeValue(context, exports);
|
||||
JS_FreeValue(context, global);
|
||||
}
|
||||
|
||||
static void _httpd_endpoint_app_blob_after_work(tf_ssb_t* ssb, int status, void* user_data)
|
||||
{
|
||||
app_blob_t* data = user_data;
|
||||
if (data->not_modified)
|
||||
{
|
||||
tf_http_respond(data->request, 304, NULL, 0, NULL, 0);
|
||||
}
|
||||
else if (data->use_handler)
|
||||
{
|
||||
_httpd_call_app_handler(ssb, data->request, data->app_blob_id, data->file, data->user_app->user, data->user_app->app);
|
||||
}
|
||||
else if (data->found)
|
||||
{
|
||||
const char* mime_type = _ext_to_content_type(strrchr(data->request->path, '.'), false);
|
||||
if (!mime_type)
|
||||
{
|
||||
mime_type = _httpd_mime_type_from_magic_bytes(data->data, data->size);
|
||||
}
|
||||
const char* headers[] = {
|
||||
"Access-Control-Allow-Origin",
|
||||
"*",
|
||||
"Content-Security-Policy",
|
||||
"sandbox allow-downloads allow-top-navigation-by-user-activation",
|
||||
"Content-Type",
|
||||
mime_type ? mime_type : "application/binary",
|
||||
"etag",
|
||||
data->etag,
|
||||
};
|
||||
tf_http_respond(data->request, 200, headers, tf_countof(headers) / 2, data->data, data->size);
|
||||
}
|
||||
tf_free(data->user_app);
|
||||
tf_free(data->data);
|
||||
tf_http_request_unref(data->request);
|
||||
tf_free(data);
|
||||
}
|
||||
|
||||
static void _httpd_endpoint_app_blob(tf_http_request_t* request)
|
||||
{
|
||||
tf_http_request_ref(request);
|
||||
tf_task_t* task = request->user_data;
|
||||
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||
app_blob_t* data = tf_malloc(sizeof(app_blob_t));
|
||||
*data = (app_blob_t) { .request = request };
|
||||
tf_ssb_run_work(ssb, _httpd_endpoint_app_blob_work, _httpd_endpoint_app_blob_after_work, data);
|
||||
}
|
||||
|
||||
typedef struct _view_t
|
||||
{
|
||||
tf_http_request_t* request;
|
||||
const char** form_data;
|
||||
void* data;
|
||||
size_t size;
|
||||
char etag[256];
|
||||
bool not_modified;
|
||||
} view_t;
|
||||
|
||||
@ -1045,7 +1246,7 @@ static void _httpd_endpoint_view_work(tf_ssb_t* ssb, void* user_data)
|
||||
{
|
||||
view_t* view = user_data;
|
||||
tf_http_request_t* request = view->request;
|
||||
char blob_id[256] = "";
|
||||
char blob_id[k_blob_id_len] = "";
|
||||
|
||||
user_app_t* user_app = _parse_user_app_from_path(request->path, "/view");
|
||||
if (user_app)
|
||||
@ -1066,10 +1267,11 @@ static void _httpd_endpoint_view_work(tf_ssb_t* ssb, void* user_data)
|
||||
|
||||
if (*blob_id)
|
||||
{
|
||||
snprintf(view->etag, sizeof(view->etag), "\"%s\"", blob_id);
|
||||
const char* if_none_match = tf_http_request_get_header(request, "if-none-match");
|
||||
char match[258];
|
||||
snprintf(match, sizeof(match), "\"%s\"", blob_id);
|
||||
if (if_none_match && strcmp(if_none_match, match))
|
||||
if (if_none_match && strcmp(if_none_match, match) == 0)
|
||||
{
|
||||
view->not_modified = true;
|
||||
}
|
||||
@ -1097,7 +1299,9 @@ static void _httpd_endpoint_view_after_work(tf_ssb_t* ssb, int status, void* use
|
||||
"Content-Security-Policy",
|
||||
"sandbox allow-downloads allow-top-navigation-by-user-activation",
|
||||
"Content-Type",
|
||||
view->data ? _httpd_mime_type_from_magic_bytes_internal(view->data, view->size) : "text/plain",
|
||||
view->data ? _httpd_mime_type_from_magic_bytes(view->data, view->size) : "text/plain",
|
||||
"etag",
|
||||
view->etag,
|
||||
filename ? "Content-Disposition" : NULL,
|
||||
filename ? content_disposition : NULL,
|
||||
};
|
||||
@ -1135,7 +1339,7 @@ typedef struct _save_t
|
||||
{
|
||||
tf_http_request_t* request;
|
||||
int response;
|
||||
char blob_id[256];
|
||||
char blob_id[k_blob_id_len];
|
||||
} save_t;
|
||||
|
||||
static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
|
||||
@ -1212,12 +1416,12 @@ static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
|
||||
size_t new_app_length = 0;
|
||||
const char* new_app_str = JS_ToCStringLen(context, &new_app_length, new_app_json);
|
||||
|
||||
char blob_id[250] = { 0 };
|
||||
char blob_id[k_blob_id_len] = { 0 };
|
||||
if (tf_ssb_db_blob_store(ssb, (const uint8_t*)new_app_str, new_app_length, blob_id, sizeof(blob_id), NULL) &&
|
||||
tf_ssb_db_set_property(ssb, user_app->user, app_path, blob_id))
|
||||
{
|
||||
tf_ssb_db_add_value_to_array_property(ssb, user_app->user, "apps", user_app->app);
|
||||
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", blob_id);
|
||||
snprintf(save->blob_id, sizeof(save->blob_id), "%s", blob_id);
|
||||
save->response = 200;
|
||||
}
|
||||
|
||||
@ -1242,10 +1446,10 @@ static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
|
||||
}
|
||||
else if (strcmp(request->path, "/save") == 0)
|
||||
{
|
||||
char blob_id[250] = { 0 };
|
||||
char blob_id[k_blob_id_len] = { 0 };
|
||||
if (tf_ssb_db_blob_store(ssb, request->body, request->content_length, blob_id, sizeof(blob_id), NULL))
|
||||
{
|
||||
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", blob_id);
|
||||
snprintf(save->blob_id, sizeof(save->blob_id), "%s", blob_id);
|
||||
save->response = 200;
|
||||
}
|
||||
}
|
||||
@ -1269,7 +1473,9 @@ static void _httpd_endpoint_save_after_work(tf_ssb_t* ssb, int status, void* use
|
||||
tf_http_request_t* request = save->request;
|
||||
if (*save->blob_id)
|
||||
{
|
||||
tf_http_respond(request, 200, NULL, 0, save->blob_id, strlen(save->blob_id));
|
||||
char body[256] = "";
|
||||
int length = snprintf(body, sizeof(body), "/%s", save->blob_id);
|
||||
tf_http_respond(request, 200, NULL, 0, body, length);
|
||||
}
|
||||
tf_http_request_unref(request);
|
||||
tf_free(save);
|
||||
@ -2099,11 +2305,16 @@ void tf_httpd_register(JSContext* context)
|
||||
tf_http_add_handler(http, "/speedscope/*", _httpd_endpoint_static, NULL, task);
|
||||
tf_http_add_handler(http, "/static/*", _httpd_endpoint_static, NULL, task);
|
||||
tf_http_add_handler(http, "/.well-known/*", _httpd_endpoint_static, NULL, task);
|
||||
tf_http_add_handler(http, "/~*/*/", _httpd_endpoint_static, NULL, task);
|
||||
tf_http_add_handler(http, "/&*.sha256", _httpd_endpoint_add_slash, NULL, task);
|
||||
tf_http_add_handler(http, "/&*.sha256/", _httpd_endpoint_static, NULL, task);
|
||||
tf_http_add_handler(http, "/*/view", _httpd_endpoint_view, NULL, task);
|
||||
tf_http_add_handler(http, "/~*/*/save", _httpd_endpoint_save, NULL, task);
|
||||
tf_http_add_handler(http, "/~*/*/delete", _httpd_endpoint_delete, NULL, task);
|
||||
tf_http_add_handler(http, "/&*.sha256/view", _httpd_endpoint_view, NULL, task);
|
||||
tf_http_add_handler(http, "/&*.sha256/*", _httpd_endpoint_app_blob, NULL, task);
|
||||
tf_http_add_handler(http, "/~{word}/{word}", _httpd_endpoint_add_slash, NULL, task);
|
||||
tf_http_add_handler(http, "/~{word}/{word}/", _httpd_endpoint_static, NULL, task);
|
||||
tf_http_add_handler(http, "/~{word}/{word}/save", _httpd_endpoint_save, NULL, task);
|
||||
tf_http_add_handler(http, "/~{word}/{word}/delete", _httpd_endpoint_delete, NULL, task);
|
||||
tf_http_add_handler(http, "/~{word}/{word}/view", _httpd_endpoint_view, NULL, task);
|
||||
tf_http_add_handler(http, "/~{word}/{word}/*", _httpd_endpoint_app_blob, NULL, task);
|
||||
tf_http_add_handler(http, "/save", _httpd_endpoint_save, NULL, task);
|
||||
|
||||
tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL);
|
||||
@ -2116,13 +2327,10 @@ void tf_httpd_register(JSContext* context)
|
||||
tf_http_add_handler(http, "/login/logout", _httpd_endpoint_logout, NULL, task);
|
||||
tf_http_add_handler(http, "/login", _httpd_endpoint_login, NULL, task);
|
||||
|
||||
JS_SetPropertyStr(context, httpd, "handlers", JS_NewObject(context));
|
||||
JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_endpoint_all, "all", 2));
|
||||
JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_endpoint_start, "start", 2));
|
||||
JS_SetPropertyStr(context, httpd, "set_http_redirect", JS_NewCFunction(context, _httpd_set_http_redirect, "set_http_redirect", 1));
|
||||
JS_SetPropertyStr(context, httpd, "auth_query", JS_NewCFunction(context, _httpd_auth_query, "auth_query", 1));
|
||||
JS_SetPropertyStr(context, httpd, "mime_type_from_magic_bytes", JS_NewCFunction(context, _httpd_mime_type_from_magic_bytes, "mime_type_from_magic_bytes", 1));
|
||||
JS_SetPropertyStr(context, httpd, "mime_type_from_extension", JS_NewCFunction(context, _httpd_mime_type_from_extension, "mime_type_from_extension", 1));
|
||||
JS_SetPropertyStr(context, global, "httpd", httpd);
|
||||
JS_FreeValue(context, global);
|
||||
}
|
||||
|
@ -346,8 +346,8 @@ static int _tf_command_publish(const char* file, int argc, char* argv[])
|
||||
{
|
||||
tf_printf("\n%s publish [options]\n\n", file);
|
||||
tf_printf("options:\n");
|
||||
tf_printf(" -y, --user user User owning identity with which to publish.\n");
|
||||
tf_printf(" -i, --identity identity Identity with which to publish message.\n");
|
||||
tf_printf(" -u, --user user User owning identity with which to publish.\n");
|
||||
tf_printf(" -i, --id identity Identity with which to publish message.\n");
|
||||
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", k_db_path_default);
|
||||
tf_printf(" -c, --content json JSON content of message to publish.\n");
|
||||
tf_printf(" -h, --help Show this usage information.\n");
|
||||
|
80
src/ssb.c
80
src/ssb.c
@ -367,6 +367,9 @@ typedef struct _tf_ssb_connection_t
|
||||
uint64_t last_notified_active;
|
||||
|
||||
int flags;
|
||||
|
||||
tf_ssb_connect_callback_t* connect_callback;
|
||||
void* connect_callback_user_data;
|
||||
} tf_ssb_connection_t;
|
||||
|
||||
static JSClassID _connection_class_id;
|
||||
@ -1321,6 +1324,12 @@ static void _tf_ssb_connection_verify_identity(tf_ssb_connection_t* connection,
|
||||
JS_SetPropertyStr(context, connection->object, "is_client", JS_TRUE);
|
||||
|
||||
connection->state = k_tf_ssb_state_verified;
|
||||
if (connection->connect_callback)
|
||||
{
|
||||
connection->connect_callback(connection, NULL, connection->connect_callback_user_data);
|
||||
connection->connect_callback = NULL;
|
||||
connection->connect_callback_user_data = NULL;
|
||||
}
|
||||
if (connection->handshake_timer.data)
|
||||
{
|
||||
uv_timer_stop(&connection->handshake_timer);
|
||||
@ -1878,6 +1887,12 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
|
||||
{
|
||||
tf_ssb_t* ssb = connection->ssb;
|
||||
connection->closing = true;
|
||||
if (connection->connect_callback)
|
||||
{
|
||||
connection->connect_callback(NULL, reason, connection->connect_callback_user_data);
|
||||
connection->connect_callback = NULL;
|
||||
connection->connect_callback_user_data = NULL;
|
||||
}
|
||||
if (!connection->destroy_reason)
|
||||
{
|
||||
connection->destroy_reason = reason;
|
||||
@ -2698,22 +2713,33 @@ static void _tf_ssb_connection_handshake_timer_callback(uv_timer_t* timer)
|
||||
}
|
||||
}
|
||||
|
||||
tf_ssb_connection_t* tf_ssb_connection_create(tf_ssb_t* ssb, const char* host, const struct sockaddr_in* addr, const uint8_t* public_key)
|
||||
static tf_ssb_connection_t* _tf_ssb_connection_create(
|
||||
tf_ssb_t* ssb, const char* host, const struct sockaddr_in* addr, const uint8_t* public_key, tf_ssb_connect_callback_t* callback, void* user_data)
|
||||
{
|
||||
for (tf_ssb_connection_t* connection = ssb->connections; connection; connection = connection->next)
|
||||
{
|
||||
if (memcmp(connection->serverpub, public_key, k_id_bin_len) == 0 && connection->state != k_tf_ssb_state_invalid)
|
||||
{
|
||||
char id[k_id_base64_len];
|
||||
tf_ssb_id_bin_to_str(id, sizeof(id), public_key);
|
||||
tf_printf("Not connecting to %s:%d, because we are already connected to %s (state = %d).\n", host, ntohs(addr->sin_port), id, connection->state);
|
||||
if (callback)
|
||||
{
|
||||
char id[k_id_base64_len];
|
||||
tf_ssb_id_bin_to_str(id, sizeof(id), public_key);
|
||||
char reason[1024];
|
||||
snprintf(reason, sizeof(reason), "Already connected to %s (state = %d).", id, connection->state);
|
||||
callback(NULL, reason, user_data);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
else if (memcmp(ssb->pub, public_key, k_id_bin_len) == 0)
|
||||
{
|
||||
char id[k_id_base64_len];
|
||||
tf_ssb_id_bin_to_str(id, sizeof(id), public_key);
|
||||
tf_printf("Not connecting to %s:%d, because they appear to be ourselves %s.\n", host, ntohs(addr->sin_port), id);
|
||||
if (callback)
|
||||
{
|
||||
char id[k_id_base64_len];
|
||||
tf_ssb_id_bin_to_str(id, sizeof(id), public_key);
|
||||
char reason[1024];
|
||||
snprintf(reason, sizeof(reason), "Not connecting to ourself: %s.", id);
|
||||
callback(NULL, reason, user_data);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@ -2731,6 +2757,8 @@ tf_ssb_connection_t* tf_ssb_connection_create(tf_ssb_t* ssb, const char* host, c
|
||||
connection->port = ntohs(addr->sin_port);
|
||||
connection->async.data = connection;
|
||||
uv_async_init(ssb->loop, &connection->async, _tf_ssb_connection_process_message_async);
|
||||
connection->connect_callback = callback;
|
||||
connection->connect_callback_user_data = user_data;
|
||||
|
||||
connection->handshake_timer.data = connection;
|
||||
uv_timer_init(ssb->loop, &connection->handshake_timer);
|
||||
@ -2836,6 +2864,8 @@ typedef struct _connect_t
|
||||
int port;
|
||||
int flags;
|
||||
uint8_t key[k_id_bin_len];
|
||||
tf_ssb_connect_callback_t* callback;
|
||||
void* user_data;
|
||||
} connect_t;
|
||||
|
||||
static void _tf_on_connect_getaddrinfo(uv_getaddrinfo_t* addrinfo, int result, struct addrinfo* info)
|
||||
@ -2847,26 +2877,36 @@ static void _tf_on_connect_getaddrinfo(uv_getaddrinfo_t* addrinfo, int result, s
|
||||
{
|
||||
struct sockaddr_in addr = *(struct sockaddr_in*)info->ai_addr;
|
||||
addr.sin_port = htons(connect->port);
|
||||
tf_ssb_connection_t* connection = tf_ssb_connection_create(connect->ssb, connect->host, &addr, connect->key);
|
||||
tf_ssb_connection_t* connection = _tf_ssb_connection_create(connect->ssb, connect->host, &addr, connect->key, connect->callback, connect->user_data);
|
||||
if (connection)
|
||||
{
|
||||
connection->flags = connect->flags;
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (connect->callback)
|
||||
{
|
||||
tf_printf("getaddrinfo(%s) => %s\n", connect->host, uv_strerror(result));
|
||||
char reason[1024];
|
||||
snprintf(reason, sizeof(reason), "uv_getaddrinfo(%s) => %s", connect->host, uv_strerror(result));
|
||||
connect->callback(NULL, reason, connect->user_data);
|
||||
}
|
||||
}
|
||||
else if (connect->callback)
|
||||
{
|
||||
connect->callback(NULL, "Shutting down.", connect->user_data);
|
||||
}
|
||||
uv_freeaddrinfo(info);
|
||||
tf_ssb_unref(connect->ssb);
|
||||
tf_free(connect);
|
||||
}
|
||||
|
||||
void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* key, int connect_flags)
|
||||
void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* key, int connect_flags, tf_ssb_connect_callback_t* callback, void* user_data)
|
||||
{
|
||||
if (ssb->shutting_down)
|
||||
{
|
||||
if (callback)
|
||||
{
|
||||
callback(NULL, "Shutting down.", user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
connect_t* connect = tf_malloc(sizeof(connect_t));
|
||||
@ -2875,6 +2915,8 @@ void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* ke
|
||||
.port = port,
|
||||
.flags = connect_flags,
|
||||
.req.data = connect,
|
||||
.callback = callback,
|
||||
.user_data = user_data,
|
||||
};
|
||||
char id[k_id_base64_len] = { 0 };
|
||||
tf_ssb_id_bin_to_str(id, sizeof(id), key);
|
||||
@ -2885,6 +2927,12 @@ void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* ke
|
||||
int r = uv_getaddrinfo(ssb->loop, &connect->req, _tf_on_connect_getaddrinfo, host, NULL, &(struct addrinfo) { .ai_family = AF_INET });
|
||||
if (r < 0)
|
||||
{
|
||||
if (callback)
|
||||
{
|
||||
char reason[1024];
|
||||
snprintf(reason, sizeof(reason), "uv_getaddr_info(%s): %s", host, uv_strerror(r));
|
||||
callback(NULL, reason, user_data);
|
||||
}
|
||||
tf_printf("uv_getaddrinfo(%s): %s\n", host, uv_strerror(r));
|
||||
tf_free(connect);
|
||||
tf_ssb_unref(ssb);
|
||||
@ -3164,16 +3212,18 @@ static bool _tf_ssb_parse_broadcast(const char* in_broadcast, tf_ssb_broadcast_t
|
||||
return false;
|
||||
}
|
||||
|
||||
void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address, int connect_flags)
|
||||
void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address, int connect_flags, tf_ssb_connect_callback_t* callback, void* user_data)
|
||||
{
|
||||
tf_ssb_broadcast_t broadcast = { 0 };
|
||||
if (_tf_ssb_parse_broadcast(address, &broadcast))
|
||||
{
|
||||
tf_ssb_connect(ssb, broadcast.host, ntohs(broadcast.addr.sin_port), broadcast.pub, connect_flags);
|
||||
tf_ssb_connect(ssb, broadcast.host, ntohs(broadcast.addr.sin_port), broadcast.pub, connect_flags, callback, user_data);
|
||||
}
|
||||
else
|
||||
else if (callback)
|
||||
{
|
||||
tf_printf("Unable to parse: %s\n", address);
|
||||
char reason[1024] = "";
|
||||
snprintf(reason, sizeof(reason), "Unable to parse: '%s'.", address);
|
||||
callback(NULL, reason, user_data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,7 @@ static void _tf_ssb_connections_get_next_after_work(tf_ssb_t* ssb, int status, v
|
||||
uint8_t key_bin[k_id_bin_len];
|
||||
if (tf_ssb_id_str_to_bin(key_bin, next->key))
|
||||
{
|
||||
tf_ssb_connect(ssb, next->host, next->port, key_bin, 0);
|
||||
tf_ssb_connect(ssb, next->host, next->port, key_bin, 0, NULL, NULL);
|
||||
}
|
||||
}
|
||||
tf_free(next);
|
||||
@ -286,7 +286,7 @@ static void _tf_ssb_connections_sync_broadcast_visit(
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_ssb_connect(ssb, host, ntohs(addr->sin_port), pub, k_tf_ssb_connect_flag_one_shot);
|
||||
tf_ssb_connect(ssb, host, ntohs(addr->sin_port), pub, k_tf_ssb_connect_flag_one_shot, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,7 +332,7 @@ static void _tf_ssb_connections_get_all_after_work(tf_ssb_t* ssb, int status, vo
|
||||
tf_ssb_connections_get_all_work_t* work = user_data;
|
||||
for (int i = 0; i < work->connections_count; i++)
|
||||
{
|
||||
tf_ssb_connect_str(ssb, work->connections[i], k_tf_ssb_connect_flag_one_shot);
|
||||
tf_ssb_connect_str(ssb, work->connections[i], k_tf_ssb_connect_flag_one_shot, NULL, NULL);
|
||||
tf_free(work->connections[i]);
|
||||
}
|
||||
tf_free(work->connections);
|
||||
|
@ -767,14 +767,6 @@ bool tf_ssb_db_blob_store(tf_ssb_t* ssb, const uint8_t* blob, size_t size, char*
|
||||
}
|
||||
tf_ssb_release_db_writer(ssb, db);
|
||||
|
||||
if (rows)
|
||||
{
|
||||
if (!out_new)
|
||||
{
|
||||
tf_printf("blob stored %s %zd => %d\n", id, size, result);
|
||||
}
|
||||
}
|
||||
|
||||
if (result && out_id)
|
||||
{
|
||||
snprintf(out_id, out_id_size, "%s", id);
|
||||
@ -1733,7 +1725,6 @@ bool tf_ssb_db_register_account(uv_loop_t* loop, sqlite3* db, JSContext* context
|
||||
{
|
||||
if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK)
|
||||
{
|
||||
tf_printf("added user to properties\n");
|
||||
result = sqlite3_step(statement) == SQLITE_DONE;
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
|
@ -90,7 +90,7 @@ void tf_ssb_export(tf_ssb_t* ssb, const char* key)
|
||||
return;
|
||||
}
|
||||
|
||||
char app_blob_id[64] = { 0 };
|
||||
char app_blob_id[k_blob_id_len] = { 0 };
|
||||
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
|
||||
sqlite3_busy_timeout(db, 10000);
|
||||
sqlite3_stmt* statement;
|
||||
|
16
src/ssb.h
16
src/ssb.h
@ -348,6 +348,14 @@ const char** tf_ssb_get_connection_ids(tf_ssb_t* ssb);
|
||||
*/
|
||||
int tf_ssb_get_connections(tf_ssb_t* ssb, tf_ssb_connection_t** out_connections, int out_connections_count);
|
||||
|
||||
/**
|
||||
** Callback for completing establishing a connection.
|
||||
** @param connection The established connection if successful or null.
|
||||
** @param reason The reason for failure if the connection failed.
|
||||
** @param user_data User data.
|
||||
*/
|
||||
typedef void(tf_ssb_connect_callback_t)(tf_ssb_connection_t* connection, const char* reason, void* user_data);
|
||||
|
||||
/**
|
||||
** Establish an SHS connection with a host.
|
||||
** @param ssb The SSB instance.
|
||||
@ -355,16 +363,20 @@ int tf_ssb_get_connections(tf_ssb_t* ssb, tf_ssb_connection_t** out_connections,
|
||||
** @param port The host's SHS port.
|
||||
** @param key The host's SSB identity.
|
||||
** @param connect_flags Flags affecting the connection.
|
||||
** @param callback Completion callback.
|
||||
** @param user_data User data to be passed to the callback.
|
||||
*/
|
||||
void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* key, int connect_flags);
|
||||
void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* key, int connect_flags, tf_ssb_connect_callback_t* callback, void* user_data);
|
||||
|
||||
/**
|
||||
** Establish an SHS connection with a host by string address.
|
||||
** @param ssb The SSB instance.
|
||||
** @param address The address.
|
||||
** @param connect_flags Flags affecting the connection.
|
||||
** @param callback Completion callback.
|
||||
** @param user_data User data to be passed to the callback.
|
||||
*/
|
||||
void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address, int connect_flags);
|
||||
void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address, int connect_flags, tf_ssb_connect_callback_t* callback, void* user_data);
|
||||
|
||||
/**
|
||||
** Begin listening for SHS connections on the given port.
|
||||
|
32
src/ssb.js.c
32
src/ssb.js.c
@ -1633,17 +1633,41 @@ static JSValue _tf_ssb_getBroadcasts(JSContext* context, JSValueConst this_val,
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct _connect_t
|
||||
{
|
||||
JSContext* context;
|
||||
JSValue promise[2];
|
||||
} connect_t;
|
||||
|
||||
static void _tf_ssb_connect_callback(tf_ssb_connection_t* connection, const char* reason, void* user_data)
|
||||
{
|
||||
connect_t* connect = user_data;
|
||||
JSContext* context = connect->context;
|
||||
JSValue arg = connection ? JS_UNDEFINED : JS_NewString(context, reason);
|
||||
JSValue result = JS_Call(context, connection ? connect->promise[0] : connect->promise[1], JS_UNDEFINED, connection ? 0 : 1, &arg);
|
||||
tf_util_report_error(context, result);
|
||||
JS_FreeValue(context, result);
|
||||
JS_FreeValue(context, connect->promise[0]);
|
||||
JS_FreeValue(context, connect->promise[1]);
|
||||
JS_FreeValue(context, arg);
|
||||
tf_free(connect);
|
||||
}
|
||||
|
||||
static JSValue _tf_ssb_connect(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JSValue result = JS_UNDEFINED;
|
||||
JSValue args = argv[0];
|
||||
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
|
||||
if (ssb)
|
||||
{
|
||||
connect_t* connect = tf_malloc(sizeof(connect_t));
|
||||
*connect = (connect_t) { .context = context };
|
||||
result = JS_NewPromiseCapability(context, connect->promise);
|
||||
if (JS_IsString(args))
|
||||
{
|
||||
const char* address_str = JS_ToCString(context, args);
|
||||
tf_printf("Connecting to %s\n", address_str);
|
||||
tf_ssb_connect_str(ssb, address_str, 0);
|
||||
tf_ssb_connect_str(ssb, address_str, 0, _tf_ssb_connect_callback, connect);
|
||||
JS_FreeCString(context, address_str);
|
||||
}
|
||||
else
|
||||
@ -1660,11 +1684,11 @@ static JSValue _tf_ssb_connect(JSContext* context, JSValueConst this_val, int ar
|
||||
tf_printf("Connecting to %s:%d\n", address_str, port_int);
|
||||
uint8_t pubkey_bin[k_id_bin_len];
|
||||
tf_ssb_id_str_to_bin(pubkey_bin, pubkey_str);
|
||||
tf_ssb_connect(ssb, address_str, port_int, pubkey_bin, 0);
|
||||
tf_ssb_connect(ssb, address_str, port_int, pubkey_bin, 0, _tf_ssb_connect_callback, connect);
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_printf("Not connecting to null.\n");
|
||||
_tf_ssb_connect_callback(NULL, "Not connecting to null.", connect);
|
||||
}
|
||||
JS_FreeCString(context, pubkey_str);
|
||||
JS_FreeCString(context, address_str);
|
||||
@ -1673,7 +1697,7 @@ static JSValue _tf_ssb_connect(JSContext* context, JSValueConst this_val, int ar
|
||||
JS_FreeValue(context, pubkey);
|
||||
}
|
||||
}
|
||||
return JS_UNDEFINED;
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct _forget_stored_connection_t
|
||||
|
@ -268,7 +268,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
|
||||
|
||||
uint8_t id0bin[k_id_bin_len];
|
||||
tf_ssb_id_str_to_bin(id0bin, id0);
|
||||
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0);
|
||||
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0, NULL, NULL);
|
||||
|
||||
tf_printf("Waiting for connection.\n");
|
||||
while (test.connection_count0 != 1 || test.connection_count1 != 1)
|
||||
@ -480,8 +480,8 @@ void tf_ssb_test_rooms(const tf_test_options_t* options)
|
||||
|
||||
uint8_t id0bin[k_id_bin_len];
|
||||
tf_ssb_id_str_to_bin(id0bin, id0);
|
||||
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0);
|
||||
tf_ssb_connect(ssb2, "127.0.0.1", 12347, id0bin, 0);
|
||||
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0, NULL, NULL);
|
||||
tf_ssb_connect(ssb2, "127.0.0.1", 12347, id0bin, 0, NULL, NULL);
|
||||
|
||||
tf_printf("Waiting for connection.\n");
|
||||
while (test.connection_count0 != 2 || test.connection_count1 != 1 || test.connection_count2 != 1)
|
||||
@ -712,7 +712,7 @@ void tf_ssb_test_bench(const tf_test_options_t* options)
|
||||
tf_ssb_register(tf_ssb_get_context(ssb1), ssb1);
|
||||
|
||||
tf_ssb_server_open(ssb0, 12347);
|
||||
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0);
|
||||
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0, NULL, NULL);
|
||||
|
||||
tf_printf("Waiting for messages.\n");
|
||||
clock_gettime(CLOCK_REALTIME, &start_time);
|
||||
@ -881,8 +881,8 @@ void tf_ssb_test_go_ssb_room(const tf_test_options_t* options)
|
||||
|
||||
tf_ssb_add_broadcasts_changed_callback(ssb0, _ssb_test_room_broadcasts_changed, NULL, NULL);
|
||||
|
||||
tf_ssb_connect_str(ssb0, "net:linode.unprompted.com:8008~shs:Q0pc/7kXQJGIlqJxuwayL2huayzddgkVDoGkYVWQS1Y=:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", 0);
|
||||
tf_ssb_connect_str(ssb1, "net:linode.unprompted.com:8008~shs:Q0pc/7kXQJGIlqJxuwayL2huayzddgkVDoGkYVWQS1Y=:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", 0);
|
||||
tf_ssb_connect_str(ssb0, "net:linode.unprompted.com:8008~shs:Q0pc/7kXQJGIlqJxuwayL2huayzddgkVDoGkYVWQS1Y=:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", 0, NULL, NULL);
|
||||
tf_ssb_connect_str(ssb1, "net:linode.unprompted.com:8008~shs:Q0pc/7kXQJGIlqJxuwayL2huayzddgkVDoGkYVWQS1Y=:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", 0, NULL, NULL);
|
||||
|
||||
uv_run(&loop, UV_RUN_DEFAULT);
|
||||
|
||||
@ -982,8 +982,8 @@ void tf_ssb_test_peer_exchange(const tf_test_options_t* options)
|
||||
tf_ssb_whoami(ssb0, id0, sizeof(id0));
|
||||
uint8_t id0bin[k_id_bin_len];
|
||||
tf_ssb_id_str_to_bin(id0bin, id0);
|
||||
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0);
|
||||
tf_ssb_connect(ssb2, "127.0.0.1", 12347, id0bin, 0);
|
||||
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0, NULL, NULL);
|
||||
tf_ssb_connect(ssb2, "127.0.0.1", 12347, id0bin, 0, NULL, NULL);
|
||||
|
||||
while (_count_broadcasts(ssb0) != 2 || _count_broadcasts(ssb1) != 1 || _count_broadcasts(ssb2) != 1)
|
||||
{
|
||||
@ -1015,6 +1015,8 @@ void tf_ssb_test_publish(const tf_test_options_t* options)
|
||||
char id[k_id_base64_len] = { 0 };
|
||||
tf_ssb_whoami(ssb, id, sizeof(id));
|
||||
|
||||
tf_ssb_destroy(ssb);
|
||||
|
||||
char executable[1024];
|
||||
size_t size = sizeof(executable);
|
||||
uv_exepath(executable, &size);
|
||||
@ -1033,8 +1035,6 @@ void tf_ssb_test_publish(const tf_test_options_t* options)
|
||||
printf("returned %d\n", WEXITSTATUS(result));
|
||||
assert(WEXITSTATUS(result) == 0);
|
||||
|
||||
tf_ssb_destroy(ssb);
|
||||
|
||||
uv_run(&loop, UV_RUN_DEFAULT);
|
||||
uv_loop_close(&loop);
|
||||
}
|
||||
|
@ -1869,7 +1869,6 @@ void tf_task_destroy(tf_task_t* task)
|
||||
{
|
||||
JSValue global = JS_GetGlobalObject(task->_context);
|
||||
JS_SetPropertyStr(task->_context, global, "httpd", JS_UNDEFINED);
|
||||
JS_SetPropertyStr(task->_context, global, "gProcesses", JS_NewObject(task->_context));
|
||||
JS_FreeValue(task->_context, global);
|
||||
}
|
||||
|
||||
|
138
src/tests.c
138
src/tests.c
@ -4,6 +4,8 @@
|
||||
#include "http.h"
|
||||
#include "log.h"
|
||||
#include "mem.h"
|
||||
#include "ssb.db.h"
|
||||
#include "ssb.h"
|
||||
#include "ssb.tests.h"
|
||||
#include "util.js.h"
|
||||
|
||||
@ -782,6 +784,140 @@ static void _test_http(const tf_test_options_t* options)
|
||||
uv_thread_join(&thread);
|
||||
}
|
||||
|
||||
static int _http_get_status_code(const char* url)
|
||||
{
|
||||
char command[1024];
|
||||
snprintf(command, sizeof(command), "curl -s -o /dev/null -w '%%{http_code}' \"%s\"", url);
|
||||
char buffer[256] = "";
|
||||
FILE* file = popen(command, "r");
|
||||
char* result = fgets(buffer, sizeof(buffer), file);
|
||||
pclose(file);
|
||||
return result ? atoi(result) : -1;
|
||||
}
|
||||
|
||||
static void _http_check_status_code(const char* url, int expected_code)
|
||||
{
|
||||
char command[1024];
|
||||
snprintf(command, sizeof(command), "curl -s -o /dev/null -w '%%{http_code}' \"%s\"", url);
|
||||
char buffer[256] = "";
|
||||
FILE* file = popen(command, "r");
|
||||
char* result = fgets(buffer, sizeof(buffer), file);
|
||||
tf_printf("%s => %s\n", command, result);
|
||||
assert(atoi(buffer) == expected_code);
|
||||
assert(file);
|
||||
int status = pclose(file);
|
||||
(void)status;
|
||||
assert(WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
static void _http_check_body_contains(const char* url, const char* expected)
|
||||
{
|
||||
char command[1024];
|
||||
snprintf(command, sizeof(command), "curl -s \"%s\"", url);
|
||||
char buffer[1024] = "";
|
||||
FILE* file = popen(command, "r");
|
||||
bool found = false;
|
||||
while (!found)
|
||||
{
|
||||
char* result = fgets(buffer, sizeof(buffer), file);
|
||||
if (!result)
|
||||
{
|
||||
break;
|
||||
}
|
||||
found = strstr(buffer, expected) != NULL;
|
||||
if (found)
|
||||
{
|
||||
tf_printf("%s => found: \"%s\"\n", url, expected);
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
tf_printf("Didn't find \"%s\" in %s.\n", expected, url);
|
||||
}
|
||||
assert(found);
|
||||
assert(file);
|
||||
int status = pclose(file);
|
||||
(void)status;
|
||||
assert(WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
static void _test_httpd(const tf_test_options_t* options)
|
||||
{
|
||||
uv_loop_t loop = { 0 };
|
||||
uv_loop_init(&loop);
|
||||
|
||||
unlink("out/test_db0.sqlite");
|
||||
char command[256];
|
||||
snprintf(command, sizeof(command), "%s run -b 0 --db-path=out/test_db0.sqlite" TEST_ARGS, options->exe_path);
|
||||
|
||||
uv_stdio_container_t stdio[] = {
|
||||
[STDIN_FILENO] = { .flags = UV_IGNORE },
|
||||
[STDOUT_FILENO] = { .flags = UV_INHERIT_FD },
|
||||
[STDERR_FILENO] = { .flags = UV_INHERIT_FD },
|
||||
};
|
||||
uv_process_t process = { 0 };
|
||||
uv_spawn(&loop, &process,
|
||||
&(uv_process_options_t) {
|
||||
.file = options->exe_path,
|
||||
.args = (char*[]) { (char*)options->exe_path, "run", "-b0", "--db-path=out/test_db0.sqlite", "--http-port=8080", "--https-port=0", NULL },
|
||||
.stdio_count = sizeof(stdio) / sizeof(*stdio),
|
||||
.stdio = stdio,
|
||||
});
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
if (_http_get_status_code("http://localhost:8080/debug") == 200)
|
||||
{
|
||||
break;
|
||||
}
|
||||
uv_sleep(1000);
|
||||
}
|
||||
|
||||
tf_ssb_t* ssb = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite", NULL);
|
||||
const char* app_id = tf_ssb_db_get_property(ssb, "core", "path:test");
|
||||
tf_ssb_destroy(ssb);
|
||||
|
||||
_http_check_status_code("http://localhost:8080/404", 404);
|
||||
_http_check_status_code("http://localhost:8080/", 303);
|
||||
_http_check_status_code("http://localhost:8080/~core/apps/", 200);
|
||||
_http_check_status_code("http://localhost:8080/~core/apps", 303);
|
||||
_http_check_status_code("http://localhost:8080/~core/apps/view", 200);
|
||||
_http_check_body_contains("http://localhost:8080/~core/apps/", "<title>Tilde Friends</title>");
|
||||
_http_check_body_contains("http://localhost:8080/~core/apps/view", "\"type\":\"tildefriends-app\"");
|
||||
_http_check_body_contains("http://localhost:8080/~core/test/hello.txt", "Hello, world!");
|
||||
_http_check_status_code("http://localhost:8080/~core/test/nonexistent.txt", 404);
|
||||
_http_check_body_contains("http://localhost:8080/&MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=.sha256/view", "Hello, world!");
|
||||
|
||||
char url[1024];
|
||||
snprintf(url, sizeof(url), "http://localhost:8080/%s/", app_id);
|
||||
_http_check_body_contains(url, "<title>Tilde Friends</title>");
|
||||
snprintf(url, sizeof(url), "http://localhost:8080/%s/view", app_id);
|
||||
_http_check_body_contains(url, "\"type\":\"tildefriends-app\"");
|
||||
snprintf(url, sizeof(url), "http://localhost:8080/%s/hello.txt", app_id);
|
||||
_http_check_body_contains(url, "Hello, world!");
|
||||
tf_free((void*)app_id);
|
||||
|
||||
uv_process_kill(&process, SIGTERM);
|
||||
uv_close((uv_handle_t*)&process, NULL);
|
||||
uv_run(&loop, UV_RUN_ONCE);
|
||||
|
||||
uv_loop_close(&loop);
|
||||
}
|
||||
|
||||
static void _test_pattern(const tf_test_options_t* options)
|
||||
{
|
||||
assert(tf_http_pattern_matches("/~core/test/", "/~core/test/"));
|
||||
assert(tf_http_pattern_matches("/~core/test/*", "/~core/test/"));
|
||||
assert(tf_http_pattern_matches("/~core/test/*", "/~core/test/blah"));
|
||||
assert(tf_http_pattern_matches("*/~core/test/", "/~core/test/"));
|
||||
assert(tf_http_pattern_matches("*/~core/test/", "blah/~core/test/"));
|
||||
assert(tf_http_pattern_matches("/~*/*/", "/~core/test/"));
|
||||
assert(tf_http_pattern_matches("/~{word}/*", "/~core/test"));
|
||||
assert(tf_http_pattern_matches("/~{word}/{word}/", "/~core/test/"));
|
||||
assert(tf_http_pattern_matches("/~{word}/{word}", "/~core/test"));
|
||||
assert(!tf_http_pattern_matches("/~{word}/{word}", "/~foo/bar/baz"));
|
||||
}
|
||||
|
||||
static void _test_auto_process_exit(uv_process_t* process, int64_t status, int termination_signal)
|
||||
{
|
||||
tf_printf("Process exit %d signal=%d.\n", (int)WEXITSTATUS(status), termination_signal);
|
||||
@ -887,6 +1023,8 @@ void tf_tests(const tf_test_options_t* options)
|
||||
#if !TARGET_OS_IPHONE
|
||||
_tf_test_run(options, "bip39", _test_bip39, false);
|
||||
_tf_test_run(options, "http", _test_http, false);
|
||||
_tf_test_run(options, "httpd", _test_httpd, false);
|
||||
_tf_test_run(options, "pattern", _test_pattern, false);
|
||||
_tf_test_run(options, "ssb", tf_ssb_test_ssb, false);
|
||||
_tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false);
|
||||
_tf_test_run(options, "ssb_following", tf_ssb_test_following, false);
|
||||
|
@ -20,6 +20,8 @@
|
||||
#include <execinfo.h>
|
||||
#endif
|
||||
|
||||
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
|
||||
|
||||
static JSValue _util_utf8_encode(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
size_t length = 0;
|
||||
@ -304,6 +306,66 @@ static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val
|
||||
return result;
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
static bool _is_mobile()
|
||||
{
|
||||
#if defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IPHONE)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static JSValue _util_defaultGlobalSettings(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
typedef struct _setting_t
|
||||
{
|
||||
const char* name;
|
||||
const char* type;
|
||||
const char* description;
|
||||
JSValue default_value;
|
||||
} setting_t;
|
||||
|
||||
const setting_t k_settings[] = {
|
||||
{ .name = "code_of_conduct", .type = "textarea", .description = "Code of conduct presented at sign-in." },
|
||||
{ .name = "blob_fetch_age_seconds",
|
||||
.type = "integer",
|
||||
.description = "Only blobs mentioned more recently than this age will be automatically fetched.",
|
||||
.default_value = _is_mobile() ? JS_NewInt32(context, (int)(0.5f * 365 * 24 * 60 * 60)) : JS_UNDEFINED },
|
||||
{ .name = "blob_expire_age_seconds",
|
||||
.type = "integer",
|
||||
.description = "Blobs older than this will be automatically deleted.",
|
||||
.default_value = _is_mobile() ? JS_NewInt32(context, (int)(1.0f * 365 * 24 * 60 * 60)) : JS_UNDEFINED },
|
||||
{ .name = "fetch_hosts", .type = "string", .description = "Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty." },
|
||||
{ .name = "http_redirect", .type = "string", .description = "If connecting by HTTP and HTTPS is configured, Location header prefix (ie, \"http://example.com\")" },
|
||||
{ .name = "index", .type = "string", .description = "Default path.", .default_value = JS_NewString(context, "/~core/apps") },
|
||||
{ .name = "index_map", .type = "textarea", .description = "Mappings from hostname to redirect path, one per line, as in: \"www.tildefriends.net=/~core/index/\"" },
|
||||
{ .name = "peer_exchange",
|
||||
.type = "boolean",
|
||||
.description = "Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.",
|
||||
.default_value = JS_FALSE },
|
||||
{ .name = "replicator", .type = "boolean", .description = "Enable message and blob replication.", .default_value = JS_TRUE },
|
||||
{ .name = "room", .type = "boolean", .description = "Enable peers to tunnel through this instance as a room.", .default_value = JS_TRUE },
|
||||
{ .name = "room_name", .type = "string", .description = "Name of the room.", .default_value = JS_NewString(context, "tilde friends tunnel") },
|
||||
{ .name = "seeds_host", .type = "string", .description = "Hostname for seed connections.", .default_value = JS_NewString(context, "seeds.tildefriends.net") },
|
||||
{ .name = "account_registration", .type = "boolean", .description = "Allow registration of new accounts.", .default_value = JS_TRUE },
|
||||
};
|
||||
|
||||
JSValue settings = JS_NewObject(context);
|
||||
for (int i = 0; i < tf_countof(k_settings); i++)
|
||||
{
|
||||
JSValue entry = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, entry, "type", JS_NewString(context, k_settings[i].type));
|
||||
JS_SetPropertyStr(context, entry, "description", JS_NewString(context, k_settings[i].description));
|
||||
JS_SetPropertyStr(context, entry, "default_value", k_settings[i].default_value);
|
||||
JS_SetPropertyStr(context, settings, k_settings[i].name, entry);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
JSValue tf_util_new_uint8_array(JSContext* context, const uint8_t* data, size_t size)
|
||||
{
|
||||
JSValue array_buffer = JS_NewArrayBufferCopy(context, data, size);
|
||||
@ -327,6 +389,7 @@ void tf_util_register(JSContext* context)
|
||||
JS_SetPropertyStr(context, global, "bip39Bytes", JS_NewCFunction(context, _util_bip39_bytes, "bip39Bytes", 1));
|
||||
JS_SetPropertyStr(context, global, "print", JS_NewCFunction(context, _util_print, "print", 1));
|
||||
JS_SetPropertyStr(context, global, "parseHttpResponse", JS_NewCFunction(context, _util_parseHttpResponse, "parseHttpResponse", 2));
|
||||
JS_SetPropertyStr(context, global, "defaultGlobalSettings", JS_NewCFunction(context, _util_defaultGlobalSettings, "defaultGlobalSettings", 2));
|
||||
JS_FreeValue(context, global);
|
||||
}
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
#define VERSION_NUMBER "0.0.24"
|
||||
#define VERSION_NAME "Honey bunches of boats."
|
||||
#define VERSION_NUMBER "0.0.25-wip"
|
||||
#define VERSION_NAME "This program kills fascists."
|
||||
|
@ -198,7 +198,12 @@ try:
|
||||
|
||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
|
||||
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
|
||||
while True:
|
||||
try:
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guest_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').click()
|
||||
@ -255,7 +260,12 @@ try:
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('new_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
|
||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
|
||||
while True:
|
||||
try:
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
|
||||
|
55
tools/buttfeed.py
Executable file
55
tools/buttfeed.py
Executable file
@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import feedparser
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--state_file', help = 'Path to a file in which to store state.')
|
||||
args = parser.parse_args()
|
||||
|
||||
k_feeds = {
|
||||
'tildefriends': 'https://dev.tildefriends.net/cory/tildefriends.rss',
|
||||
'erlbutt': 'https://github.com/cmoid/erlbutt/commits/main.atom',
|
||||
'habitat': 'https://tildegit.org/jeremylist/habitat.rss',
|
||||
'manyverse': 'https://gitlab.com/staltz/manyverse/-/commits/master?format=atom',
|
||||
'ahau': 'https://gitlab.com/ahau/ahau/-/commits/master.atom',
|
||||
}
|
||||
|
||||
def fix_title(entry):
|
||||
if entry.summary.startswith('<a href=') and '\n' in entry.summary and entry.summary and '\n' in entry.summary:
|
||||
return entry.summary.split('\n')[1]
|
||||
return entry.title.split('\n')[0]
|
||||
|
||||
def get_entries():
|
||||
results = []
|
||||
for name, url in k_feeds.items():
|
||||
feed = feedparser.parse(url)
|
||||
for entry in feed.entries:
|
||||
results.append((time.mktime(entry.get('updated_parsed')), name, entry.link, fix_title(entry)))
|
||||
results.sort()
|
||||
results.reverse()
|
||||
return results
|
||||
|
||||
state = {}
|
||||
if args.state_file:
|
||||
try:
|
||||
with open(args.state_file, 'r') as f:
|
||||
state = json.load(f)
|
||||
except:
|
||||
pass
|
||||
cutoff = state.get('last_update') or (time.time() - 2 * 7 * 24 * 60 * 60)
|
||||
|
||||
entries = [entry for entry in get_entries() if entry[0] > cutoff]
|
||||
if entries:
|
||||
text = '\n'.join([f' * [{entry[1]}] [{entry[3]}]({entry[2]})' for entry in entries])
|
||||
state['last_update'] = entries[0][0]
|
||||
if args.state_file:
|
||||
content = json.dumps({'type': 'post', 'text': text, 'mentions': []})
|
||||
subprocess.check_call(['out/debug/tildefriends', 'publish', '--user', 'cory', '--id', '@DnYDqFfmxdNkYQlpflF9Wkltk2HIhJ5u1MW5njKPLzM=.ed25519', '--content', content])
|
||||
with open(args.state_file, 'w') as f:
|
||||
json.dump(state, f)
|
||||
else:
|
||||
print(text)
|
Loading…
Reference in New Issue
Block a user