diff --git a/go-server/.gitignore b/go-server/.gitignore new file mode 100644 index 00000000..40968e31 --- /dev/null +++ b/go-server/.gitignore @@ -0,0 +1 @@ +identity.key diff --git a/go-server/chatroom.go b/go-server/chatroom.go index 04e7463b..fd2dbe94 100644 --- a/go-server/chatroom.go +++ b/go-server/chatroom.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "github.com/libp2p/go-libp2p/core/peer" @@ -18,6 +17,7 @@ const ChatRoomBufSize = 128 type ChatRoom struct { // Messages is a channel of messages received from other peers in the chat room Messages chan *ChatMessage + SysMessages chan *ChatMessage ctx context.Context ps *pubsub.PubSub @@ -60,6 +60,7 @@ func JoinChatRoom(ctx context.Context, ps *pubsub.PubSub, selfID peer.ID, nickna nick: nickname, roomName: roomName, Messages: make(chan *ChatMessage, ChatRoomBufSize), + SysMessages: make(chan *ChatMessage, ChatRoomBufSize), } // start reading messages from the subscription in a loop @@ -69,16 +70,7 @@ func JoinChatRoom(ctx context.Context, ps *pubsub.PubSub, selfID peer.ID, nickna // Publish sends a message to the pubsub topic. func (cr *ChatRoom) Publish(message string) error { - m := ChatMessage{ - Message: message, - SenderID: cr.self.Pretty(), - SenderNick: cr.nick, - } - msgBytes, err := json.Marshal(m) - if err != nil { - return err - } - return cr.topic.Publish(cr.ctx, msgBytes) + return cr.topic.Publish(cr.ctx, []byte(message)) } func (cr *ChatRoom) ListPeers() []peer.ID { @@ -99,12 +91,9 @@ func (cr *ChatRoom) readLoop() { } cm := new(ChatMessage) cm.Message = string(msg.Data) - cm.SenderID = string(msg.ID) - cm.SenderNick = "unknown" + cm.SenderID = msg.ID + cm.SenderNick = string(msg.ID[len(msg.ID)-8]) - if err != nil { - continue - } // send valid messages onto the Messages channel cr.Messages <- cm } diff --git a/go-server/identity.go b/go-server/identity.go new file mode 100644 index 00000000..7f59f241 --- /dev/null +++ b/go-server/identity.go @@ -0,0 +1,49 @@ +package main +// Borrowed from https://github.com/libp2p/go-libp2p-relay-daemon/blob/master/identity.go + +import ( + "fmt" + "os" + + "github.com/libp2p/go-libp2p/core/crypto" +) + +// LoadIdentity reads a private key from the given path and, if it does not +// exist, generates a new one. +func LoadIdentity(idPath string) (crypto.PrivKey, error) { + if _, err := os.Stat(idPath); err == nil { + return ReadIdentity(idPath) + } else if os.IsNotExist(err) { + fmt.Printf("Generating peer identity in %s\n", idPath) + return GenerateIdentity(idPath) + } else { + return nil, err + } +} + +// ReadIdentity reads a private key from the given path. +func ReadIdentity(path string) (crypto.PrivKey, error) { + bytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return crypto.UnmarshalPrivateKey(bytes) +} + +// GenerateIdentity writes a new random private key to the given path. +func GenerateIdentity(path string) (crypto.PrivKey, error) { + privk, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 0) + if err != nil { + return nil, err + } + + bytes, err := crypto.MarshalPrivateKey(privk) + if err != nil { + return nil, err + } + + err = os.WriteFile(path, bytes, 0400) + + return privk, err +} \ No newline at end of file diff --git a/go-server/main.go b/go-server/main.go index 65bf2d6d..c5bbd07c 100644 --- a/go-server/main.go +++ b/go-server/main.go @@ -4,6 +4,8 @@ import ( "context" "flag" "fmt" + "io/ioutil" + "log" "os" "sync" "time" @@ -28,7 +30,7 @@ const DiscoveryInterval = time.Hour // DiscoveryServiceTag is used in our mDNS advertisements to discover other chat peers. const DiscoveryServiceTag = "universal-connectivity" -var ChatMsgChan chan *ChatMessage +var SysMsgChan chan *ChatMessage // Borrowed from https://medium.com/rahasak/libp2p-pubsub-peer-discovery-with-kademlia-dht-c8b131550ac7 // NewDHT attempts to connect to a bunch of bootstrap peers and returns a new DHT. @@ -76,7 +78,7 @@ func Discover(ctx context.Context, h host.Host, dht *dht.IpfsDHT, rendezvous str discovery.Advertise(ctx, routingDiscovery, rendezvous) - ticker := time.NewTicker(time.Second * 1) + ticker := time.NewTicker(time.Second * 10) defer ticker.Stop() for { @@ -108,19 +110,45 @@ func Discover(ctx context.Context, h host.Host, dht *dht.IpfsDHT, rendezvous str } func LogMsgf(f string, msg ...any) { - ChatMsgChan <- &ChatMessage{Message: fmt.Sprintf(f, msg...), SenderID: "system", SenderNick: "system"} + SysMsgChan <- &ChatMessage{Message: fmt.Sprintf(f, msg...), SenderID: "system", SenderNick: "system"} } func main() { // parse some flags to set our nickname and the room to join nickFlag := flag.String("nick", "", "nickname to use in chat. will be generated if empty") roomFlag := flag.String("room", "universal-connectivity", "name of chat room to join") + idPath := flag.String("identity", "identity.key", "path to the private key (PeerID) file") + useLogger := flag.Bool("logger", false, "write logs to file") flag.Parse() + if *useLogger { + f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Println("failed to open log file", err) + log.SetOutput(ioutil.Discard) + } else { + defer f.Close() + log.SetOutput(f) + } + } else { + log.SetOutput(ioutil.Discard) + } + ctx := context.Background() + // load our private key to generate the same peerID each time + privk, err := LoadIdentity(*idPath) + if err != nil { + panic(err) + } + // create a new libp2p Host that listens on a random TCP port - h, err := libp2p.New(libp2p.Transport(quicTransport.NewTransport), libp2p.Transport(webtransport.New), libp2p.ListenAddrStrings("/ip4/0.0.0.0/udp/0/quic-v1", "/ip4/0.0.0.0/udp/0/quic-v1/webtransport", "/ip6/::/udp/0/quic-v1", "/ip6/::/udp/0/quic-v1/webtransport")) + h, err := libp2p.New( + libp2p.Identity(privk), + libp2p.Transport(quicTransport.NewTransport), + libp2p.Transport(webtransport.New), + libp2p.ListenAddrStrings("/ip4/0.0.0.0/udp/0/quic-v1", "/ip4/0.0.0.0/udp/0/quic-v1/webtransport", "/ip6/::/udp/0/quic-v1", "/ip6/::/udp/0/quic-v1/webtransport"), + ) if err != nil { panic(err) } @@ -145,7 +173,7 @@ func main() { if err != nil { panic(err) } - ChatMsgChan = cr.Messages + SysMsgChan = cr.SysMessages // setup DHT with empty discovery peers // so this will be a discovery peer for others diff --git a/go-server/ui.go b/go-server/ui.go index 51725bd1..16569e67 100644 --- a/go-server/ui.go +++ b/go-server/ui.go @@ -3,6 +3,7 @@ package main import ( "fmt" "io" + "log" "time" "github.com/gdamore/tcell/v2" @@ -19,6 +20,7 @@ type ChatUI struct { peersList *tview.TextView msgW io.Writer + sysW io.Writer inputCh chan string doneCh chan struct{} } @@ -41,6 +43,19 @@ func NewChatUI(cr *ChatRoom) *ChatUI { app.Draw() }) + // make a text view to contain our error messages + sysBox := tview.NewTextView() + sysBox.SetDynamicColors(true) + sysBox.SetBorder(true) + sysBox.SetTitle("System") + + // text views are io.Writers, but they don't automatically refresh. + // this sets a change handler to force the app to redraw when we get + // new messages to display. + sysBox.SetChangedFunc(func() { + app.Draw() + }) + // an input field for typing messages into inputCh := make(chan string, 32) input := tview.NewInputField(). @@ -87,8 +102,9 @@ func NewChatUI(cr *ChatRoom) *ChatUI { flex := tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(chatPanel, 0, 1, false). - AddItem(input, 1, 1, true) + AddItem(chatPanel, 0, 3, false). + AddItem(sysBox, 0, 2, false). + AddItem(input, 2, 1, true) app.SetRoot(flex, true) @@ -97,6 +113,7 @@ func NewChatUI(cr *ChatRoom) *ChatUI { app: app, peersList: peersList, msgW: msgBox, + sysW: sysBox, inputCh: inputCh, doneCh: make(chan struct{}, 1), } @@ -138,6 +155,13 @@ func (ui *ChatUI) displayChatMessage(cm *ChatMessage) { fmt.Fprintf(ui.msgW, "%s %s\n", prompt, cm.Message) } +// displayChatMessage writes a ChatMessage from the room to the message window, +// with the sender's nick highlighted in green. +func (ui *ChatUI) displaySysMessage(cm *ChatMessage) { + fmt.Fprintf(ui.sysW, "%s\n", cm.Message) + log.Println(cm.Message) +} + // displaySelfMessage writes a message from ourself to the message window, // with our nick highlighted in yellow. func (ui *ChatUI) displaySelfMessage(msg string) { @@ -166,6 +190,10 @@ func (ui *ChatUI) handleEvents() { // when we receive a message from the chat room, print it to the message window ui.displayChatMessage(m) + case s := <-ui.cr.SysMessages: + // when we receive a message from the chat room, print it to the message window + ui.displaySysMessage(s) + case <-peerRefreshTicker.C: // refresh the list of peers in the chat room periodically ui.refreshPeers()