diff --git a/package-lock.json b/package-lock.json index 64fedc3..e3e4c50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,11 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/PeerManager.ts b/src/PeerManager.ts index 3887eb9..d07b13b 100644 --- a/src/PeerManager.ts +++ b/src/PeerManager.ts @@ -5,7 +5,7 @@ // how? do "perfect negotiation" with bootstrap peer. All logic here moves to BP. import { generateID } from "IDUtils"; -import { log } from "log"; +import { log, logID } from "log"; // Use a broadcast channel to only have one peer manager for multiple tabs, // then we won't need to have a session ID as all queries for a peerID will be coming from the same peer manager @@ -29,6 +29,15 @@ export class PeerManager { connectPromise: { resolve: Function, reject: Function } | null = null; pingPeers: RTCPeerConnection[] = []; + watchdogPeriodSeconds: number = 10; + + // async watchdog() { + // // Check that we're connected to at least N peers. If not, reconnect to the bootstrap server. + + // if (this.peers.size === 0) { + // await this.sendHello2(); + // } + // } websocketSend(message: any) { if (!this.websocket) { @@ -168,18 +177,55 @@ export class PeerManager { this.routingTable = new Map(); this.userID = userID; this.peerID = peerID; - } + disconnect() { + this.websocket?.close(); + for (let peer of this.peers.values()) { + peer.disconnect(); + + } + + } + connect() { + // setInterval(this.watchdog.bind(this), this.watchdogPeriodSeconds * 1000); + + setInterval(()=>{ + + + if (!this.isBootstrapPeer && this.peers.size === 0) { + console.log.apply(null, log("No peers connected :(")); + if (this.websocket?.readyState === WebSocket.OPEN) { + this.sendHello2(); + } + } + + let output = `local peerID:${logID(this.peerID)}` + "\n"; + for (let [peerID, peer] of this.peers) { + output += `${logID(peerID)}: ${peer.rtcPeer?.connectionState}` + "\n"; + } + + console.log.apply(null, log(output)); + + + + }, 5000); + let connectPromise = new Promise((resolve, reject) => { this.connectPromise = { resolve, reject }; }); try { - this.websocket = new WebSocket( - `wss://${window.location.hostname}:${window.location.port}/ws`, - ); + let hostname = globalThis?.location?.hostname ?? 'ddln.app'; + + let port = globalThis?.location?.port ?? '443'; + + let wsURL = `wss://${hostname}:${port}/ws`; + + console.log(`wsURL: ${wsURL}`); + + this.websocket = new WebSocket(wsURL); } catch (error: any) { throw new Error(error.message); } @@ -237,14 +283,15 @@ export class PeerManager { } - async disconnect(remotePeerID: string) { + async disconnectFromPeer(remotePeerID: string) { let peer = this.peers.get(remotePeerID); if (!peer) { - console.log.apply(null, log(`PeerManager.disconnect: couln't find peer ${remotePeerID}`)); + console.log.apply(null, log(`PeerManager.disconnect: couldn't find peer ${remotePeerID}`)); return; } - + + console.log.apply(null, log(`PeerManager.disconnect: disconnecting peer ${remotePeerID}`)); await peer.disconnect(); this.peers.delete(remotePeerID); } @@ -295,8 +342,6 @@ export class PeerManager { } } - - interface Message { type: string; from_peer: string; @@ -306,98 +351,6 @@ interface Message { peer_message: any; } -// // Initially this wil be the bootstrap peer, We'll keep a connection to it and it will keep a list of all connected peers. -// // Eventually we will replace this with connecting via other peers. -// class Signaler { -// websocket: WebSocket; - -// sessionID: string; -// userID: string; -// peerID: string; -// bootstrapPeerID: string = ""; -// private isBootstrapPeer: boolean = false; -// private onConnected: Function; -// peerRoutes: Map = new Map(); - - - -// constructor(userID: string, peerID: string, isBootstrapPeer: boolean, onConnected: Function) { -// this.onConnected = onConnected; -// this.isBootstrapPeer = isBootstrapPeer; -// this.sessionID = generateID(); -// this.userID = userID; -// this.peerID = peerID; - - -// try { -// this.websocket = new WebSocket( -// `wss://${window.location.hostname}:${window.location.port}/ws`, -// ); -// } catch (error: any) { -// throw new Error(error.message); -// } - -// this.websocket.onopen = async (event) => { -// console.log.apply(null, log("signaler:ws:onopen"); -// await this.sendHello2(); -// }; - -// this.websocket.onmessage = this.onMessage.bind(this); -// } - - - - - - -// connect() { -// } - -// onMessage(event: MessageEvent) { -// let messageJSON = event.data; -// let message: any = null; - -// try { -// message = JSON.parse(messageJSON); -// } catch (e) { -// console.log.apply(null, log(e); -// throw new Error(); -// } - -// console.log.apply(null, log("->signaler:", message); - -// if (message.type === "hello2") { - -// if (!this.isBootstrapPeer) { -// this.bootstrapPeerID = message.bootstrapPeers[0]; -// } - -// this.onConnected(this.bootstrapPeerID); -// } - -// if (message.type == "peer_message") { - -// if (message.message.type = "rtc_connect") { - -// } - -// let connection = this.peerRoutes.get(message.from_peer); -// if (!connection) { -// console.log.apply(null, log("Can't find peer for peer message:", message); -// return; -// } -// connection.onSignalerMessage(message); -// } -// } - - -// route(remotePeerID:string, peerConnection:PeerConnection) { -// this.peerRoutes.set(remotePeerID, peerConnection) -// } - -// // sendPeerMessage -// } - class PeerConnection { private remotePeerID: string; // private signaler: Signaler; @@ -462,7 +415,7 @@ class PeerConnection { if (!this.dataChannel) { throw new Error(); } - console.log.apply(null, log("data channel is open!")); + console.log.apply(null, log("data channel is open to: ", this.remotePeerID, " from: ", this.peerManager.peerID)); this.send({ type: "hello datachannel", from: this.peerManager.peerID, to: this.remotePeerID}); // this.dataChannel?.send(`{typeHello datachannel from: ${this.peerManager.peerID}`); @@ -480,6 +433,11 @@ class PeerConnection { console.log.apply(null, log("->datachannel: ", e.data)) this.onMessage(e.data); } + + this.dataChannel.onclose = (e: Event) => { + console.log.apply(null, log(`datachannel from peer ${this.remotePeerID} closed, disconnecting peer.`)); + this.peerManager.disconnectFromPeer(this.remotePeerID); + } } async connect() { @@ -494,10 +452,10 @@ class PeerConnection { } // When the connection is closed, tell the peer manager that this connection has gone away - if (this.rtcPeer.connectionState === "disconnected") { + if (this.rtcPeer.connectionState === "failed") { this.peerManager.onPeerDisconnected(this.remotePeerID); - // window.setTimeout(async () => { await this.peerManager.connectToPeer(this.remotePeerID) }, 10_000); + // globalThis.setTimeout(async () => { await this.peerManager.connectToPeer(this.remotePeerID) }, 10_000); } if (this.rtcPeer.connectionState === "connected") { @@ -640,8 +598,9 @@ class PeerConnection { } - async disconnect() { + disconnect() { this.rtcPeer?.close(); + this.rtcPeer = null; } send(message: any) { @@ -714,167 +673,4 @@ class PeerConnection { // this.peerManger.onMessage(this.remotePeerID, message); } -} - -// export class PeerConnection2 { - -// id: string; - -// private makingOffer:boolean = false; -// private ignoreOffer:boolean = false; - -// rtcPeer: RTCPeerConnection; - -// signaler:WebSocket; - -// static config = { -// iceServers: [ -// { urls: "stun:stun.l.google.com" }, -// { urls: "stun:stun1.l.google.com" }, -// { urls: "stun:stun2.l.google.com" }, -// { urls: "stun:stun3.l.google.com" }, -// { urls: "stun:stun4.l.google.com" }, -// ], -// }; - -// constructor(remotePeerID: string, signaler:WebSocket) { -// this.id = remotePeerID; - -// this.rtcPeer = new RTCPeerConnection(PeerConnection2.config); - -// this.signaler = signaler;; - -// this.rtcPeer.onnegotiationneeded = async () => { -// try { -// this.makingOffer = true; -// await this.rtcPeer.setLocalDescription(); -// signaler.send(JSON.stringify({ description: this.rtcPeer.localDescription })); -// } catch (err) { -// console.error(err); -// } finally { -// this.makingOffer = false; -// } -// }; - -// this.rtcPeer.onicecandidate = ({ candidate }) => signaler.send(JSON.stringify({ candidate })); - -// this.ignoreOffer = false; -// } - -// onSignallerMessage = async ({ data: { description, candidate } }: MessageEvent) => { -// try { -// if (description) { -// // const offerCollision = -// // description.type === "offer" && -// // (this.makingOffer || this.rtcPeer.signalingState !== "stable"); - -// // this.ignoreOffer = !polite && offerCollision; -// if (this.ignoreOffer) { -// return; -// } - -// await this.rtcPeer.setRemoteDescription(description); -// if (description.type === "offer") { -// await this.rtcPeer.setLocalDescription(); -// this.signaler.send(JSON.stringify({ description: this.rtcPeer.localDescription })); -// } -// } else if (candidate) { -// try { -// await this.rtcPeer.addIceCandidate(candidate); -// } catch (err) { -// if (!this.ignoreOffer) { -// throw err; -// } -// } -// } -// } catch (err) { -// console.error(err); -// } -// }; - -// } - -// // const config = { -// // iceServers: [{ urls: "stun:stun.mystunserver.tld" }], -// // }; - -// // let polite = true; - -// // const signaler = new SignalingChannel(); -// // const signaler: any = {} -// // const rtcPeer = new RTCPeerConnection(config); - -// // // const constraints = { audio: true, video: true }; -// // const selfVideo = document.querySelector("video.selfview"); -// // const remoteVideo = document.querySelector("video.remoteview"); - -// // async function start() { -// // try { -// // const stream = await navigator.mediaDevices.getUserMedia(constraints); - -// // for (const track of stream.getTracks()) { -// // pc.addTrack(track, stream); -// // } -// // selfVideo.srcObject = stream; -// // } catch (err) { -// // console.error(err); -// // } -// // } - -// // rtcPeer.ontrack = ({ track, streams }) => { -// // track.onunmute = () => { -// // if (remoteVideo.srcObject) { -// // return; -// // } -// // remoteVideo.srcObject = streams[0]; -// // }; -// // }; - -// // makingOffer = false; - -// // rtcPeer.onnegotiationneeded = async () => { -// // try { -// // // makingOffer = true; -// // await rtcPeer.setLocalDescription(); -// // signaler.send({ description: rtcPeer.localDescription }); -// // } catch (err) { -// // console.error(err); -// // } finally { -// // makingOffer = false; -// // } -// // }; - -// // rtcPeer.onicecandidate = ({ candidate }) => signaler.send({ candidate }); - -// // let ignoreOffer = false; - -// // signaler.onmessage = async ({ data: { description, candidate } }: MessageEvent) => { -// // try { -// // if (description) { -// // const offerCollision = -// // description.type === "offer" && -// // // (makingOffer || rtcPeer.signalingState !== "stable"); - -// // ignoreOffer = !polite && offerCollision; -// // if (ignoreOffer) { -// // return; -// // } - -// // await rtcPeer.setRemoteDescription(description); -// // if (description.type === "offer") { -// // await rtcPeer.setLocalDescription(); -// // signaler.send({ description: rtcPeer.localDescription }); -// // } -// // } else if (candidate) { -// // try { -// // await rtcPeer.addIceCandidate(candidate); -// // } catch (err) { -// // if (!ignoreOffer) { -// // throw err; -// // } -// // } -// // } -// // } catch (err) { -// // console.error(err); -// // } -// // }; +} \ No newline at end of file diff --git a/src/bootstrap_main.ts b/src/bootstrap_main.ts new file mode 100644 index 0000000..bc763a1 --- /dev/null +++ b/src/bootstrap_main.ts @@ -0,0 +1,2157 @@ +// Attempt to get bootstrap peer to run under Deno +// Check if RTCPeerConnection is supported. + + + + +// TODO: virtual list, only rerender what's needed so things can keep playing. + + + +/* +Problems + 1. Can't delete, very annoying + Tombstones. Send all IDs and all Tombstones. Ask only for posts that we don't get a tombstone for. Don't send posts we have a tombstone for? + + Posts don't propagate, you need to refresh to see new posts. + Broadcast when we post to all peers we know about. + + 3. Posting is slow because too long to render + 2. Can't follow people + 4. Can't like or reply to posts + +user + posts + media + tombstones + following + profile + name + description + profile pic + + +Restruucture the app around the data. App/WS split is messy. Clean it up. + +*/ + + +// import * as ForceGraph3D from "3d-force-graph"; +// import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds } from "db"; +import { generateID } from "IDUtils"; +import { PeerManager } from "PeerManager"; + +import {log, logID, renderLog, setLogVisibility} from "log" + +// import {PeerConnection} from "webRTC"; + +// declare let WebTorrent: any; + +// declare let ForceGraph3D: any; +// declare let marked: any; +// declare let QRCode: any; +// let posts:any; +// let keyBase = "dandelion_posts_v1_" +// let key:string = ""; + +// interface PostTimestamp { +// year: number, +// month: number, +// day: number, +// hour: number, +// minute: number, +// second: number, +// } + +// function waitMs(durationMs: number) { +// return new Promise(resolve => setTimeout(resolve, durationMs)); +// } + +// function uuidToBytes(uuid: string): Uint8Array { +// return new Uint8Array(uuid.match(/[a-fA-F0-9]{2}/g)!.map((hex) => parseInt(hex, 16))); +// } + +// // Base58 character set +// const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; +// // Base58 encoding +// // Base58 encoding +// function encodeBase58(buffer: Uint8Array): string { +// let carry; +// const digits = [0]; + +// for (const byte of buffer) { +// carry = byte; +// for (let i = 0; i < digits.length; i++) { +// carry += digits[i] << 8; +// digits[i] = carry % 58; +// carry = Math.floor(carry / 58); +// } +// while (carry > 0) { +// digits.push(carry % 58); +// carry = Math.floor(carry / 58); +// } +// } + +// let result = ''; +// for (const digit of digits.reverse()) { +// result += BASE58_ALPHABET[digit]; +// } + +// // Handle leading zero bytes +// for (const byte of buffer) { +// if (byte === 0x00) { +// result = BASE58_ALPHABET[0] + result; +// } else { +// break; +// } +// } + +// return result; +// } + +// // Convert UUID v4 to Base58 +// function uuidToBase58(uuid: string): string { +// const bytes = uuidToBytes(uuid); +// return encodeBase58(bytes); +// } + +// // function log(message:string) { +// // console.log.apply(null, log(message); +// // let log = document.getElementById("log"); +// // let newlog = document.createElement('span'); +// // newlog.innerHTML = `
${message}
`; +// // log?.appendChild(newlog); + +// // } + + + + + +// interface StoragePost { +// data: Post; +// } + +// class Post { +// post_timestamp: Date; +// post_id: string; +// author: string; +// author_id: string; +// text: string; +// image_data: ArrayBuffer | null; + + +// importedFrom: "twitter" | null; +// importSource: any; + +// constructor( +// author: string, +// author_id: string, +// text: string, +// post_timestamp: Date, +// imageData: ArrayBuffer | null = null, +// importedFrom: "twitter" | null = null, +// importSource: any = null) { + +// this.post_timestamp = post_timestamp; +// this.post_id = generateID(); + +// this.author = author; +// this.author_id = author_id; +// this.text = text; +// this.image_data = imageData; + +// this.importedFrom = importedFrom; +// this.importSource = importSource; +// } +// } + +// globalThis.addEventListener('scroll', () => { +// // Total height of the document +// const totalPageHeight = document.body.scrollHeight; + +// // Current scroll position +// const scrollPoint = globalThis.scrollY + globalThis.innerHeight; + +// // Check if scrolled to bottom +// if (scrollPoint >= totalPageHeight) { +// // console.log.apply(null, log('Scrolled to the bottom!')); +// // console.log.apply(null, log(scrollPoint, totalPageHeight)); +// } + +// }); + + + +// // let peer = await new PeerConnection(peer_id); + +// // let connectionReply = await wsConnection.send('hello'); +// // for (let peer of connectionReply) { +// // let peerConnection = await wsConnection.send('connect', peer.id); +// // if (peerConnection) { +// // this.peers.push(peerConnection); +// // let postIDs = await peerConnection.getPostIDs(); +// // let postsWeDontHave = this.diffPostIDs(postIDs); + +// // let newPosts = await peerConnection.getPosts(postsWeDontHave); + +// // this.addPosts(newPosts); + +// // } +// // } + +// async function bytesToBase64DataUrl(bytes: Uint8Array, type = "application/octet-stream") { +// return await new Promise((resolve, reject) => { +// const reader = Object.assign(new FileReader(), { +// onload: () => resolve(reader.result), +// onerror: () => reject(reader.error), +// }); +// reader.readAsDataURL(new File([bytes], "", { type })); +// }); +// } + +// async function arrayBufferToBase64(buffer: ArrayBuffer) { +// var bytes = new Uint8Array(buffer); +// return (await bytesToBase64DataUrl(bytes) as string).replace("data:application/octet-stream;base64,", ""); +// } + +// async function base64ToArrayBuffer(base64String: string) { +// let response = await fetch("data:application/octet-stream;base64," + base64String); +// let arrayBuffer = await response.arrayBuffer(); +// return arrayBuffer; +// } + +// async function compressString(input: string) { +// // Convert the string to a Uint8Array +// const textEncoder = new TextEncoder(); +// const inputArray = textEncoder.encode(input); + +// // Create a CompressionStream +// const compressionStream = new CompressionStream('gzip'); +// const writer = compressionStream.writable.getWriter(); + +// // Write the data and close the stream +// writer.write(inputArray); +// writer.close(); + +// // Read the compressed data from the stream +// const compressedArray = await new Response(compressionStream.readable).arrayBuffer(); + +// // Convert the compressed data to a Uint8Array +// return new Uint8Array(compressedArray); +// } + +// interface PeerMessage { +// type: string; +// from: string; +// to: string; +// from_peername: string; +// from_username: string; +// message: any; +// } + +// // class Signaler { +// // websocket: WebSocket | null = null; + +// // websocketPingInterval: number = 0; + + + +// // connect() { +// // if (this.websocket?.readyState === WebSocket.OPEN) { +// // return; +// // } + +// // globalThis.clearInterval(this.websocketPingInterval); +// // if (this.websocket) { this.websocket.close() }; + +// // try { +// // this.websocket = new WebSocket(`wss://${globalThis.location.hostname}:${globalThis.location.port}/ws`); +// // } catch (error: any) { +// // console.log.apply(null, log(error.message); +// // return; +// // } + +// // this.websocket.onopen = async (event) => { +// // console.log.apply(null, log("ws:connected"));; +// // await this.sendHello(); + +// // // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have. +// // let helloRefreshIntervalPeriod = 120; +// // if (app.isHeadless) { +// // console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod) +// // this.helloRefreshInterval = globalThis.setInterval(() => { +// // console.log.apply(null, log("wsConnection: Hello refresh.") + +// // if (!navigator.onLine) { +// // return; +// // } +// // this.sendHello(); +// // }, helloRefreshIntervalPeriod * 1000); +// // } + +// // this.websocketPingInterval = globalThis.setInterval(() => { +// // if (!navigator.onLine) { +// // return; +// // } +// // this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username }); +// // }, 10_000) +// // }; + +// // this.websocket.onclose = (event) => { +// // console.log.apply(null, log("ws:disconnected"));; +// // // this.retry *= 2; +// // console.log.apply(null, log(`Retrying in ${this.retry} seconds`));; +// // globalThis.setTimeout(() => { this.connect(); }, this.retry * 1000); +// // }; + +// // this.websocket.onmessage = (event) => { +// // // log('ws:<-' + event.data.slice(0, 240)); +// // let data = JSON.parse(event.data); + +// // let { type } = data; + +// // let handler = this.messageHandlers.get(type); +// // if (!handler) { +// // console.warn(`Got a message we can't handle:`, type); +// // return; +// // } + +// // handler(data); + +// // }; + +// // this.websocket.onerror = (event) => { +// // console.log.apply(null, log('ws:error: ' + event));; +// // }; +// // } +// // } + +// // disconnect() { +// // this.websocket?.close(); +// // } +// // } + + +// // Connect websocket +// // send hello +// // get bootstrap peer ID +// // WebRTC connect to bootstrap peer +// // ask Bootstrap peer for peers that have users we care about. +// // for now, bootstrap peer will connect to all peers and will tell us about them, moving all logic from the server to the BSP +// // WebRTC Connect to peers that might have posts we need +// // query those peers and do existing logic. + +// class wsConnection { +// websocket: WebSocket | null = null; +// sessionID = ""; +// userID = ""; +// peerID = ""; +// rtcPeerDescription: RTCSessionDescription | null = null; +// UserIDsToSync: Set; +// websocketPingInterval: number = 0; +// helloRefreshInterval: number = 0; +// retry = 10; +// state = 'disconnected'; +// // peers: Map = new Map(); + +// messageHandlers: Map void> = new Map(); +// peerMessageHandlers: Map void> = new Map(); +// seenPeers: Map = new Map(); + + +// constructor(userID: string, peerID: string, IDsToSync: Set, rtcPeerDescription: RTCSessionDescription) { +// this.rtcPeerDescription = rtcPeerDescription; +// this.sessionID = generateID(); +// this.userID = userID; +// this.peerID = peerID; +// this.UserIDsToSync = new Set(IDsToSync); + +// this.messageHandlers.set('hello', this.helloResponseHandler.bind(this)); +// this.messageHandlers.set('hello2', this.hello2ResponseHandler.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.peerMessageHandlers.set('send_webrtc_offer', this.sendWebRTCOfferHandler.bind(this)); +// // this.peerMessageHandlers.set('send_webrtc_offer_response', this.getPostIdsForUserResponseHandler.bind(this)); + + + +// globalThis.addEventListener('beforeunload', () => this.disconnect()); + +// this.connect(); +// } + +// // So we don't need custom logic everywhere we use this, I just wrapped it. +// shouldSyncUserID(userID: string) { +// if (app.isHeadless) { +// return true; +// } + +// return this.UserIDsToSync.has(userID); +// } + +// async send(message: any) { +// let json = "" +// try { +// json = JSON.stringify(message); +// // console.log.apply(null, log("*******", (await compressString(json)).byteLength, json.length); +// } catch (e) { +// console.log.apply(null, log(e, "wsConnection send: Couldn't serialize message", message)); +// } +// // log(`ws->${json.slice(0, 240)}`) +// this.websocket!.send(json); + +// } + +// pongHandler(data: any) { +// } + + +// async sendWebRTCDescription(description: RTCSessionDescription | null) { + +// console.log.apply(null, log("description:", description)); +// this.send({ type: "rtc_session_description", description: description }); +// } + +// async getPostIdsForUserResponseHandler(data: any) { +// // log(`getPostsForUserResponse: ${data}`) + +// let message = data.message; +// console.log.apply(null, log(`Net: got ${message.post_ids.length} post IDs for user ${logID(message.user_id)} from peer ${logID(data.from)}`));; + + +// let startTime = app.timerStart(); +// let postIds = await checkPostIds(message.user_id, message.post_ids); +// console.log.apply(null, log(`ID Check for user ${logID(message.user_id)} took ${app.timerDelta().toFixed(2)}ms`));; +// console.log.apply(null, log(`Need ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`));; + +// if (postIds.length === 0) { +// return; +// } + +// console.log.apply(null, log(`Net: Req ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`)); +// let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user", post_ids: postIds, user_id: message.user_id } } + +// this.send(responseMessage); +// } + + + +// // static async compressArrayBuffer(data: ArrayBuffer): Promise { +// // 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; +// // } +// 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' +// ]); + +// async getPostIdsForUserHandler(data: any) { +// let message = data.message; +// let postIds = await getAllIds(message.user_id) ?? []; +// postIds = postIds.filter((postID: string) => !this.postBlockList.has(postID)); +// if (postIds.length === 0) { +// console.log.apply(null, log(`Net: I know about user ${logID(message.user_id)} but I have 0 posts, so I'm not sending any to to peer ${logID(data.from)}`));; +// return; +// } +// console.log.apply(null, log(`Net: Sending ${postIds.length} post Ids for user ${logID(message.user_id)} to peer ${logID(data.from)}`)); + +// let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_post_ids_for_user_response", post_ids: postIds, user_id: message.user_id } } +// this.send(responseMessage); +// } + +// async broadcastNewPost(userID: string, post: any) { + +// let newPost = { ...post } +// if (post.image_data) { +// newPost.image_data = await arrayBufferToBase64(post.image_data); +// } + +// for (let [peerID, peerInfo] of this.seenPeers.entries()) { +// console.log.apply(null, log(`broadcastNewPost: sending new post to ${logID(peerID)}:${peerInfo.peerName}:${peerInfo.userName}`));; + +// this.sendPostsForUser(peerID, app.userID, [newPost]) +// } +// } + + +// async sendPostsForUser(toPeerID: string, userID: string, posts: any) { +// let responseMessage = { +// type: "peer_message", +// from: app.peerID, +// to: toPeerID, +// from_username: app.username, +// from_peername: app.peername, +// message: { +// type: "get_posts_for_user_response", +// posts: posts, +// user_id: userID +// } +// } + +// return this.send(responseMessage); + +// } + +// // Send posts to peer +// async getPostsForUserHandler(data: any) { +// let message = data.message; +// let posts = await getPostsByIds(message.user_id, message.post_ids) ?? []; + +// console.log.apply(null, log(`Net: Sending ${posts.length} posts for user ${logID(message.user_id)} to peer ${logID(data.from)}`));; + +// app.timerStart(); +// let output = []; + +// console.log.apply(null, log("Serializing images")); +// for (let post of posts) { +// let newPost = (post as any).data; + +// if (newPost.image_data) { +// // let compressedData = await wsConnection.compressArrayBuffer(newPost.image_data); +// // console.log.apply(null, log((newPost.image_data.byteLength - compressedData.byteLength) / 1024 / 1024); + +// // TODO don't do this, use Blobs direclty! +// // https://developer.chrome.com/blog/blob-support-for-Indexeddb-landed-on-chrome-dev + +// newPost.image_data = await arrayBufferToBase64(newPost.image_data); + +// } + +// // let megs = JSON.stringify(newPost).length/1024/1024; +// // console.log.apply(null, log(`getPostsForUserHandler id:${newPost.post_id} post length:${megs}`); +// output.push(newPost); +// } + +// let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user_response", posts: output, user_id: message.user_id } } + +// console.log.apply(null, log("Sending posts")); +// await this.sendPostsForUser(data.from, message.user_id, output); +// let sendTime = app.timerDelta(); +// console.log.apply(null, log(`getPostsForUserHandler send took: ${sendTime.toFixed(2)}ms`));; + +// } + + + +// // Got posts from peer +// async getPostsForUserReponseHandler(data: any) { +// app.timerStart(); +// let message = data.message; +// console.log.apply(null, 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. +// // Once we support delete then we we could delete these posts in a sensible way. +// if (this.postBlockList.has(post.post_id)) { +// console.log.apply(null, log(`Skipping blocked post: ${post.post_id}`));; +// continue; +// } + +// // HACK - some posts had the wrong author ID +// if (message.user_id === app.userID) { +// post.author_id = app.userID; +// } + +// post.post_timestamp = new Date(post.post_timestamp); +// if (post.image_data) { +// post.image_data = await base64ToArrayBuffer(post.image_data); +// } +// } +// console.log.apply(null, log(`Merging same user peer posts...`)); +// await mergeDataArray(message.user_id, data.message.posts); + +// let receiveTime = app.timerDelta(); + +// console.log.apply(null, log(`getPostsForUserReponseHandler receive took: ${receiveTime.toFixed(2)}ms`));; + + +// if (message.user_id === app.getPreferentialUserID() || app.following.has(message.user_id)) { +// app.render(); +// } +// } + + + + +// async peerMessageHandler(data: PeerMessage) { +// // log(`peerMessageHandler ${JSON.stringify(data)}`) + +// this.seenPeers.set(data.from, { peerName: data.from_peername, userName: data.from_username }); + +// let peerMessageType = data.message.type; + +// let handler = this.peerMessageHandlers.get(peerMessageType); + +// if (!handler) { +// console.error(`got peer message type we don't have a handler for: ${peerMessageType}`); +// return; +// } + +// handler(data); +// } + +// userBlockList = new Set([ +// '5d63f0b2-a842-41bf-bf06-e0e4f6369271', +// '5f1b85c4-b14c-454c-8df1-2cacc93f8a77', +// // 'bba3ad24-9181-4e22-90c8-c265c80873ea' +// ]) + + +// // Hello2 +// // Goal, connect to bootstrap peer, ask bootstrap peer for peers that have posts from users that we care about. get peers, connect to those peers, sync. +// // how? do "perfect negotiation" with bootstrap peer. All logic here moves to BP. + +// async sendHello2() { +// this.send({ +// type: "hello2", +// user_id: this.userID, +// user_name: app.username, +// peer_id: this.peerID, +// session_id: this.sessionID, +// peer_name: app.peername, +// is_bootstrap_peer: app.isBootstrapPeer, +// peer_description: this.rtcPeerDescription +// }); +// } + +// async sendHello() { +// // 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.shouldSyncUserID(userID)) +// .filter(userID => !this.userBlockList.has(userID)) +// .filter(async userID => (await getAllIds(userID)).length > 0); // TODO:EASYOPT getting all the IDs is unecessary, replace it with a test to get a single ID. + +// console.log.apply(null, 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 }); +// } + +// hello2ResponseHandler(data: any) { + +// } + +// helloResponseHandler(data: any) { + +// let users = []; +// let receivedUsers = Object.entries(data.userPeers); +// console.log.apply(null, 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.apply(null, log('helloResponseHandler', e)); +// } + +// let getAllUsers = app.router.route !== App.Route.USER +// if (getAllUsers) { +// users = [...users, ...Object.entries(data.userPeers).filter(userID => this.shouldSyncUserID(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.apply(null, log("Skipping user on blocklist:", userID)); +// continue; +// } + +// // this.peers.set(userID, [...peerIDs]); + +// for (let peerID of [...peerIDs]) { +// if (peerID === this.peerID) { +// continue; +// } + +// console.log.apply(null, 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; +// } + +// globalThis.clearInterval(this.websocketPingInterval); +// if (this.websocket) { this.websocket.close() }; + +// try { +// this.websocket = new WebSocket(`wss://${globalThis.location.hostname}:${globalThis.location.port}/ws`); +// } catch (error: any) { +// console.log.apply(null, log(error.message)); +// return; +// } + +// this.websocket.onopen = async (event) => { +// console.log.apply(null, log("ws:connected"));; +// await this.sendHello2(); + +// // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have. +// // let helloRefreshIntervalPeriod = 120; +// // if (app.isHeadless) { +// // console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod) +// // this.helloRefreshInterval = globalThis.setInterval(() => { +// // console.log.apply(null, log("wsConnection: Hello refresh.") + +// // if (!navigator.onLine) { +// // return; +// // } +// // this.sendHello(); +// // }, helloRefreshIntervalPeriod * 1000); +// // } + +// this.websocketPingInterval = globalThis.setInterval(() => { +// if (!navigator.onLine) { +// return; +// } +// this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username }); +// }, 10_000) +// }; + +// // this.websocket.onopen = async (event) => { +// // console.log.apply(null, log("ws:connected"));; +// // await this.sendHello(); + +// // // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have. +// // let helloRefreshIntervalPeriod = 120; +// // if (app.isHeadless) { +// // console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod) +// // this.helloRefreshInterval = globalThis.setInterval(() => { +// // console.log.apply(null, log("wsConnection: Hello refresh.") + +// // if (!navigator.onLine) { +// // return; +// // } +// // this.sendHello(); +// // }, helloRefreshIntervalPeriod * 1000); +// // } + +// // this.websocketPingInterval = globalThis.setInterval(() => { +// // if (!navigator.onLine) { +// // return; +// // } +// // this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username }); +// // }, 10_000) +// // }; + +// this.websocket.onclose = (event) => { +// console.log.apply(null, log("ws:disconnected"));; +// // this.retry *= 2; +// console.log.apply(null, log(`Retrying in ${this.retry} seconds`));; +// globalThis.setTimeout(() => { this.connect(); }, this.retry * 1000); +// }; + +// this.websocket.onmessage = (event) => { +// // log('ws:<-' + event.data.slice(0, 240)); +// let data = JSON.parse(event.data); + +// let { type } = data; + +// let handler = this.messageHandlers.get(type); +// if (!handler) { +// console.warn(`Got a message we can't handle:`, type); +// return; +// } + +// handler(data); + +// }; + +// this.websocket.onerror = (event) => { +// console.log.apply(null, log('ws:error: ' + event));; +// }; +// } + +// disconnect() { +// this.websocket?.close(); +// } +// } + +class App { + username: string = ''; + peername: string = ''; + userID: string = ''; + peerID: string = ''; + following: Set = new Set(); +// posts: StoragePost[] = []; + isHeadless: boolean = false; + isBootstrapPeer: boolean = false; + showLog: boolean = false; + markedAvailable = false; + limitPosts = 50; +// websocket: wsConnection | null = null; + // vizGraph: any | null = null; + qrcode: any = null; + connectURL: string = ""; + firstRun = false; + peerManager: PeerManager | null = null; + + async connect() { + this.peerManager = new PeerManager(this.userID, this.peerID, this.isBootstrapPeer); + this.registerRPCs(); + console.log.apply(null, log("*************** before peerManager.connect"));; + + // We use promises here to only return from this call once we're connected to the boostrap peer + // and the datachannel is open. + // Might want to take this a step further and only return once we're connected to an initial set of peers? + // we could return progress information as we connect and have the app subscribe to that? + + // Would be lovely to show a little display of peers connecting, whether you're connected directly to a friend's peer etc. + // Basically that live "dandelion" display. + + this.peerManager.registerRPC('getPostIDsForUser', (userID: any) => { + return [1, 2, 3, 4, 5] + }); + + await this.peerManager.connect(); + console.log.apply(null, log("*************** after peerManager.connect"));; + + + if (!this.isBootstrapPeer) { + let postIDs = await this.peerManager.rpc.getPostIDsForUser(this.peerManager.bootstrapPeerID, this.userID); + console.log.apply(null, log("peerManager.rpc.getPostIDsForUser", postIDs)); + } + + + + } + +// getPreferentialUserID() { +// return this.router.userID.length !== 0 ? this.router.userID : this.userID; +// } + +// initMarkdown() { +// if (typeof marked === "undefined") { +// return; +// } + +// const renderer = new marked.Renderer(); +// renderer.link = (href: any, title: string, text: string) => { +// return `${text}`; +// }; +// marked.setOptions({ renderer: renderer }); + +// this.markedAvailable = true; +// } + + // arrayBufferToBase64(buffer: ArrayBuffer) { + // return new Promise((resolve, reject) => { + // const blob = new Blob([buffer], { type: 'application/octet-stream' }); + // const reader = new FileReader(); + + // reader.onloadend = () => { + // const dataUrl = reader.result as string; + // if (!dataUrl) { + // resolve(null); + // return; + // } + // const base64 = dataUrl.split(',')[1]; + // resolve(base64); + // }; + + // reader.onerror = (error) => { + // reject(error); + // }; + + // reader.readAsDataURL(blob); + // }); + // } + +// async createTestData() { +// let postsTestData = await (await fetch("./postsTestData.json")).json(); + +// return postsTestData; +// } + +// time = 0; + +// timerStart() { +// this.time = performance.now(); +// } + +// timerDelta() { +// return performance.now() - this.time; +// } + +// getFixedTweetText(entry: any) { + + +// let fullText = entry.tweet.full_text; + +// let linkMarkdown = ""; +// for (const url of entry.tweet.entities.urls) { +// linkMarkdown = `[${url.display_url}](${url.expanded_url})`; +// fullText = fullText.replace(url.url, linkMarkdown); +// } + +// return fullText +// } + +// downloadBinary(data: ArrayBuffer, filename: string, mimeType: string = 'application/octet-stream') { +// // Create a blob from the ArrayBuffer with the specified MIME type +// const blob = new Blob([data], { type: mimeType }); + +// // Create object URL from the blob +// const url = globalThis.URL.createObjectURL(blob); + +// // Create temporary link element +// const link = document.createElement('a'); +// link.href = url; +// link.download = filename; + +// // Append link to body, click it, and remove it +// document.body.appendChild(link); +// link.click(); +// document.body.removeChild(link); + +// // Clean up the object URL +// globalThis.URL.revokeObjectURL(url); +// } + +// downloadJson(data: any, filename = 'data.json') { +// const jsonString = JSON.stringify(data); +// const blob = new Blob([jsonString], { type: 'application/json' }); +// const url = globalThis.URL.createObjectURL(blob); +// const link = document.createElement('a'); +// link.href = url; +// link.download = filename; +// document.body.appendChild(link); +// link.click(); +// document.body.removeChild(link); +// globalThis.URL.revokeObjectURL(url); +// } + +// async importPostsForUser(userID: string, posts: string) { + +// } + +// async exportPostsForUser(userID: string) { + +// let posts = await getAllData(userID); + +// let output = []; + +// console.log.apply(null, log("Serializing images")); +// for (let post of posts) { +// let newPost = (post as any).data; + +// if (newPost.image_data) { +// newPost.image_data = await arrayBufferToBase64(newPost.image_data); +// } + +// output.push(newPost); +// } + +// let compressedData = await compressString(JSON.stringify(output)); + +// const d = new Date(); +// const timestamp = `${d.getFullYear() +// }_${String(d.getMonth() + 1).padStart(2, '0') +// }_${String(d.getDate()).padStart(2, '0') +// }_${String(d.getHours()).padStart(2, '0') +// }_${String(d.getMinutes()).padStart(2, '0') +// }_${String(d.getSeconds()).padStart(2, '0')}`; + + +// this.downloadBinary(compressedData, `ddln_${this.username}_export_${timestamp}.json.gz`); +// } + +// async importTweetArchive(userID: string, tweetArchive: any[]) { +// console.log.apply(null, log("Importing tweet archive")); +// let postsTestData: any[] = []; + +// // let response = await fetch("./tweets.js"); +// // let tweetsText = await response.text(); +// // tweetsText = tweetsText.replace("globalThis.YTD.tweets.part0", "globalThis.tweetData"); + +// // new Function(tweetsText)(); + + +// // let tweets = JSON.parse(tweetJSON); +// let count = 0; + +// for (let entry of tweetArchive) { +// // if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) { +// // continue; +// // } + +// let mediaURL: string = entry.tweet?.entities?.media?.[0]?.media_url_https; +// let isImage = false; +// if (mediaURL) { +// isImage = mediaURL.includes('jpg'); +// } + +// let imageData = null; +// // if (isImage) { +// // try { +// // let response = await fetch(mediaURL); +// // await waitMs(100); +// // if (response.status === 200) { +// // imageData = await response.arrayBuffer(); +// // } +// // console.log.apply(null, log(imageData); +// // } catch (e) { +// // console.log.apply(null, log(e); +// // } + +// // } + +// let timeStamp = new Date(entry.tweet.created_at); +// let tweetText = this.getFixedTweetText(entry); +// let newPost = new Post('bobbydigitales', userID, tweetText, timeStamp, imageData, 'twitter', entry); + +// postsTestData.push(newPost); + +// count++; +// if (count % 100 === 0) { +// console.log.apply(null, log(`Imported ${count} posts...`));; +// // render(postsTestData); +// } + +// // if (count == 100-1) { +// // break; +// // } + +// } +// return postsTestData; +// } + +// async createTestData3(userID: string) { +// let posts = await (await (fetch('./posts.json'))).json(); + +// return posts; +// } + +// async registerServiceWorker() { +// if (!("serviceWorker" in navigator)) { +// return; +// } + +// let registrations = await navigator.serviceWorker.getRegistrations(); +// if (registrations.length > 0) { +// console.log.apply(null, log("Service worker already registered.")); +// return registrations[0]; +// } + +// navigator.serviceWorker +// .register("/sw.js") +// .then((registration) => { +// console.log.apply(null, log("Service Worker registered with scope:", registration.scope)); +// return registration; +// }) +// .catch((error) => { +// console.error("Service Worker registration failed:", error); +// }); +// } + +// async compressImage(imageData: ArrayBuffer, mimeType: string, quality = 0.5): Promise { +// let uncompressedByteLength = imageData.byteLength; +// console.log.apply(null, log(`compressImage input:${mimeType} size:${(uncompressedByteLength / 1024).toFixed(2)}KBi quality:${quality}`));; + +// try { +// // Convert ArrayBuffer to Blob +// const blob = new Blob([imageData], { type: mimeType }); + +// const bitmap = await createImageBitmap(blob, { +// imageOrientation: 'none', + +// // resizeWidth: desiredWidth, +// // resizeHeight: desiredHeight, +// // resizeQuality: 'high', +// }); + +// // const bitmap = await createImageBitmap(bitmapTemp, { +// // imageOrientation: 'none', + +// // resizeWidth: 600, +// // resizeHeight: 800, +// // // resizeHeight: (bitmapTemp.height / bitmapTemp.width) * 600, +// // resizeQuality: 'high', +// // }) + +// //drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) + + +// // Create a canvas and draw the image onto it +// // let scale = 1/32; +// // let scaledWidth = bitmap.width*scale; +// // let scaledHeight = bitmap.height*scale; + +// // let scale = 1/32; + +// let scaledWidth = bitmap.width; +// let scaledHeight = bitmap.height; + +// let resizeThreshold = 600; +// if (scaledWidth > resizeThreshold) { +// scaledWidth = resizeThreshold; +// scaledHeight = (bitmap.height / bitmap.width) * resizeThreshold; +// } + +// const canvas = document.createElement('canvas'); +// canvas.width = scaledWidth; +// canvas.height = scaledHeight; +// const ctx = canvas.getContext('2d'); +// ctx!.imageSmoothingEnabled = true; +// ctx!.imageSmoothingQuality = 'high'; +// canvas!.getContext('2d')!.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, scaledWidth, scaledHeight); + + +// // Compress the image and get the result as an ArrayBuffer +// const compressedBlob = await new Promise((resolve, reject) => +// canvas.toBlob( +// (blob) => (blob ? resolve(blob as Blob) : reject(new Error('Compression failed.'))), +// 'image/jpeg', +// quality +// ) +// ); + + +// // TODO: Don't need to do this as we'll be storing blobs directly. +// let compressedArrayBuffer = await (compressedBlob as Blob).arrayBuffer(); +// let compressedByteLength = compressedArrayBuffer.byteLength; + +// let percent = (uncompressedByteLength / compressedByteLength) +// console.log.apply(null, log(`compressImage: compressedSize:${(compressedArrayBuffer.byteLength / 1024).toFixed(2)}KBi ${percent.toFixed(2)}:1 compression`));; +// return compressedArrayBuffer; +// } catch (e) { +// console.error(e); +// return null; +// } +// } + +// async createNewPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4") { +// if ((typeof postText !== "string") || postText.length === 0) { +// console.log.apply(null, log("Not posting an empty string...")); +// return; +// } + +// if (mediaData && +// (mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') && +// (mediaData as ArrayBuffer).byteLength > 500 * 1024) { +// let compressedImage = await this.compressImage(mediaData as ArrayBuffer, mimeType, 0.9); +// if (compressedImage) { +// mediaData = compressedImage as ArrayBuffer; +// } +// } + +// let post = new Post(this.username, userID, postText, new Date(), mediaData); +// // this.posts.push(post); +// // localStorage.setItem(key, JSON.stringify(posts)); +// addData(userID, post); + +// this.websocket?.broadcastNewPost(userID, post); + + +// this.render(); +// } + + getPeerID() { + let id = localStorage.getItem("peer_id"); + + if (!id) { + console.log.apply(null, log(`Didn't find a peer ID, generating one`));; + id = generateID(); + localStorage.setItem("peer_id", id); + } + + return id; + } + + getUserID() { + let id = localStorage.getItem("dandelion_id"); + + if (!id) { + console.log.apply(null, log(`Didn't find a user ID, generating one`));; + id = generateID(); + localStorage.setItem("dandelion_id", id); + } + + return id; + } + + animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal']; + adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy']; + snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait'] + + hashIdToIndices(id: string) { + let indices = []; + for (let char of id) { + if (char !== '0' && char !== '-') { + indices.push(parseInt(char, 16)); + if (indices.length == 2) { + break; + } + } + } + return [indices[0], indices[1]]; + } + + funkyName(id: string, listOne: string[], listTwo: string[]) { + let [one, two] = this.hashIdToIndices(id); + let first = listOne[one % this.adjectives.length]; + let second = listTwo[two % this.animals.length]; + return { first, second } + } + + getUsername() { + let username = localStorage.getItem("dandelion_username"); + + if (username && username !== "not_set") { + return username; + } + + let { first: adjective, second: animal } = this.funkyName(this.userID, this.adjectives, this.animals); + username = `${adjective}_${animal}` + localStorage.setItem("dandelion_username", username); + + return username; + } + + getPeername() { + let { first: adjective, second: snake } = this.funkyName(this.peerID, this.adjectives, this.snakes); + let peername = `${adjective}_${snake}` + return peername; + } + +// setFont(fontName: string, fontSize: string) { + +// let content = document.getElementById('content'); + +// if (!content) { +// return; +// } + +// content.style.fontFamily = fontName; +// content.style.fontSize = fontSize; + +// let textArea = document.getElementById('textarea_post'); +// if (!textArea) { +// return; +// } + +// textArea.style.fontFamily = fontName; +// textArea.style.fontSize = fontSize; +// } + +// initOffline(connection: wsConnection) { +// // Event listener for going offline +// globalThis.addEventListener('offline', () => { +// console.log.apply(null, log("offline")); +// }); + +// // Event listener for going online +// globalThis.addEventListener('online', async () => { +// console.log.apply(null, log("online")); +// // connection.connect(); +// this.render(); +// }); + +// console.log.apply(null, log(`Online status: ${navigator.onLine ? "online" : "offline"}`)); + +// } + +// selectFile(contentType: string): Promise { +// return new Promise(resolve => { +// let input = document.createElement('input'); +// input.type = 'file'; +// // input.multiple = multiple; +// input.accept = contentType; + +// input.onchange = () => { +// if (input.files == null) { +// resolve(null); +// return; +// } + +// let files = Array.from(input.files); + +// // if (multiple) +// // resolve(files); +// // else +// resolve(files[0]); +// }; + +// input.click(); +// }); +// } + +// readFile(file: File): Promise { +// // Always return a Promise +// return new Promise((resolve, reject) => { +// let content = ''; +// const reader = new FileReader(); +// // Wait till complete +// reader.onloadend = function (e: any) { +// content = e.target.result; +// resolve(content); +// }; +// // Make sure to handle error states +// reader.onerror = function (e: any) { +// reject(e); +// }; +// 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'; +// setLogVisibility(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; + + +// } + +// button(elementName: string) { +// return document.getElementById(elementName) as HTMLButtonElement; +// } + +// div(elementName: string) { +// return document.getElementById(elementName) as HTMLDivElement; +// } + +// 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 toggleDark = document.getElementById('toggle_dark') 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; +// // let addPic = document.getElementById('button_add_pic') as HTMLDivElement; +// // toggleDark.addEventListener('click', () => { +// // document.documentElement.style.setProperty('--main-bg-color', 'white'); +// // document.documentElement.style.setProperty('--main-fg-color', 'black'); +// // }) + +// let homeButton = this.div('home-button'); +// homeButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/`) + +// let profileButton = this.div('profile-button'); +// profileButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/user/${this.userID}`) + +// let monitorButton = this.div('monitor_button'); +// monitorButton.addEventListener('click', async () => { +// navContainer.classList.toggle('active'); +// this.showInfo() +// }); + +// let navContainer = this.div('nav-container'); +// let burgerMenuButton = this.div('burger-menu-button'); +// burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active')); + +// let exportButton = this.button("export-button"); +// exportButton.addEventListener('click', async e => { + +// await this.exportPostsForUser(this.userID) +// }); + +// let composeButton = this.div('compose-button'); +// composeButton.addEventListener('click', e => { +// document.getElementById('compose')!.style.display = 'block'; +// document.getElementById('textarea_post')?.focus(); +// }); + + +// let filePicker = document.getElementById('file-input') as HTMLInputElement; +// filePicker?.addEventListener('change', async (event: any) => { +// for (let file of filePicker.files as any) { +// let buffer = await file.arrayBuffer(); +// await this.createNewPost(this.userID, 'image...', buffer, file.type); +// } + +// // Reset so that if they pick the same image again, we still get the change event. +// filePicker.value = ''; +// }); + +// let filePickerLabel = document.getElementById('file-input-label'); +// filePickerLabel?.addEventListener('click', () => { +// console.log.apply(null, log("Add pic...")); +// }) + + +// let usernameField = document.getElementById('username'); +// usernameField?.addEventListener('input', (event: any) => { +// this.username = event.target.innerText; +// localStorage.setItem("dandelion_username", this.username); +// }) + +// // importTweetsButton.addEventListener('click', async () => { +// // let file = await this.selectFile('text/*'); + +// // console.log.apply(null, log(file); +// // if (file == null) { +// // return; +// // } + +// // let tweetData = await this.readFile(file); +// // tweetData = tweetData.replace('globalThis.YTD.tweets.part0 = ', ''); +// // const tweets = JSON.parse(tweetData); + +// // let imported_posts = await this.importTweetArchive(userID, tweets); +// // clearData(userID); +// // // posts = posts.reverse(); +// // addDataArray(userID, imported_posts); +// // this.render(); + +// // }); + +// // clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() }); + + +// let postButton = document.getElementById("button_post") as HTMLButtonElement; +// let postText = document.getElementById("textarea_post") as HTMLTextAreaElement; + +// if (!(postButton && postText)) { +// throw new Error(); +// } + +// postText.addEventListener('paste', async (e) => { +// const dataTransfer = e.clipboardData +// const file = dataTransfer!.files[0]; +// let buffer = await file.arrayBuffer(); +// await this.createNewPost(this.userID, 'image...', buffer, file.type as any); +// }); + +// postButton.addEventListener("click", () => { +// this.createNewPost(userID, postText.value); +// postText.value = ""; +// document.getElementById('compose')!.style.display = 'none'; +// }); + +// // updateApp.addEventListener("click", () => { +// // registration?.active?.postMessage({ type: "update_app" }); +// // }); + + +// // ddlnLogoButton.addEventListener('click', async () => { +// // this.showInfo() +// // }); + +// } + +// async getPostsForFeed() { + +// // get N posts from each user and sort them by date. +// // 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: StoragePost[] = []; +// for (let followedID of this.following.keys()) { +// posts = posts.concat(await getData(followedID, new Date(2022, 8), new Date())); +// // console.log.apply(null, log(followedID); +// } + +// // @ts-ignore +// posts = posts.sort((a, b) => a.post_timestamp - b.post_timestamp); + +// return posts; +// } + +// async loadFollowersFromStorage(userID: string): Promise { + +// // Rob +// if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') { +// return [ +// 'b38b623c-c3fa-4351-9cab-50233c99fa4e', +// '6d774268-16cd-4e86-8bbe-847a0328893d', // Sean +// '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin +// 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO +// 'bba3ad24-9181-4e22-90c8-c265c80873ea', // Harry +// '8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona +// ] +// } + +// // Martin +// if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') { +// return [ +// 'b38b623c-c3fa-4351-9cab-50233c99fa4e', +// 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO +// ] +// } + +// // Fiona +// if (userID === '8f6802be-c3b6-46c1-969c-5f90cbe01479') { +// return [ +// 'b38b623c-c3fa-4351-9cab-50233c99fa4e', // Rob +// 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO +// '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin +// ] +// } + +// return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :) +// } + +// async loadPostsFromStorage(userID: string, postID?: string) { + +// this.timerStart(); +// let posts: StoragePost[] = []; + +// // if (postID) { +// // posts = await gePostForUser(userID, postID); +// // } + +// posts = await getData(userID, new Date(2022, 8), new Date()); + +// if (posts.length > 0) { +// console.log.apply(null, log(`Loaded ${posts.length} posts in ${this.timerDelta().toFixed(2)}ms`));; +// return posts; +// } + +// // posts = await createTestData2(userID); + +// // log("Adding test data..."); +// // addDataArray(userID, posts); +// // return await getData(userID, new Date(2022, 8), new Date()); +// } + +// async listUsers() { +// let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', '')); +// if (knownUsers.length === 0) { +// return; +// } + +// let preferredId = app.getPreferentialUserID() +// for (let userID of knownUsers as string[]) { +// // if (userID === preferredId) { +// // continue; +// // } + +// // let ids = await getAllIds(userID); +// // if (ids.length === 0) { +// // console.log.apply(null, log(`Purging user ${userID}`); +// // indexedDB.deleteDatabase(`user_${userID}`); +// // continue; +// // } + +// console.log.apply(null, log(`${document.location.origin}/user/${userID}`)); + +// // console.log.apply(null, log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID); +// } + +// } + + +// async initDB() { +// let db = await openDatabase(this.userID); +// } + +// query_findPeersForUser(message: any) { +// let havePostsForUser = true; +// if (havePostsForUser) { +// return this.peerID; +// } + +// return false; +// } + + async registerRPCs() { + if (!this.peerManager) { + throw new Error(); + } + + this.peerManager.registerRPC('ping', (args: any) => { + return {id:this.peerID, user:this.userID, user_name:this.username, peer_name:this.peername}; + }); + + if (!this.isBootstrapPeer) { + let pong = await this.peerManager.rpc.ping(this.peerManager.bootstrapPeerID); + console.log.apply(null, log('pong from: ', pong)); + } + + // this.peerManager.registerRPC('getPostIDsForUser', (args: any) => { + // this.sync.getPostsForUser + // }); + + } + +// async testPeerManager() { +// if (!this.peerManager) { +// throw new Error(); +// } + +// this.peerManager.registerRPC('getPostIDsForUser', (args: any) => { +// return [1, 2, 3, 4, 5] +// }); + +// let postIDs = await this.peerManager.rpc.getPostIDsForUser("dummy_peer", "bloop"); + +// console.log.apply(null, log("peerManager.rpc.getPostIDsForUser", postIDs)); + +// // this.peerManager.registerSearchQuery('find_peers_for_user', this.query_findPeersForUser); + +// // let peers = await this.peerManager.search('find_peers_for_user', { 'user_id': 'bloop' }); + +// } + + async main() { + + // Do capability detection here and report in a simple way if things we need don't exist with guidance on how to resolve it. + + // let urlParams = (new URL(globalThis.location.href)).searchParams; + // if (urlParams.has('log')) { + // this.showInfo(); + // } + + this.isHeadless = true;///\bHeadlessChrome\//.test(navigator.userAgent); + this.isBootstrapPeer = true;//urlParams.has("bootstrap"); + if (this.isBootstrapPeer) { + console.log.apply(null, log(`This is a bootstrap peer`));; + } + + this.peerID = this.getPeerID(); + this.peername = this.getPeername(); + this.userID = this.getUserID(); + this.username = this.getUsername(); + + this.connect(); +} + + // this.registerRPCs(); + // this.testPeerManager(); + +// // let peer: RTCPeerConnection | null = null; +// // // if (globalThis.RTCPeerConnection) { +// // peer = new RTCPeerConnection({ +// // iceServers: [ +// // { urls: "stun:ddln.app" }, +// // // { urls: "turn:ddln.app", username: "a", credential: "b" }, +// // { urls: "stun:stun.l.google.com" }, // keeping this for now as my STUN server is not return ipv6 +// // // { urls: "stun:stun1.l.google.com" }, +// // // { urls: "stun:stun2.l.google.com" }, +// // // { urls: "stun:stun3.l.google.com" }, +// // // { urls: "stun:stun4.l.google.com" }, +// // ] +// // }); + +// // peer.createDataChannel('boop'); + +// // peer.onicecandidate = ({ candidate }) => { log(`WRTC:${candidate?.address} ${candidate?.protocol} ${candidate?.type} ${(candidate as any)?.url}`) }; +// // peer.onnegotiationneeded = async (event) => { +// // console.log.apply(null, log("on negotiation needed fired"));; + +// // let makingOffer = false; + +// // try { +// // makingOffer = true; +// // await peer.setLocalDescription(); + +// // let IDsToSync = this.following; +// // if (this.router.route === App.Route.USER) { +// // IDsToSync = new Set([this.router.userID]); +// // } + +// // if (!peer.localDescription) { +// // return; +// // } + +// // // this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync, peer.localDescription); +// // // log(peer.localDescription.type + ":" + peer.localDescription.sdp); +// // // this.initOffline(this.websocket); + +// // // this.websocket?.sendWebRTCDescription(peer.localDescription); +// // } catch (err) { +// // console.error(err); +// // } finally { +// // makingOffer = false; +// // } + +// // } + + + + +// // peer.createOffer().then((description)=>{ +// // peer.setLocalDescription(description) +// // console.log.apply(null, log("RTC: " + description.sdp + description.type));; +// // }); + +// // } + +// // 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 + +// // Render +// // Load all images async +// // Start the process of figuring out what posts we need +// // Download posts once all current images are loaded + + +// // globalThis.resizeTo(645, 900); + +// // this.initLogo() + + +// this.getRoute(); + +// if (this.router.route === App.Route.CONNECT) { +// console.log.apply(null, log('connect', this.router.userID)); +// localStorage.setItem("dandelion_id", this.router.userID); +// localStorage.removeItem("dandelion_username"); +// } + + +// await this.initDB(); + +// this.connectURL = `${document.location.origin}/connect/${this.userID}`; +// document.getElementById('connectURL')!.innerHTML = `connect`; + + + +// this.isHeadless = urlParams.has('headless'); + +// let limitPostsParam = urlParams.get('limitPosts'); +// if (limitPostsParam) { +// this.limitPosts = parseInt(limitPostsParam); +// } + +// let time = 0; +// let delta = 0; +// // let isPersisted = await navigator?.storage?.persisted(); +// // if (!isPersisted) { +// // debugger; +// // const isPersisted = await navigator.storage.persist(); +// // console.log.apply(null, log(`Persisted storage granted: ${isPersisted}`));; +// // } + +// // log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`); + +// this.initMarkdown(); + +// // let main = await fetch("/main.js"); +// // let code = await main.text(); +// // console.log.apply(null, log(code); +// // registration.active.postMessage({type:"updateMain", code:code}); + +// // this.posts = await this.loadPosts(userID) ?? []; + +// // debugger; + +// await this.render(); // , (postID:string)=>{this.deletePost(userID, postID)} + +// if ((performance as any)?.memory) { +// console.log.apply(null, log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)); +// } + +// // if (navigator?.storage) { +// // let storageUsed = (await navigator?.storage?.estimate())?.usage/1024/1024 +// // } + +// // if (urlParams.get("sw") === "true") { +// let registration; +// registration = await this.registerServiceWorker(); +// // } + +// document.getElementById('username')!.innerText = `${this.username}`; +// document.getElementById('peername')!.innerText = `peername:${this.peername}`; +// 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); + + + +// console.log.apply(null, log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`));; + +// // await this.purgeEmptyUsers(); + + + +// // this.listUsers() + + +// // this.createNetworkViz(); + +// // const client = new WebTorrent() + +// // // Sintel, a free, Creative Commons movie +// // const torrentId = 'magnet:?xt=urn:btih:6091e199a8d9272a40dd9a25a621a5c355d6b0be&dn=WING+IT!+-+Blender+Open+Movie+1080p.mp4&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337'; + +// // client.add(torrentId, function (torrent: any) { +// // // Torrents can contain many files. Let's use the .mp4 file +// // const file = torrent.files.find(function (file: any) { +// // return file.name.endsWith('.mp4') +// // }) + +// // // Display the file by adding it to the DOM. +// // // Supports video, audio, image files, and more! +// // file.appendTo(document.getElementById('torrent-content')); +// // }) +// } + +// renderWelcome(contentDiv: HTMLDivElement) { +// contentDiv.innerHTML = `
+// Welcome to Dandelion v0.1!
+// Loading posts for the default feed... +//
+// `; +// } + +// // keep a map of posts to dom nodes. +// // on re-render +// // posts that are not in our list that we need at add +// // posts that are in our list that we need to remove + +// private renderedPosts = new Map(); + +// async render() { +// if (this.isHeadless) { +// console.log.apply(null, log('Headless so skipping render...')); +// return; +// } + +// performance.mark("render-start"); +// this.timerStart(); + + +// let existingPosts = this.posts; + + + +// this.posts = []; +// switch (this.router.route) { +// case App.Route.HOME: +// case App.Route.CONNECT: { +// this.following = new Set(await this.loadFollowersFromStorage(this.userID) ?? []); +// this.posts = await this.getPostsForFeed(); +// // this.posts = await this.loadPostsFromStorage(this.userID) ?? []; +// // let compose = document.getElementById('compose'); +// // if (!compose) { +// // break; +// // } +// // compose.style.display = "block"; +// break; +// } +// case App.Route.USER: { +// this.posts = await this.loadPostsFromStorage(this.router.userID) ?? []; +// let compose = document.getElementById('compose'); +// if (!compose) { +// break; +// } + +// compose.style.display = "none"; +// break; +// } +// case App.Route.POST: { +// this.posts = await this.loadPostsFromStorage(this.router.userID, this.router.postID) ?? []; +// let compose = document.getElementById('compose'); +// if (!compose) { +// break; +// } +// compose.style.display = "none"; +// break; +// } +// default: { +// console.log.apply(null, log("Render: got a route I didn't understand. Rendering HOME:", this.router.route)); +// this.posts = await this.loadPostsFromStorage(this.userID) ?? []; +// break; +// } +// } +// let contentDiv = document.getElementById("content"); +// if (!contentDiv) { +// throw new Error(); +// } +// if (this.posts.length === 0) { +// this.renderWelcome(contentDiv as HTMLDivElement); +// return; +// } + + +// // let existingPostSet = new Set(existingPosts.map(post => post.post_id)); +// // let incomingPostSet = new Set(this.posts.map(post => post.post_id)); + +// // let addedPosts = []; +// // for (let post of this.posts) { +// // if (!existingPostSet.has(post.post_id)) { +// // addedPosts.push(post); +// // } +// // } + +// // let deletedPosts = []; +// // for (let post of existingPosts) { +// // if (!incomingPostSet.has(post.post_id)) { +// // deletedPosts.push(post); +// // } +// // } + +// // console.log.apply(null, log("added:", addedPosts, "removed:", deletedPosts); + +// const fragment = document.createDocumentFragment(); + +// contentDiv.innerHTML = ""; +// let count = 0; + +// this.renderedPosts.clear(); +// let first = true; +// for (let i = this.posts.length - 1; i >= 0; i--) { +// let postData = this.posts[i]; +// // this.postsSet.add(postData); +// // TODO return promises for all image loads and await those. +// let post = this.renderPost(postData.data, first); +// first = false; +// // this.renderedPosts.set(postData.post_id, post); +// if (post) { +// fragment.appendChild(post); +// count++; +// } +// if (count > this.limitPosts) { +// break; +// } +// } + + +// if (!contentDiv) { +// throw new Error("Couldn't get content div!"); +// } + +// contentDiv.appendChild(fragment); + +// let renderTime = this.timerDelta(); + +// console.log.apply(null, log(`render took: ${renderTime.toFixed(2)}ms`));; +// performance.mark("render-end"); +// performance.measure('render-time', 'render-start', 'render-end'); + + + +// // if ((performance as any)?.memory) { +// // console.log.apply(null, log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)); +// // } + +// } + +// async deletePost(userID: string, postID: string) { +// deleteData(userID, postID) + +// this.render(); +// } + +// renderPost(post: Post, first: boolean) { +// if (!(post.hasOwnProperty("text"))) { +// throw new Error("Post is malformed!"); +// } +// let containerDiv = document.createElement("div"); + +// let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`; + +// let deleteButton = document.createElement('button'); deleteButton.innerText = 'delete'; +// deleteButton.onclick = () => { this.deletePost(post.author_id, post.post_id) }; + +// // let editButton = document.createElement('button'); editButton.innerText = 'edit'; +// let shareButton = document.createElement('button'); shareButton.innerText = 'share'; +// shareButton.onclick = async () => { +// let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`; + +// await navigator.clipboard.writeText(shareUrl) +// }; + +// let ownPost = post.author_id === this.userID; + +// let markdown = post.text; +// if (this.markedAvailable) { +// markdown = marked.parse(post.text); +// } + +// // if (markdown.includes("${first ? '' : '
'} +//
+// @${post.author} - +// ${post.post_timestamp.toLocaleDateString()} +// +// ${ownPost ? `` : ''} +// ${ownPost ? `` : ''} +// +//
+//
${markdown}
+// ` + +// containerDiv.innerHTML = postTemplate; + + +// if (ownPost) { +// containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton); +// // containerDiv.querySelector('#editButton')?.appendChild(editButton); +// } + + +// containerDiv.querySelector('#shareButton')?.appendChild(shareButton); + + +// if (!("image_data" in post && post.image_data)) { +// // containerDiv.appendChild(timestampDiv); +// return containerDiv; +// // return null; +// } + +// let image = document.createElement("img"); +// image.title = `${(post.image_data.byteLength / 1024 / 1024).toFixed(2)}MBytes`; +// // const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' }); +// const blob = new Blob([post.image_data as ArrayBuffer]); +// const url = URL.createObjectURL(blob); +// image.onload = () => { +// URL.revokeObjectURL(url); +// }; + +// image.src = url; +// // image.src = image.src = "data:image/png;base64," + post.image; +// image.className = "postImage"; +// // image.onclick = () => { App.maximizeElement(image) }; + +// containerDiv.appendChild(image); +// // containerDiv.appendChild(timestampDiv); + +// return containerDiv; +// } + +// static maximizeElement(element: HTMLImageElement) { +// element.style.transform = "scale(2.0)" +// } + +// router = { +// route: App.Route.HOME, +// userID: '', +// postID: '', +// mediaID: '' +// } + +// getRoute() { +// let path = document.location.pathname; +// console.log.apply(null, log("router: path ", path)); + +// const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))"; + +// const match = path.match(new RegExp(regex)); + +// if (match) { +// if (match[8]) { // Check for the connect route +// this.router.userID = match[8]; +// this.router.route = App.Route.CONNECT; +// } else { + +// this.router.userID = match[2]; +// this.router.postID = match[4]; +// this.router.mediaID = match[6]; + +// if (this.router.mediaID) { +// this.router.route = App.Route.MEDIA; +// } else if (this.router.postID) { +// this.router.route = App.Route.POST; +// } else { +// this.router.route = App.Route.USER; +// } +// } +// } + +// console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route])); + +// // user = /user/ +// // post = /user//post/ +// // media = /user//post//media/ +// // group = /group/ID/post/ +// // hashtag = /hashtag/ -- maybe only hastags in groups +// // home = / + +// } + +// } + +// namespace App { +// export enum Route { +// USER, +// POST, +// MEDIA, +// GROUP, +// HOME, +// CONNECT, +// }; + +// // export function connect() { +// // throw new Error("Function not implemented."); +// // } + +// // export function connect() { +// // throw new Error("Function not implemented."); +// // } +} + + + +let app = new App(); + +app.main(); + +// globalThis.addEventListener("load", app.main.bind(app)); diff --git a/src/log.ts b/src/log.ts index e7b7aac..96c2caf 100644 --- a/src/log.ts +++ b/src/log.ts @@ -2,6 +2,10 @@ let logLines: string[] = []; let logLength = 100; let logVisible = false; +export function logID(ID: string) { + return ID.substring(0, 5); +} + export function setLogVisibility(visible:boolean) { logVisible = visible; } diff --git a/src/main2.ts b/src/main2.ts index 18e2157..e1c0706 100644 --- a/src/main2.ts +++ b/src/main2.ts @@ -35,7 +35,7 @@ import { openDatabase, getData, addData, addDataArray, clearData, deleteData, me import { generateID } from "IDUtils"; import { PeerManager } from "PeerManager"; -import {log, renderLog, setLogVisibility} from "log" +import {log, logID, renderLog, setLogVisibility} from "log" // import {PeerConnection} from "webRTC"; @@ -109,12 +109,6 @@ function uuidToBase58(uuid: string): string { return encodeBase58(bytes); } - -function logID(ID: string) { - return ID.substring(0, 5); -} - - // function log(message:string) { // console.log.apply(null, log(message); // let log = document.getElementById("log"); @@ -175,8 +169,8 @@ window.addEventListener('scroll', () => { // Check if scrolled to bottom if (scrollPoint >= totalPageHeight) { - console.log.apply(null, log('Scrolled to the bottom!')); - console.log.apply(null, log(scrollPoint, totalPageHeight)); + // console.log.apply(null, log('Scrolled to the bottom!')); + // console.log.apply(null, log(scrollPoint, totalPageHeight)); } }); diff --git a/static/main2.d.ts b/static/main2.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/static/main2.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/static/main2.js b/static/main2.js new file mode 100644 index 0000000..b5e8231 --- /dev/null +++ b/static/main2.js @@ -0,0 +1,1623 @@ +// TODO: virtual list, only rerender what's needed so things can keep playing. +/* +Problems + 1. Can't delete, very annoying + Tombstones. Send all IDs and all Tombstones. Ask only for posts that we don't get a tombstone for. Don't send posts we have a tombstone for? + + Posts don't propagate, you need to refresh to see new posts. + Broadcast when we post to all peers we know about. + + 3. Posting is slow because too long to render + 2. Can't follow people + 4. Can't like or reply to posts + +user + posts + media + tombstones + following + profile + name + description + profile pic + + +Restruucture the app around the data. App/WS split is messy. Clean it up. + +*/ +// import * as ForceGraph3D from "3d-force-graph"; +import { openDatabase, getData, addData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds } from "db"; +import { generateID } from "IDUtils"; +import { PeerManager } from "PeerManager"; +import { log, logID, renderLog, setLogVisibility } from "log"; +// let posts:any; +// let keyBase = "dandelion_posts_v1_" +// let key:string = ""; +// interface PostTimestamp { +// year: number, +// month: number, +// day: number, +// hour: number, +// minute: number, +// second: number, +// } +function waitMs(durationMs) { + return new Promise(resolve => setTimeout(resolve, durationMs)); +} +function uuidToBytes(uuid) { + return new Uint8Array(uuid.match(/[a-fA-F0-9]{2}/g).map((hex) => parseInt(hex, 16))); +} +// Base58 character set +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; +// Base58 encoding +// Base58 encoding +function encodeBase58(buffer) { + let carry; + const digits = [0]; + for (const byte of buffer) { + carry = byte; + for (let i = 0; i < digits.length; i++) { + carry += digits[i] << 8; + digits[i] = carry % 58; + carry = Math.floor(carry / 58); + } + while (carry > 0) { + digits.push(carry % 58); + carry = Math.floor(carry / 58); + } + } + let result = ''; + for (const digit of digits.reverse()) { + result += BASE58_ALPHABET[digit]; + } + // Handle leading zero bytes + for (const byte of buffer) { + if (byte === 0x00) { + result = BASE58_ALPHABET[0] + result; + } + else { + break; + } + } + return result; +} +// Convert UUID v4 to Base58 +function uuidToBase58(uuid) { + const bytes = uuidToBytes(uuid); + return encodeBase58(bytes); +} +class Post { + constructor(author, author_id, text, post_timestamp, imageData = null, importedFrom = null, importSource = null) { + this.post_timestamp = post_timestamp; + this.post_id = generateID(); + this.author = author; + this.author_id = author_id; + this.text = text; + this.image_data = imageData; + this.importedFrom = importedFrom; + this.importSource = importSource; + } +} +window.addEventListener('scroll', () => { + // Total height of the document + const totalPageHeight = document.body.scrollHeight; + // Current scroll position + const scrollPoint = window.scrollY + window.innerHeight; + // Check if scrolled to bottom + if (scrollPoint >= totalPageHeight) { + // console.log.apply(null, log('Scrolled to the bottom!')); + // console.log.apply(null, log(scrollPoint, totalPageHeight)); + } +}); +// let peer = await new PeerConnection(peer_id); +// let connectionReply = await wsConnection.send('hello'); +// for (let peer of connectionReply) { +// let peerConnection = await wsConnection.send('connect', peer.id); +// if (peerConnection) { +// this.peers.push(peerConnection); +// let postIDs = await peerConnection.getPostIDs(); +// let postsWeDontHave = this.diffPostIDs(postIDs); +// let newPosts = await peerConnection.getPosts(postsWeDontHave); +// this.addPosts(newPosts); +// } +// } +async function bytesToBase64DataUrl(bytes, type = "application/octet-stream") { + return await new Promise((resolve, reject) => { + const reader = Object.assign(new FileReader(), { + onload: () => resolve(reader.result), + onerror: () => reject(reader.error), + }); + reader.readAsDataURL(new File([bytes], "", { type })); + }); +} +async function arrayBufferToBase64(buffer) { + var bytes = new Uint8Array(buffer); + return (await bytesToBase64DataUrl(bytes)).replace("data:application/octet-stream;base64,", ""); +} +async function base64ToArrayBuffer(base64String) { + let response = await fetch("data:application/octet-stream;base64," + base64String); + let arrayBuffer = await response.arrayBuffer(); + return arrayBuffer; +} +async function compressString(input) { + // Convert the string to a Uint8Array + const textEncoder = new TextEncoder(); + const inputArray = textEncoder.encode(input); + // Create a CompressionStream + const compressionStream = new CompressionStream('gzip'); + const writer = compressionStream.writable.getWriter(); + // Write the data and close the stream + writer.write(inputArray); + writer.close(); + // Read the compressed data from the stream + const compressedArray = await new Response(compressionStream.readable).arrayBuffer(); + // Convert the compressed data to a Uint8Array + return new Uint8Array(compressedArray); +} +// class Signaler { +// websocket: WebSocket | null = null; +// websocketPingInterval: number = 0; +// connect() { +// if (this.websocket?.readyState === WebSocket.OPEN) { +// return; +// } +// window.clearInterval(this.websocketPingInterval); +// if (this.websocket) { this.websocket.close() }; +// try { +// this.websocket = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/ws`); +// } catch (error: any) { +// console.log.apply(null, log(error.message); +// return; +// } +// this.websocket.onopen = async (event) => { +// console.log.apply(null, log("ws:connected"));; +// await this.sendHello(); +// // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have. +// let helloRefreshIntervalPeriod = 120; +// if (app.isHeadless) { +// console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod) +// this.helloRefreshInterval = window.setInterval(() => { +// console.log.apply(null, log("wsConnection: Hello refresh.") +// if (!navigator.onLine) { +// return; +// } +// this.sendHello(); +// }, helloRefreshIntervalPeriod * 1000); +// } +// this.websocketPingInterval = window.setInterval(() => { +// if (!navigator.onLine) { +// return; +// } +// this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username }); +// }, 10_000) +// }; +// this.websocket.onclose = (event) => { +// console.log.apply(null, log("ws:disconnected"));; +// // this.retry *= 2; +// console.log.apply(null, log(`Retrying in ${this.retry} seconds`));; +// window.setTimeout(() => { this.connect(); }, this.retry * 1000); +// }; +// this.websocket.onmessage = (event) => { +// // log('ws:<-' + event.data.slice(0, 240)); +// let data = JSON.parse(event.data); +// let { type } = data; +// let handler = this.messageHandlers.get(type); +// if (!handler) { +// console.warn(`Got a message we can't handle:`, type); +// return; +// } +// handler(data); +// }; +// this.websocket.onerror = (event) => { +// console.log.apply(null, log('ws:error: ' + event));; +// }; +// } +// } +// disconnect() { +// this.websocket?.close(); +// } +// } +// Connect websocket +// send hello +// get bootstrap peer ID +// WebRTC connect to bootstrap peer +// ask Bootstrap peer for peers that have users we care about. +// for now, bootstrap peer will connect to all peers and will tell us about them, moving all logic from the server to the BSP +// WebRTC Connect to peers that might have posts we need +// query those peers and do existing logic. +class wsConnection { + constructor(userID, peerID, IDsToSync, rtcPeerDescription) { + this.websocket = null; + this.sessionID = ""; + this.userID = ""; + this.peerID = ""; + this.rtcPeerDescription = null; + this.websocketPingInterval = 0; + this.helloRefreshInterval = 0; + this.retry = 10; + this.state = 'disconnected'; + // peers: Map = new Map(); + this.messageHandlers = new Map(); + this.peerMessageHandlers = new Map(); + this.seenPeers = new Map(); + // static async compressArrayBuffer(data: ArrayBuffer): Promise { + // 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.rtcPeerDescription = rtcPeerDescription; + this.sessionID = generateID(); + this.userID = userID; + this.peerID = peerID; + this.UserIDsToSync = new Set(IDsToSync); + this.messageHandlers.set('hello', this.helloResponseHandler.bind(this)); + this.messageHandlers.set('hello2', this.hello2ResponseHandler.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.peerMessageHandlers.set('send_webrtc_offer', this.sendWebRTCOfferHandler.bind(this)); + // this.peerMessageHandlers.set('send_webrtc_offer_response', this.getPostIdsForUserResponseHandler.bind(this)); + window.addEventListener('beforeunload', () => this.disconnect()); + this.connect(); + } + // So we don't need custom logic everywhere we use this, I just wrapped it. + shouldSyncUserID(userID) { + if (app.isHeadless) { + return true; + } + return this.UserIDsToSync.has(userID); + } + async send(message) { + let json = ""; + try { + json = JSON.stringify(message); + // console.log.apply(null, log("*******", (await compressString(json)).byteLength, json.length); + } + catch (e) { + console.log.apply(null, log(e, "wsConnection send: Couldn't serialize message", message)); + } + // log(`ws->${json.slice(0, 240)}`) + this.websocket.send(json); + } + pongHandler(data) { + } + async sendWebRTCDescription(description) { + console.log.apply(null, log("description:", description)); + this.send({ type: "rtc_session_description", description: description }); + } + async getPostIdsForUserResponseHandler(data) { + // log(`getPostsForUserResponse: ${data}`) + let message = data.message; + console.log.apply(null, log(`Net: got ${message.post_ids.length} post IDs for user ${logID(message.user_id)} from peer ${logID(data.from)}`)); + ; + let startTime = app.timerStart(); + let postIds = await checkPostIds(message.user_id, message.post_ids); + console.log.apply(null, log(`ID Check for user ${logID(message.user_id)} took ${app.timerDelta().toFixed(2)}ms`)); + ; + console.log.apply(null, log(`Need ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`)); + ; + if (postIds.length === 0) { + return; + } + console.log.apply(null, log(`Net: Req ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`)); + let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user", post_ids: postIds, user_id: message.user_id } }; + this.send(responseMessage); + } + async getPostIdsForUserHandler(data) { + let message = data.message; + let postIds = await getAllIds(message.user_id) ?? []; + postIds = postIds.filter((postID) => !this.postBlockList.has(postID)); + if (postIds.length === 0) { + console.log.apply(null, log(`Net: I know about user ${logID(message.user_id)} but I have 0 posts, so I'm not sending any to to peer ${logID(data.from)}`)); + ; + return; + } + console.log.apply(null, log(`Net: Sending ${postIds.length} post Ids for user ${logID(message.user_id)} to peer ${logID(data.from)}`)); + let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_post_ids_for_user_response", post_ids: postIds, user_id: message.user_id } }; + this.send(responseMessage); + } + async broadcastNewPost(userID, post) { + let newPost = { ...post }; + if (post.image_data) { + newPost.image_data = await arrayBufferToBase64(post.image_data); + } + for (let [peerID, peerInfo] of this.seenPeers.entries()) { + console.log.apply(null, log(`broadcastNewPost: sending new post to ${logID(peerID)}:${peerInfo.peerName}:${peerInfo.userName}`)); + ; + this.sendPostsForUser(peerID, app.userID, [newPost]); + } + } + async sendPostsForUser(toPeerID, userID, posts) { + let responseMessage = { + type: "peer_message", + from: app.peerID, + to: toPeerID, + from_username: app.username, + from_peername: app.peername, + message: { + type: "get_posts_for_user_response", + posts: posts, + user_id: userID + } + }; + return this.send(responseMessage); + } + // Send posts to peer + async getPostsForUserHandler(data) { + let message = data.message; + let posts = await getPostsByIds(message.user_id, message.post_ids) ?? []; + console.log.apply(null, log(`Net: Sending ${posts.length} posts for user ${logID(message.user_id)} to peer ${logID(data.from)}`)); + ; + app.timerStart(); + let output = []; + console.log.apply(null, log("Serializing images")); + for (let post of posts) { + let newPost = post.data; + if (newPost.image_data) { + // let compressedData = await wsConnection.compressArrayBuffer(newPost.image_data); + // console.log.apply(null, log((newPost.image_data.byteLength - compressedData.byteLength) / 1024 / 1024); + // TODO don't do this, use Blobs direclty! + // https://developer.chrome.com/blog/blob-support-for-Indexeddb-landed-on-chrome-dev + newPost.image_data = await arrayBufferToBase64(newPost.image_data); + } + // let megs = JSON.stringify(newPost).length/1024/1024; + // console.log.apply(null, log(`getPostsForUserHandler id:${newPost.post_id} post length:${megs}`); + output.push(newPost); + } + let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user_response", posts: output, user_id: message.user_id } }; + console.log.apply(null, log("Sending posts")); + await this.sendPostsForUser(data.from, message.user_id, output); + let sendTime = app.timerDelta(); + console.log.apply(null, log(`getPostsForUserHandler send took: ${sendTime.toFixed(2)}ms`)); + ; + } + // Got posts from peer + async getPostsForUserReponseHandler(data) { + app.timerStart(); + let message = data.message; + console.log.apply(null, 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. + // Once we support delete then we we could delete these posts in a sensible way. + if (this.postBlockList.has(post.post_id)) { + console.log.apply(null, log(`Skipping blocked post: ${post.post_id}`)); + ; + continue; + } + // HACK - some posts had the wrong author ID + if (message.user_id === app.userID) { + post.author_id = app.userID; + } + post.post_timestamp = new Date(post.post_timestamp); + if (post.image_data) { + post.image_data = await base64ToArrayBuffer(post.image_data); + } + } + console.log.apply(null, log(`Merging same user peer posts...`)); + await mergeDataArray(message.user_id, data.message.posts); + let receiveTime = app.timerDelta(); + console.log.apply(null, log(`getPostsForUserReponseHandler receive took: ${receiveTime.toFixed(2)}ms`)); + ; + if (message.user_id === app.getPreferentialUserID() || app.following.has(message.user_id)) { + app.render(); + } + } + async peerMessageHandler(data) { + // log(`peerMessageHandler ${JSON.stringify(data)}`) + this.seenPeers.set(data.from, { peerName: data.from_peername, userName: data.from_username }); + let peerMessageType = data.message.type; + let handler = this.peerMessageHandlers.get(peerMessageType); + if (!handler) { + console.error(`got peer message type we don't have a handler for: ${peerMessageType}`); + return; + } + handler(data); + } + // Hello2 + // Goal, connect to bootstrap peer, ask bootstrap peer for peers that have posts from users that we care about. get peers, connect to those peers, sync. + // how? do "perfect negotiation" with bootstrap peer. All logic here moves to BP. + async sendHello2() { + this.send({ + type: "hello2", + user_id: this.userID, + user_name: app.username, + peer_id: this.peerID, + session_id: this.sessionID, + peer_name: app.peername, + is_bootstrap_peer: app.isBootstrapPeer, + peer_description: this.rtcPeerDescription + }); + } + async sendHello() { + // 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.shouldSyncUserID(userID)) + .filter(userID => !this.userBlockList.has(userID)) + .filter(async (userID) => (await getAllIds(userID)).length > 0); // TODO:EASYOPT getting all the IDs is unecessary, replace it with a test to get a single ID. + console.log.apply(null, 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 }); + } + hello2ResponseHandler(data) { + } + helloResponseHandler(data) { + let users = []; + let receivedUsers = Object.entries(data.userPeers); + console.log.apply(null, 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.apply(null, log('helloResponseHandler', e)); + } + let getAllUsers = app.router.route !== App.Route.USER; + if (getAllUsers) { + users = [...users, ...Object.entries(data.userPeers).filter(userID => this.shouldSyncUserID(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.apply(null, log("Skipping user on blocklist:", userID)); + continue; + } + // this.peers.set(userID, [...peerIDs]); + for (let peerID of [...peerIDs]) { + if (peerID === this.peerID) { + continue; + } + console.log.apply(null, 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; + } + window.clearInterval(this.websocketPingInterval); + if (this.websocket) { + this.websocket.close(); + } + ; + try { + this.websocket = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/ws`); + } + catch (error) { + console.log.apply(null, log(error.message)); + return; + } + this.websocket.onopen = async (event) => { + console.log.apply(null, log("ws:connected")); + ; + await this.sendHello2(); + // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have. + // let helloRefreshIntervalPeriod = 120; + // if (app.isHeadless) { + // console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod) + // this.helloRefreshInterval = window.setInterval(() => { + // console.log.apply(null, log("wsConnection: Hello refresh.") + // if (!navigator.onLine) { + // return; + // } + // this.sendHello(); + // }, helloRefreshIntervalPeriod * 1000); + // } + this.websocketPingInterval = window.setInterval(() => { + if (!navigator.onLine) { + return; + } + this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username }); + }, 10000); + }; + // this.websocket.onopen = async (event) => { + // console.log.apply(null, log("ws:connected"));; + // await this.sendHello(); + // // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have. + // let helloRefreshIntervalPeriod = 120; + // if (app.isHeadless) { + // console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod) + // this.helloRefreshInterval = window.setInterval(() => { + // console.log.apply(null, log("wsConnection: Hello refresh.") + // if (!navigator.onLine) { + // return; + // } + // this.sendHello(); + // }, helloRefreshIntervalPeriod * 1000); + // } + // this.websocketPingInterval = window.setInterval(() => { + // if (!navigator.onLine) { + // return; + // } + // this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username }); + // }, 10_000) + // }; + this.websocket.onclose = (event) => { + console.log.apply(null, log("ws:disconnected")); + ; + // this.retry *= 2; + console.log.apply(null, log(`Retrying in ${this.retry} seconds`)); + ; + window.setTimeout(() => { this.connect(); }, this.retry * 1000); + }; + this.websocket.onmessage = (event) => { + // log('ws:<-' + event.data.slice(0, 240)); + let data = JSON.parse(event.data); + let { type } = data; + let handler = this.messageHandlers.get(type); + if (!handler) { + console.warn(`Got a message we can't handle:`, type); + return; + } + handler(data); + }; + this.websocket.onerror = (event) => { + console.log.apply(null, log('ws:error: ' + event)); + ; + }; + } + disconnect() { + this.websocket?.close(); + } +} +class App { + constructor() { + this.username = ''; + this.peername = ''; + this.userID = ''; + this.peerID = ''; + this.following = new Set(); + this.posts = []; + this.isHeadless = false; + this.isBootstrapPeer = false; + this.showLog = false; + this.markedAvailable = false; + this.limitPosts = 50; + this.websocket = null; + // vizGraph: any | null = null; + this.qrcode = null; + this.connectURL = ""; + this.firstRun = false; + this.peerManager = null; + this.time = 0; + this.animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal']; + this.adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy']; + this.snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait']; + // keep a map of posts to dom nodes. + // on re-render + // posts that are not in our list that we need at add + // posts that are in our list that we need to remove + this.renderedPosts = new Map(); + this.router = { + route: App.Route.HOME, + userID: '', + postID: '', + mediaID: '' + }; + } + async connect() { + this.peerManager = new PeerManager(this.userID, this.peerID, this.isBootstrapPeer); + this.registerRPCs(); + console.log.apply(null, log("*************** before peerManager.connect")); + ; + // We use promises here to only return from this call once we're connected to the boostrap peer + // and the datachannel is open. + // Might want to take this a step further and only return once we're connected to an initial set of peers? + // we could return progress information as we connect and have the app subscribe to that? + // Would be lovely to show a little display of peers connecting, whether you're connected directly to a friend's peer etc. + // Basically that live "dandelion" display. + this.peerManager.registerRPC('getPostIDsForUser', (userID) => { + return [1, 2, 3, 4, 5]; + }); + await this.peerManager.connect(); + console.log.apply(null, log("*************** after peerManager.connect")); + ; + if (!this.isBootstrapPeer) { + let postIDs = await this.peerManager.rpc.getPostIDsForUser(this.peerManager.bootstrapPeerID, this.userID); + console.log.apply(null, log("peerManager.rpc.getPostIDsForUser", postIDs)); + } + } + getPreferentialUserID() { + return this.router.userID.length !== 0 ? this.router.userID : this.userID; + } + initMarkdown() { + if (typeof marked === "undefined") { + return; + } + const renderer = new marked.Renderer(); + renderer.link = (href, title, text) => { + return `${text}`; + }; + marked.setOptions({ renderer: renderer }); + this.markedAvailable = true; + } + // arrayBufferToBase64(buffer: ArrayBuffer) { + // return new Promise((resolve, reject) => { + // const blob = new Blob([buffer], { type: 'application/octet-stream' }); + // const reader = new FileReader(); + // reader.onloadend = () => { + // const dataUrl = reader.result as string; + // if (!dataUrl) { + // resolve(null); + // return; + // } + // const base64 = dataUrl.split(',')[1]; + // resolve(base64); + // }; + // reader.onerror = (error) => { + // reject(error); + // }; + // reader.readAsDataURL(blob); + // }); + // } + async createTestData() { + let postsTestData = await (await fetch("./postsTestData.json")).json(); + return postsTestData; + } + timerStart() { + this.time = performance.now(); + } + timerDelta() { + return performance.now() - this.time; + } + getFixedTweetText(entry) { + let fullText = entry.tweet.full_text; + let linkMarkdown = ""; + for (const url of entry.tweet.entities.urls) { + linkMarkdown = `[${url.display_url}](${url.expanded_url})`; + fullText = fullText.replace(url.url, linkMarkdown); + } + return fullText; + } + downloadBinary(data, filename, mimeType = 'application/octet-stream') { + // Create a blob from the ArrayBuffer with the specified MIME type + const blob = new Blob([data], { type: mimeType }); + // Create object URL from the blob + const url = window.URL.createObjectURL(blob); + // Create temporary link element + const link = document.createElement('a'); + link.href = url; + link.download = filename; + // Append link to body, click it, and remove it + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + // Clean up the object URL + window.URL.revokeObjectURL(url); + } + 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 importPostsForUser(userID, posts) { + } + async exportPostsForUser(userID) { + let posts = await getAllData(userID); + let output = []; + console.log.apply(null, log("Serializing images")); + for (let post of posts) { + let newPost = post.data; + if (newPost.image_data) { + newPost.image_data = await arrayBufferToBase64(newPost.image_data); + } + output.push(newPost); + } + let compressedData = await compressString(JSON.stringify(output)); + const d = new Date(); + const timestamp = `${d.getFullYear()}_${String(d.getMonth() + 1).padStart(2, '0')}_${String(d.getDate()).padStart(2, '0')}_${String(d.getHours()).padStart(2, '0')}_${String(d.getMinutes()).padStart(2, '0')}_${String(d.getSeconds()).padStart(2, '0')}`; + this.downloadBinary(compressedData, `ddln_${this.username}_export_${timestamp}.json.gz`); + } + async importTweetArchive(userID, tweetArchive) { + console.log.apply(null, log("Importing tweet archive")); + let postsTestData = []; + // let response = await fetch("./tweets.js"); + // let tweetsText = await response.text(); + // tweetsText = tweetsText.replace("window.YTD.tweets.part0", "window.tweetData"); + // new Function(tweetsText)(); + // let tweets = JSON.parse(tweetJSON); + let count = 0; + for (let entry of tweetArchive) { + // if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) { + // continue; + // } + let mediaURL = entry.tweet?.entities?.media?.[0]?.media_url_https; + let isImage = false; + if (mediaURL) { + isImage = mediaURL.includes('jpg'); + } + let imageData = null; + // if (isImage) { + // try { + // let response = await fetch(mediaURL); + // await waitMs(100); + // if (response.status === 200) { + // imageData = await response.arrayBuffer(); + // } + // console.log.apply(null, log(imageData); + // } catch (e) { + // console.log.apply(null, log(e); + // } + // } + let timeStamp = new Date(entry.tweet.created_at); + let tweetText = this.getFixedTweetText(entry); + let newPost = new Post('bobbydigitales', userID, tweetText, timeStamp, imageData, 'twitter', entry); + postsTestData.push(newPost); + count++; + if (count % 100 === 0) { + console.log.apply(null, log(`Imported ${count} posts...`)); + ; + // render(postsTestData); + } + // if (count == 100-1) { + // break; + // } + } + return postsTestData; + } + async createTestData3(userID) { + let posts = await (await (fetch('./posts.json'))).json(); + return posts; + } + async registerServiceWorker() { + if (!("serviceWorker" in navigator)) { + return; + } + let registrations = await navigator.serviceWorker.getRegistrations(); + if (registrations.length > 0) { + console.log.apply(null, log("Service worker already registered.")); + return registrations[0]; + } + navigator.serviceWorker + .register("/sw.js") + .then((registration) => { + console.log.apply(null, log("Service Worker registered with scope:", registration.scope)); + return registration; + }) + .catch((error) => { + console.error("Service Worker registration failed:", error); + }); + } + async compressImage(imageData, mimeType, quality = 0.5) { + let uncompressedByteLength = imageData.byteLength; + console.log.apply(null, log(`compressImage input:${mimeType} size:${(uncompressedByteLength / 1024).toFixed(2)}KBi quality:${quality}`)); + ; + try { + // Convert ArrayBuffer to Blob + const blob = new Blob([imageData], { type: mimeType }); + const bitmap = await createImageBitmap(blob, { + imageOrientation: 'none', + // resizeWidth: desiredWidth, + // resizeHeight: desiredHeight, + // resizeQuality: 'high', + }); + // const bitmap = await createImageBitmap(bitmapTemp, { + // imageOrientation: 'none', + // resizeWidth: 600, + // resizeHeight: 800, + // // resizeHeight: (bitmapTemp.height / bitmapTemp.width) * 600, + // resizeQuality: 'high', + // }) + //drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) + // Create a canvas and draw the image onto it + // let scale = 1/32; + // let scaledWidth = bitmap.width*scale; + // let scaledHeight = bitmap.height*scale; + // let scale = 1/32; + let scaledWidth = bitmap.width; + let scaledHeight = bitmap.height; + let resizeThreshold = 600; + if (scaledWidth > resizeThreshold) { + scaledWidth = resizeThreshold; + scaledHeight = (bitmap.height / bitmap.width) * resizeThreshold; + } + const canvas = document.createElement('canvas'); + canvas.width = scaledWidth; + canvas.height = scaledHeight; + const ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + canvas.getContext('2d').drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, scaledWidth, scaledHeight); + // Compress the image and get the result as an ArrayBuffer + const compressedBlob = await new Promise((resolve, reject) => canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('Compression failed.'))), 'image/jpeg', quality)); + // TODO: Don't need to do this as we'll be storing blobs directly. + let compressedArrayBuffer = await compressedBlob.arrayBuffer(); + let compressedByteLength = compressedArrayBuffer.byteLength; + let percent = (uncompressedByteLength / compressedByteLength); + console.log.apply(null, log(`compressImage: compressedSize:${(compressedArrayBuffer.byteLength / 1024).toFixed(2)}KBi ${percent.toFixed(2)}:1 compression`)); + ; + return compressedArrayBuffer; + } + catch (e) { + console.error(e); + return null; + } + } + async createNewPost(userID, postText, mediaData, mimeType) { + if ((typeof postText !== "string") || postText.length === 0) { + console.log.apply(null, log("Not posting an empty string...")); + return; + } + if (mediaData && + (mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') && + mediaData.byteLength > 500 * 1024) { + let compressedImage = await this.compressImage(mediaData, mimeType, 0.9); + if (compressedImage) { + mediaData = compressedImage; + } + } + let post = new Post(this.username, userID, postText, new Date(), mediaData); + // this.posts.push(post); + // localStorage.setItem(key, JSON.stringify(posts)); + addData(userID, post); + this.websocket?.broadcastNewPost(userID, post); + this.render(); + } + getPeerID() { + let id = localStorage.getItem("peer_id"); + if (!id) { + console.log.apply(null, log(`Didn't find a peer ID, generating one`)); + ; + id = generateID(); + localStorage.setItem("peer_id", id); + } + return id; + } + getUserID() { + let id = localStorage.getItem("dandelion_id"); + if (!id) { + console.log.apply(null, log(`Didn't find a user ID, generating one`)); + ; + id = generateID(); + localStorage.setItem("dandelion_id", id); + } + return id; + } + hashIdToIndices(id) { + let indices = []; + for (let char of id) { + if (char !== '0' && char !== '-') { + indices.push(parseInt(char, 16)); + if (indices.length == 2) { + break; + } + } + } + return [indices[0], indices[1]]; + } + funkyName(id, listOne, listTwo) { + let [one, two] = this.hashIdToIndices(id); + let first = listOne[one % this.adjectives.length]; + let second = listTwo[two % this.animals.length]; + return { first, second }; + } + getUsername() { + let username = localStorage.getItem("dandelion_username"); + if (username && username !== "not_set") { + return username; + } + let { first: adjective, second: animal } = this.funkyName(this.userID, this.adjectives, this.animals); + username = `${adjective}_${animal}`; + localStorage.setItem("dandelion_username", username); + return username; + } + getPeername() { + let { first: adjective, second: snake } = this.funkyName(this.peerID, this.adjectives, this.snakes); + let peername = `${adjective}_${snake}`; + return peername; + } + setFont(fontName, fontSize) { + let content = document.getElementById('content'); + if (!content) { + return; + } + content.style.fontFamily = fontName; + content.style.fontSize = fontSize; + let textArea = document.getElementById('textarea_post'); + if (!textArea) { + return; + } + textArea.style.fontFamily = fontName; + textArea.style.fontSize = fontSize; + } + initOffline(connection) { + // Event listener for going offline + window.addEventListener('offline', () => { + console.log.apply(null, log("offline")); + }); + // Event listener for going online + window.addEventListener('online', async () => { + console.log.apply(null, log("online")); + // connection.connect(); + this.render(); + }); + console.log.apply(null, log(`Online status: ${navigator.onLine ? "online" : "offline"}`)); + } + selectFile(contentType) { + return new Promise(resolve => { + let input = document.createElement('input'); + input.type = 'file'; + // input.multiple = multiple; + input.accept = contentType; + input.onchange = () => { + if (input.files == null) { + resolve(null); + return; + } + let files = Array.from(input.files); + // if (multiple) + // resolve(files); + // else + resolve(files[0]); + }; + input.click(); + }); + } + readFile(file) { + // Always return a Promise + return new Promise((resolve, reject) => { + let content = ''; + const reader = new FileReader(); + // Wait till complete + reader.onloadend = function (e) { + content = e.target.result; + resolve(content); + }; + // Make sure to handle error states + reader.onerror = function (e) { + reject(e); + }; + 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'; + setLogVisibility(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; + } + button(elementName) { + return document.getElementById(elementName); + } + div(elementName) { + return document.getElementById(elementName); + } + 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") as HTMLButtonElement; + // let toggleDark = document.getElementById('toggle_dark') 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; + // let addPic = document.getElementById('button_add_pic') as HTMLDivElement; + // toggleDark.addEventListener('click', () => { + // document.documentElement.style.setProperty('--main-bg-color', 'white'); + // document.documentElement.style.setProperty('--main-fg-color', 'black'); + // }) + let homeButton = this.div('home-button'); + homeButton.addEventListener('click', e => window.location.href = `${window.location.origin}/`); + let profileButton = this.div('profile-button'); + profileButton.addEventListener('click', e => window.location.href = `${window.location.origin}/user/${this.userID}`); + let monitorButton = this.div('monitor_button'); + monitorButton.addEventListener('click', async () => { + navContainer.classList.toggle('active'); + this.showInfo(); + }); + let navContainer = this.div('nav-container'); + let burgerMenuButton = this.div('burger-menu-button'); + burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active')); + let exportButton = this.button("export-button"); + exportButton.addEventListener('click', async (e) => { + await this.exportPostsForUser(this.userID); + }); + let composeButton = this.div('compose-button'); + composeButton.addEventListener('click', e => { + document.getElementById('compose').style.display = 'block'; + document.getElementById('textarea_post')?.focus(); + }); + let filePicker = document.getElementById('file-input'); + filePicker?.addEventListener('change', async (event) => { + for (let file of filePicker.files) { + let buffer = await file.arrayBuffer(); + await this.createNewPost(this.userID, 'image...', buffer, file.type); + } + // Reset so that if they pick the same image again, we still get the change event. + filePicker.value = ''; + }); + let filePickerLabel = document.getElementById('file-input-label'); + filePickerLabel?.addEventListener('click', () => { + console.log.apply(null, log("Add pic...")); + }); + let usernameField = document.getElementById('username'); + usernameField?.addEventListener('input', (event) => { + this.username = event.target.innerText; + localStorage.setItem("dandelion_username", this.username); + }); + // importTweetsButton.addEventListener('click', async () => { + // let file = await this.selectFile('text/*'); + // console.log.apply(null, log(file); + // if (file == null) { + // return; + // } + // let tweetData = await this.readFile(file); + // tweetData = tweetData.replace('window.YTD.tweets.part0 = ', ''); + // const tweets = JSON.parse(tweetData); + // let imported_posts = await this.importTweetArchive(userID, tweets); + // clearData(userID); + // // posts = posts.reverse(); + // addDataArray(userID, imported_posts); + // this.render(); + // }); + // clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() }); + let postButton = document.getElementById("button_post"); + let postText = document.getElementById("textarea_post"); + if (!(postButton && postText)) { + throw new Error(); + } + postText.addEventListener('paste', async (e) => { + const dataTransfer = e.clipboardData; + const file = dataTransfer.files[0]; + let buffer = await file.arrayBuffer(); + await this.createNewPost(this.userID, 'image...', buffer, file.type); + }); + postButton.addEventListener("click", () => { + this.createNewPost(userID, postText.value); + postText.value = ""; + document.getElementById('compose').style.display = 'none'; + }); + // updateApp.addEventListener("click", () => { + // registration?.active?.postMessage({ type: "update_app" }); + // }); + // ddlnLogoButton.addEventListener('click', async () => { + // this.showInfo() + // }); + } + async getPostsForFeed() { + // get N posts from each user and sort them by date. + // 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 = []; + for (let followedID of this.following.keys()) { + posts = posts.concat(await getData(followedID, new Date(2022, 8), new Date())); + // console.log.apply(null, log(followedID); + } + // @ts-ignore + posts = posts.sort((a, b) => a.post_timestamp - b.post_timestamp); + return posts; + } + async loadFollowersFromStorage(userID) { + // Rob + if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') { + return [ + 'b38b623c-c3fa-4351-9cab-50233c99fa4e', + '6d774268-16cd-4e86-8bbe-847a0328893d', // Sean + '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin + 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO + 'bba3ad24-9181-4e22-90c8-c265c80873ea', // Harry + '8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona + ]; + } + // Martin + if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') { + return [ + 'b38b623c-c3fa-4351-9cab-50233c99fa4e', + 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO + ]; + } + // Fiona + if (userID === '8f6802be-c3b6-46c1-969c-5f90cbe01479') { + return [ + 'b38b623c-c3fa-4351-9cab-50233c99fa4e', // Rob + 'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO + '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin + ]; + } + return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :) + } + async loadPostsFromStorage(userID, postID) { + this.timerStart(); + let posts = []; + // if (postID) { + // posts = await gePostForUser(userID, postID); + // } + posts = await getData(userID, new Date(2022, 8), new Date()); + if (posts.length > 0) { + console.log.apply(null, log(`Loaded ${posts.length} posts in ${this.timerDelta().toFixed(2)}ms`)); + ; + return posts; + } + // posts = await createTestData2(userID); + // log("Adding test data..."); + // addDataArray(userID, posts); + // return await getData(userID, new Date(2022, 8), new Date()); + } + async listUsers() { + let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', '')); + if (knownUsers.length === 0) { + return; + } + let preferredId = app.getPreferentialUserID(); + for (let userID of knownUsers) { + // if (userID === preferredId) { + // continue; + // } + // let ids = await getAllIds(userID); + // if (ids.length === 0) { + // console.log.apply(null, log(`Purging user ${userID}`); + // indexedDB.deleteDatabase(`user_${userID}`); + // continue; + // } + console.log.apply(null, log(`${document.location.origin}/user/${userID}`)); + // console.log.apply(null, log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID); + } + } + async initDB() { + let db = await openDatabase(this.userID); + } + query_findPeersForUser(message) { + let havePostsForUser = true; + if (havePostsForUser) { + return this.peerID; + } + return false; + } + async registerRPCs() { + if (!this.peerManager) { + throw new Error(); + } + this.peerManager.registerRPC('ping', (args) => { + return { id: this.peerID, user: this.userID, user_name: this.username, peer_name: this.peername }; + }); + if (!this.isBootstrapPeer) { + let pong = await this.peerManager.rpc.ping(this.peerManager.bootstrapPeerID); + console.log.apply(null, log('pong from: ', pong)); + } + // this.peerManager.registerRPC('getPostIDsForUser', (args: any) => { + // this.sync.getPostsForUser + // }); + } + async testPeerManager() { + if (!this.peerManager) { + throw new Error(); + } + this.peerManager.registerRPC('getPostIDsForUser', (args) => { + return [1, 2, 3, 4, 5]; + }); + let postIDs = await this.peerManager.rpc.getPostIDsForUser("dummy_peer", "bloop"); + console.log.apply(null, log("peerManager.rpc.getPostIDsForUser", postIDs)); + // this.peerManager.registerSearchQuery('find_peers_for_user', this.query_findPeersForUser); + // let peers = await this.peerManager.search('find_peers_for_user', { 'user_id': 'bloop' }); + } + async main() { + // Do capability detection here and report in a simple way if things we need don't exist with guidance on how to resolve it. + let urlParams = (new URL(window.location.href)).searchParams; + if (urlParams.has('log')) { + this.showInfo(); + } + this.isHeadless = /\bHeadlessChrome\//.test(navigator.userAgent); + this.isBootstrapPeer = urlParams.has("bootstrap"); + if (this.isBootstrapPeer) { + console.log.apply(null, log(`This is a bootstrap peer`)); + ; + } + this.peerID = this.getPeerID(); + this.peername = this.getPeername(); + this.userID = this.getUserID(); + this.username = this.getUsername(); + this.connect(); + // this.registerRPCs(); + // this.testPeerManager(); + // let peer: RTCPeerConnection | null = null; + // // if (window.RTCPeerConnection) { + // peer = new RTCPeerConnection({ + // iceServers: [ + // { urls: "stun:ddln.app" }, + // // { urls: "turn:ddln.app", username: "a", credential: "b" }, + // { urls: "stun:stun.l.google.com" }, // keeping this for now as my STUN server is not return ipv6 + // // { urls: "stun:stun1.l.google.com" }, + // // { urls: "stun:stun2.l.google.com" }, + // // { urls: "stun:stun3.l.google.com" }, + // // { urls: "stun:stun4.l.google.com" }, + // ] + // }); + // peer.createDataChannel('boop'); + // peer.onicecandidate = ({ candidate }) => { log(`WRTC:${candidate?.address} ${candidate?.protocol} ${candidate?.type} ${(candidate as any)?.url}`) }; + // peer.onnegotiationneeded = async (event) => { + // console.log.apply(null, log("on negotiation needed fired"));; + // let makingOffer = false; + // try { + // makingOffer = true; + // await peer.setLocalDescription(); + // let IDsToSync = this.following; + // if (this.router.route === App.Route.USER) { + // IDsToSync = new Set([this.router.userID]); + // } + // if (!peer.localDescription) { + // return; + // } + // // this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync, peer.localDescription); + // // log(peer.localDescription.type + ":" + peer.localDescription.sdp); + // // this.initOffline(this.websocket); + // // this.websocket?.sendWebRTCDescription(peer.localDescription); + // } catch (err) { + // console.error(err); + // } finally { + // makingOffer = false; + // } + // } + // peer.createOffer().then((description)=>{ + // peer.setLocalDescription(description) + // console.log.apply(null, log("RTC: " + description.sdp + description.type));; + // }); + // } + // 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 + // Render + // Load all images async + // Start the process of figuring out what posts we need + // Download posts once all current images are loaded + // window.resizeTo(645, 900); + // this.initLogo() + this.getRoute(); + if (this.router.route === App.Route.CONNECT) { + console.log.apply(null, log('connect', this.router.userID)); + localStorage.setItem("dandelion_id", this.router.userID); + localStorage.removeItem("dandelion_username"); + } + await this.initDB(); + this.connectURL = `${document.location.origin}/connect/${this.userID}`; + document.getElementById('connectURL').innerHTML = `connect`; + this.isHeadless = urlParams.has('headless'); + let limitPostsParam = urlParams.get('limitPosts'); + if (limitPostsParam) { + this.limitPosts = parseInt(limitPostsParam); + } + let time = 0; + let delta = 0; + // let isPersisted = await navigator?.storage?.persisted(); + // if (!isPersisted) { + // debugger; + // const isPersisted = await navigator.storage.persist(); + // console.log.apply(null, log(`Persisted storage granted: ${isPersisted}`));; + // } + // log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`); + this.initMarkdown(); + // let main = await fetch("/main.js"); + // let code = await main.text(); + // console.log.apply(null, log(code); + // registration.active.postMessage({type:"updateMain", code:code}); + // this.posts = await this.loadPosts(userID) ?? []; + // debugger; + await this.render(); // , (postID:string)=>{this.deletePost(userID, postID)} + if (performance?.memory) { + console.log.apply(null, log(`memory used: ${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)); + } + // if (navigator?.storage) { + // let storageUsed = (await navigator?.storage?.estimate())?.usage/1024/1024 + // } + // if (urlParams.get("sw") === "true") { + let registration; + registration = await this.registerServiceWorker(); + // } + document.getElementById('username').innerText = `${this.username}`; + document.getElementById('peername').innerText = `peername:${this.peername}`; + 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); + console.log.apply(null, log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`)); + ; + // await this.purgeEmptyUsers(); + // this.listUsers() + // this.createNetworkViz(); + // const client = new WebTorrent() + // // Sintel, a free, Creative Commons movie + // const torrentId = 'magnet:?xt=urn:btih:6091e199a8d9272a40dd9a25a621a5c355d6b0be&dn=WING+IT!+-+Blender+Open+Movie+1080p.mp4&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337'; + // client.add(torrentId, function (torrent: any) { + // // Torrents can contain many files. Let's use the .mp4 file + // const file = torrent.files.find(function (file: any) { + // return file.name.endsWith('.mp4') + // }) + // // Display the file by adding it to the DOM. + // // Supports video, audio, image files, and more! + // file.appendTo(document.getElementById('torrent-content')); + // }) + } + renderWelcome(contentDiv) { + contentDiv.innerHTML = `
+ Welcome to Dandelion v0.1!
+ Loading posts for the default feed... +
+ `; + } + async render() { + if (this.isHeadless) { + console.log.apply(null, log('Headless so skipping render...')); + return; + } + performance.mark("render-start"); + this.timerStart(); + let existingPosts = this.posts; + this.posts = []; + switch (this.router.route) { + case App.Route.HOME: + case App.Route.CONNECT: { + this.following = new Set(await this.loadFollowersFromStorage(this.userID) ?? []); + this.posts = await this.getPostsForFeed(); + // this.posts = await this.loadPostsFromStorage(this.userID) ?? []; + // let compose = document.getElementById('compose'); + // if (!compose) { + // break; + // } + // compose.style.display = "block"; + break; + } + case App.Route.USER: { + this.posts = await this.loadPostsFromStorage(this.router.userID) ?? []; + let compose = document.getElementById('compose'); + if (!compose) { + break; + } + compose.style.display = "none"; + break; + } + case App.Route.POST: { + this.posts = await this.loadPostsFromStorage(this.router.userID, this.router.postID) ?? []; + let compose = document.getElementById('compose'); + if (!compose) { + break; + } + compose.style.display = "none"; + break; + } + default: { + console.log.apply(null, log("Render: got a route I didn't understand. Rendering HOME:", this.router.route)); + this.posts = await this.loadPostsFromStorage(this.userID) ?? []; + break; + } + } + let contentDiv = document.getElementById("content"); + if (!contentDiv) { + throw new Error(); + } + if (this.posts.length === 0) { + this.renderWelcome(contentDiv); + return; + } + // let existingPostSet = new Set(existingPosts.map(post => post.post_id)); + // let incomingPostSet = new Set(this.posts.map(post => post.post_id)); + // let addedPosts = []; + // for (let post of this.posts) { + // if (!existingPostSet.has(post.post_id)) { + // addedPosts.push(post); + // } + // } + // let deletedPosts = []; + // for (let post of existingPosts) { + // if (!incomingPostSet.has(post.post_id)) { + // deletedPosts.push(post); + // } + // } + // console.log.apply(null, log("added:", addedPosts, "removed:", deletedPosts); + const fragment = document.createDocumentFragment(); + contentDiv.innerHTML = ""; + let count = 0; + this.renderedPosts.clear(); + let first = true; + for (let i = this.posts.length - 1; i >= 0; i--) { + let postData = this.posts[i]; + // this.postsSet.add(postData); + // TODO return promises for all image loads and await those. + let post = this.renderPost(postData.data, first); + first = false; + // this.renderedPosts.set(postData.post_id, post); + if (post) { + fragment.appendChild(post); + count++; + } + if (count > this.limitPosts) { + break; + } + } + if (!contentDiv) { + throw new Error("Couldn't get content div!"); + } + contentDiv.appendChild(fragment); + let renderTime = this.timerDelta(); + console.log.apply(null, log(`render took: ${renderTime.toFixed(2)}ms`)); + ; + performance.mark("render-end"); + performance.measure('render-time', 'render-start', 'render-end'); + // if ((performance as any)?.memory) { + // console.log.apply(null, log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)); + // } + } + async deletePost(userID, postID) { + deleteData(userID, postID); + this.render(); + } + renderPost(post, first) { + if (!(post.hasOwnProperty("text"))) { + throw new Error("Post is malformed!"); + } + let containerDiv = document.createElement("div"); + let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`; + let deleteButton = document.createElement('button'); + deleteButton.innerText = 'delete'; + deleteButton.onclick = () => { this.deletePost(post.author_id, post.post_id); }; + // let editButton = document.createElement('button'); editButton.innerText = 'edit'; + let shareButton = document.createElement('button'); + shareButton.innerText = 'share'; + shareButton.onclick = async () => { + let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`; + await navigator.clipboard.writeText(shareUrl); + }; + let ownPost = post.author_id === this.userID; + let markdown = post.text; + if (this.markedAvailable) { + markdown = marked.parse(post.text); + } + // if (markdown.includes("${first ? '' : '
'} +
+ @${post.author} - + ${post.post_timestamp.toLocaleDateString()} + + ${ownPost ? `` : ''} + ${ownPost ? `` : ''} + +
+
${markdown}
+ `; + containerDiv.innerHTML = postTemplate; + if (ownPost) { + containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton); + // containerDiv.querySelector('#editButton')?.appendChild(editButton); + } + containerDiv.querySelector('#shareButton')?.appendChild(shareButton); + if (!("image_data" in post && post.image_data)) { + // containerDiv.appendChild(timestampDiv); + return containerDiv; + // return null; + } + let image = document.createElement("img"); + image.title = `${(post.image_data.byteLength / 1024 / 1024).toFixed(2)}MBytes`; + // const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' }); + const blob = new Blob([post.image_data]); + const url = URL.createObjectURL(blob); + image.onload = () => { + URL.revokeObjectURL(url); + }; + image.src = url; + // image.src = image.src = "data:image/png;base64," + post.image; + image.className = "postImage"; + // image.onclick = () => { App.maximizeElement(image) }; + containerDiv.appendChild(image); + // containerDiv.appendChild(timestampDiv); + return containerDiv; + } + static maximizeElement(element) { + element.style.transform = "scale(2.0)"; + } + getRoute() { + let path = document.location.pathname; + console.log.apply(null, log("router: path ", path)); + const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))"; + const match = path.match(new RegExp(regex)); + if (match) { + if (match[8]) { // Check for the connect route + this.router.userID = match[8]; + this.router.route = App.Route.CONNECT; + } + else { + this.router.userID = match[2]; + this.router.postID = match[4]; + this.router.mediaID = match[6]; + if (this.router.mediaID) { + this.router.route = App.Route.MEDIA; + } + else if (this.router.postID) { + this.router.route = App.Route.POST; + } + else { + this.router.route = App.Route.USER; + } + } + } + console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route])); + // user = /user/ + // post = /user//post/ + // media = /user//post//media/ + // group = /group/ID/post/ + // hashtag = /hashtag/ -- maybe only hastags in groups + // home = / + } +} +(function (App) { + let Route; + (function (Route) { + Route[Route["USER"] = 0] = "USER"; + Route[Route["POST"] = 1] = "POST"; + Route[Route["MEDIA"] = 2] = "MEDIA"; + Route[Route["GROUP"] = 3] = "GROUP"; + Route[Route["HOME"] = 4] = "HOME"; + Route[Route["CONNECT"] = 5] = "CONNECT"; + })(Route = App.Route || (App.Route = {})); + ; + // export function connect() { + // throw new Error("Function not implemented."); + // } + // export function connect() { + // throw new Error("Function not implemented."); + // } +})(App || (App = {})); +let app = new App(); +window.addEventListener("load", app.main.bind(app)); diff --git a/static/main2.js.map b/static/main2.js.map new file mode 100644 index 0000000..8e8b329 --- /dev/null +++ b/static/main2.js.map @@ -0,0 +1 @@ +{"version":3,"file":"main2.js","sourceRoot":"","sources":["../src/main2.ts"],"names":[],"mappings":"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"} \ No newline at end of file