@yz-social/civildefense.io 4.4.1
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/README.md +68 -0
- package/announce.js +24 -0
- package/docs/YZ-Brief.pdf +0 -0
- package/index.js +2 -0
- package/movie/camera.jpg +0 -0
- package/movie/movie.js +151 -0
- package/movie/script.js +272 -0
- package/movie/test.js +2 -0
- package/nginx/nginx.conf +83 -0
- package/nginx/yz.social +81 -0
- package/package.json +34 -0
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -0
- package/public/about/CivilDefense.mp4 +0 -0
- package/public/about/In case of Nazis, use CivilDefense.io.png +0 -0
- package/public/about/broadcast.png +0 -0
- package/public/about/civil-defense.png +0 -0
- package/public/about/conversation-and-image-high.png +0 -0
- package/public/about/conversation-and-image.png +0 -0
- package/public/about/en.html +157 -0
- package/public/about/es.html +142 -0
- package/public/about/flood-fire-ice-high.png +0 -0
- package/public/about/flood-fire-ice.png +0 -0
- package/public/about/hero-high.png +0 -0
- package/public/about/hero.png +0 -0
- package/public/about/index.html +8 -0
- package/public/about/nazis.png +0 -0
- package/public/about/script.js +69 -0
- package/public/about/streaming-radio-high.png +0 -0
- package/public/about/streaming-radio.png +0 -0
- package/public/about/style.css +306 -0
- package/public/control-safe-rectangle.html +40 -0
- package/public/favicon.ico +0 -0
- package/public/images/Achtung.png +0 -0
- package/public/images/YZ Owl.png +0 -0
- package/public/images/civil-defense-122.png +0 -0
- package/public/images/civil-defense-192.png +0 -0
- package/public/images/civil-defense-240.png +0 -0
- package/public/images/civil-defense-512.png +0 -0
- package/public/images/civil-defense.png +0 -0
- package/public/images/hero-small.png +0 -0
- package/public/images/qr-scan.svg +2 -0
- package/public/images/qr.png +0 -0
- package/public/images/qr.svg +155 -0
- package/public/images/recenter.svg +5 -0
- package/public/images/share.png +0 -0
- package/public/images/share.svg +2 -0
- package/public/index.html +170 -0
- package/public/javascripts/agent.js +324 -0
- package/public/javascripts/display.js +25 -0
- package/public/javascripts/hashtags.js +205 -0
- package/public/javascripts/main.js +394 -0
- package/public/javascripts/map.js +725 -0
- package/public/javascripts/p2pWebNetwork.js +245 -0
- package/public/javascripts/s2.js +77 -0
- package/public/javascripts/scripting.js +112 -0
- package/public/javascripts/service-manager.js +149 -0
- package/public/javascripts/translations.js +139 -0
- package/public/javascripts/versions.js +23 -0
- package/public/manifest.json +25 -0
- package/public/owl.ico +0 -0
- package/public/platformer.html +52 -0
- package/public/robots.txt +6 -0
- package/public/service-worker.js +218 -0
- package/public/stylesheets/style.css +442 -0
- package/routes/index.js +123 -0
- package/server/app.js +116 -0
- package/server/bridge.js +397 -0
- package/server/dirname.js +15 -0
- package/server/getLocation.js +18 -0
- package/server/identity.js +111 -0
- package/server/location.json +12 -0
- package/spec/axonSpec.gratuitousNameChangeForSignal +246 -0
- package/spec/axonSpec.js +280 -0
- package/spec/axonSpec.jsRemoveThePartAfterJS +252 -0
- package/spec/civildefenseSpec.js +61 -0
- package/spec/pubsubSpec.js +128 -0
- package/spec/support/jasmine.mjs +14 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--md-filter-chip-label-text-font: roboto;
|
|
3
|
+
--md-sys-color-primary: #0A2E7C;
|
|
4
|
+
--md-sys-color-outline: var(--md-sys-color-primary);
|
|
5
|
+
--md-filter-chip-selected-leading-icon-color: var(--md-sys-color-primary);
|
|
6
|
+
|
|
7
|
+
--md-sys-color-secondary: #BD2E2F;
|
|
8
|
+
--md-filled-text-field-input-text-placeholder-color: var(--md-sys-color-secondary);
|
|
9
|
+
|
|
10
|
+
--md-sys-color-on-surface: #191c1c;
|
|
11
|
+
--md-sys-color-secondary-container: white; /* selected filter chip */
|
|
12
|
+
--md-sys-color-surface-container-highest: white; /* filled text field */
|
|
13
|
+
--md-menu-container-color: white;
|
|
14
|
+
/* --md-filled-text-field-container-shape: 16px; */
|
|
15
|
+
/* --md-filter-chip-container-shape: 16px; */
|
|
16
|
+
--md-sys-shape-corner-full: 12px;
|
|
17
|
+
|
|
18
|
+
--about-image-size: 80px;
|
|
19
|
+
|
|
20
|
+
/* --md-sys-color-tertiary-container: blue; */
|
|
21
|
+
/* --md-sys-color-background: yellow; */
|
|
22
|
+
/* --md-sys-color-primary-container: green; /\* #B8C2D7; /\* #cce8e7;*\/ */
|
|
23
|
+
/* --md-sys-color-surface: wheat; */
|
|
24
|
+
/* --md-sys-color-surface-container: magenta; */
|
|
25
|
+
/* --md-sys-color-surface-container-high: blue; */
|
|
26
|
+
/* --md-sys-color-surface-container-highest: purple; */
|
|
27
|
+
/* --md-sys-color-surface-container-lowest: purple; */
|
|
28
|
+
}
|
|
29
|
+
* {
|
|
30
|
+
margin: 0;
|
|
31
|
+
padding: 0;
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
body {
|
|
36
|
+
font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
color: var(--md-sys-color-primary);
|
|
39
|
+
}
|
|
40
|
+
.leaflet-bar a, .leaflet-container a.leaflet-popup-close-button, .leaflet-popup-content-wrapper {
|
|
41
|
+
color: var(--md-sys-color-primary);
|
|
42
|
+
border-color: var(--md-sys-color-primary);
|
|
43
|
+
}
|
|
44
|
+
.leaflet-touch .leaflet-bar {
|
|
45
|
+
border-radius: 12px;
|
|
46
|
+
& > a:first-child { border-top-left-radius: 12px; border-top-right-radius: 12px; }
|
|
47
|
+
& > a:last-child { border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; }
|
|
48
|
+
}
|
|
49
|
+
:not(md-filled-icon-button) > md-icon {
|
|
50
|
+
color: var(--md-sys-color-primary);
|
|
51
|
+
}
|
|
52
|
+
.remove, .icon.leading, .reply-input md-tonal-icon-button md-icon {
|
|
53
|
+
color: var(--md-sys-color-primary);
|
|
54
|
+
--md-menu-item-supporting-text-color: var(--md-sys-color-primary);
|
|
55
|
+
--md-menu-item-label-text-color: var(--md-sys-color-primary);
|
|
56
|
+
}
|
|
57
|
+
md-outlined-text-field {
|
|
58
|
+
--md-outlined-text-field-label-text-color: var(--md-sys-color-secondary);
|
|
59
|
+
--md-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);
|
|
60
|
+
--md-outlined-field-outline-color: var(--md-sys-color-primary);
|
|
61
|
+
}
|
|
62
|
+
.pubtag-choice {
|
|
63
|
+
color: unset;
|
|
64
|
+
}
|
|
65
|
+
:not(.alert) .leaflet-popup-content {
|
|
66
|
+
font-family: 'Roboto';
|
|
67
|
+
min-width: 80px;
|
|
68
|
+
}
|
|
69
|
+
.alert .leaflet-popup-content, .display-content {
|
|
70
|
+
max-width: 90vw;
|
|
71
|
+
min-width: min(84vw, 600px);
|
|
72
|
+
max-height: 80vh;
|
|
73
|
+
overflow: auto;
|
|
74
|
+
}
|
|
75
|
+
#map {
|
|
76
|
+
width: 100vw;
|
|
77
|
+
height: 100vh;
|
|
78
|
+
position: absolute;
|
|
79
|
+
top: 0;
|
|
80
|
+
left: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.info-banner {
|
|
84
|
+
position: absolute;
|
|
85
|
+
top: 20px;
|
|
86
|
+
left: 50%;
|
|
87
|
+
transform: translateX(-50%);
|
|
88
|
+
background: rgba(255, 255, 255, 0.95);
|
|
89
|
+
padding: 12px 20px;
|
|
90
|
+
border-radius: 12px;
|
|
91
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
92
|
+
z-index: 1000;
|
|
93
|
+
font-size: 14px;
|
|
94
|
+
text-align: center;
|
|
95
|
+
max-width: 90%;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.loading {
|
|
99
|
+
color: var(--md-sys-color-primary);
|
|
100
|
+
font-weight: 500;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.error {
|
|
104
|
+
color: var(--md-sys-color-secondary);
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.instructions {
|
|
109
|
+
color: #059669;
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.faded {
|
|
114
|
+
opacity: 0;
|
|
115
|
+
transition: opacity 2s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#mapCapture {
|
|
119
|
+
height: 100vh;
|
|
120
|
+
}
|
|
121
|
+
#mapCapture > img {
|
|
122
|
+
position: absolute;
|
|
123
|
+
opacity: 0;
|
|
124
|
+
left: 5px;
|
|
125
|
+
top: 5px;
|
|
126
|
+
z-index: 1100;
|
|
127
|
+
}
|
|
128
|
+
#map + img.about-image {
|
|
129
|
+
pointer-events: none;
|
|
130
|
+
}
|
|
131
|
+
.about-button {
|
|
132
|
+
position: absolute;
|
|
133
|
+
top: 5px;
|
|
134
|
+
right: 5px;
|
|
135
|
+
|
|
136
|
+
background: none;
|
|
137
|
+
border: none;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
padding: 0;
|
|
140
|
+
}
|
|
141
|
+
button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon-button, md-filled-icon-button, md-tonal-icon-button, md-elevated-button, md-checkbox, .attachment > md-icon {
|
|
142
|
+
transition: transform 0.2s ease;
|
|
143
|
+
&:hover { transform: scale(1.1); }
|
|
144
|
+
}
|
|
145
|
+
.leaflet-marker-icon:hover { filter: saturate(4) !important; }
|
|
146
|
+
.about-image {
|
|
147
|
+
width: var(--about-image-size);
|
|
148
|
+
height: auto;
|
|
149
|
+
display: block;
|
|
150
|
+
}
|
|
151
|
+
#subPopoverControls {
|
|
152
|
+
z-index:675;
|
|
153
|
+
pointer-events: none;
|
|
154
|
+
width: 100dvw;
|
|
155
|
+
height: 100dvh;
|
|
156
|
+
position: fixed;
|
|
157
|
+
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.5));
|
|
158
|
+
}
|
|
159
|
+
#subPopoverControls > * > * {
|
|
160
|
+
pointer-events: auto;
|
|
161
|
+
}
|
|
162
|
+
.button-container {
|
|
163
|
+
bottom: 0;
|
|
164
|
+
left: 5px;
|
|
165
|
+
position: absolute;
|
|
166
|
+
display: flex;
|
|
167
|
+
gap: 5px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.watching-hashtags {
|
|
171
|
+
position: absolute;
|
|
172
|
+
width: calc(100% - 130px);
|
|
173
|
+
bottom: calc((sign(calc(100vh - 100dvh)) - 1) * -18px); /* 18px for attribution in desktop, none if iphone dynamic viewsize. */
|
|
174
|
+
right: 5px;
|
|
175
|
+
flex-direction: row-reverse;
|
|
176
|
+
flex-wrap: wrap-reverse;
|
|
177
|
+
align-items: flex-end;
|
|
178
|
+
}
|
|
179
|
+
.watching-hashtags md-filled-text-field.newtag {
|
|
180
|
+
--md-filled-field-top-space: 0px;
|
|
181
|
+
--md-filled-field-bottom-space: 0px;
|
|
182
|
+
--md-filled-text-field-leading-space: 8px;
|
|
183
|
+
--md-filled-text-field-trailing-space: 0px;
|
|
184
|
+
width: 125px;
|
|
185
|
+
height: 34px;
|
|
186
|
+
}
|
|
187
|
+
.alert-marker {
|
|
188
|
+
text-shadow: 8px -3px 10px rgb(0 0 0 / 60%);
|
|
189
|
+
animation: pulse ease 0.8s 1;
|
|
190
|
+
}
|
|
191
|
+
.alert-commented {
|
|
192
|
+
display: none;
|
|
193
|
+
height: 40px;
|
|
194
|
+
border-radius: 20px;
|
|
195
|
+
background-image: radial-gradient(transparent 30%, var(--md-sys-color-secondary), transparent 70%);
|
|
196
|
+
animation: pulse ease 0.8s 1;
|
|
197
|
+
}
|
|
198
|
+
.alert-pin {
|
|
199
|
+
text-shadow: 8px -3px 10px rgb(0 0 0 / 60%);
|
|
200
|
+
font-size: 30px;
|
|
201
|
+
line-height: 30px;
|
|
202
|
+
position: absolute;
|
|
203
|
+
top: 5px;
|
|
204
|
+
left: 5px;
|
|
205
|
+
text-align: center;
|
|
206
|
+
}
|
|
207
|
+
@keyframes pulse {
|
|
208
|
+
0% { filter: saturate(1); }
|
|
209
|
+
25% { filter: saturate(4); }
|
|
210
|
+
50% { filter: saturate(1); }
|
|
211
|
+
75% { filter: saturate(0); }
|
|
212
|
+
100% { filter: saturate(1); }
|
|
213
|
+
}
|
|
214
|
+
@keyframes pulse2 { /* for restarting */
|
|
215
|
+
0% { filter: saturate(1); }
|
|
216
|
+
25% { filter: saturate(4); }
|
|
217
|
+
50% { filter: saturate(1); }
|
|
218
|
+
75% { filter: saturate(0); }
|
|
219
|
+
100% { filter: saturate(1); }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#checkForUpdates, #downloadUpdates, #updateContainer md-elevated-button {
|
|
223
|
+
width: 150px;
|
|
224
|
+
}
|
|
225
|
+
.source-updates {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
gap: 10px;
|
|
229
|
+
}
|
|
230
|
+
.pub {
|
|
231
|
+
animation: pulse ease 0.8s 1;
|
|
232
|
+
--_label-text-weight: 800;
|
|
233
|
+
--_selected-label-text-color: var(--md-sys-color-secondary);
|
|
234
|
+
--md-filter-chip-selected-leading-icon-color: var(--md-sys-color-secondary);
|
|
235
|
+
}
|
|
236
|
+
md-filter-chip {
|
|
237
|
+
--md-sys-color-surface-container-low: #B8C2D7; /* non-selected elevated filter chip */
|
|
238
|
+
}
|
|
239
|
+
md-filter-chip minidenticon-svg + md-icon-button {
|
|
240
|
+
bottom: 10px;
|
|
241
|
+
}
|
|
242
|
+
md-filter-chip md-icon-button {
|
|
243
|
+
position: relative;
|
|
244
|
+
bottom: 3px;
|
|
245
|
+
right: 3px;
|
|
246
|
+
}
|
|
247
|
+
md-filter-chip[selected]:not(.pub) md-icon::after {
|
|
248
|
+
content: "radio_button_unchecked";
|
|
249
|
+
}
|
|
250
|
+
md-filter-chip[selected].pub md-icon::after {
|
|
251
|
+
content: "radio_button_checked";
|
|
252
|
+
}
|
|
253
|
+
md-filter-chip:not([selected]) md-icon::after {
|
|
254
|
+
content: "cancel";
|
|
255
|
+
}
|
|
256
|
+
md-filter-chip.pub md-icon {
|
|
257
|
+
color: var(--md-sys-color-secondary);
|
|
258
|
+
}
|
|
259
|
+
md-filter-chip minidenticon-svg {
|
|
260
|
+
background: white;
|
|
261
|
+
}
|
|
262
|
+
md-outlined-icon-button minidenticon-svg, md-outlined-icon-button img {
|
|
263
|
+
height: 40px;
|
|
264
|
+
width: 40px;
|
|
265
|
+
border-radius: 12px;
|
|
266
|
+
}
|
|
267
|
+
.alert span minidenticon-svg {
|
|
268
|
+
display: inline-block;
|
|
269
|
+
height: 20px;
|
|
270
|
+
width: 20px;
|
|
271
|
+
vertical-align: text-bottom;
|
|
272
|
+
}
|
|
273
|
+
.alert-pin minidenticon-svg {
|
|
274
|
+
display: inline-block;
|
|
275
|
+
position: relative;
|
|
276
|
+
height: 40px;
|
|
277
|
+
width: 40px;
|
|
278
|
+
top: 7px;
|
|
279
|
+
background: white;
|
|
280
|
+
}
|
|
281
|
+
.alert form {
|
|
282
|
+
display: flex;
|
|
283
|
+
flex-direction: column;
|
|
284
|
+
gap: 10px;
|
|
285
|
+
}
|
|
286
|
+
.alert div.post-input {
|
|
287
|
+
display: flex;
|
|
288
|
+
flex-direction: row;
|
|
289
|
+
gap: 5px;
|
|
290
|
+
margin-top: 10px;
|
|
291
|
+
min-width: 300px;
|
|
292
|
+
}
|
|
293
|
+
.alert div.post-input md-outlined-text-field {
|
|
294
|
+
width: 210px;
|
|
295
|
+
}
|
|
296
|
+
.alert p, .alert span {
|
|
297
|
+
font-size: 18px;
|
|
298
|
+
}
|
|
299
|
+
.alert .actions {
|
|
300
|
+
margin-top: 10px;
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-direction: row;
|
|
303
|
+
gap: 5px;
|
|
304
|
+
}
|
|
305
|
+
.alert md-filled-button {
|
|
306
|
+
width: 100px;
|
|
307
|
+
}
|
|
308
|
+
.alert md-icon {
|
|
309
|
+
width: 22px;
|
|
310
|
+
height: 22px;
|
|
311
|
+
}
|
|
312
|
+
md-menu {
|
|
313
|
+
min-width: 150px;
|
|
314
|
+
}
|
|
315
|
+
.attribution {
|
|
316
|
+
padding: 5px;
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: row;
|
|
319
|
+
align-items: center;
|
|
320
|
+
gap: 5px;
|
|
321
|
+
}
|
|
322
|
+
.attribution > div:last-child {
|
|
323
|
+
margin-left: auto;
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
}
|
|
327
|
+
md-outlined-button {
|
|
328
|
+
padding: 10px;
|
|
329
|
+
}
|
|
330
|
+
.attribution > div:last-child > span {
|
|
331
|
+
text-wrap: nowrap;
|
|
332
|
+
}
|
|
333
|
+
md-menu-item minidenticon-svg {
|
|
334
|
+
display: inline-block;
|
|
335
|
+
height: 20px;
|
|
336
|
+
width: 20px;
|
|
337
|
+
}
|
|
338
|
+
.reply {
|
|
339
|
+
margin-top: 5px;
|
|
340
|
+
}
|
|
341
|
+
.reply > span {
|
|
342
|
+
padding: 5px;
|
|
343
|
+
}
|
|
344
|
+
.reply > img, .reply > .attachment, .reply > a > img {
|
|
345
|
+
width: 100%;
|
|
346
|
+
border-style: solid;
|
|
347
|
+
}
|
|
348
|
+
.reply .attribution {
|
|
349
|
+
border-style: solid solid none solid;
|
|
350
|
+
border-radius: 12px 12px 0 0;
|
|
351
|
+
}
|
|
352
|
+
.reply .message {
|
|
353
|
+
color: black;
|
|
354
|
+
border-color: var(--md-sys-color-primary);
|
|
355
|
+
border-width: 1px;
|
|
356
|
+
border-radius: 0 0 12px 12px;
|
|
357
|
+
border-style: solid;
|
|
358
|
+
width: 100%;
|
|
359
|
+
padding-left: 5px;
|
|
360
|
+
padding-right: 5px;
|
|
361
|
+
display: inline-block;
|
|
362
|
+
}
|
|
363
|
+
.attribution-metadata {
|
|
364
|
+
display: flex;
|
|
365
|
+
flex-direction: column;
|
|
366
|
+
align-items: end;
|
|
367
|
+
}
|
|
368
|
+
.alert {
|
|
369
|
+
font-family: roboto;
|
|
370
|
+
}
|
|
371
|
+
.reply .attachment md-icon {
|
|
372
|
+
vertical-align: bottom;
|
|
373
|
+
}
|
|
374
|
+
.attachment-preview {
|
|
375
|
+
margin-top: 15px;
|
|
376
|
+
text-align: right;
|
|
377
|
+
}
|
|
378
|
+
input[type="file"] {
|
|
379
|
+
display: none;
|
|
380
|
+
}
|
|
381
|
+
.reply-input {
|
|
382
|
+
width: 100%;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.button-container button {
|
|
386
|
+
background: none;
|
|
387
|
+
border: none;
|
|
388
|
+
}
|
|
389
|
+
.button-image {
|
|
390
|
+
width: 40px;
|
|
391
|
+
height: auto;
|
|
392
|
+
background: none;
|
|
393
|
+
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.4));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#qrContainer, #aboutContainer, #updateContainer, #correspondentContainer {
|
|
397
|
+
position: absolute;
|
|
398
|
+
top: 0;
|
|
399
|
+
width: 100vw;
|
|
400
|
+
height: 100vh;
|
|
401
|
+
background: transparent;
|
|
402
|
+
z-index: 600;
|
|
403
|
+
display: flex;
|
|
404
|
+
justify-content: center; /* Centers horizontally */
|
|
405
|
+
align-items: center; /* Centers vertically */
|
|
406
|
+
}
|
|
407
|
+
.display-content {
|
|
408
|
+
background: white;
|
|
409
|
+
position: relative;
|
|
410
|
+
|
|
411
|
+
padding: 15px;
|
|
412
|
+
border-radius: 12px;
|
|
413
|
+
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
|
|
414
|
+
}
|
|
415
|
+
.display-content > * {
|
|
416
|
+
margin-top: 10px;
|
|
417
|
+
}
|
|
418
|
+
.display-content ul {
|
|
419
|
+
margin-left: 15px;
|
|
420
|
+
}
|
|
421
|
+
#correspondentContainer md-outlined-button {
|
|
422
|
+
max-width: 70vw;
|
|
423
|
+
}
|
|
424
|
+
.handle-avatar {
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 5px;
|
|
428
|
+
}
|
|
429
|
+
#correspondentContainer .handle {
|
|
430
|
+
color: var(--md-sys-color-secondary);
|
|
431
|
+
}
|
|
432
|
+
#qrContainer > .display-content {
|
|
433
|
+
padding: 0;
|
|
434
|
+
min-width: unset;
|
|
435
|
+
}
|
|
436
|
+
.describe {
|
|
437
|
+
margin-bottom: 5px;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.hidden {
|
|
441
|
+
display: none !important;
|
|
442
|
+
}
|
package/routes/index.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
export var router = express.Router();
|
|
3
|
+
|
|
4
|
+
// The DHT runs a hook here to connect nodes to each other, which uses WebSockets during
|
|
5
|
+
// the connection and then gets out of the way.
|
|
6
|
+
// It also runs some headless peers here to provide stable continuity to the network.
|
|
7
|
+
|
|
8
|
+
// But that isn't ready just yet. So temporarily, the DHT pub/sub is handled in-memory
|
|
9
|
+
// here, communicating through WebSocket for the whole client session.
|
|
10
|
+
|
|
11
|
+
// Each client is identified by its websocket connection. When a client subscribes to
|
|
12
|
+
// a key, we ensure that its websocket appears in the set maintained for that key.
|
|
13
|
+
// When a publish event happens for a given key, we signal all the websockets in that
|
|
14
|
+
// key's set. At that point we also add the published event to a set of "sticky"
|
|
15
|
+
// publishes for the key, with a timed removal set up for the future. Any client
|
|
16
|
+
// newly subscribing to a key is sent the key's current set of sticky values.
|
|
17
|
+
|
|
18
|
+
// When a client's user moves or resizes the map, the set of keys to subscribe to is
|
|
19
|
+
// recomputed. The client unsubscribes from any keys it is no longer interested in,
|
|
20
|
+
// and subscribes to new keys of interest. Because each event is published at a wide
|
|
21
|
+
// range of scales, it is likely to be a frequent occurrence that even a major move
|
|
22
|
+
// or rescale results in the map showing exactly the same events as before, albeit
|
|
23
|
+
// supplied through different subscriptions.
|
|
24
|
+
|
|
25
|
+
const SUBSCRIPTION_TIMEOUT = 60 * 60e3; // Delete after an hour. Must be renewed by app.
|
|
26
|
+
const PUBLISH_TIMEOUT = 10 * 60e3; // Delete after 10 minutes.
|
|
27
|
+
const timeouts = {pub: {}, sub: {}};
|
|
28
|
+
function expire(type, subject, remover, timeout) { // Cancellably schedule remover() to fire at timeout.
|
|
29
|
+
timeouts[type][subject] = setTimeout(remover, timeout);
|
|
30
|
+
}
|
|
31
|
+
function cancel(type, subject) { // Cancel a sheduled expiration.
|
|
32
|
+
clearTimeout(timeouts[type][subject]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// pub maps eventName => {[subject]: storageItem, ...}, where subject is the message id. Entries purged after PUBLISH_TIMEOUT.
|
|
36
|
+
// sub maps eventName => {[subject]: ws, ...}, where subject is the subscriber id. Entries purged after SUBSCRIPTION_TIMEOUT.
|
|
37
|
+
const data = {pub: {}, sub: {}};
|
|
38
|
+
function removeBucket(type, eventName, subject, bucket = data[type][eventName]) { // Remove from data.
|
|
39
|
+
if (!bucket) return;
|
|
40
|
+
delete bucket[subject];
|
|
41
|
+
if (Object.keys(bucket).length) return;
|
|
42
|
+
delete data[type][eventName];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setSticky(eventName, storageItem) { // Associate string eventName, for use by getSticky.
|
|
46
|
+
const {payload, subject} = storageItem;
|
|
47
|
+
const bucket = data.pub[eventName] ||= {};
|
|
48
|
+
function removeMessage() {
|
|
49
|
+
removeBucket('pub', eventName, subject, bucket);
|
|
50
|
+
}
|
|
51
|
+
cancel('pub', subject);
|
|
52
|
+
if (payload === null) removeMessage();
|
|
53
|
+
else {
|
|
54
|
+
bucket[subject] = JSON.stringify(storageItem);
|
|
55
|
+
expire('pub', subject, removeMessage, PUBLISH_TIMEOUT);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getSticky(eventName) { // Answer array of previously set strings that are still associated with eventName.
|
|
59
|
+
return Object.values(data.pub[eventName] || {});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
router.ws('/ws', function(ws, req, next) {
|
|
63
|
+
// no on('connection') needed; connection is already made
|
|
64
|
+
function deleteFromKeySubs(eventName, subject, keySubs = data.sub[eventName]) {
|
|
65
|
+
removeBucket('sub', eventName, subject, keySubs);
|
|
66
|
+
}
|
|
67
|
+
function deleteWS() {
|
|
68
|
+
for (const eventName in data.sub) {
|
|
69
|
+
const keySubs = data.sub[eventName];
|
|
70
|
+
for (const [subject, socket] of Object.entries(keySubs)) {
|
|
71
|
+
if (ws === socket) deleteFromKeySubs(eventName, subject, keySubs);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let heartbeat = setInterval(() => ws.ping(), 10e3);
|
|
76
|
+
ws.on('message', message => {
|
|
77
|
+
const {eventName, type, subject, payload, ...rest} = JSON.parse(message);
|
|
78
|
+
let keySubs = data.sub[eventName] ||= {};
|
|
79
|
+
switch (type) {
|
|
80
|
+
case 'pub':
|
|
81
|
+
const subscribedSockets = Object.values(keySubs);
|
|
82
|
+
//console.log('publishing', eventName, 'to', Object.keys(keySubs));
|
|
83
|
+
for (const ws of subscribedSockets) ws.send(message);
|
|
84
|
+
setSticky(eventName, {eventName, subject, payload, ...rest, type: 'event'});
|
|
85
|
+
break;
|
|
86
|
+
case 'ext':
|
|
87
|
+
cancel('pub', subject);
|
|
88
|
+
expire('pub', subject, () => removeBucket('pub', eventName, subject), PUBLISH_TIMEOUT);
|
|
89
|
+
break;
|
|
90
|
+
case 'sub':
|
|
91
|
+
cancel('sub', subject);
|
|
92
|
+
if (payload) {
|
|
93
|
+
//console.log('subscribing', eventName, 'among', Object.keys(keySubs));
|
|
94
|
+
// In the DHT, the payload is the node name so that we can fire the event to it later.
|
|
95
|
+
// Here we have/store the websocket.
|
|
96
|
+
if (subject !== payload) throw new Error('Subscription with payload:', payload, 'and subject:', subject);
|
|
97
|
+
keySubs[subject] = ws;
|
|
98
|
+
for (const string of getSticky(eventName)) {
|
|
99
|
+
console.log('sending sticky', string);
|
|
100
|
+
ws.send(string);
|
|
101
|
+
}
|
|
102
|
+
expire('sub', subject, () => deleteFromKeySubs(eventName, subject), SUBSCRIPTION_TIMEOUT);
|
|
103
|
+
} else {
|
|
104
|
+
deleteFromKeySubs(eventName, subject, keySubs);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
console.error(`Unrecognized type ${type}`, message);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
ws.on('close', () => {
|
|
113
|
+
console.log(`Client disconnected`);
|
|
114
|
+
clearInterval(heartbeat);
|
|
115
|
+
deleteWS();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
ws.on('error', error => {
|
|
119
|
+
console.error('WebSocket error:', error);
|
|
120
|
+
deleteWS();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
package/server/app.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import {cpus, availableParallelism } from 'node:os';
|
|
5
|
+
import cluster from 'node:cluster';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import logger from 'morgan';
|
|
9
|
+
import yargs from 'yargs';
|
|
10
|
+
import { hideBin } from 'yargs/helpers';
|
|
11
|
+
import { resolve } from './dirname.js';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const logicalCores = availableParallelism();
|
|
15
|
+
|
|
16
|
+
const argv = yargs(hideBin(process.argv))
|
|
17
|
+
.usage(`Start an http server for Alert and with nPortals nodes to connect through. Model description "${cpus()[0].model}", ${logicalCores} logical cores.`)
|
|
18
|
+
.option('nPortals', {
|
|
19
|
+
alias: 'p',
|
|
20
|
+
type: 'number',
|
|
21
|
+
default: logicalCores,
|
|
22
|
+
description: "The number of steady nodes that handle initial connections."
|
|
23
|
+
})
|
|
24
|
+
.option('baseURL', {
|
|
25
|
+
type: 'string',
|
|
26
|
+
default: 'http://localhost:3000/kdht',
|
|
27
|
+
description: "The base URL of the portal server through which to bootstrap."
|
|
28
|
+
})
|
|
29
|
+
.option('externalBaseURL', {
|
|
30
|
+
type: 'string',
|
|
31
|
+
default: '',
|
|
32
|
+
description: "The base URL of the some other portal server to which we should connect ours, if any."
|
|
33
|
+
})
|
|
34
|
+
.option('announce', {
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
default: false,
|
|
37
|
+
description: "Announce our availability on the DHT, so that other nodes can enter through us if their original portal goes down."
|
|
38
|
+
})
|
|
39
|
+
.option('fixedSpacing', {
|
|
40
|
+
type: 'number',
|
|
41
|
+
default: 2,
|
|
42
|
+
description: "Minimum seconds to add between each portal."
|
|
43
|
+
})
|
|
44
|
+
.options('variableSpacing', {
|
|
45
|
+
type: 'number',
|
|
46
|
+
default: 5,
|
|
47
|
+
description: "Additional variable seconds (+/- variableSpacing/2) to add to fixedSpacing between each portal."
|
|
48
|
+
})
|
|
49
|
+
.option('verbose', {
|
|
50
|
+
alias: 'v',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default: false,
|
|
53
|
+
description: "Run with verbose logging."
|
|
54
|
+
})
|
|
55
|
+
.parse();
|
|
56
|
+
|
|
57
|
+
if (cluster.isPrimary) { // Parent process with portal webserver through which clienta can bootstrap
|
|
58
|
+
const port = parseInt((new URL(argv.baseURL)).port || '80');
|
|
59
|
+
process.title = 'yz.social';
|
|
60
|
+
const app = express();
|
|
61
|
+
app.use(logger(':date[iso] :status :method :url :res[content-length] - :response-time ms'));
|
|
62
|
+
|
|
63
|
+
// if (argv.announce) { // The default is to not announce.
|
|
64
|
+
// let announce = null;
|
|
65
|
+
// // Different portals can be set up in different ways, with no easy place to look to know our domain name.
|
|
66
|
+
// // So here we wait until someone connects to the server, and make note of the request host.
|
|
67
|
+
// app.use((req, res, next) => {
|
|
68
|
+
// if (!announce && req.hostname !== 'localhost') {
|
|
69
|
+
// const host = req.headers["x-forwarded-host"] || req.headers.host; // Including port, if specified
|
|
70
|
+
// announce = `node ${resolve('announce.js')} --portalURL https://${host}/kdht`;
|
|
71
|
+
// console.log(announce);
|
|
72
|
+
// exec(announce);
|
|
73
|
+
// setInterval(() => exec(announce), 12 * 60 * 60e3); // Every 12 hours that we are running.
|
|
74
|
+
// }
|
|
75
|
+
// next();
|
|
76
|
+
// });
|
|
77
|
+
// }
|
|
78
|
+
|
|
79
|
+
// We must allow expressWs to bach the internals of app before
|
|
80
|
+
// pulling in routes/index.js. Thus a dynamic import is used so that
|
|
81
|
+
// we can control when routes/index.js is processed.
|
|
82
|
+
// expressWs(app);
|
|
83
|
+
// const Yz = await import('./routes/index.js');
|
|
84
|
+
|
|
85
|
+
//console.log(`${cpus()[0].model}, ${logicalCores} logical cores. Starting ${argv.nPortals}.`);
|
|
86
|
+
app.use(express.json());
|
|
87
|
+
|
|
88
|
+
app.use('/images', express.static(resolve('../public/images'), {
|
|
89
|
+
maxAge: '1d',
|
|
90
|
+
etag: true,
|
|
91
|
+
immutable: true
|
|
92
|
+
}));
|
|
93
|
+
app.use(express.static(resolve('../public'), {
|
|
94
|
+
extensions: ['js'] // Some dependencies refer to .js files as relative pathnames, with the .js missing.
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
app.listen(port);
|
|
98
|
+
console.log('Listening on', port, 'and starting', argv.nPortals, 'nodes.');
|
|
99
|
+
for (let i = 0; i < argv.nPortals; i++) cluster.fork();
|
|
100
|
+
} else {
|
|
101
|
+
process.title = 'axona-starting';
|
|
102
|
+
const { P2PWebNetwork } = await import('../public/javascripts/p2pWebNetwork.js');
|
|
103
|
+
await P2PWebNetwork.delay(cluster.worker?.id * 1e3); // One second between startups.
|
|
104
|
+
const { location:region } = await import('./getLocation.js'); // First invocation caches.
|
|
105
|
+
const network = await P2PWebNetwork.create({region});
|
|
106
|
+
process.title = 'axona-' + network.identity.id;
|
|
107
|
+
let update = null//setInterval(() => network.info(network.health().axonRoles.map(role => role.topic)), 5e3);
|
|
108
|
+
process.on('SIGINT', async () => { // Leave the network politely.
|
|
109
|
+
console.log(process.title, 'Shutdown for Ctrl+C');
|
|
110
|
+
clearInterval(update)
|
|
111
|
+
await network.disconnect();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
114
|
+
await network.host();
|
|
115
|
+
|
|
116
|
+
}
|