add deno run script

This commit is contained in:
bobbydigitales
2024-11-08 09:46:19 -08:00
parent 6ce693cefc
commit 6c692c4b9f
13 changed files with 383 additions and 283 deletions

2
deno/go.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
sudo /root/.deno/bin/deno run --inspect --allow-all --unstable-temporal server.ts

View File

@@ -7,12 +7,21 @@
import { serveDir } from "jsr:@std/http/file-server"
const memoryCache = false;
const memoryResponseMap: Map<string, Response> = new Map();
// deno-lint-ignore-file prefer-const no-explicit-any
async function serveFile(filename: string) {
// console.log(filename)
if (!memoryCache) {
const file = await Deno.readFile("../" + filename);
const newResponse = new Response(file);
if (filename.endsWith('.js')) {
newResponse.headers.set('content-type', 'application/javascript')
}
return newResponse;
}
const response = memoryResponseMap.get(filename);
if (response) {
@@ -26,6 +35,7 @@ async function serveFile(filename: string) {
newResponse.headers.set('content-type', 'application/javascript')
}
console.log(`Caching: ${filename}`);
memoryResponseMap.set(filename, newResponse);
return newResponse.clone();
@@ -137,9 +147,9 @@ interface PeerMessage {
function peerMessageHandler(m: PeerMessage, _socket: WebSocket) {
console.log(`Peer message type ${m.message.type} from ${colorID(m.from)}:${m.from_peername}:${m.from_username} to ${colorID(m.to)}`)
console.log(`pm:${m.message.type} f:${colorID(m.from)}:${m.from_peername}:${m.from_username} t:${colorID(m.to)}`)
let toPeer = peerSockets.get(m.to);
const toPeer = peerSockets.get(m.to);
if (!toPeer) {
console.log(`Couln't find peer ${m.to}`)
return null;
@@ -151,7 +161,7 @@ function peerMessageHandler(m: PeerMessage, _socket: WebSocket) {
return null;
}
let messageToSend = JSON.stringify(m);
const messageToSend = JSON.stringify(m);
// console.log("ws->", toPeer, messageToSend);
toPeer.send(messageToSend)
return null;
@@ -256,7 +266,8 @@ function handler(request: Request, info: any) {
if (url.pathname.includes("/static/")) {
return serveDir(request, { fsRoot: "../" });
return serveFile(url.pathname);
// return serveDir(request, { fsRoot: "../" });
}
return serveFile("/static/index.html")

View File

@@ -300,8 +300,12 @@ export async function getData(userID: string, lowerID: Date, upperID: Date): Pro
const getAllRequest = index.getAll(keyRangeValue);
getAllRequest.onsuccess = () => {
const records = getAllRequest.result.map((item: any) => item.data);
resolve(records);
// let records = [];
// for (let record of getAllRequest.result) {
// records.push(record);
// }
// // const records = getAllRequest.result.map((item: any) => item.data);
resolve(getAllRequest.result);
};
getAllRequest.onerror = () => {

View File

@@ -145,6 +145,8 @@ function log(message: string) {
if (logLines.length > 10) {
logLines = logLines.slice(logLines.length - logLength);
}
renderLog();
}
function generateID() {
@@ -155,6 +157,10 @@ function generateID() {
return uuidv4();
}
interface StoragePost {
data: Post;
}
class Post {
post_timestamp: Date;
post_id: string;
@@ -288,6 +294,7 @@ class wsConnection {
websocket: WebSocket | null = null;
userID = "";
peerID = "";
UserIDsToSync: Set<string>;
websocketPingInterval: number = 0;
helloRefreshInterval: number = 0;
retry = 10;
@@ -299,6 +306,26 @@ class wsConnection {
seenPeers: Map<string, any> = new Map();
constructor(userID: string, peerID: string, IDsToSync: Set<string>) {
this.userID = userID;
this.peerID = peerID;
this.UserIDsToSync = new Set(IDsToSync);
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
this.messageHandlers.set('pong', this.pongHandler);
this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
this.peerMessageHandlers.set('get_post_ids_for_user', this.getPostIdsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_post_ids_for_user_response', this.getPostIdsForUserResponseHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserReponseHandler.bind(this));
window.addEventListener('beforeunload', () => this.disconnect());
this.connect();
}
async send(message: any) {
let json = ""
try {
@@ -312,54 +339,6 @@ class wsConnection {
}
helloResponseHandler(data: any) {
let users = [];
let receivedUsers = Object.entries(data.userPeers);
log(`Net: got ${receivedUsers.length} users from bootstrap peer.`)
try {
let preferentialID = app.getPreferentialID();
let currentUserPeers = data.userPeers[preferentialID];
users.push([preferentialID, currentUserPeers]);
delete data.userPeers[preferentialID];
} catch (e) {
console.log('helloResponseHandler', e);
}
let getAllUsers = app.router.route !== App.Route.USER
if (getAllUsers) {
users = [...users, ...Object.entries(data.userPeers)];
}
// log(`Net: got ${users.length} users from bootstrap peer. \n${users.map((user)=>user[0]).join('\n')}`)
for (let [userID, peerIDs] of users) {
if (this.userBlockList.has(userID)) {
console.log("Skipping user on blocklist:", userID)
continue;
}
// this.peers.set(userID, [...peerIDs]);
for (let peerID of [...peerIDs]) {
if (peerID === this.peerID) {
continue;
}
log(`Net: Req post IDs for user ${logID(userID)} from peer ${logID(peerID)}`);
this.send({
type: "peer_message",
from: this.peerID,
from_username: app.username,
from_peername: app.peername,
to: peerID,
message: { type: "get_post_ids_for_user", user_id: userID }
})
}
}
}
pongHandler(data: any) {
}
@@ -504,7 +483,7 @@ class wsConnection {
for (let post of message.posts) {
// HACK: Some posts have insanely large images, so I'm gonna skip them.
// If we supported delete then we we could delete these posts in a sensible way.
// Once we support delete then we we could delete these posts in a sensible way.
if (this.postBlockList.has(post.post_id)) {
log(`Skipping blocked post: ${post.post_id}`);
continue;
@@ -528,7 +507,7 @@ class wsConnection {
log(`getPostsForUserReponseHandler receive took: ${receiveTime.toFixed(2)}ms`);
if (message.user_id === app.getPreferentialID()) {
if (message.user_id === app.getPreferentialUserID() || app.following.has(message.user_id)) {
app.render();
}
}
@@ -556,17 +535,69 @@ class wsConnection {
userBlockList = new Set([
'5d63f0b2-a842-41bf-bf06-e0e4f6369271',
'5f1b85c4-b14c-454c-8df1-2cacc93f8a77',
'bba3ad24-9181-4e22-90c8-c265c80873ea'
// 'bba3ad24-9181-4e22-90c8-c265c80873ea'
])
async sendHello() {
let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
knownUsers = knownUsers.filter((userID) => userID && !this.userBlockList.has(userID));
knownUsers = knownUsers.filter(async (userID) => userID && (await getAllIds(userID)).length > 0);
// TODO only get users you're following here. ✅
let knownUsers = [...(await indexedDB.databases())].map(db => db.name?.replace('user_', '')).filter(userID => userID !== undefined);
knownUsers = knownUsers
.filter(userID => this.UserIDsToSync.has(userID))
.filter(userID => !this.userBlockList.has(userID))
.filter(async userID => (await getAllIds(userID)).length > 0); // TODO getting all the IDs is unecessary, replace it with a test to get a single ID.
console.log('Net: Sending known users', knownUsers.map(userID => logID(userID ?? "")));
return await this.send({ type: "hello", user_id: this.userID, user_name: app.username, peer_id: this.peerID, peer_name: app.peername, known_users: knownUsers });
}
helloResponseHandler(data: any) {
let users = [];
let receivedUsers = Object.entries(data.userPeers);
log(`Net: got ${receivedUsers.length} users from bootstrap peer.`)
try {
let preferentialUserID = app.getPreferentialUserID();
let currentUserPeers = data.userPeers[preferentialUserID];
users.push([preferentialUserID, currentUserPeers]);
delete data.userPeers[preferentialUserID];
} catch (e) {
console.log('helloResponseHandler', e);
}
let getAllUsers = app.router.route !== App.Route.USER
if (getAllUsers) {
users = [...users, ...Object.entries(data.userPeers).filter(userID => this.UserIDsToSync.has(userID[0]))];
}
// log(`Net: got ${users.length} users from bootstrap peer. \n${users.map((user)=>user[0]).join('\n')}`)
for (let [userID, peerIDs] of users) {
if (this.userBlockList.has(userID)) {
console.log("Skipping user on blocklist:", userID)
continue;
}
// this.peers.set(userID, [...peerIDs]);
for (let peerID of [...peerIDs]) {
if (peerID === this.peerID) {
continue;
}
log(`Net: Req post IDs for user ${logID(userID)} from peer ${logID(peerID)}`);
this.send({
type: "peer_message",
from: this.peerID,
from_username: app.username,
from_peername: app.peername,
to: peerID,
message: { type: "get_post_ids_for_user", user_id: userID }
})
}
}
}
connect(): void {
if (this.websocket?.readyState === WebSocket.OPEN) {
return;
@@ -639,30 +670,6 @@ class wsConnection {
disconnect() {
this.websocket?.close();
}
constructor(userID: string, peerID: string) {
this.userID = userID;
this.peerID = peerID;
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
this.messageHandlers.set('pong', this.pongHandler);
this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
this.peerMessageHandlers.set('get_post_ids_for_user', this.getPostIdsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_post_ids_for_user_response', this.getPostIdsForUserResponseHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserReponseHandler.bind(this));
this.connect();
if (!this.websocket) {
// set a timer and retry?
}
}
}
class App {
@@ -671,7 +678,7 @@ class App {
userID: string = '';
peerID: string = '';
following: Set<string> = new Set();
posts: Post[] = [];
posts: StoragePost[] = [];
isHeadless: boolean = false;
showLog: boolean = false;
markedAvailable = false;
@@ -681,7 +688,7 @@ class App {
qrcode: any = null;
connectURL: string = "";
getPreferentialID() {
getPreferentialUserID() {
return this.router.userID.length !== 0 ? this.router.userID : this.userID;
}
@@ -752,6 +759,19 @@ class App {
return fullText
}
downloadJson(data: any, filename = 'data.json') {
const jsonString = JSON.stringify(data);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
async exportPostsForUser(userID: string) {
let posts = await getAllData(userID);
@@ -769,10 +789,7 @@ class App {
output.push(newPost);
}
let json = JSON.stringify(output);
console.log(json);
this.downloadJson(output, `ddln_${this.username}_export`);
}
async importTweetArchive(userID: string, tweetArchive: any[]) {
@@ -1116,10 +1133,43 @@ class App {
});
}
initButtons(userID: string, posts: Post[], registration: ServiceWorkerRegistration | undefined) {
async lazyCreateQRCode() {
if (this.qrcode != null) {
return;
}
this.qrcode = await new QRCode(document.getElementById('qrcode'), {
text: this.connectURL,
width: 150,
height: 150,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}
showInfo() {
let infoElement = document.getElementById('info');
if (infoElement === null) {
return;
}
infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none';
logVisible = infoElement.style.display == 'block';
renderLog();
this.lazyCreateQRCode();
(document.querySelector('#qrcode > img') as HTMLImageElement).classList.add('qrcode_image');
(document.querySelector('#qrcode > canvas') as HTMLImageElement).classList.add('qrcode_image');
this.showLog = true;
}
initButtons(userID: string, posts: StoragePost[], registration: ServiceWorkerRegistration | undefined) {
// let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
// let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
let importTweetsButton = document.getElementById("import_tweets") as HTMLButtonElement;
let exportButton = document.getElementById("export_button") as HTMLButtonElement;
let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement;
let updateApp = document.getElementById("update_app") as HTMLButtonElement;
let ddlnLogoButton = document.getElementById('ddln_logo_button') as HTMLDivElement;
@@ -1128,6 +1178,8 @@ class App {
let filePicker = document.getElementById('file_input') as HTMLInputElement;
let toggleDark = document.getElementById('toggle_dark') as HTMLButtonElement;
exportButton.addEventListener('click', async e => await this.exportPostsForUser(this.userID));
toggleDark.addEventListener('click', () => {
document.documentElement.style.setProperty('--main-bg-color', 'white');
document.documentElement.style.setProperty('--main-fg-color', 'black');
@@ -1201,31 +1253,9 @@ class App {
registration?.active?.postMessage({ type: "update_app" });
});
let infoElement = document.getElementById('info');
if (infoElement === null) {
return;
}
ddlnLogoButton.addEventListener('click', async () => {
infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none';
logVisible = infoElement.style.display == 'block';
renderLog();
if (this.qrcode != null) {
return;
}
this.qrcode = await new QRCode(document.getElementById('qrcode'), {
text: this.connectURL,
width: 150,
height: 150,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
(document.querySelector('#qrcode > img') as HTMLImageElement).classList.add('qrcode_image');
(document.querySelector('#qrcode > canvas') as HTMLImageElement).classList.add('qrcode_image');
this.showInfo()
});
}
@@ -1235,7 +1265,7 @@ class App {
// This isn't really going to work very well.
// Eventually we'll need a db that only has followed user posts so we can get them chronologically
//
let posts: Post[] = [];
let posts: StoragePost[] = [];
for (let followedID of this.following.keys()) {
posts = posts.concat(await getData(followedID, new Date(2022, 8), new Date()));
// console.log(followedID);
@@ -1260,13 +1290,20 @@ class App {
]
}
if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') {
return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e',
'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
]
}
return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :)
}
async loadPostsFromStorage(userID: string, postID?: string) {
this.timerStart();
let posts: Post[] = [];
let posts: StoragePost[] = [];
// if (postID) {
// posts = await gePostForUser(userID, postID);
@@ -1292,7 +1329,7 @@ class App {
return;
}
let preferredId = app.getPreferentialID()
let preferredId = app.getPreferentialUserID()
for (let userID of knownUsers as string[]) {
if (userID === preferredId) {
continue;
@@ -1421,7 +1458,7 @@ class App {
async main() {
// await this.exportPostsForUser('bba3ad24-9181-4e22-90c8-c265c80873ea');
// await this.exportPostsForUser('b38b623c-c3fa-4351-9cab-50233c99fa4e');
// Get initial state and route from URL and user agent etc
@@ -1452,10 +1489,13 @@ class App {
this.userID = this.getUserID();
this.username = this.getUsername();
this.connectURL = `https://${document.location.hostname}/connect/${this.userID}`;
document.getElementById('connectURL')!.innerHTML = `<a href="${this.connectURL}">connect</a>`;
let urlParams = (new URL(window.location.href)).searchParams;
if (urlParams.has('log')) {
document.getElementById('info')!.style.display = "block";
this.showLog = true;
this.showInfo();
}
if (urlParams.has('headless')) {
@@ -1510,16 +1550,18 @@ class App {
this.initButtons(this.userID, this.posts, registration);
this.connectURL = `https://${document.location.hostname}/connect/${this.userID}`;
document.getElementById('connectURL')!.innerHTML = `<a href="${this.connectURL}">connect</a>`;
log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`);
await this.purgeEmptyUsers();
this.websocket = new wsConnection(this.userID, this.peerID);
window.addEventListener('beforeunload', () => { this.websocket?.disconnect() })
let IDsToSync = this.following;
if (this.router.route === App.Route.USER) {
IDsToSync = new Set([this.router.userID]);
}
this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync);
this.initOffline(this.websocket);
// this.createNetworkViz();
@@ -1541,6 +1583,10 @@ class App {
// })
}
renderWelcome(contentDiv: HTMLDivElement) {
contentDiv.innerHTML = `<div style="font-size:32px">Doing complicated shennanigans to load posts for you so just hang on a minute, ok!?</div>`;
}
// keep a map of posts to dom nodes.
// on re-render
@@ -1607,7 +1653,7 @@ class App {
throw new Error();
}
if (this.posts.length === 0) {
contentDiv.innerHTML = `<div style="font-size:32px">Doing complicated shennanigans to load posts for you so just hang on a minute, ok!?</div>`;
this.renderWelcome(contentDiv);
return;
}
@@ -1631,8 +1677,6 @@ class App {
// console.log("added:", addedPosts, "removed:", deletedPosts);
const fragment = document.createDocumentFragment();
contentDiv.innerHTML = "";
@@ -1643,10 +1687,9 @@ class App {
let postData = this.posts[i];
// this.postsSet.add(postData);
// return promises for all image loads and await those.
let post = this.renderPost(postData);
this.renderedPosts.set(postData.post_id, post);
let post = this.renderPost(postData.data);
// this.renderedPosts.set(postData.post_id, post);
if (post) {
fragment.appendChild(post);
count++;
@@ -1671,9 +1714,9 @@ class App {
if ((performance as any)?.memory) {
log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)
}
// if ((performance as any)?.memory) {
// log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)
// }
}
@@ -1745,6 +1788,7 @@ class App {
containerDiv.querySelector('#shareButton')?.appendChild(shareButton);
if (!("image_data" in post && post.image_data)) {
// containerDiv.appendChild(timestampDiv);
return containerDiv;

View File

@@ -8,7 +8,6 @@ const contentToCache = [
'/static/main.js',
'/static/lib/marked.min.js',
'/static/lib/qrcode.min.js',
'/static/lib/d3.js',
'/static/db.js',
'/static/favicon.ico'
];

View File

@@ -226,8 +226,12 @@ export async function getData(userID, lowerID, upperID) {
return new Promise((resolve, reject) => {
const getAllRequest = index.getAll(keyRangeValue);
getAllRequest.onsuccess = () => {
const records = getAllRequest.result.map((item) => item.data);
resolve(records);
// let records = [];
// for (let record of getAllRequest.result) {
// records.push(record);
// }
// // const records = getAllRequest.result.map((item: any) => item.data);
resolve(getAllRequest.result);
};
getAllRequest.onerror = () => {
console.error('Transaction failed:', getAllRequest.error?.message);

File diff suppressed because one or more lines are too long

View File

@@ -66,6 +66,7 @@
<!-- <button id="button_font1" >font1</button>
<button id="button_font2" >font2 </button> -->
<button id="import_tweets">import</button>
<button id="export_button">export</button>
<button id="clear_posts">clear </button>
<button id="update_app">check for updates</button>
<button id="toggle_dark">light/dark</button>
@@ -73,11 +74,11 @@
<textarea cols="60" rows="6" id="textarea_post"></textarea>
<div class="right">
<label for="file_input" id="file_input_label" class="button">photo</label>
<label for="file_input" id="file_input_label" class="button button-big">photo</label>
<input type="file" id="file_input" accept="image/*" multiple style="display:none">
<!-- <button id="button_add_pic" >🏞️</button> -->
<button id="button_post">post</button>
<button id="button_post" class="button button-big">post</button>
</div>
</div>
<!-- <div id="torrent-content"></div> -->

View File

@@ -147,15 +147,13 @@ a {
}
}
@media (prefer) {
#ddln_logo_button {
.logo {
width: 32px;
height: 32px;
image-rendering: pixelated;
background-image: url('/static/favicon.ico');
background-repeat: no-repeat;
background-size: cover;
}
}
#torrent-content {
@@ -169,7 +167,7 @@ a {
button,
.button {
font-size: small;
font-size: 12px;
background-color: var(--main-bg-color);
border-radius: 10px;
padding-left: 10px;
@@ -184,6 +182,10 @@ button,
cursor: pointer;
}
.button-big {
font-size: large;
}
video {
width: 100%
}

View File

@@ -115,6 +115,7 @@ function log(message) {
if (logLines.length > 10) {
logLines = logLines.slice(logLines.length - logLength);
}
renderLog();
}
function generateID() {
if (self.crypto.hasOwnProperty("randomUUID")) {
@@ -201,6 +202,53 @@ async function compressString(input) {
return new Uint8Array(compressedArray);
}
class wsConnection {
constructor(userID, peerID, IDsToSync) {
this.websocket = null;
this.userID = "";
this.peerID = "";
this.websocketPingInterval = 0;
this.helloRefreshInterval = 0;
this.retry = 10;
this.state = 'disconnected';
// peers: Map<string, string[]> = new Map();
this.messageHandlers = new Map();
this.peerMessageHandlers = new Map();
this.seenPeers = new Map();
// static async compressArrayBuffer(data: ArrayBuffer): Promise<ArrayBuffer> {
// const compressionStream = new CompressionStream('gzip'); // You can also use 'deflate', 'deflate-raw', etc.
// const compressedStream = new Response(
// new Blob([data]).stream().pipeThrough(compressionStream)
// );
// const compressedArrayBuffer = await compressedStream.arrayBuffer();
// return compressedArrayBuffer;
// }
this.postBlockList = new Set([
'1c71f53c-c467-48e4-bc8c-39005b37c0d5',
'64203497-f77b-40d6-9e76-34d17372e72a',
'243130d8-4a41-471e-8898-5075f1bd7aec',
'e01eff89-5100-4b35-af4c-1c1bcb007dd0',
'194696a2-d850-4bb0-98f7-47416b3d1662',
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
'dd1d92aa-aa24-4166-a925-94ba072a9048'
]);
this.userBlockList = new Set([
'5d63f0b2-a842-41bf-bf06-e0e4f6369271',
'5f1b85c4-b14c-454c-8df1-2cacc93f8a77',
// 'bba3ad24-9181-4e22-90c8-c265c80873ea'
]);
this.userID = userID;
this.peerID = peerID;
this.UserIDsToSync = new Set(IDsToSync);
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
this.messageHandlers.set('pong', this.pongHandler);
this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
this.peerMessageHandlers.set('get_post_ids_for_user', this.getPostIdsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_post_ids_for_user_response', this.getPostIdsForUserResponseHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserReponseHandler.bind(this));
window.addEventListener('beforeunload', () => this.disconnect());
this.connect();
}
async send(message) {
let json = "";
try {
@@ -213,46 +261,6 @@ class wsConnection {
// log(`ws->${json.slice(0, 240)}`)
this.websocket.send(json);
}
helloResponseHandler(data) {
let users = [];
let receivedUsers = Object.entries(data.userPeers);
log(`Net: got ${receivedUsers.length} users from bootstrap peer.`);
try {
let preferentialID = app.getPreferentialID();
let currentUserPeers = data.userPeers[preferentialID];
users.push([preferentialID, currentUserPeers]);
delete data.userPeers[preferentialID];
}
catch (e) {
console.log('helloResponseHandler', e);
}
let getAllUsers = app.router.route !== App.Route.USER;
if (getAllUsers) {
users = [...users, ...Object.entries(data.userPeers)];
}
// log(`Net: got ${users.length} users from bootstrap peer. \n${users.map((user)=>user[0]).join('\n')}`)
for (let [userID, peerIDs] of users) {
if (this.userBlockList.has(userID)) {
console.log("Skipping user on blocklist:", userID);
continue;
}
// this.peers.set(userID, [...peerIDs]);
for (let peerID of [...peerIDs]) {
if (peerID === this.peerID) {
continue;
}
log(`Net: Req post IDs for user ${logID(userID)} from peer ${logID(peerID)}`);
this.send({
type: "peer_message",
from: this.peerID,
from_username: app.username,
from_peername: app.peername,
to: peerID,
message: { type: "get_post_ids_for_user", user_id: userID }
});
}
}
}
pongHandler(data) {
}
async getPostIdsForUserResponseHandler(data) {
@@ -341,7 +349,7 @@ class wsConnection {
console.log(`Net: got ${message.posts.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`);
for (let post of message.posts) {
// HACK: Some posts have insanely large images, so I'm gonna skip them.
// If we supported delete then we we could delete these posts in a sensible way.
// Once we support delete then we we could delete these posts in a sensible way.
if (this.postBlockList.has(post.post_id)) {
log(`Skipping blocked post: ${post.post_id}`);
continue;
@@ -359,7 +367,7 @@ class wsConnection {
await mergeDataArray(message.user_id, data.message.posts);
let receiveTime = app.timerDelta();
log(`getPostsForUserReponseHandler receive took: ${receiveTime.toFixed(2)}ms`);
if (message.user_id === app.getPreferentialID()) {
if (message.user_id === app.getPreferentialUserID() || app.following.has(message.user_id)) {
app.render();
}
}
@@ -375,12 +383,55 @@ class wsConnection {
handler(data);
}
async sendHello() {
let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
knownUsers = knownUsers.filter((userID) => userID && !this.userBlockList.has(userID));
knownUsers = knownUsers.filter(async (userID) => userID && (await getAllIds(userID)).length > 0);
// TODO only get users you're following here. ✅
let knownUsers = [...(await indexedDB.databases())].map(db => db.name?.replace('user_', '')).filter(userID => userID !== undefined);
knownUsers = knownUsers
.filter(userID => this.UserIDsToSync.has(userID))
.filter(userID => !this.userBlockList.has(userID))
.filter(async (userID) => (await getAllIds(userID)).length > 0); // TODO getting all the IDs is unecessary, replace it with a test to get a single ID.
console.log('Net: Sending known users', knownUsers.map(userID => logID(userID ?? "")));
return await this.send({ type: "hello", user_id: this.userID, user_name: app.username, peer_id: this.peerID, peer_name: app.peername, known_users: knownUsers });
}
helloResponseHandler(data) {
let users = [];
let receivedUsers = Object.entries(data.userPeers);
log(`Net: got ${receivedUsers.length} users from bootstrap peer.`);
try {
let preferentialUserID = app.getPreferentialUserID();
let currentUserPeers = data.userPeers[preferentialUserID];
users.push([preferentialUserID, currentUserPeers]);
delete data.userPeers[preferentialUserID];
}
catch (e) {
console.log('helloResponseHandler', e);
}
let getAllUsers = app.router.route !== App.Route.USER;
if (getAllUsers) {
users = [...users, ...Object.entries(data.userPeers).filter(userID => this.UserIDsToSync.has(userID[0]))];
}
// log(`Net: got ${users.length} users from bootstrap peer. \n${users.map((user)=>user[0]).join('\n')}`)
for (let [userID, peerIDs] of users) {
if (this.userBlockList.has(userID)) {
console.log("Skipping user on blocklist:", userID);
continue;
}
// this.peers.set(userID, [...peerIDs]);
for (let peerID of [...peerIDs]) {
if (peerID === this.peerID) {
continue;
}
log(`Net: Req post IDs for user ${logID(userID)} from peer ${logID(peerID)}`);
this.send({
type: "peer_message",
from: this.peerID,
from_username: app.username,
from_peername: app.peername,
to: peerID,
message: { type: "get_post_ids_for_user", user_id: userID }
});
}
}
}
connect() {
if (this.websocket?.readyState === WebSocket.OPEN) {
return;
@@ -443,54 +494,6 @@ class wsConnection {
disconnect() {
this.websocket?.close();
}
constructor(userID, peerID) {
this.websocket = null;
this.userID = "";
this.peerID = "";
this.websocketPingInterval = 0;
this.helloRefreshInterval = 0;
this.retry = 10;
this.state = 'disconnected';
// peers: Map<string, string[]> = new Map();
this.messageHandlers = new Map();
this.peerMessageHandlers = new Map();
this.seenPeers = new Map();
// static async compressArrayBuffer(data: ArrayBuffer): Promise<ArrayBuffer> {
// const compressionStream = new CompressionStream('gzip'); // You can also use 'deflate', 'deflate-raw', etc.
// const compressedStream = new Response(
// new Blob([data]).stream().pipeThrough(compressionStream)
// );
// const compressedArrayBuffer = await compressedStream.arrayBuffer();
// return compressedArrayBuffer;
// }
this.postBlockList = new Set([
'1c71f53c-c467-48e4-bc8c-39005b37c0d5',
'64203497-f77b-40d6-9e76-34d17372e72a',
'243130d8-4a41-471e-8898-5075f1bd7aec',
'e01eff89-5100-4b35-af4c-1c1bcb007dd0',
'194696a2-d850-4bb0-98f7-47416b3d1662',
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
'dd1d92aa-aa24-4166-a925-94ba072a9048'
]);
this.userBlockList = new Set([
'5d63f0b2-a842-41bf-bf06-e0e4f6369271',
'5f1b85c4-b14c-454c-8df1-2cacc93f8a77',
'bba3ad24-9181-4e22-90c8-c265c80873ea'
]);
this.userID = userID;
this.peerID = peerID;
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
this.messageHandlers.set('pong', this.pongHandler);
this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
this.peerMessageHandlers.set('get_post_ids_for_user', this.getPostIdsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_post_ids_for_user_response', this.getPostIdsForUserResponseHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserReponseHandler.bind(this));
this.connect();
if (!this.websocket) {
// set a timer and retry?
}
}
}
class App {
constructor() {
@@ -524,7 +527,7 @@ class App {
mediaID: ''
};
}
getPreferentialID() {
getPreferentialUserID() {
return this.router.userID.length !== 0 ? this.router.userID : this.userID;
}
initMarkdown() {
@@ -576,6 +579,18 @@ class App {
}
return fullText;
}
downloadJson(data, filename = 'data.json') {
const jsonString = JSON.stringify(data);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
async exportPostsForUser(userID) {
let posts = await getAllData(userID);
let output = [];
@@ -587,8 +602,7 @@ class App {
}
output.push(newPost);
}
let json = JSON.stringify(output);
console.log(json);
this.downloadJson(output, `ddln_${this.username}_export`);
}
async importTweetArchive(userID, tweetArchive) {
log("Importing tweet archive");
@@ -847,10 +861,37 @@ class App {
reader.readAsText(file);
});
}
async lazyCreateQRCode() {
if (this.qrcode != null) {
return;
}
this.qrcode = await new QRCode(document.getElementById('qrcode'), {
text: this.connectURL,
width: 150,
height: 150,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}
showInfo() {
let infoElement = document.getElementById('info');
if (infoElement === null) {
return;
}
infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none';
logVisible = infoElement.style.display == 'block';
renderLog();
this.lazyCreateQRCode();
document.querySelector('#qrcode > img').classList.add('qrcode_image');
document.querySelector('#qrcode > canvas').classList.add('qrcode_image');
this.showLog = true;
}
initButtons(userID, posts, registration) {
// let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
// let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
let importTweetsButton = document.getElementById("import_tweets");
let exportButton = document.getElementById("export_button");
let clearPostsButton = document.getElementById("clear_posts");
let updateApp = document.getElementById("update_app");
let ddlnLogoButton = document.getElementById('ddln_logo_button');
@@ -858,6 +899,7 @@ class App {
let filePickerLabel = document.getElementById('file_input_label');
let filePicker = document.getElementById('file_input');
let toggleDark = document.getElementById('toggle_dark');
exportButton.addEventListener('click', async (e) => await this.exportPostsForUser(this.userID));
toggleDark.addEventListener('click', () => {
document.documentElement.style.setProperty('--main-bg-color', 'white');
document.documentElement.style.setProperty('--main-fg-color', 'black');
@@ -912,27 +954,8 @@ class App {
updateApp.addEventListener("click", () => {
registration?.active?.postMessage({ type: "update_app" });
});
let infoElement = document.getElementById('info');
if (infoElement === null) {
return;
}
ddlnLogoButton.addEventListener('click', async () => {
infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none';
logVisible = infoElement.style.display == 'block';
renderLog();
if (this.qrcode != null) {
return;
}
this.qrcode = await new QRCode(document.getElementById('qrcode'), {
text: this.connectURL,
width: 150,
height: 150,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
document.querySelector('#qrcode > img').classList.add('qrcode_image');
document.querySelector('#qrcode > canvas').classList.add('qrcode_image');
this.showInfo();
});
}
async getPostsForFeed() {
@@ -960,6 +983,12 @@ class App {
'8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona
];
}
if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') {
return [
'b38b623c-c3fa-4351-9cab-50233c99fa4e',
'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
];
}
return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :)
}
async loadPostsFromStorage(userID, postID) {
@@ -983,7 +1012,7 @@ class App {
if (knownUsers.length === 0) {
return;
}
let preferredId = app.getPreferentialID();
let preferredId = app.getPreferentialUserID();
for (let userID of knownUsers) {
if (userID === preferredId) {
continue;
@@ -1081,7 +1110,7 @@ class App {
console.log(`create viz network took ${this.timerDelta()}ms`);
}
async main() {
// await this.exportPostsForUser('bba3ad24-9181-4e22-90c8-c265c80873ea');
// await this.exportPostsForUser('b38b623c-c3fa-4351-9cab-50233c99fa4e');
// Get initial state and route from URL and user agent etc
// Set local state (userid etc) based on that.
// Init libraries
@@ -1102,10 +1131,11 @@ class App {
this.peername = this.getPeername();
this.userID = this.getUserID();
this.username = this.getUsername();
this.connectURL = `https://${document.location.hostname}/connect/${this.userID}`;
document.getElementById('connectURL').innerHTML = `<a href="${this.connectURL}">connect</a>`;
let urlParams = (new URL(window.location.href)).searchParams;
if (urlParams.has('log')) {
document.getElementById('info').style.display = "block";
this.showLog = true;
this.showInfo();
}
if (urlParams.has('headless')) {
this.isHeadless = true;
@@ -1146,12 +1176,13 @@ class App {
document.getElementById('user_id').innerText = `user_id:${this.userID}`;
document.getElementById('peer_id').innerText = `peer_id:${this.peerID}`;
this.initButtons(this.userID, this.posts, registration);
this.connectURL = `https://${document.location.hostname}/connect/${this.userID}`;
document.getElementById('connectURL').innerHTML = `<a href="${this.connectURL}">connect</a>`;
log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`);
await this.purgeEmptyUsers();
this.websocket = new wsConnection(this.userID, this.peerID);
window.addEventListener('beforeunload', () => { this.websocket?.disconnect(); });
let IDsToSync = this.following;
if (this.router.route === App.Route.USER) {
IDsToSync = new Set([this.router.userID]);
}
this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync);
this.initOffline(this.websocket);
// this.createNetworkViz();
// const client = new WebTorrent()
@@ -1167,6 +1198,9 @@ class App {
// file.appendTo(document.getElementById('torrent-content'));
// })
}
renderWelcome(contentDiv) {
contentDiv.innerHTML = `<div style="font-size:32px">Doing complicated shennanigans to load posts for you so just hang on a minute, ok!?</div>`;
}
async render() {
if (this.isHeadless) {
console.log('Headless so skipping render...');
@@ -1218,7 +1252,7 @@ class App {
throw new Error();
}
if (this.posts.length === 0) {
contentDiv.innerHTML = `<div style="font-size:32px">Doing complicated shennanigans to load posts for you so just hang on a minute, ok!?</div>`;
this.renderWelcome(contentDiv);
return;
}
// let existingPostSet = new Set(existingPosts.map(post => post.post_id));
@@ -1244,8 +1278,8 @@ class App {
let postData = this.posts[i];
// this.postsSet.add(postData);
// return promises for all image loads and await those.
let post = this.renderPost(postData);
this.renderedPosts.set(postData.post_id, post);
let post = this.renderPost(postData.data);
// this.renderedPosts.set(postData.post_id, post);
if (post) {
fragment.appendChild(post);
count++;
@@ -1262,9 +1296,9 @@ class App {
log(`render took: ${renderTime.toFixed(2)}ms`);
performance.mark("render-end");
performance.measure('render-time', 'render-start', 'render-end');
if (performance?.memory) {
log(`memory used: ${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`);
}
// if ((performance as any)?.memory) {
// log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)
// }
}
async deletePost(userID, postID) {
deleteData(userID, postID);

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,6 @@ const contentToCache = [
'/static/main.js',
'/static/lib/marked.min.js',
'/static/lib/qrcode.min.js',
'/static/lib/d3.js',
'/static/db.js',
'/static/favicon.ico'
];

View File

@@ -1 +1 @@
{"version":3,"file":"sw.js","sourceRoot":"","sources":["../src/sw.ts"],"names":[],"mappings":";AAAA,MAAM,QAAQ,GAAG,KAAK,CAAC;AACvB,yBAAyB;AACzB,MAAM,SAAS,GAAG,oBAAoB,CAAC;AAEvC,MAAM,cAAc,GAAG;IACrB,oBAAoB;IACpB,kBAAkB;IAClB,iBAAiB;IACjB,2BAA2B;IAC3B,2BAA2B;IAC3B,mBAAmB;IACnB,eAAe;IACf,qBAAqB;CACtB,CAAC;AAEF,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAM,EAAE,EAAE;IAC1C,CAAC,CAAC,SAAS,CACT,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CACT,qDAAqD,EACrD,cAAc,CACf,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,EAAE,CACL,CAAC;AACJ,CAAC,CAAC,CAAC;AAGH,KAAK,UAAU,oBAAoB,CAAC,KAAU;IAE5C,IAAI,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAEzC,IAAI,QAAQ,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAEhD,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAChF,CAAC;IAED,MAAM,YAAY,GAAG,CAAC,KAAK,IAAI,EAAE;QAC/B,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAE7E,IAAI,eAAe,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC;YACH,eAAe,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAEpE,OAAO,IAAI,QAAQ,CAAC,wBAAwB,EAAE;gBAC5C,MAAM,EAAE,GAAG;gBACX,UAAU,EAAE,6BAA6B;gBACzC,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QAED,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,gCAAgC,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAEnF,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,wCAAwC,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAEhG,CAAC;QAED,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,2CAA2C,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9F,OAAO,eAAe,CAAC;IACzB,CAAC,CAAC,EAAE,CAAC;IAGL,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,2DAA2D,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9G,OAAO,QAAQ,IAAI,YAAY,CAAC;IAEhC,yBAAyB;IACzB,sDAAsD;IACtD,4BAA4B;IAC5B,IAAI;IAIJ,yBAAyB;IACzB,6BAA6B;IAC7B,wCAAwC;IACxC,oCAAoC;IACpC,kDAAkD;IAClD,+CAA+C;IAC/C,iEAAiE;IACjE,sCAAsC;IACtC,gBAAgB;IAChB,2CAA2C;IAC3C,YAAY;IACZ,OAAO;AACT,CAAC;AAED,yCAAyC;AACzC,kEAAkE;AAElE,+CAA+C;AAE/C,qBAAqB;AACrB,+FAA+F;AAC/F,0DAA0D;AAC1D,yBAAyB;AACzB,sCAAsC;AACtC,QAAQ;AACR,yBAAyB;AACzB,MAAM;AAEN,mCAAmC;AACnC,oEAAoE;AACpE,uBAAuB;AACvB,MAAM;AAEN,0FAA0F;AAC1F,gDAAgD;AAChD,UAAU;AACV,2IAA2I;AAC3I,kBAAkB;AAClB,oDAAoD;AACpD,MAAM;AACN,qBAAqB;AACrB,IAAI;AAEJ,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,UAAU,KAAU;IACjD,KAAK,CAAC,WAAW,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/C,uCAAuC;AACzC,CAAC,CAAC,CAAC;AAEH,gBAAgB,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IACtC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAE3D,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,YAAY;YACf,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3C,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACpE,yCAAyC;YAEzC,KAAK,IAAI,IAAI,IAAI,cAAc,EAAE,CAAC;gBAChC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACrB,CAAC;YAED,MAAM,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;YACnC,MAAM;IACV,CAAC;AACH,CAAC,CAAC,CAAC"}
{"version":3,"file":"sw.js","sourceRoot":"","sources":["../src/sw.ts"],"names":[],"mappings":"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"}