@yz-social/civildefense.io 4.5.4 → 4.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/public/javascripts/agent.js +2 -2
- package/public/javascripts/alert.js +24 -23
- package/public/javascripts/conversation.js +1 -2
- package/public/javascripts/hashtags.js +1 -1
- package/public/javascripts/map.js +1 -1
- package/public/javascripts/p2pWebNetwork.js +8 -7
- package/public/javascripts/versions.js +2 -2
- package/public/service-worker.js +2 -2
- package/movie/camera.jpg +0 -0
- package/movie/movie.js +0 -151
- package/movie/script.js +0 -272
- package/movie/test.js +0 -2
- package/nginx/nginx.conf +0 -83
- package/nginx/yz.social +0 -81
- package/public/about/In case of Nazis, use CivilDefense.io.png +0 -0
- package/server/bridge.js +0 -397
- package/server/identity.js +0 -111
- package/spec/axonSpec.gratuitousNameChangeForSignal +0 -246
- package/spec/axonSpec.jsRemoveThePartAfterJS +0 -252
- package/spec/civildefenseSpec.js +0 -61
- package/spec/pubsubSpec.js +0 -128
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yz-social/civildefense.io",
|
|
3
3
|
"description": "Browser app to safely share live sightings on a map.",
|
|
4
|
-
"version": "4.5.
|
|
4
|
+
"version": "4.5.6",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"map",
|
|
7
7
|
"browser",
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "npm stop; node ./server/app.js",
|
|
12
|
-
"stop": "pkill yz.social"
|
|
12
|
+
"stop": "pkill yz.social",
|
|
13
|
+
"postinstall": "touch server/location.json; chmod a+w server/location.json"
|
|
13
14
|
},
|
|
14
15
|
"type": "module",
|
|
15
16
|
"exports": {
|
|
16
17
|
".": "./index.js"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@axona/protocol": "github:axona-net/axona-protocol#
|
|
20
|
+
"@axona/protocol": "github:axona-net/axona-protocol#5bf6f888cbbb5a58b9700c11582a4b6ca4c3141e",
|
|
20
21
|
"cors": "^2.8.6",
|
|
21
22
|
"express": "^4.22.1",
|
|
22
23
|
"leaflet": "^1.9.4",
|
|
@@ -70,7 +70,7 @@ export class Agent {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
async setPublicData(data) { // Subscription to public data has fired. Update value, but do not not re-publish.
|
|
73
|
-
let {payload,
|
|
73
|
+
let {payload, tag, type, topic, ts} = data; // fixme remove topic and ts
|
|
74
74
|
// WARNING: IF we chunkify avatars, and we use since:'all', then we need lock out asynchronous
|
|
75
75
|
// decoding of later timestamps, or of null payloads.
|
|
76
76
|
// if (payload && (type === 'avatar')) {
|
|
@@ -79,7 +79,7 @@ export class Agent {
|
|
|
79
79
|
// payload = dataURL;
|
|
80
80
|
// }
|
|
81
81
|
this.updateValue(payload, 'public', type, false);
|
|
82
|
-
if (
|
|
82
|
+
if (tag) this.publicMsgId[type] = tag;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
static agents = {}; // tag => Agent
|
|
@@ -12,7 +12,7 @@ import { getContainingCells, findCoverCellsByCenterAndPoint } from './s2.js';
|
|
|
12
12
|
const { localStorage, getComputedStyle, URL, URLSearchParams, domtoimage } = globalThis;
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
export function getShareableURL(
|
|
15
|
+
export function getShareableURL(tag = null, tags = Hashtags.getSubscribe()) { // Answer a url that reflects application state.
|
|
16
16
|
const params = new URLSearchParams(location.search);
|
|
17
17
|
const zoom = map.getZoom();
|
|
18
18
|
const { lat, lng } = map.getCenter();
|
|
@@ -21,7 +21,7 @@ export function getShareableURL(subject = null, tags = Hashtags.getSubscribe())
|
|
|
21
21
|
if (lat !== null) params.set('lat', lat);
|
|
22
22
|
if (lng !== null) params.set('lng', lng);
|
|
23
23
|
if (zoom !== null) params.set('z', zoom);
|
|
24
|
-
if (
|
|
24
|
+
if (tag !== null) params.set('alert', tag);
|
|
25
25
|
return new URL(`?${params.toString()}`, location);
|
|
26
26
|
}
|
|
27
27
|
export async function share(properties) { // Invoke platform share API on properties.
|
|
@@ -61,14 +61,14 @@ export async function share(properties) { // Invoke platform share API on prope
|
|
|
61
61
|
|
|
62
62
|
const ttl = 24 * 60 * 60e3; // 24 hours
|
|
63
63
|
let openOnReceive = null;
|
|
64
|
-
export function go({lat = null, lng = null, zoom = null,
|
|
64
|
+
export function go({lat = null, lng = null, zoom = null, alert = null}) { // Go to specified location (if any) and open marker (if any).
|
|
65
65
|
if (lat !== null && lng !== null) {
|
|
66
66
|
if (zoom) map.flyTo({lat, lng}, zoom);
|
|
67
67
|
else map.flyTo({lat, lng});
|
|
68
68
|
}
|
|
69
69
|
openOnReceive = null;
|
|
70
|
-
if (
|
|
71
|
-
Alert.openPopup(
|
|
70
|
+
if (alert) {
|
|
71
|
+
Alert.openPopup(alert) || (openOnReceive = alert);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -77,9 +77,8 @@ let subscriptionsRegion;
|
|
|
77
77
|
// We do not record exactly where you were looking across sessions, but we do record the containing level 9 cell.
|
|
78
78
|
let lastLevel9Cell; // S2 level 9 cells average a radius of about 10km ~ 6.5 miles.
|
|
79
79
|
|
|
80
|
-
let last = []; // Last published lat, lng,
|
|
80
|
+
let last = []; // Last published lat, lng, tag
|
|
81
81
|
const maxPublish = 5;
|
|
82
|
-
// Publish an alert to all applicable eventNames, canceling as required. Promises subject (msgId).
|
|
83
82
|
let publishing = false;
|
|
84
83
|
|
|
85
84
|
export class Alert extends Conversation { // A wrapper around L.marker
|
|
@@ -116,6 +115,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
116
115
|
subscriptions = newKeys;
|
|
117
116
|
subscriptionsRegion = region;
|
|
118
117
|
}
|
|
118
|
+
// Publish an alert to all applicable eventNames, canceling as required. Promises tag (msgId).
|
|
119
119
|
static async publish({lat, lng,
|
|
120
120
|
originalPosting = undefined,
|
|
121
121
|
hashtag = Hashtags.getPublish(),
|
|
@@ -129,7 +129,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
129
129
|
// However, the 'unpublishing' (if any) is invoked first.
|
|
130
130
|
// To do this, we must hash the eventName ourselves.
|
|
131
131
|
//console.log('publish', {lat, lng, hashtag, payload, cancel, subject, issuedTime, rest});
|
|
132
|
-
if (publishing) { console.log('skiping overlappying publish'); return; } // do not stack them up.
|
|
132
|
+
if (publishing) { console.log('skiping overlappying publish'); return null; } // do not stack them up.
|
|
133
133
|
try {
|
|
134
134
|
publishing = true;
|
|
135
135
|
|
|
@@ -154,7 +154,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
154
154
|
for (const cell of oldCells) {
|
|
155
155
|
const eventName = alertTopic(cell, hashtag);
|
|
156
156
|
// Note: we cannot unpublish replies by others, but they expire after a while anyway.
|
|
157
|
-
await contact.publish({eventName, region, subject, payload: null});
|
|
157
|
+
await contact.publish({eventName, region, killTag: subject, payload: null});
|
|
158
158
|
throttleMS && await P2PWebNetwork.delay(throttleMS);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
@@ -172,7 +172,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
172
172
|
lastFillIn = null;
|
|
173
173
|
}
|
|
174
174
|
} else {
|
|
175
|
-
await contact.publish({eventName, region, subject, payload: null});
|
|
175
|
+
await contact.publish({eventName, region, killTag: subject, payload: null});
|
|
176
176
|
throttleMS && await P2PWebNetwork.delay(throttleMS);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
@@ -191,8 +191,8 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
191
191
|
static closePopup() { // Close any open popup.
|
|
192
192
|
map.closePopup();
|
|
193
193
|
}
|
|
194
|
-
static openPopup(
|
|
195
|
-
const wrapper = this.getItem(
|
|
194
|
+
static openPopup(alertTag) { // Open the marker specified by subject.
|
|
195
|
+
const wrapper = this.getItem(alertTag);
|
|
196
196
|
wrapper?.openPopup();
|
|
197
197
|
}
|
|
198
198
|
async openPopup() { // Open this wrapper's popup, and resolve any waiting promise.
|
|
@@ -230,8 +230,8 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
230
230
|
wrapper.initChangeHashtag(popupAttribution);
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
|
-
static ensure({
|
|
234
|
-
const alert = super.ensure({tag
|
|
233
|
+
static ensure({tag, topic, ts, issuedTime, ...rest}) { // Add marker at position with appropriate fade if not already present.
|
|
234
|
+
const alert = super.ensure({tag, subject: tag, issuedTime, ...rest}); // Does not include topic or ts. fixme: get rid of subject. fixme: why is ts different?
|
|
235
235
|
if (!alert) return null;
|
|
236
236
|
// Regardless of initialize vs update, reset fader.
|
|
237
237
|
const now = Date.now(),
|
|
@@ -242,7 +242,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
242
242
|
alert.destroyer = setTimeout(() => alert.destroy(), remaining);
|
|
243
243
|
return alert;
|
|
244
244
|
}
|
|
245
|
-
initialize({payload, hashtag, subject, agent, issuedTime, ...rest}) { // Set up the marker.
|
|
245
|
+
initialize({payload, hashtag, subject, agent, issuedTime, ...rest}) { // Set up the marker for a newly received alert.
|
|
246
246
|
if (!payload) return null; // Do not cache. E.g., Received a delete event without the initial creation.
|
|
247
247
|
const icon = this.constructor.makeIcon(hashtag);
|
|
248
248
|
const {lat, lng, originalPosting} = payload;
|
|
@@ -261,7 +261,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
261
261
|
networkPromise.then(async contact => {
|
|
262
262
|
contact.subscribe({eventName: subject, region, handler: data => this.ensure(data)});
|
|
263
263
|
});
|
|
264
|
-
this.showNotification({
|
|
264
|
+
this.showNotification({agent, issuedTime});
|
|
265
265
|
return this;
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -409,9 +409,9 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
409
409
|
|
|
410
410
|
// Each reply is separately published by its author, and only they can modify/unpublish it.
|
|
411
411
|
async ensure(data) { // Add or update reply for this marker.
|
|
412
|
-
data.
|
|
412
|
+
data.subject = data.tag; //fixme
|
|
413
413
|
const reply = super.ensure(data);
|
|
414
|
-
if (reply) {
|
|
414
|
+
if (reply) { // TODO? Move to AlertReply.initialize()?
|
|
415
415
|
const {agent, issuedTime, payload} = data;
|
|
416
416
|
const {file} = payload;
|
|
417
417
|
if (file) {
|
|
@@ -453,17 +453,18 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
453
453
|
deleteReply(replyElement) {
|
|
454
454
|
resetInactivityTimer();
|
|
455
455
|
const {region} = this;
|
|
456
|
-
networkPromise.then(async contact => contact.publish({eventName: this.subject, region,
|
|
456
|
+
networkPromise.then(async contact => contact.publish({eventName: this.subject, region, killTag: replyElement.dataset.subject, payload: null}));
|
|
457
457
|
}
|
|
458
|
-
showNotification({issuedTime = this.issuedTime, body = '', agent = this.agent,
|
|
458
|
+
showNotification({issuedTime = this.issuedTime, body = '', agent = this.agent, alert = this.subject, lat = this.lat, lng = this.lng, hashtag = this.hashtag}) {
|
|
459
459
|
// Give OS notification that comes back to here, unless act is us.
|
|
460
|
-
|
|
460
|
+
// All notifications on the same alert (e.g., the post and each reply) have the same tag, so OS can collapse them.
|
|
461
|
+
if (agent === Agent.tag || !notificationsAllowed()) return;
|
|
461
462
|
navigator.serviceWorker.ready.then(registration => {
|
|
462
463
|
const timestamp = issuedTime;
|
|
463
464
|
const icon = new URL('./images/civil-defense-192.png', location.href).href;
|
|
464
|
-
const url = getShareableURL(
|
|
465
|
+
const url = getShareableURL(alert, [hashtag]).href; // For opening page when it has been closed.
|
|
465
466
|
const data = {lat, lng, url};
|
|
466
|
-
const options = {icon, timestamp, tag, body, data};
|
|
467
|
+
const options = {icon, timestamp, tag: alert, body, data};
|
|
467
468
|
console.log('showNotification', hashtag, options);
|
|
468
469
|
registration.showNotification(hashtag, options);
|
|
469
470
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// TODO:
|
|
2
|
-
// - clarify common lifecycle management, including existing reply to not get re-initialized
|
|
3
2
|
// - In Alert, get rid of subject (just use tag)
|
|
4
3
|
// - key off 'deleted' instead of payload:null, and bring handling here
|
|
5
4
|
// - Bring pub/sub here, customized for SpatialConversation subclass
|
|
@@ -20,7 +19,7 @@ export class Tagged { // Maintains cached existence within a (possibly instance-
|
|
|
20
19
|
for (const key in properties) {
|
|
21
20
|
const existing = this[key];
|
|
22
21
|
const proposed = properties[key];
|
|
23
|
-
if (JSON.stringify(existing) !== JSON.stringify(proposed)) throw new Error(`Cannot update ${key} ${existing} to ${proposed}.`);
|
|
22
|
+
if (JSON.stringify(existing) !== JSON.stringify(proposed)) throw new Error(`Cannot update '${key}' from ${existing} to ${proposed}.`);
|
|
24
23
|
}
|
|
25
24
|
return this;
|
|
26
25
|
}
|
|
@@ -84,7 +84,7 @@ export const Hashtags = {
|
|
|
84
84
|
localStorage.setItem('hashtags', JSON.stringify(this.hashtags));
|
|
85
85
|
if (resetSubscriptions) {
|
|
86
86
|
Alert.updateSubscriptions();
|
|
87
|
-
Alert.items.forEach(wrapper => this.hashtags[wrapper.hashtag] || wrapper.destroy());
|
|
87
|
+
Object.values(Alert.items).forEach(wrapper => this.hashtags[wrapper.hashtag] || wrapper.destroy());
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
90
|
chipset: document.body.querySelector('.watching-hashtags'), // Element containing the user's chips.
|
|
@@ -48,7 +48,7 @@ export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if ne
|
|
|
48
48
|
const tagsArray = tags?.split(',') || [];
|
|
49
49
|
tagsArray.forEach(tag => Hashtags.add(decodeURIComponent(tag)));
|
|
50
50
|
Hashtags.onchange({resetSubscriptions: false}); // Too early to subscribe, but will be done during initialization.
|
|
51
|
-
go({lat: params.get('lat'), lng: params.get('lng'), zoom: params.get('z'),
|
|
51
|
+
go({lat: params.get('lat'), lng: params.get('lng'), zoom: params.get('z'), alert: params.get('alert')});
|
|
52
52
|
// We don't need the query parameters now. Get rid of them. They're annoying.
|
|
53
53
|
const copy = new URL(location);
|
|
54
54
|
const dht = copy.searchParams.get('dht');
|
|
@@ -2,7 +2,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
2
2
|
import { createNodeIdentity, createAuthorIdentity, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION } from '@axona/protocol';
|
|
3
3
|
import { stringToBytes, bytesToString, publishChunkedBytes, receiveChunkedBytes } from '@axona/protocol/std';
|
|
4
4
|
import { connect } from '@axona/protocol/connect.js';
|
|
5
|
-
globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection);
|
|
6
5
|
const { BigInt, URL, File, pica } = globalThis;
|
|
7
6
|
|
|
8
7
|
/* Example:
|
|
@@ -25,6 +24,7 @@ export class P2PWebNetwork {
|
|
|
25
24
|
} = {}) {
|
|
26
25
|
// Promise a ready-to-use network peer.
|
|
27
26
|
region = await region;
|
|
27
|
+
|
|
28
28
|
const { peer, nodeIdentity, transport, status, disconnect } = await connect({
|
|
29
29
|
bridge: bridgeUrl,
|
|
30
30
|
location: region,
|
|
@@ -163,10 +163,10 @@ export class P2PWebNetwork {
|
|
|
163
163
|
const {message, deleted, msgId, signerPubkey, topic, ts} = envelope;
|
|
164
164
|
this.debug('received', {msgId, topic, ts, signerPubkey, deleted, message});
|
|
165
165
|
if (deleted) {
|
|
166
|
-
handler({
|
|
166
|
+
handler({tag: msgId, payload: null, agent: signerPubkey, topic, ts}); // fixme remove topic, ts here and below.
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
|
-
handler({...message, agent: signerPubkey,
|
|
169
|
+
handler({...message, agent: signerPubkey, tag: msgId, topic, ts});
|
|
170
170
|
};
|
|
171
171
|
await this.peer.sub(topic, callback, {since});
|
|
172
172
|
} else {
|
|
@@ -174,17 +174,18 @@ export class P2PWebNetwork {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
static currentPublishIdentity = null;
|
|
177
|
-
async publish({eventName, region, owner, signWith = this.constructor.currentPublishIdentity, issuedTime = Date.now(),
|
|
177
|
+
async publish({eventName, region, owner, signWith = this.constructor.currentPublishIdentity, issuedTime = Date.now(), killTag, payload, ...rest}) {
|
|
178
178
|
// Publish data to subscribers of eventName.
|
|
179
|
+
if (killTag && payload) throw new Error(`Specify killTag (${killTag}) or payload ($(JSON.stringify(payload)}), but not both.`);
|
|
179
180
|
await this.attachment; // Get connected.
|
|
180
181
|
const topic = {region, name: eventName};
|
|
181
182
|
if (owner) topic.owner = owner;
|
|
182
183
|
const options = {signWith};
|
|
183
|
-
this.debug('published', {topic,
|
|
184
|
+
this.debug('published', {topic, killTag, payload, issuedTime, rest, signWith});
|
|
184
185
|
if (payload) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
|
|
185
186
|
// The next would not normally happen, but until since:'latest' works, we need a way to send a null payload and have the handler delete the entry.
|
|
186
|
-
if (!
|
|
187
|
-
return await this.peer.kill(topic,
|
|
187
|
+
if (!killTag) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
|
|
188
|
+
return await this.peer.kill(topic, killTag, options);
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
host() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import pkg from '
|
|
1
|
+
import pkg from '../../package.json' with {type: 'json'};
|
|
2
2
|
export const appVersion = pkg.version; // Overall semver of app. Used in display, and for comparison by service worker.
|
|
3
|
-
export const dataVersion = appVersion.split('.')[0]; // Compatability differentiator used below.
|
|
3
|
+
export const dataVersion = globalThis.process?.env.EVENT_VERSION || appVersion.split('.')[0]; // Compatability differentiator used below.
|
|
4
4
|
|
|
5
5
|
export function stripLeadingEmoji(string) { // Return string without any leading emoji (which might be of varying
|
|
6
6
|
// length) followed by an optional emoji break character and any whitespace.
|
package/public/service-worker.js
CHANGED
|
@@ -208,10 +208,10 @@ self.addEventListener('notificationclick', event => {
|
|
|
208
208
|
clients
|
|
209
209
|
.matchAll({type: 'window', includeUncontrolled: true})
|
|
210
210
|
.then(async clientList => {
|
|
211
|
-
console.log('notification', {title, body, data, clientList});
|
|
211
|
+
console.log('notification', {title, body, tag, data, clientList});
|
|
212
212
|
for (const client of clientList) {
|
|
213
213
|
console.log('notification click found client');
|
|
214
|
-
return client.focus().then(() => client.postMessage({method: 'go', params: {
|
|
214
|
+
return client.focus().then(() => client.postMessage({method: 'go', params: {alert: tag, ...data}}));
|
|
215
215
|
}
|
|
216
216
|
// Client has been closed. Open one.
|
|
217
217
|
console.log('notification click opening client', data.url);
|
package/movie/camera.jpg
DELETED
|
Binary file
|