peers syncing for a single user

This commit is contained in:
bobbydigitales
2024-09-20 20:54:54 -07:00
parent 88f5cb677b
commit 998840ec2c
17 changed files with 1099 additions and 733 deletions

45
db.js
View File

@@ -3,6 +3,7 @@
// name: string;
// email: string;
// }
// Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917
const postStoreName = "posts";
let keyBase = "dandelion_posts_v1_";
let key = "";
@@ -117,6 +118,50 @@ export async function addDataArray(userID, array) {
console.error('Error in opening database:', error);
}
}
export async function mergeDataArray(userID, array) {
try {
const db = await openDatabase(userID);
const transaction = db.transaction(postStoreName, "readwrite");
const store = transaction.objectStore(postStoreName);
const index = store.index("postIDIndex");
transaction.oncomplete = () => {
console.log("Transaction completed successfully");
db.close();
};
transaction.onerror = (event) => {
console.error("Transaction error:", event.target.error);
db.close();
};
let postsToWrite = [];
for (let post of array) {
try {
let havePost = await new Promise((resolve, reject) => {
const getRequest = index.getKey(post.post_id);
getRequest.onerror = (e) => {
console.log(e.target.error);
reject(e);
};
getRequest.onsuccess = async (e) => {
const key = e.target.result;
resolve(key !== undefined);
};
});
// console.log(post.post_id, havePost);
if (!havePost) {
postsToWrite.push(post);
}
}
catch (error) {
console.error("Error processing post:", error);
}
}
console.log(`Writing ${postsToWrite.length} posts`);
await addDataArray(userID, postsToWrite);
}
catch (error) {
console.error("Error in opening database:", error);
}
}
export async function getData(userID, lowerID, upperID) {
const db = await openDatabase(userID);
const transaction = db.transaction(postStoreName, "readonly");

File diff suppressed because one or more lines are too long

View File

@@ -1,55 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIENTCCAx2gAwIBAgISA0tgMbNwsOJA3iNAkTnMQwBRMA0GCSqGSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
EwJSMzAeFw0yNDAzMTIwMDAyMDJaFw0yNDA2MTAwMDAyMDFaMCQxIjAgBgNVBAMT
GWRhbmRlbGlvbi5hbmRlcmJlcmcuY28udWswWTATBgcqhkjOPQIBBggqhkjOPQMB
BwNCAASEd7+0ppvG0iyaAjv8urB8LVYKSIYO8GLp6H9P2fwZjzWXfC8iQMhP9jaW
aQaAl73gS4iXpODDzYJ/Hwi6siPto4ICHDCCAhgwDgYDVR0PAQH/BAQDAgeAMB0G
A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1Ud
DgQWBBTSo2KL0pYAUZ1gdOwVAQk8GmoCITAfBgNVHSMEGDAWgBQULrMXt1hWy65Q
CUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6Ly9y
My5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iub3Jn
LzAkBgNVHREEHTAbghlkYW5kZWxpb24uYW5kZXJiZXJnLmNvLnVrMBMGA1UdIAQM
MAowCAYGZ4EMAQIBMIIBBQYKKwYBBAHWeQIEAgSB9gSB8wDxAHYASLDja9qmRzQP
5WoC+p0w6xxSActW3SyB2bu/qznYhHMAAAGOMC5ElAAABAMARzBFAiBpEIL68r0M
XSzwppDSd+KLGdfyKzFjYmgdBw29Ol+IywIhANMk6NUGYXEeqhMjLV5ZDj5OG6AF
orRzRgxsWtZYteM5AHcAO1N3dT4tuYBOizBbBv5AO2fYT8P0x70ADS1yb+H61BcA
AAGOMC5EmgAABAMASDBGAiEAl6ot3y1mAFXw3ZNVeJOwAscHc4/d6bCEgaqFlcCy
iS8CIQDuYEcvEJV2TyXGFyrqAKlmw7E1WO63OWhc6uE+EwsGAjANBgkqhkiG9w0B
AQsFAAOCAQEAVZCdcCiSvDhRkLTPtZUK74RDkH3DJtTM6bVHxdZXHEINlR62LyRo
TI76XZXL4iLQWzu3XzPw0X0DyNnhyR4EGsy+yRZEPbBWtCIyz0Mulw+V5vxlLHEe
+7z9qus0mVU8Yt0fRHDc7oQEY+hqPIuFl/sekCkqL16Oy+kSuzNiB71f50fiYsp0
jb50fGb1ykcimCwehUQPeKcC9O88eE7Hxk7lD86VV6s/IyrfKze+AfvRcYGpGYWg
PB/v71LjtKrLNcPggQ0DQD4kRhR3/NqU+Tp6h17sb7WYLH0IgTc0VoSyNQg/rWet
g9zvvsrT6zrzSxcbocs4SVNFGF/mfv3WKw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
nLRbwHOoq7hHwg==
-----END CERTIFICATE-----

View File

@@ -14,154 +14,7 @@
<!-- <script src="/lib/webtorrent/webtorrent_1_8_0.min.js"></script> -->
<link rel="manifest" href="/app.webmanifest">
<style>
@font-face {
font-family: 'Virgil';
/* Define the name you want to reference your font with */
src: url('./virgil.woff2') format('woff2');
/* URL to the font file and format */
font-weight: normal;
/* Define the weight of the font */
font-style: normal;
/* Define the style of the font */
}
@font-face {
font-family: 'Bookerly';
/* Define the name you want to reference your font with */
src: url('./bookerly.woff2') format('woff2');
/* URL to the font file and format */
font-weight: normal;
/* Define the weight of the font */
font-style: normal;
/* Define the style of the font */
}
body {
font-family: Bookerly, sans-serif;
color: rgb(202, 208, 211);
background-color: black;
/* Use the font with a fallback */
}
hr {
border-color: rgb(60, 60, 60);
}
#textarea_post {
font-size: medium;
font-family: Bookerly;
background-color: rgb(0, 0, 0);
color: rgb(202, 208, 211);
width: 100%;
/* Make the textarea take up 100% of the body's width */
box-sizing: border-box;
/* Include padding and border in the element's total width and height */
padding-left: 30px;
padding-right: 30px;
border: 1px solid rgb(132, 136, 138);
resize: vertical;
/* Allows the textarea to be resized vertically only */
border-radius: 40px;
}
.header {
font-size: 18px;
}
.flex-container {
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 0px;
/* Add some padding around the flex container */
}
.content {
max-width: 600px;
/* Your preferred max width for the content */
flex: 1;
/* Shorthand for flex-grow, flex-shrink and flex-basis */
min-width: 300px;
/* Minimum width the content can shrink to */
padding: 20px;
box-shadow: 0 0 5px rgb(60, 60, 60);
text-align: left;
overflow-x: hidden;
/* Hide horizontal overflow inside the flex container */
line-height: 1.3;
}
.embed {
max-width: 800px;
border-color: red;
border: 1px, solid;
padding: 20px;
}
.postImage {
width: 100%;
}
#log {
font-family: monospace;
text-wrap: nowrap;
font-size: 10px;
margin-bottom: 20px;
height: 150px;
width: 50%;
}
.button {
text-align: right;
}
#buttons {
margin-left: 40px;
}
#button_post {
margin-right:40px;
}
a {
color: rgb(29, 155, 240);
}
.logo {
width: 32px;
height: 32px;
image-rendering: pixelated;
}
#torrent-content {
border:solid 1px;
width:800px;
}
.img-button {
cursor: pointer;
}
button {
background-color: rgb(0, 0, 0);
border-radius: 10px;
padding-left:10px;
padding-right:10px;
padding-top:5px;
padding-bottom:5px;
margin-left:5px;
color:rgb(255,255,255);
/* border:solid 1px white; */
border: 1px solid rgb(132, 136, 138);
color: rgb(202, 208, 211);
cursor: pointer;
}
</style>
<link rel="stylesheet" href="/main.css">
</head>
<body>
@@ -174,15 +27,23 @@
<div id="status"></div>
<div id="info" style="display:none">
<div id="info" style="display:block">
<div id="profile">
<span class="form_label">username:</span><span class="form_field" id="username" contenteditable="true">unnamed</span>
</div>
<div id="following">
<div>fiona</div>
<div>fiona</div>
</div>
<div id="log" ></div>
<div id="connectURL"></div>
<div id="qrcode"></div>
<!-- <dotlottie-player src="https://lottie.host/272b60dd-462d-42a3-8ed6-fec4143633d6/X4FxBascRI.json" background="transparent" speed="1" style="width: 300px; height: 300px" direction="1" playMode="normal" loop controls autoplay></dotlottie-player> -->
</div>
<!-- <div id="peer_display"><canvas></canvas></div> -->
<div id="buttons">
<button id="button_font1" >font1</button>
<button id="button_font2" >font2 </button>
<!-- <button id="button_font1" >font1</button>
<button id="button_font2" >font2 </button> -->
<button id="import_tweets" >import</button>
<button id="clear_posts" >clear </button>
<button id="update_app" >check for updates</button>

BIN
main

Binary file not shown.

322
main.go
View File

@@ -1,58 +1,20 @@
// TODO
// 1. Convery network messages to flatbuffers so we don't need to parse JSON in Go.
// Use binary WS messages. Generate for Go and JS and use that everywhere. We can
// also use this for WebRTC messages.
// 2. Keep a list of all nodes that bootstrapped in the last N minutes.
// 3. When a node bootstraps, send it a random list of the nodes we know about.
// 4. Do the signalling to connect those nodes.
// 5. Each node will know about N other nodes. To find get the data for a feed,
// a node will need to find another node that has that data. To do that we'll need to
// implement a search message that is sent to all currently connected nodes, and they
// forward to all their nodes, passing back the address of the node that has the data.
// Once we find it, we'll do the signalling to connect to it via Web RTC via our existing connected nodes.
// -----
// Feeds. People can curate feeds which can be any combination of hashtags, serch terms and users.
// Invite-only communities. Just block everyone else even if they post to it.
// Limit to friends and friends of friends
// MVP
// You connect to the person you want to get the post from to get the post
// they give you the post
// If they're offline, you can't get their updates.
// This is very stupid, but it's simplest thing.
// The bootstrap server connects you to them directly via WebRTC
// This will make the thing actually function as a little toy for people to play with.
// This will let us test whether background tabs respond to webrtc requests.
// THEN
// Need to have identity sorted out
// When you read someone's posts, you also cache them locally
// cache priority goes mutuals->people you follow->people who you folllow, follow, so you're always
// caching your mutual's posts
// Posts are samll, so caching per-post will work fine.
// Then the process is for the bootstrap server to remember all nodes and what they're caching
// This will allow distributed content delivery but put a memory and bendwidth strain on the
// bootstrap sever. Look into Web Transport for the raspberry pi overhead. Could buy a few more RPIs
// and make a little cluster
// ✅ Domain name so we can get a certificate and serve HTTPS / HTTP3
// Think about compiling Typescript on initial access and caching the JS in a service worker
// so you don't need a build system to change things.
// Think about self-hosting the client so the system can be completely self-hosted
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
// "strings"
"github.com/andybalholm/brotli"
"github.com/gorilla/websocket"
@@ -61,7 +23,7 @@ import (
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return origin == "https://ddlion.net"
return origin == "https://ddlion.net" || origin == "https://ddln.app"
},
}
@@ -74,7 +36,7 @@ type Message struct {
Type string `json:"type"`
}
type MessageHandler func([]byte, *websocket.Conn) error
type MessageHandler func([]byte, *Peer) ([]byte, error)
var messageHandlers = make(map[string]MessageHandler)
@@ -82,19 +44,45 @@ func registerHandler(messageType string, handler MessageHandler) {
messageHandlers[messageType] = handler
}
func dispatchMessage(message []byte, conn *websocket.Conn) error {
func dispatchMessage(message []byte, peer *Peer) ([]byte, error) {
var msg Message
if err := json.Unmarshal(message, &msg); err != nil {
return err
return nil, err
}
handler, ok := messageHandlers[msg.Type]
if !ok {
log.Printf("No handler registered for message type: %s", msg.Type)
return nil
err := fmt.Errorf("no handler registered for message type: %s", msg.Type)
return []byte(fmt.Sprintf(`{"type":"error", "message": "%s"}`, err.Error())), nil
}
return handler(message, conn)
return handler(message, peer)
}
const (
writeWait = 10 * time.Second
)
type Peer struct {
conn *websocket.Conn
send chan []byte
lastActive time.Time
}
func removePeer(peerID string, peer *Peer) {
delete(peerConnections, peerID)
for userID, peers := range userPeers {
delete(peers, peerID)
if len(peers) == 0 {
delete(userPeers, userID)
}
}
delete(connectionPeers, peer.conn)
// Close the peer's send channel
close(peer.send)
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
@@ -109,39 +97,93 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn.SetCloseHandler(websocketCloseHandler)
// Create a Peer object with a buffered channel for sending messages
peer := &Peer{
conn: conn,
send: make(chan []byte, 256),
lastActive: time.Now(),
}
// Start the write loop in a separate goroutine
go writePump(peer)
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("ReadMessage error:", err)
break
}
log.Printf("recv: %s", message)
if err := dispatchMessage(message, conn); err != nil {
peer.lastActive = time.Now()
fmt.Println("ws<-", connectionPeers[conn], ":", string(message[:min(80, len(message))]))
response, err := dispatchMessage(message, peer)
if err != nil {
log.Printf("Error dispatching message: %v", err)
}
if response != nil {
// Send the response to the write loop
peer.send <- response
}
}
// Clean up when the connection is closed
close(peer.send)
peerID := connectionPeers[peer.conn]
if peerID != "" {
delete(peerConnections, peerID)
}
}
func writePump(peer *Peer) {
defer func() {
peer.conn.Close()
}()
for {
select {
case message, ok := <-peer.send:
if !ok {
// Channel closed, close the connection
peer.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
peer.conn.SetWriteDeadline(time.Now().Add(writeWait))
fmt.Println("ws->", connectionPeers[peer.conn], ":", string(message[:min(80, len(message))]))
err := peer.conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Println("WriteMessage error:", err)
return
}
}
}
}
// Example handlers
func handlePing(message []byte, conn *websocket.Conn) error {
func handlePing(message []byte, peer *Peer) ([]byte, error) {
var pingMsg struct {
Type string `json:"type"`
PeerID string `json:"peer_id"`
}
if err := json.Unmarshal(message, &pingMsg); err != nil {
return err
return nil, err
}
log.Printf("Received ping from peer: %s", pingMsg.PeerID)
return nil
// log.Printf("Received ping from peer: %s", pingMsg.PeerID)
return []byte(`{"type":"pong"}`), nil
}
type PeerSet map[string]struct{}
var userPeers = make(map[string]PeerSet)
var peerConnections = make(map[string]*websocket.Conn)
var peerConnections = make(map[string]*Peer)
var connectionPeers = make(map[*websocket.Conn]string)
func handleHello(message []byte, conn *websocket.Conn) error {
func handleHello(message []byte, peer *Peer) ([]byte, error) {
var m struct {
Type string `json:"type"`
@@ -150,32 +192,65 @@ func handleHello(message []byte, conn *websocket.Conn) error {
}
if err := json.Unmarshal(message, &m); err != nil {
return err
return nil, err
}
// log.Printf("Received hello from peer: %s, user:%s", m.PeerID, m.UserID)
if userPeers[m.UserID] == nil {
userPeers[m.UserID] = make(PeerSet)
}
userPeers[m.UserID][m.PeerID] = struct{}{}
peerConnections[m.PeerID] = conn
peerConnections[m.PeerID] = peer
connectionPeers[peer.conn] = m.PeerID
jsonData, _ := json.MarshalIndent(userPeers, "", " ")
fmt.Println(string(jsonData), peerConnections)
log.Printf("Received connect from peer: %s, user:%s", m.PeerID, m.UserID)
return nil
// return all the peers we know about, with their user_id and peer_id
return []byte(fmt.Sprintf(`{"type":"hello", "userPeers": %s}`, string(jsonData))), nil
}
// LoggingHandler logs requests and delegates them to the underlying handler.
// type LoggingHandler struct {
// handler http.Handler
// }
func handlePeerMessage(message []byte, peer *Peer) ([]byte, error) {
// func (lh *LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// log.Printf("Serving file: %s", r.URL.Path)
// lh.handler.ServeHTTP(w, r)
// }
type InnerMessage struct {
Type string `json:"type"`
UserID string `json:"user_id"`
}
type PeerMessage struct {
Type string `json:"type"`
From string `json:"from"`
To string `json:"to"`
Message InnerMessage `json:"message"`
}
var m PeerMessage
if err := json.Unmarshal(message, &m); err != nil {
return nil, err
}
fmt.Printf("peer message type %s from %s to %s with message length %d\n", m.Message.Type, m.From, m.To, len(message))
toPeer := peerConnections[m.To]
if toPeer == nil {
fmt.Printf("Couldn't find peer %s\n", m.To)
fmt.Println(peerConnections)
return nil, nil
}
// Send the message to the recipient's send channel
select {
case toPeer.send <- message:
default:
fmt.Println("Could not send message to peer; channel full or closed")
}
// No response for this type of message
return nil, nil
}
// BrotliResponseWriter wraps http.ResponseWriter to support Brotli compression
type brotliResponseWriter struct {
@@ -207,8 +282,11 @@ func noDirListing(h http.Handler, root string) http.HandlerFunc {
log.Printf("Serving: %s to ip %s, useragent %s", r.URL.Path, r.RemoteAddr, r.UserAgent())
// w.Header().Set("Cache-Control", "no-cache")
// Check if client supports Brotli encoding
if strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
// if strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
if false {
w.Header().Set("Content-Encoding", "br")
w.Header().Del("Content-Length") // Cannot know content length with compressed data
@@ -231,31 +309,115 @@ func noDirListing(h http.Handler, root string) http.HandlerFunc {
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
// Create a channel to receive OS signals for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Create a channel to signal when the program should shut down
done := make(chan bool)
// Define the directory to serve and the port to listen on
dir := "./"
port := 6789
addr := ":" + strconv.Itoa(port)
log.Printf("Starting server on %s", addr)
// Register handlers
// Register message handlers
registerHandler("hello", handleHello)
registerHandler("ping", handlePing)
registerHandler("peer_message", handlePeerMessage)
// Set up file server and WebSocket endpoint
fs := http.FileServer(http.Dir(dir))
// loggingHandler := &LoggingHandler{handler: fs}
// http.Handle("/", loggingHandler)
http.Handle("/", noDirListing(fs, dir))
http.HandleFunc("/ws", handleWebSocket)
// Configure and start the HTTP server
// Configure the HTTP server
server := &http.Server{
Addr: addr,
Handler: nil, // nil uses the default ServeMux, which we configured above
Handler: nil, // Use the default ServeMux
}
log.Printf("Server is configured and serving on port %d...", port)
log.Fatal(server.ListenAndServeTLS("/etc/letsencrypt/live/ddlion.net/fullchain.pem", "/etc/letsencrypt/live/ddlion.net/privkey.pem"))
// Start the inactivity monitor goroutine
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
now := time.Now()
// Collect inactive peers
var inactivePeers []string
for peerID, peer := range peerConnections {
if now.Sub(peer.lastActive) > 60*time.Second {
inactivePeers = append(inactivePeers, peerID)
}
}
// Remove inactive peers
for _, peerID := range inactivePeers {
peer := peerConnections[peerID]
if peer != nil {
log.Printf("Peer %s inactive for more than 60 seconds. Closing connection.", peerID)
peer.conn.Close()
removePeer(peerID, peer)
}
}
}
}
}()
// Run a goroutine to handle graceful shutdown
go func() {
sig := <-sigChan
fmt.Println()
fmt.Println("Received signal:", sig)
// Perform cleanup here
fmt.Println("Shutting down gracefully...")
// Create a context with timeout for the shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Attempt to gracefully shut down the server
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server Shutdown Failed:%+v", err)
}
// Signal that shutdown is complete
close(done)
}()
// Start the HTTP server in a separate goroutine
go func() {
log.Printf("Server is configured and serving on port %d...", port)
if err := server.ListenAndServeTLS(
"/etc/letsencrypt/live/ddlion.net/fullchain.pem",
"/etc/letsencrypt/live/ddlion.net/privkey.pem",
); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not listen on %s: %v\n", addr, err)
}
}()
fmt.Println("Program is running. Press Ctrl+C to exit.")
// Wait for the shutdown signal
<-done
fmt.Println("Program has exited.")
}

211
main.js
View File

@@ -1,4 +1,5 @@
import { getData, addData, addDataArray, clearData, deleteData } from "./db.js";
// TODO: virtual list, only rerender what's needed so things can keep playing.
import { getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData } from "./db.js";
// let posts:any;
// let keyBase = "dandelion_posts_v1_"
// let key:string = "";
@@ -19,6 +20,7 @@ function uuidv4() {
let logLines = [];
let logLength = 10;
function log(message) {
console.log(message);
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
if (logLines.length > 10) {
logLines = logLines.slice(logLines.length - logLength);
@@ -59,8 +61,9 @@ window.addEventListener('scroll', () => {
// You can perform your action here
}
});
// let peer = await new PeerConnection(peer_id);
// let connectionReply = await wsConnection.send('hello');
// for (let peeer of connectionReply) {
// for (let peer of connectionReply) {
// let peerConnection = await wsConnection.send('connect', peer.id);
// if (peerConnection) {
// this.peers.push(peerConnection);
@@ -71,6 +74,67 @@ window.addEventListener('scroll', () => {
// }
// }
class wsConnection {
send(message) {
let json = "";
try {
json = JSON.stringify(message);
}
catch (e) {
console.log(e, "wsConnection send: Couldn't serialize message", message);
}
log(`ws->${json.slice(0, 80)}`);
this.websocket.send(json);
}
helloResponseHandler(data) {
for (let [userID, peerIDs] of Object.entries(data.userPeers)) {
this.peers.set(userID, [...Object.keys(peerIDs)]);
for (let peerID of [...Object.keys(peerIDs)]) {
if (peerID === this.peerID) {
continue;
}
this.send({
type: "peer_message",
from: this.peerID,
to: peerID,
message: { type: "get_posts_for_user", user_id: userID }
});
}
}
}
pongHandler(data) {
}
async getPostsForUserResponseHandler(data) {
// log(`getPostsForUserResponse: ${data}`)
let message = data.message;
console.log(`getPostsForUserResponseHandler Got ${message.posts.length} from peer ${data.from}`);
for (let post of message.posts) {
post.post_timestamp = new Date(post.post_timestamp);
}
console.log(`Merging same user peer posts...`);
await mergeDataArray(message.user_id, data.message.posts);
if (message.user_id === this.userID) {
app.posts = await app.loadPosts(this.userID) ?? [];
app.render(app.posts);
}
}
async getPostsForUserHandler(data) {
let message = data.message;
let posts = await getAllData(message.user_id) ?? []; // this doesn't get all posts!
posts = posts.map((post) => post.data);
// let posts = await getAllData(message.user_id) ?? [];
let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, message: { type: "get_posts_for_user_response", posts: posts, user_id: message.user_id } };
this.send(responseMessage);
}
async peerMessageHandler(data) {
log(`peerMessageHandler ${data}`);
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);
}
connect() {
if (this.websocket?.readyState === WebSocket.OPEN) {
return;
@@ -87,24 +151,32 @@ class wsConnection {
console.log(error.message);
return;
}
this.websocket.onopen = (evt) => {
this.websocket.onopen = (event) => {
log("ws:connected");
this.websocket.send(`{"type":"hello", "user_id": "${this.userID}", "peer_id":"${this.peerID}"}`);
this.send({ type: "hello", user_id: this.userID, peer_id: this.peerID });
this.websocketPingInterval = window.setInterval(() => {
if (!navigator.onLine) {
return;
}
this.websocket.send(`{"type":"ping", "peer_id": "${this.peerID}"}`);
this.send({ type: "ping", peer_id: this.peerID });
}, 10000);
};
this.websocket.onclose = (evt) => {
this.websocket.onclose = (event) => {
log("ws:disconnected");
// this.retry *= 2;
log(`Retrying in ${this.retry} seconds`);
window.setTimeout(() => { this.connect(); }, this.retry * 1000);
};
this.websocket.onmessage = (event) => {
log('ws:response: ' + event.data);
log('ws:<-' + event.data.slice(0, 80));
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) => {
log('ws:error: ' + event);
@@ -120,36 +192,28 @@ class wsConnection {
this.websocketPingInterval = 0;
this.retry = 10;
this.state = 'disconnected';
this.peers = new Map();
this.messageHandlers = new Map();
this.peerMessageHandlers = new Map();
this.userID = userID;
this.peerID = peerID;
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
this.messageHandlers.set('pong', this.pongHandler);
this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserResponseHandler.bind(this));
this.connect();
if (!this.websocket) {
// set a timer and retry?
}
}
}
// function connectWebsocket(userID: string) {
// let websocket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
// websocket.onopen = function (evt) {
// log("Websocket: CONNECTED");
// websocket.send(`{"messageType":"connect", "id": "${userID}"}`);
// let websocketPingInterval = window.setInterval(() => { websocket.send(`{"messageType":"ping", "id": "${userID}"}`); }, 5000)
// };
// websocket.onclose = function (evt) {
// log("Websocket: DISCONNECTED");
// };
// websocket.onmessage = function (evt) {
// log('Websocket: RESPONSE: ' + evt.data);
// };
// websocket.onerror = function (evt) {
// log('Websocket: ERROR: ' + evt);
// };
// return websocket;
// }
class App {
constructor() {
this.username = '';
this.userID = '';
this.peerID = '';
this.posts = [];
this.time = 0;
}
initMarkdown() {
@@ -207,9 +271,9 @@ class App {
// 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;
}
// 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) {
@@ -266,16 +330,16 @@ class App {
console.error("Service Worker registration failed:", error);
});
}
addPost(userID, posts, postText) {
addPost(userID, postText) {
if ((typeof postText !== "string") || postText.length === 0) {
log("Not posting an empty string...");
return;
}
let post = new Post(`bobbydigitales`, userID, postText, new Date());
posts.push(post);
let post = new Post(this.username, userID, postText, new Date());
this.posts.push(post);
// localStorage.setItem(key, JSON.stringify(posts));
addData(userID, post);
this.render(posts);
this.render(this.posts);
}
getPeerID() {
let id = localStorage.getItem("peer_id");
@@ -293,6 +357,14 @@ class App {
}
return id;
}
getUsername() {
let username = localStorage.getItem("dandelion_username");
if (!username) {
username = "not_set";
localStorage.setItem("dandelion_username", username);
}
return username;
}
setFont(fontName, fontSize) {
let content = document.getElementById('content');
if (!content) {
@@ -313,9 +385,11 @@ class App {
log("offline");
});
// Event listener for going online
window.addEventListener('online', () => {
window.addEventListener('online', async () => {
log("online");
connection.connect();
this.posts = await this.loadPosts(this.userID) ?? [];
this.render(this.posts);
});
log(`Online status: ${navigator.onLine ? "online" : "offline"}`);
}
@@ -357,14 +431,17 @@ class App {
});
}
initButtons(userID, posts, registration) {
let font1Button = document.getElementById("button_font1");
let font2Button = document.getElementById("button_font2");
// let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
// let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
let importTweetsButton = document.getElementById("import_tweets");
let clearPostsButton = document.getElementById("clear_posts");
let updateApp = document.getElementById("update_app");
let ddlnLogoButton = document.getElementById('ddln-logo-button');
font1Button.addEventListener('click', () => { this.setFont('Bookerly', '16px'); });
font2Button.addEventListener('click', () => { this.setFont('Virgil', '16px'); });
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(file);
@@ -388,7 +465,7 @@ class App {
throw new Error();
}
postButton.addEventListener("click", () => {
this.addPost(userID, posts, postText.value);
this.addPost(userID, postText.value);
postText.value = "";
});
updateApp.addEventListener("click", () => {
@@ -416,40 +493,54 @@ class App {
let urlParams = (new URL(window.location.href)).searchParams;
let connection_userID = urlParams.get('connect');
let registration = undefined;
if (urlParams.get("sw") === "true") {
registration = await this.registerServiceWorker();
}
// if (urlParams.get("sw") === "true") {
registration = await this.registerServiceWorker();
// }
if (connection_userID) {
console.log('connect', connection_userID);
localStorage.setItem("dandelion_id", connection_userID);
}
let posts = [];
this.username = this.getUsername();
document.getElementById('username').innerText = this.username;
let userID = this.getUserID();
let peerID = this.getPeerID();
this.userID = userID;
this.peerID = peerID;
let connectURL = `https://${document.location.hostname}?connect=${this.userID}`;
document.getElementById('connectURL').innerHTML = `<a href="${connectURL}">connect</a>`;
let qrcode = await new QRCode(document.getElementById('qrcode'), {
text: connectURL,
width: 256,
height: 256,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
let qrcodeImage = document.querySelector('#qrcode > img');
qrcodeImage.classList.add('qrcode_image');
log(`user:${userID} peer:${peerID}`);
let websocket = new wsConnection(userID, peerID);
window.addEventListener('beforeunload', () => { websocket.disconnect(); });
this.initOffline(websocket);
this.initButtons(userID, posts, registration);
this.initButtons(userID, this.posts, registration);
let time = 0;
let delta = 0;
if (navigator.storage && navigator.storage.persist && !navigator.storage.persisted) {
debugger;
const isPersisted = await navigator.storage.persist();
log(`Persisted storage granted: ${isPersisted}`);
}
log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
// let isPersisted = await navigator?.storage?.persisted();
// if (!isPersisted) {
// debugger;
// const isPersisted = await navigator.storage.persist();
// 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(code);
// registration.active.postMessage({type:"updateMain", code:code});
posts = await this.loadPosts(userID) ?? [];
this.posts = await this.loadPosts(userID) ?? [];
// debugger;
this.timerStart();
this.render(posts); // , (postID:string)=>{this.deletePost(userID, postID)}
this.render(this.posts); // , (postID:string)=>{this.deletePost(userID, postID)}
let renderTime = this.timerDelta();
log(`render took: ${renderTime.toFixed(2)}ms`);
if (performance?.memory) {
@@ -476,7 +567,6 @@ class App {
}
contentDiv.innerHTML = "";
let count = 0;
new QRCode(document.getElementById('qrcode'), `https://ddlion.net/?connect=${this.userID}`);
for (let i = posts.length - 1; i >= 0; i--) {
let postData = posts[i];
let post = this.renderPost(postData, posts);
@@ -493,8 +583,10 @@ class App {
}
contentDiv.appendChild(fragment);
}
deletePost(userID, postID) {
async deletePost(userID, postID) {
deleteData(userID, postID);
this.posts = await this.loadPosts(userID) ?? [];
this.render(this.posts);
}
renderPost(post, posts) {
if (!(post.hasOwnProperty("text"))) {
@@ -504,13 +596,20 @@ class App {
let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`;
let deleteButton = document.createElement('button');
deleteButton.innerText = 'delete';
// deleteButton.onclick = ()=>{deletefunc(post.post_id)};
let editButton = document.createElement('button');
editButton.innerText = 'edit';
deleteButton.onclick = () => { this.deletePost(this.userID, post.post_id); };
let postTemplate = `<div><hr>
<div><span class='header' title='${timestamp}'>@${post.author} - ${post.post_timestamp.toLocaleDateString()}</span></div>
<div>
<span class='header' title='${timestamp}'>@${post.author} -
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
</span>
<span id="deleteButton"></span><span id="editButton"></span></div>
<div>${marked.parse(post.text)}</div>
</div>`;
containerDiv.innerHTML = postTemplate;
containerDiv.appendChild(deleteButton);
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
containerDiv.querySelector('#editButton')?.appendChild(editButton);
// if (!("image_data" in post && post.image_data)) {
// containerDiv.appendChild(timestampDiv);
// return containerDiv;

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgh092YwNP2/D6bshI
vzZNcEn/bpxtz7h3Otown6UXP6GhRANCAASEd7+0ppvG0iyaAjv8urB8LVYKSIYO
8GLp6H9P2fwZjzWXfC8iQMhP9jaWaQaAl73gS4iXpODDzYJ/Hwi6siPt
-----END PRIVATE KEY-----

View File

@@ -4,6 +4,9 @@
// email: string;
// }
// Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917
const postStoreName: string = "posts";
let keyBase = "dandelion_posts_v1_"
let key = "";
@@ -156,6 +159,60 @@ export async function addDataArray(userID: string, array: any[]): Promise<void>
}
}
export async function mergeDataArray(userID: string, array:any[]): Promise<void> {
try {
const db = await openDatabase(userID);
const transaction = db.transaction(postStoreName, "readwrite");
const store = transaction.objectStore(postStoreName);
const index = store.index("postIDIndex");
transaction.oncomplete = () => {
console.log("Transaction completed successfully");
db.close();
};
transaction.onerror = (event) => {
console.error("Transaction error:", (event.target as any).error);
db.close();
};
let postsToWrite:any = [];
for (let post of array) {
try {
let havePost = await new Promise<boolean>((resolve, reject) => {
const getRequest = index.getKey(post.post_id);
getRequest.onerror = (e) => {
console.log((e.target as IDBRequest).error);
reject(e);
};
getRequest.onsuccess = async (e) => {
const key = (e.target as IDBRequest).result;
resolve(key !== undefined)
};
});
// console.log(post.post_id, havePost);
if (!havePost ) {
postsToWrite.push(post);
}
} catch (error) {
console.error("Error processing post:", error);
}
}
console.log(`Writing ${postsToWrite.length} posts`);
await addDataArray(userID, postsToWrite);
} catch (error) {
console.error("Error in opening database:", error);
}
}
export async function getData(userID: string, lowerID: Date, upperID: Date): Promise<any | undefined> {
const db = await openDatabase(userID);
const transaction = db.transaction(postStoreName, "readonly");
@@ -191,6 +248,7 @@ export async function getData(userID: string, lowerID: Date, upperID: Date): Pro
});
}
export async function getAllData(userID: string): Promise<any | undefined> {
const db = await openDatabase(userID);
const transaction = db.transaction(postStoreName, "readonly");

View File

@@ -1,4 +1,8 @@
import { openDatabase, getData, addData, addDataArray, clearData, deleteData} from "./db.js"
// TODO: virtual list, only rerender what's needed so things can keep playing.
import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData } from "./db.js"
declare let WebTorrent: any;
@@ -31,6 +35,7 @@ function uuidv4() {
let logLines: string[] = [];
let logLength = 10;
function log(message: string) {
console.log(message);
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
if (logLines.length > 10) {
logLines = logLines.slice(logLines.length - logLength);
@@ -93,8 +98,10 @@ window.addEventListener('scroll', () => {
// let peer = await new PeerConnection(peer_id);
// let connectionReply = await wsConnection.send('hello');
// for (let peeer of connectionReply) {
// for (let peer of connectionReply) {
// let peerConnection = await wsConnection.send('connect', peer.id);
// if (peerConnection) {
// this.peers.push(peerConnection);
@@ -108,7 +115,6 @@ window.addEventListener('scroll', () => {
// }
// }
class wsConnection {
websocket: WebSocket | null = null;
userID = "";
@@ -116,7 +122,88 @@ class wsConnection {
websocketPingInterval: number = 0;
retry = 10;
state = 'disconnected';
peers: Map<string, string[]> = new Map();
messageHandlers: Map<string, (event: any) => void> = new Map();
peerMessageHandlers: Map<string, (data: any) => void> = new Map();
send(message: any) {
let json = ""
try {
json = JSON.stringify(message);
} catch (e) {
console.log(e, "wsConnection send: Couldn't serialize message", message);
}
log(`ws->${json.slice(0,80)}`)
this.websocket!.send(json);
}
helloResponseHandler(data: any) {
for (let [userID, peerIDs] of Object.entries(data.userPeers)) {
this.peers.set(userID, [...Object.keys(peerIDs as any)]);
for (let peerID of [...Object.keys(peerIDs as any)]) {
if (peerID === this.peerID) {
continue;
}
this.send({
type:"peer_message",
from:this.peerID,
to:peerID,
message:{type:"get_posts_for_user", user_id:userID}
})
}
}
}
pongHandler(data: any) {
}
async getPostsForUserResponseHandler(data: any) {
// log(`getPostsForUserResponse: ${data}`)
let message = data.message;
console.log(`getPostsForUserResponseHandler Got ${message.posts.length} from peer ${data.from}`);
for (let post of message.posts) {
post.post_timestamp = new Date(post.post_timestamp);
}
console.log(`Merging same user peer posts...`)
await mergeDataArray(message.user_id, data.message.posts)
if (message.user_id === this.userID) {
app.posts = await app.loadPosts(this.userID) ?? [];
app.render(app.posts);
}
}
async getPostsForUserHandler(data: any) {
let message = data.message;
let posts = await getAllData(message.user_id) ?? []; // this doesn't get all posts!
posts = posts.map((post:any)=>post.data)
// let posts = await getAllData(message.user_id) ?? [];
let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, message: { type: "get_posts_for_user_response", posts: posts, user_id: message.user_id } }
this.send(responseMessage)
}
async peerMessageHandler(data: any) {
log(`peerMessageHandler ${data}`)
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);
}
connect(): void {
if (this.websocket?.readyState === WebSocket.OPEN) {
@@ -133,18 +220,18 @@ class wsConnection {
return;
}
this.websocket.onopen = (evt) => {
this.websocket.onopen = (event) => {
log("ws:connected");
this.websocket!.send(`{"type":"hello", "user_id": "${this.userID}", "peer_id":"${this.peerID}"}`);
this.send({type:"hello", user_id: this.userID, peer_id:this.peerID});
this.websocketPingInterval = window.setInterval(() => {
if (!navigator.onLine) {
return;
}
this.websocket!.send(`{"type":"ping", "peer_id": "${this.peerID}"}`);
this.send({type:"ping", peer_id: this.peerID});
}, 10_000)
};
this.websocket.onclose = (evt) => {
this.websocket.onclose = (event) => {
log("ws:disconnected");
// this.retry *= 2;
log(`Retrying in ${this.retry} seconds`);
@@ -152,7 +239,20 @@ class wsConnection {
};
this.websocket.onmessage = (event) => {
log('ws:response: ' + event.data);
log('ws:<-' + event.data.slice(0,80));
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) => {
@@ -168,6 +268,13 @@ class wsConnection {
constructor(userID: string, peerID: string) {
this.userID = userID;
this.peerID = peerID;
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
this.messageHandlers.set('pong', this.pongHandler);
this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserResponseHandler.bind(this));
this.connect();
if (!this.websocket) {
@@ -177,34 +284,11 @@ class wsConnection {
}
}
// function connectWebsocket(userID: string) {
// let websocket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
// websocket.onopen = function (evt) {
// log("Websocket: CONNECTED");
// websocket.send(`{"messageType":"connect", "id": "${userID}"}`);
// let websocketPingInterval = window.setInterval(() => { websocket.send(`{"messageType":"ping", "id": "${userID}"}`); }, 5000)
// };
// websocket.onclose = function (evt) {
// log("Websocket: DISCONNECTED");
// };
// websocket.onmessage = function (evt) {
// log('Websocket: RESPONSE: ' + evt.data);
// };
// websocket.onerror = function (evt) {
// log('Websocket: ERROR: ' + evt);
// };
// return websocket;
// }
class App {
userID:string = '';
peerID:string = '';
username: string = '';
userID: string = '';
peerID: string = '';
posts: Post[] = [];
initMarkdown() {
const renderer = new marked.Renderer();
@@ -267,7 +351,7 @@ class App {
return fullText
}
async importTweetArchive(userID: string, tweetArchive: any[]) {
async importTweetArchive(userID: string, tweetArchive: any[]) {
log("Importing tweet archive")
let postsTestData: any[] = [];
@@ -282,9 +366,9 @@ class App {
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;
}
// 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;
@@ -355,19 +439,19 @@ class App {
});
}
addPost(userID: string, posts: Post[], postText: string) {
addPost(userID: string, postText: string) {
if ((typeof postText !== "string") || postText.length === 0) {
log("Not posting an empty string...")
return;
}
let post = new Post(`bobbydigitales`, userID, postText, new Date());
let post = new Post(this.username, userID, postText, new Date());
posts.push(post);
this.posts.push(post);
// localStorage.setItem(key, JSON.stringify(posts));
addData(userID, post)
this.render(posts);
this.render(this.posts);
}
@@ -393,6 +477,17 @@ class App {
return id;
}
getUsername() {
let username = localStorage.getItem("dandelion_username");
if (!username) {
username = "not_set"
localStorage.setItem("dandelion_username", username);
}
return username;
}
setFont(fontName: string, fontSize: string) {
let content = document.getElementById('content');
@@ -420,10 +515,11 @@ class App {
});
// Event listener for going online
window.addEventListener('online', () => {
window.addEventListener('online', async () => {
log("online")
connection.connect();
this.posts = await this.loadPosts(this.userID) ?? [];
this.render(this.posts);
});
log(`Online status: ${navigator.onLine ? "online" : "offline"}`)
@@ -474,15 +570,18 @@ class App {
}
initButtons(userID: string, posts: Post[], registration: ServiceWorkerRegistration | undefined) {
let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
// 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 clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement;
let updateApp = document.getElementById("update_app") as HTMLButtonElement;
let ddlnLogoButton = document.getElementById('ddln-logo-button') as HTMLDivElement;
font1Button.addEventListener('click', () => { this.setFont('Bookerly', '16px') });
font2Button.addEventListener('click', () => { this.setFont('Virgil', '16px') });
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/*');
@@ -516,7 +615,7 @@ class App {
}
postButton.addEventListener("click", () => {
this.addPost(userID, posts, postText.value);
this.addPost(userID, postText.value);
postText.value = "";
});
@@ -530,7 +629,7 @@ class App {
return;
}
ddlnLogoButton.addEventListener('click', ()=>{infoElement.style.display=='none'? infoElement.style.display='block' : infoElement.style.display='none';});
ddlnLogoButton.addEventListener('click', () => { infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none'; });
}
async loadPosts(userID: string) {
@@ -550,42 +649,59 @@ class App {
// return await getData(userID, new Date(2022, 8), new Date());
}
async main() {
async main() {
let urlParams = (new URL(window.location.href)).searchParams;
let connection_userID = urlParams.get('connect');
let registration = undefined;
if (urlParams.get("sw") === "true") {
// if (urlParams.get("sw") === "true") {
registration = await this.registerServiceWorker();
}
// }
if (connection_userID) {
console.log('connect', connection_userID);
localStorage.setItem("dandelion_id", connection_userID);
}
let posts: Post[] = [];
this.username = this.getUsername();
document.getElementById('username')!.innerText = this.username;
let userID = this.getUserID();
let peerID = this.getPeerID();
this.userID = userID;
this.peerID = peerID;
let connectURL = `https://${document.location.hostname}?connect=${this.userID}`;
document.getElementById('connectURL')!.innerHTML = `<a href="${connectURL}">connect</a>`;
let qrcode = await new QRCode(document.getElementById('qrcode'), {
text: connectURL,
width: 256,
height: 256,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
let qrcodeImage:HTMLImageElement = document.querySelector('#qrcode > img') as HTMLImageElement;
qrcodeImage.classList.add('qrcode_image');
log(`user:${userID} peer:${peerID}`);
let websocket = new wsConnection(userID, peerID);
window.addEventListener('beforeunload', () => { websocket.disconnect() })
this.initOffline(websocket);
this.initButtons(userID, posts, registration);
this.initButtons(userID, this.posts, registration);
let time = 0;
let delta = 0;
if (navigator.storage && navigator.storage.persist && !navigator.storage.persisted) {
debugger;
const isPersisted = await navigator.storage.persist();
log(`Persisted storage granted: ${isPersisted}`);
}
// let isPersisted = await navigator?.storage?.persisted();
// if (!isPersisted) {
// debugger;
// const isPersisted = await navigator.storage.persist();
// log(`Persisted storage granted: ${isPersisted}`);
// }
log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
// log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
this.initMarkdown();
@@ -594,12 +710,12 @@ class App {
// console.log(code);
// registration.active.postMessage({type:"updateMain", code:code});
posts = await this.loadPosts(userID) ?? [];
this.posts = await this.loadPosts(userID) ?? [];
// debugger;
this.timerStart();
this.render(posts); // , (postID:string)=>{this.deletePost(userID, postID)}
this.render(this.posts); // , (postID:string)=>{this.deletePost(userID, postID)}
let renderTime = this.timerDelta();
log(`render took: ${renderTime.toFixed(2)}ms`);
@@ -626,7 +742,7 @@ class App {
// })
}
render(posts: Post[]) {
render(posts: Post[]) {
const fragment = document.createDocumentFragment();
let contentDiv = document.getElementById("content");
if (!contentDiv) {
@@ -635,9 +751,6 @@ class App {
contentDiv.innerHTML = "";
let count = 0;
new QRCode(document.getElementById('qrcode'), `https://ddlion.net/?connect=${this.userID}`);
for (let i = posts.length - 1; i >= 0; i--) {
let postData = posts[i];
@@ -661,11 +774,13 @@ class App {
}
deletePost(userID:string, postID:string) {
async deletePost(userID: string, postID: string) {
deleteData(userID, postID)
this.posts = await this.loadPosts(userID) ?? [];
this.render(this.posts);
}
renderPost(post: Post, posts:Post[]) {
renderPost(post: Post, posts: Post[]) {
if (!(post.hasOwnProperty("text"))) {
throw new Error("Post is malformed!");
}
@@ -673,19 +788,24 @@ class App {
let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`;
let deleteButton = document.createElement('button');
deleteButton.innerText = 'delete';
// deleteButton.onclick = ()=>{deletefunc(post.post_id)};
let deleteButton = document.createElement('button'); deleteButton.innerText = 'delete';
let editButton = document.createElement('button'); editButton.innerText = 'edit';
deleteButton.onclick = () => { this.deletePost(this.userID, post.post_id) };
let postTemplate =
`<div><hr>
<div><span class='header' title='${timestamp}'>@${post.author} - ${post.post_timestamp.toLocaleDateString()}</span></div>
`<div><hr>
<div>
<span class='header' title='${timestamp}'>@${post.author} -
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
</span>
<span id="deleteButton"></span><span id="editButton"></span></div>
<div>${marked.parse(post.text)}</div>
</div>`
containerDiv.innerHTML = postTemplate;
containerDiv.appendChild(deleteButton);
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
containerDiv.querySelector('#editButton')?.appendChild(editButton);
// if (!("image_data" in post && post.image_data)) {
// containerDiv.appendChild(timestampDiv);

View File

@@ -2,16 +2,16 @@
const cacheName = "dandelion_cache_v1";
const contentToCache = [
"/index.html",
"/main.js",
"/marked.min.js",
"/db.js",
"/bookerly.woff2",
"/virgil.woff2",
"/favicon.ico"
'/index.html',
'/main.css',
'/main.js',
'lib//marked.min.js',
'lib/qrcode.min.js',
'/db.js',
'/favicon.ico'
];
self.addEventListener("install", (e:any) => {
self.addEventListener("install", (e: any) => {
e.waitUntil(
(async () => {
const cache = await caches.open(cacheName);
@@ -24,49 +24,53 @@ self.addEventListener("install", (e:any) => {
);
});
self.addEventListener("fetch", (e:any) => {
e.respondWith(
(async () => {
const r = await caches.match(e.request);
if (r) {
console.log(
`[Service Worker] Cache hit for resource: ${e.request.url}`
);
return r;
}
async function responder(event: any) {
console.log('Fetching', event.request.url);
let response;
try {
console.log(
`[Service Worker] Cache miss, attempting to fetch resource: ${e.request.url}`
);
let response = await fetch(event.request);
response = await fetch(e.request);
} catch (e) {
console.warn(e);
}
const cache = await caches.open(cacheName);
console.log(
`[Service Worker] Adding resource to cache: ${e.request.url}`
);
if (!response) {
console.log('Fetch failed, falling back to cache', event.request.url);
let cacheMatch = await caches.match(event.request);
if (!cacheMatch) {
// DUnno what to return here!
}
return cacheMatch;
}
if (!response) {
throw new Error(`Failed to fetch resource: ${e.request.url}`)
}
cache.put(e.request, response.clone());
return response;
})()
);
if (response.status === 206) {
console.log('Not caching partial content');
return response;
}
console.log('Fetch successful, updating cache');
const cache = await caches.open(cacheName);
try {
cache.put(event.request, response.clone()).catch((error)=>console.log('failed to cache', event.request, error));
} catch (e) {
console.log('failed to cache', event.request)
}
return response;
}
self.addEventListener('fetch', function (event: any) {
event.respondWith(responder(event));
});
addEventListener("message", async (e) => {
console.log(`Message received: ${e.data}`);
console.log(`Message received:`, e.data);
switch (e.data.type) {
case "updateMain":
case "update_app":
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: main.js`);
cache.put("/main.js", new Response());
console.log(`[Service Worker] Caching resources`);
// cache.put("/main.js", new Response());
for (let item of contentToCache) {
cache.delete(item);
}
await cache.addAll(contentToCache);
break;
}
});

View File

@@ -1,83 +1,112 @@
const config = {
iceServers: [{ urls: "stun: stun.l.google.com" }],
};
class PeerManager {
connect(peerID:string) {
// Connect to the peer that has the peer id peerID
}
let localConnection = new RTCPeerConnection();
function handleSendChannelStatusChange() {
console.log(handleSendChannelStatusChange);
disconnect(peerID:string) {
}
}
let sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;
let remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;
localConnection.onicecandidate = (e:any) =>
!e.candidate ||
remoteConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
remoteConnection.onicecandidate = (e) =>
!e.candidate ||
localConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
class PeerConnection {
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" },
],};
}
function handleCreateDescriptionError(error:any) {
console.log(`Unable to create an offer: ${error.toString()}`);
}
function handleLocalAddCandidateSuccess() {
console.log('handleLocalAddCandidateSuccess');
}
function handleRemoteAddCandidateSuccess() {
console.log('handleRemoteAddCandidateSuccess');
const config = {
iceServers: [{ urls: "stun:stun.mystunserver.tld" }],
};
}
let polite = true;
function handleAddCandidateError() {
console.log("Oh noes! addICECandidate failed!");
}
// const signaler = new SignalingChannel();
const signaler:any = {}
const pc = new RTCPeerConnection(config);
localConnection
.createOffer()
.then((offer) => localConnection.setLocalDescription(offer))
.then(() =>
remoteConnection.setRemoteDescription(localConnection.localDescription as RTCSessionDescriptionInit),
)
.then(() => remoteConnection.createAnswer())
.then((answer) => remoteConnection.setLocalDescription(answer))
.then(() =>
localConnection.setRemoteDescription(remoteConnection.localDescription as RTCSessionDescriptionInit),
)
.catch(handleCreateDescriptionError);
function handleReceiveChannelStatusChange(event:any) {
let receiveChannel = event.channel;
const constraints = { audio: true, video: true };
const selfVideo = document.querySelector("video.selfview");
const remoteVideo = document.querySelector("video.remoteview");
if (receiveChannel) {
console.log(
`Receive channel's status has changed to ${receiveChannel.readyState}`,
);
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);
}
}
function receiveChannelCallback(event:any) {
let receiveChannel = event.channel;
receiveChannel.onmessage = handleReceiveMessage;
receiveChannel.onopen = handleReceiveChannelStatusChange;
receiveChannel.onclose = handleReceiveChannelStatusChange;
}
function sendMessage(message:string) {
sendChannel.send(message);
}
pc.ontrack = ({ track, streams }) => {
track.onunmute = () => {
// if (remoteVideo.srcObject) {
// return;
// }
// remoteVideo.srcObject = streams[0];
};
};
function handleReceiveMessage(event:any) {
console.log(event.data);
let makingOffer = false;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
} catch (err) {
console.error(err);
} finally {
makingOffer = false;
}
};
pc.onicecandidate = ({ candidate }) => signaler.send({ candidate });
let ignoreOffer = false;
signaler.onmessage = async ({ data: { description, candidate } }: MessageEvent) => {
try {
if (description) {
const offerCollision =
description.type === "offer" &&
(makingOffer || pc.signalingState !== "stable");
ignoreOffer = !polite && offerCollision;
if (ignoreOffer) {
return;
}
await pc.setRemoteDescription(description);
if (description.type === "offer") {
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
}
} else if (candidate) {
try {
await pc.addIceCandidate(candidate);
} catch (err) {
if (!ignoreOffer) {
throw err;
}
}
}
} catch (err) {
console.error(err);
}
};

72
sw.js
View File

@@ -2,13 +2,13 @@
// Establish a cache name
const cacheName = "dandelion_cache_v1";
const contentToCache = [
"/index.html",
"/main.js",
"/marked.min.js",
"/db.js",
"/bookerly.woff2",
"/virgil.woff2",
"/favicon.ico"
'/index.html',
'/main.css',
'/main.js',
'lib//marked.min.js',
'lib/qrcode.min.js',
'/db.js',
'/favicon.ico'
];
self.addEventListener("install", (e) => {
e.waitUntil((async () => {
@@ -17,37 +17,45 @@ self.addEventListener("install", (e) => {
await cache.addAll(contentToCache);
})());
});
self.addEventListener("fetch", (e) => {
e.respondWith((async () => {
const r = await caches.match(e.request);
if (r) {
console.log(`[Service Worker] Cache hit for resource: ${e.request.url}`);
return r;
async function responder(event) {
console.log('Fetching', event.request.url);
let response = await fetch(event.request);
if (!response) {
console.log('Fetch failed, falling back to cache', event.request.url);
let cacheMatch = await caches.match(event.request);
if (!cacheMatch) {
// DUnno what to return here!
}
let response;
try {
console.log(`[Service Worker] Cache miss, attempting to fetch resource: ${e.request.url}`);
response = await fetch(e.request);
}
catch (e) {
console.warn(e);
}
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Adding resource to cache: ${e.request.url}`);
if (!response) {
throw new Error(`Failed to fetch resource: ${e.request.url}`);
}
cache.put(e.request, response.clone());
return cacheMatch;
}
if (response.status === 206) {
console.log('Not caching partial content');
return response;
})());
}
console.log('Fetch successful, updating cache');
const cache = await caches.open(cacheName);
try {
cache.put(event.request, response.clone()).catch((error) => console.log('failed to cache', event.request, error));
}
catch (e) {
console.log('failed to cache', event.request);
}
return response;
}
self.addEventListener('fetch', function (event) {
event.respondWith(responder(event));
});
addEventListener("message", async (e) => {
console.log(`Message received: ${e.data}`);
console.log(`Message received:`, e.data);
switch (e.data.type) {
case "updateMain":
case "update_app":
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: main.js`);
cache.put("/main.js", new Response());
console.log(`[Service Worker] Caching resources`);
// cache.put("/main.js", new Response());
for (let item of contentToCache) {
cache.delete(item);
}
await cache.addAll(contentToCache);
break;
}
});

View File

@@ -1 +1 @@
{"version":3,"file":"sw.js","sourceRoot":"","sources":["src/sw.ts"],"names":[],"mappings":";AAAA,yBAAyB;AACzB,MAAM,SAAS,GAAG,oBAAoB,CAAC;AAEvC,MAAM,cAAc,GAAG;IACvB,aAAa;IACb,UAAU;IACV,gBAAgB;IAChB,QAAQ;IACR,iBAAiB;IACjB,eAAe;IACf,cAAc;CACb,CAAC;AAEF,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAK,EAAE,EAAE;IACzC,CAAC,CAAC,SAAS,CACT,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CACT,qDAAqD,EACrD,cAAc,CACf,CAAC;QACF,MAAM,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IACrC,CAAC,CAAC,EAAE,CACL,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAK,EAAE,EAAE;IACvC,CAAC,CAAC,WAAW,CACX,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,EAAE,CAAC;YACN,OAAO,CAAC,GAAG,CACT,4CAA4C,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAC5D,CAAC;YACF,OAAO,CAAC,CAAC;QACX,CAAC;QAED,IAAI,QAAQ,CAAC;QACb,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CACT,8DAA8D,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAC9E,CAAC;YAEF,QAAQ,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CACT,8CAA8C,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAC9D,CAAC;QAEF,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QAC/D,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;QACvC,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC,EAAE,CACL,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,gBAAgB,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IACtC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAE3C,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,YAAY;YACf,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3C,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;YAC9D,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,QAAQ,EAAE,CAAC,CAAC;YACtC,MAAM;IACV,CAAC;AACH,CAAC,CAAC,CAAC"}
{"version":3,"file":"sw.js","sourceRoot":"","sources":["src/sw.ts"],"names":[],"mappings":"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"}

140
webRTC.js
View File

@@ -1,56 +1,96 @@
"use strict";
const config = {
iceServers: [{ urls: "stun: stun.l.google.com" }],
};
let localConnection = new RTCPeerConnection();
function handleSendChannelStatusChange() {
console.log(handleSendChannelStatusChange);
}
let sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;
let remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;
localConnection.onicecandidate = (e) => !e.candidate ||
remoteConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
remoteConnection.onicecandidate = (e) => !e.candidate ||
localConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
function handleCreateDescriptionError(error) {
console.log(`Unable to create an offer: ${error.toString()}`);
}
function handleLocalAddCandidateSuccess() {
console.log('handleLocalAddCandidateSuccess');
}
function handleRemoteAddCandidateSuccess() {
console.log('handleRemoteAddCandidateSuccess');
}
function handleAddCandidateError() {
console.log("Oh noes! addICECandidate failed!");
}
localConnection
.createOffer()
.then((offer) => localConnection.setLocalDescription(offer))
.then(() => remoteConnection.setRemoteDescription(localConnection.localDescription))
.then(() => remoteConnection.createAnswer())
.then((answer) => remoteConnection.setLocalDescription(answer))
.then(() => localConnection.setRemoteDescription(remoteConnection.localDescription))
.catch(handleCreateDescriptionError);
function handleReceiveChannelStatusChange(event) {
let receiveChannel = event.channel;
if (receiveChannel) {
console.log(`Receive channel's status has changed to ${receiveChannel.readyState}`);
class PeerManager {
connect(peerID) {
// Connect to the peer that has the peer id peerID
}
disconnect(peerID) {
}
}
function receiveChannelCallback(event) {
let receiveChannel = event.channel;
receiveChannel.onmessage = handleReceiveMessage;
receiveChannel.onopen = handleReceiveChannelStatusChange;
receiveChannel.onclose = handleReceiveChannelStatusChange;
class PeerConnection {
}
function sendMessage(message) {
sendChannel.send(message);
}
function handleReceiveMessage(event) {
console.log(event.data);
PeerConnection.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" },
],
};
const config = {
iceServers: [{ urls: "stun:stun.mystunserver.tld" }],
};
let polite = true;
// const signaler = new SignalingChannel();
const signaler = {};
const pc = 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);
}
}
pc.ontrack = ({ track, streams }) => {
track.onunmute = () => {
// if (remoteVideo.srcObject) {
// return;
// }
// remoteVideo.srcObject = streams[0];
};
};
let makingOffer = false;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
}
catch (err) {
console.error(err);
}
finally {
makingOffer = false;
}
};
pc.onicecandidate = ({ candidate }) => signaler.send({ candidate });
let ignoreOffer = false;
signaler.onmessage = async ({ data: { description, candidate } }) => {
try {
if (description) {
const offerCollision = description.type === "offer" &&
(makingOffer || pc.signalingState !== "stable");
ignoreOffer = !polite && offerCollision;
if (ignoreOffer) {
return;
}
await pc.setRemoteDescription(description);
if (description.type === "offer") {
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
}
}
else if (candidate) {
try {
await pc.addIceCandidate(candidate);
}
catch (err) {
if (!ignoreOffer) {
throw err;
}
}
}
}
catch (err) {
console.error(err);
}
};
//# sourceMappingURL=webRTC.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"webRTC.js","sourceRoot":"","sources":["src/webRTC.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"webRTC.js","sourceRoot":"","sources":["src/webRTC.ts"],"names":[],"mappings":"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"}