diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2025-11-25 20:20:52 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-25 20:20:52 +0100 |
| commit | c9f4d26216327def79cf2d48152dda14c778112b (patch) | |
| tree | f6a3e3f4b87c585aba13b7f948291449cf589431 | |
| parent | 9b4caf3e5ffad219ea55ea6db7ad19940c5f83d9 (diff) | |
| download | noshitalk-c9f4d26216327def79cf2d48152dda14c778112b.tar.gz noshitalk-c9f4d26216327def79cf2d48152dda14c778112b.tar.xz noshitalk-c9f4d26216327def79cf2d48152dda14c778112b.zip | |
Delete noshitalk-client.go
| -rw-r--r-- | noshitalk-client.go | 947 |
1 files changed, 0 insertions, 947 deletions
diff --git a/noshitalk-client.go b/noshitalk-client.go deleted file mode 100644 index bed425c..0000000 --- a/noshitalk-client.go +++ /dev/null @@ -1,947 +0,0 @@ -package main - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/ecdh" - "crypto/hmac" - cryptorand "crypto/rand" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "math/rand" - "net" - "net/url" - "os" - "os/signal" - "path/filepath" - "regexp" - "strings" - "syscall" - "time" - - "github.com/awnumar/memguard" - "golang.org/x/net/proxy" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/widget" - "fyne.io/fyne/v2/theme" -) - -type ChatClient struct { - conn net.Conn - gcm cipher.AEAD - sharedSecret *memguard.Enclave - app fyne.App - window fyne.Window - messages *widget.List - messageList []string - input *widget.Entry - status *widget.Label - connectBtn *widget.Button - serverEntry *widget.Entry - debugLog *widget.Entry - connected bool - autoReconnect bool - lastServer string - userList *widget.List - onlineUsers []string - username string - fingerprint string - signKey *ecdh.PrivateKey - encryptKey *ecdh.PrivateKey -} - -type Message struct { - From string `json:"from"` - To string `json:"to,omitempty"` - Content string `json:"content"` - Time string `json:"time"` - Type string `json:"type"` -} - -type UserListMessage struct { - Type string `json:"type"` - Users []string `json:"users"` - Time string `json:"time"` -} - -type IdentityFile struct { - Version string `json:"version"` - Username string `json:"username"` - Fingerprint string `json:"fingerprint"` - SignKeyPair KeyPair `json:"signKeyPair"` - EncryptKeyPair KeyPair `json:"encryptKeyPair"` -} - -type KeyPair struct { - PrivateKey string `json:"privateKey"` - PublicKey string `json:"publicKey"` -} - -var ( - version = "1.0" - onionRegex = regexp.MustCompile(`^[a-z2-7]{56}\.onion(:[0-9]{1,5})?$`) -) - -const ( - messageBlockSize = 256 - minRandomDelay = 50 - maxRandomDelay = 200 -) - -func main() { - memguard.CatchInterrupt() - defer memguard.Purge() - - chatApp := app.NewWithID("noshitalk-gui") - client := &ChatClient{ - app: chatApp, - messageList: []string{}, - autoReconnect: true, - onlineUsers: []string{}, - } - - if err := client.initializeIdentity(); err != nil { - fmt.Printf("Identity initialization failed: %v\n", err) - fmt.Printf("Continuing with temporary identity\n\n") - } - - client.createMainWindow() - client.enableAutoReconnect() - client.window.ShowAndRun() -} - -func (c *ChatClient) initializeIdentity() error { - homeDir, err := os.UserHomeDir() - if err != nil { - return err - } - - keyPath := filepath.Join(homeDir, ".noshitalk", "gui-identity.noshikey") - - if _, err := os.Stat(keyPath); err == nil { - return c.loadIdentity(keyPath) - } - - return c.generateNewIdentity(keyPath) -} - -func (c *ChatClient) generateNewIdentity(keyPath string) error { - fmt.Printf("Generating new identity\n") - - curve := ecdh.X25519() - - signKey, err := curve.GenerateKey(cryptorand.Reader) - if err != nil { - return err - } - - encryptKey, err := curve.GenerateKey(cryptorand.Reader) - if err != nil { - return err - } - - signPubKey := signKey.PublicKey().Bytes() - hash := sha256.Sum256(signPubKey) - fingerprint := hex.EncodeToString(hash[:16]) - - username := fmt.Sprintf("gui_%s", fingerprint[:8]) - - c.signKey = signKey - c.encryptKey = encryptKey - c.fingerprint = fingerprint - c.username = username - - fmt.Printf("Identity: %s\n", fingerprint) - fmt.Printf("Username: %s\n", username) - - return c.saveIdentity(keyPath) -} - -func (c *ChatClient) loadIdentity(keyPath string) error { - fmt.Printf("Loading identity from %s\n", keyPath) - - data, err := os.ReadFile(keyPath) - if err != nil { - return err - } - - var identity IdentityFile - if err := json.Unmarshal(data, &identity); err != nil { - return err - } - - curve := ecdh.X25519() - - signPrivBytes, err := hex.DecodeString(identity.SignKeyPair.PrivateKey) - if err != nil { - return err - } - - encryptPrivBytes, err := hex.DecodeString(identity.EncryptKeyPair.PrivateKey) - if err != nil { - return err - } - - signKey, err := curve.NewPrivateKey(signPrivBytes) - if err != nil { - return err - } - - encryptKey, err := curve.NewPrivateKey(encryptPrivBytes) - if err != nil { - return err - } - - c.signKey = signKey - c.encryptKey = encryptKey - c.fingerprint = identity.Fingerprint - c.username = identity.Username - - fmt.Printf("Identity loaded: %s\n", identity.Fingerprint) - fmt.Printf("Username: %s\n\n", identity.Username) - - return nil -} - -func (c *ChatClient) saveIdentity(keyPath string) error { - dir := filepath.Dir(keyPath) - if err := os.MkdirAll(dir, 0700); err != nil { - return err - } - - identity := IdentityFile{ - Version: "1.0", - Username: c.username, - Fingerprint: c.fingerprint, - SignKeyPair: KeyPair{ - PrivateKey: hex.EncodeToString(c.signKey.Bytes()), - PublicKey: hex.EncodeToString(c.signKey.PublicKey().Bytes()), - }, - EncryptKeyPair: KeyPair{ - PrivateKey: hex.EncodeToString(c.encryptKey.Bytes()), - PublicKey: hex.EncodeToString(c.encryptKey.PublicKey().Bytes()), - }, - } - - data, err := json.MarshalIndent(identity, "", " ") - if err != nil { - return err - } - - if err := os.WriteFile(keyPath, data, 0600); err != nil { - return err - } - - fmt.Printf("Identity saved to %s\n\n", keyPath) - return nil -} - -func (c *ChatClient) createMainWindow() { - c.window = c.app.NewWindow(fmt.Sprintf("NoshiTalk GUI v%s", version)) - c.window.SetIcon(theme.ComputerIcon()) - c.window.Resize(fyne.NewSize(1200, 800)) - - c.status = widget.NewLabel("Disconnected") - c.status.TextStyle.Bold = true - - c.serverEntry = widget.NewEntry() - c.serverEntry.SetText("") - c.serverEntry.SetPlaceHolder("abc...xyz.onion:8080") - - c.connectBtn = widget.NewButton("Connect", c.connectToServer) - c.connectBtn.Importance = widget.HighImportance - - c.messages = widget.NewList( - func() int { - return len(c.messageList) - }, - func() fyne.CanvasObject { - return widget.NewLabel("Template") - }, - func(i widget.ListItemID, o fyne.CanvasObject) { - o.(*widget.Label).SetText(c.messageList[i]) - }, - ) - - c.userList = widget.NewList( - func() int { - return len(c.onlineUsers) - }, - func() fyne.CanvasObject { - return widget.NewButton("", nil) - }, - func(i widget.ListItemID, o fyne.CanvasObject) { - if i < len(c.onlineUsers) { - btn := o.(*widget.Button) - username := c.onlineUsers[i] - btn.SetText(username) - btn.OnTapped = func() { - c.onUserClick(username) - } - } - }, - ) - - c.input = widget.NewEntry() - c.input.SetPlaceHolder("Message...") - c.input.Disable() - c.input.OnSubmitted = c.sendMessage - - autoReconnectCheck := widget.NewCheck("Auto-reconnect", func(checked bool) { - c.autoReconnect = checked - }) - - panicBtn := widget.NewButton("PANIC", c.panic) - panicBtn.Importance = widget.DangerImportance - - identityInfo := widget.NewLabel("Identity: Loading...") - if c.fingerprint != "" { - identityInfo.SetText(fmt.Sprintf("ID: %s", c.fingerprint[:16])) - } - - securityPanel := widget.NewCard("Security", "", - container.NewVBox( - widget.NewLabel("Tor only\nE2E encrypted\nEphemeral messages\nmemguard protected"), - autoReconnectCheck, - identityInfo, - panicBtn, - )) - - c.debugLog = widget.NewEntry() - c.debugLog.MultiLine = true - c.debugLog.Wrapping = fyne.TextWrapWord - c.debugLog.SetText("Ready\n") - - autoReconnectCheck.SetChecked(true) - - debugScroll := container.NewScroll(c.debugLog) - debugScroll.SetMinSize(fyne.NewSize(350, 300)) - - debugPanel := widget.NewCard("Debug", "", debugScroll) - - clearLogBtn := widget.NewButton("Clear", func() { - c.debugLog.SetText("Log cleared\n") - }) - - rightPanel := container.NewVBox( - securityPanel, - debugPanel, - clearLogBtn, - ) - - connectionPanel := container.NewVBox( - widget.NewLabel("Server:"), - c.serverEntry, - c.connectBtn, - c.status, - ) - - userListScroll := container.NewScroll(c.userList) - userListScroll.SetMinSize(fyne.NewSize(200, 0)) - - userListPanel := container.NewBorder( - widget.NewCard("Online", "", widget.NewLabel("")), - nil, nil, nil, - userListScroll, - ) - - leftSplit := container.NewHSplit(userListPanel, c.messages) - leftSplit.SetOffset(0.20) - - chatArea := container.NewHSplit(leftSplit, rightPanel) - chatArea.SetOffset(0.70) - - bottomBar := container.NewBorder( - nil, nil, - widget.NewLabel("Input: "), - widget.NewButton("Send", func() { c.sendMessage(c.input.Text) }), - c.input, - ) - - content := container.NewBorder( - connectionPanel, - bottomBar, - nil, nil, - chatArea, - ) - - c.window.SetContent(content) -} - -func (c *ChatClient) enableAutoReconnect() { - go func() { - lastAttempt := time.Now() - for { - time.Sleep(5 * time.Second) - if !c.connected && c.autoReconnect && c.lastServer != "" { - if time.Since(lastAttempt) < 10*time.Second { - continue - } - lastAttempt = time.Now() - - c.addDebugLog("Auto-reconnecting to " + c.lastServer) - fyne.Do(func() { - c.serverEntry.SetText(c.lastServer) - c.connectToServer() - }) - } - } - }() -} - -func validateOnionAddress(addr string) error { - if addr == "" { - return fmt.Errorf("empty address") - } - - if !strings.Contains(addr, ".onion") { - return fmt.Errorf("only .onion addresses allowed") - } - - if !onionRegex.MatchString(addr) { - return fmt.Errorf("invalid .onion format") - } - - return nil -} - -func (c *ChatClient) connectToServer() { - if c.connected { - c.disconnect() - return - } - - serverAddr := c.serverEntry.Text - if serverAddr == "" { - c.showError("Error", "Enter .onion address") - return - } - - if err := validateOnionAddress(serverAddr); err != nil { - c.showError("Invalid Address", err.Error()) - return - } - - c.lastServer = serverAddr - fyne.Do(func() { - c.debugLog.SetText("Connecting\n") - }) - c.addDebugLog("Target: " + serverAddr) - - fyne.Do(func() { - c.status.SetText("Connecting...") - c.connectBtn.Disable() - }) - - progress := dialog.NewCustom("Connecting", "Cancel", - widget.NewProgressBarInfinite(), c.window) - progress.Show() - - go func() { - defer fyne.Do(func() { progress.Hide() }) - - conn, err := c.connectThroughTor(serverAddr) - if err != nil { - c.showError("Connection Error", err.Error()) - c.addDebugLog(fmt.Sprintf("Connection failed: %v", err)) - fyne.Do(func() { - c.connectBtn.Enable() - c.status.SetText("Connection failed") - }) - return - } - - c.conn = conn - fyne.Do(func() { - c.status.SetText("Encrypting...") - }) - c.addDebugLog("Starting ECDH") - - curve := ecdh.X25519() - - var encryptKey *ecdh.PrivateKey - if c.encryptKey != nil { - encryptKey = c.encryptKey - c.addDebugLog("Using persistent identity: " + c.fingerprint) - } else { - var err error - encryptKey, err = curve.GenerateKey(cryptorand.Reader) - if err != nil { - c.showError("Crypto Error", err.Error()) - c.disconnect() - return - } - c.addDebugLog("Using temporary identity") - } - - encryptKeyBuffer := memguard.NewBufferFromBytes(encryptKey.Bytes()) - defer encryptKeyBuffer.Destroy() - - publicKey := encryptKey.PublicKey() - publicKeyBytes := publicKey.Bytes() - c.addDebugLog("Sending public key") - - c.conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) - if _, err := c.conn.Write(publicKeyBytes); err != nil { - c.showError("Connection Error", err.Error()) - c.disconnect() - return - } - c.conn.SetWriteDeadline(time.Time{}) - - c.addDebugLog("Receiving server key") - serverPublicKeyBytes := make([]byte, 32) - if _, err := io.ReadFull(conn, serverPublicKeyBytes); err != nil { - c.showError("Connection Error", err.Error()) - c.disconnect() - return - } - - serverPublicKey, err := curve.NewPublicKey(serverPublicKeyBytes) - if err != nil { - c.showError("Crypto Error", err.Error()) - c.disconnect() - return - } - - c.addDebugLog("Calculating shared secret") - - sharedSecret, err := encryptKey.ECDH(serverPublicKey) - if err != nil { - c.showError("Crypto Error", err.Error()) - c.disconnect() - return - } - - c.addDebugLog("Starting HMAC auth") - if err := c.performMutualAuth(sharedSecret); err != nil { - c.showError("Auth Error", err.Error()) - c.disconnect() - return - } - c.addDebugLog("Auth successful") - - c.addDebugLog("Setting up AES-GCM") - block, err := aes.NewCipher(sharedSecret[:32]) - if err != nil { - c.showError("Crypto Error", err.Error()) - c.disconnect() - return - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - c.showError("Crypto Error", err.Error()) - c.disconnect() - return - } - - sharedSecretBuffer := memguard.NewBufferFromBytes(sharedSecret) - c.sharedSecret = sharedSecretBuffer.Seal() - sharedSecretBuffer.Destroy() - - c.gcm = gcm - c.connected = true - - fyne.Do(func() { - c.input.Enable() - c.connectBtn.SetText("Disconnect") - c.connectBtn.OnTapped = c.disconnect - c.connectBtn.Enable() - c.status.SetText("Connected (E2E)") - }) - - c.addMessage("System", "Connected via Tor") - c.addMessage("System", "E2E encryption active") - c.addDebugLog("Session established") - - go c.receiveMessages() - }() -} - -func (c *ChatClient) connectThroughTor(serverAddr string) (net.Conn, error) { - c.addDebugLog("Starting Tor connection") - - var conn net.Conn - proxyURLs := []string{ - "socks5://127.0.0.1:9050", - "socks5://localhost:9050", - } - - for _, proxyURL := range proxyURLs { - torProxyUrl, _ := url.Parse(proxyURL) - if torProxyUrl == nil { - continue - } - - baseDialer := &net.Dialer{ - Timeout: 90 * time.Second, - KeepAlive: 30 * time.Second, - } - - dialer, dialErr := proxy.FromURL(torProxyUrl, baseDialer) - if dialErr != nil { - continue - } - - c.addDebugLog("Connecting via " + proxyURL) - - var connErr error - conn, connErr = dialer.Dial("tcp", serverAddr) - if connErr != nil { - c.addDebugLog("Failed: " + connErr.Error()) - continue - } else { - c.addDebugLog("Connected via " + proxyURL) - break - } - } - - if conn == nil { - return nil, fmt.Errorf("Tor connection failed") - } - - c.addDebugLog("TCP established") - return conn, nil -} - -func (c *ChatClient) performMutualAuth(sharedSecret []byte) error { - c.conn.SetDeadline(time.Now().Add(30 * time.Second)) - defer c.conn.SetDeadline(time.Time{}) - - serverChallenge := make([]byte, 32) - if _, err := io.ReadFull(c.conn, serverChallenge); err != nil { - return fmt.Errorf("receive challenge failed: %v", err) - } - - h := hmac.New(sha256.New, sharedSecret) - h.Write(serverChallenge) - clientResponse := h.Sum(nil) - - clientChallenge := make([]byte, 32) - if _, err := cryptorand.Read(clientChallenge); err != nil { - return fmt.Errorf("challenge generation failed: %v", err) - } - - if _, err := c.conn.Write(clientResponse); err != nil { - return fmt.Errorf("send response failed: %v", err) - } - if _, err := c.conn.Write(clientChallenge); err != nil { - return fmt.Errorf("send challenge failed: %v", err) - } - - serverResponse := make([]byte, 32) - if _, err := io.ReadFull(c.conn, serverResponse); err != nil { - return fmt.Errorf("receive response failed: %v", err) - } - - h.Reset() - h.Write(clientChallenge) - expectedServerMAC := h.Sum(nil) - - if !hmac.Equal(serverResponse, expectedServerMAC) { - return fmt.Errorf("server authentication failed") - } - - return nil -} - -func (c *ChatClient) addDebugLog(message string) { - timestamp := time.Now().Format("15:04:05") - logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message) - - fyne.Do(func() { - c.debugLog.SetText(c.debugLog.Text + logEntry) - c.debugLog.CursorRow = len(strings.Split(c.debugLog.Text, "\n")) - 1 - }) -} - -func (c *ChatClient) disconnect() { - if !c.connected { - return - } - - c.connected = false - c.addDebugLog("Disconnecting") - - if c.conn != nil { - c.sendEncryptedMessage("/quit") - time.Sleep(100 * time.Millisecond) - c.conn.Close() - c.conn = nil - } - - if c.sharedSecret != nil { - buf, _ := c.sharedSecret.Open() - if buf != nil { - buf.Destroy() - } - c.sharedSecret = nil - } - c.gcm = nil - - fyne.Do(func() { - c.input.Disable() - c.connectBtn.SetText("Connect") - c.connectBtn.OnTapped = c.connectToServer - c.connectBtn.Enable() - c.status.SetText("Disconnected") - }) - - c.addMessage("System", "Disconnected") - c.addDebugLog("Keys wiped") -} - -func (c *ChatClient) panic() { - c.addDebugLog("PANIC MODE") - - if c.conn != nil { - c.conn.Close() - c.conn = nil - } - - if c.sharedSecret != nil { - buf, _ := c.sharedSecret.Open() - if buf != nil { - buf.Destroy() - } - c.sharedSecret = nil - } - - if c.signKey != nil { - c.signKey = nil - } - - if c.encryptKey != nil { - c.encryptKey = nil - } - - c.gcm = nil - c.connected = false - c.fingerprint = "" - c.username = "" - - fyne.Do(func() { - c.input.Disable() - c.connectBtn.SetText("Connect") - c.connectBtn.OnTapped = c.connectToServer - c.connectBtn.Enable() - c.status.SetText("PANIC - All keys destroyed") - }) - - c.addMessage("System", "PANIC: Keys destroyed") - c.addDebugLog("Memory wiped") - - memguard.Purge() -} - -func (c *ChatClient) sendMessage(message string) { - if !c.connected || message == "" { - return - } - - c.addDebugLog("Sending: " + message) - - err := c.sendEncryptedMessage(message) - if err != nil { - c.addDebugLog("Send error: " + err.Error()) - c.showError("Send Error", err.Error()) - return - } - - c.addMessage("You", message) - fyne.Do(func() { - c.input.SetText("") - }) -} - -func padMessage(plaintext []byte) []byte { - currentLen := len(plaintext) - paddedLen := ((currentLen / messageBlockSize) + 1) * messageBlockSize - padLen := paddedLen - currentLen - - result := make([]byte, 2+paddedLen) - binary.BigEndian.PutUint16(result[0:2], uint16(currentLen)) - copy(result[2:], plaintext) - - if padLen > 0 { - padding := make([]byte, padLen) - cryptorand.Read(padding) - copy(result[2+currentLen:], padding) - } - - return result -} - -func unpadMessage(paddedData []byte) ([]byte, error) { - if len(paddedData) < 2 { - return nil, fmt.Errorf("data too short") - } - - originalLen := binary.BigEndian.Uint16(paddedData[0:2]) - - if int(originalLen) > len(paddedData)-2 { - return nil, fmt.Errorf("invalid padding") - } - - return paddedData[2 : 2+originalLen], nil -} - -func randomDelay() { - delay := minRandomDelay + rand.Intn(maxRandomDelay-minRandomDelay) - time.Sleep(time.Duration(delay) * time.Millisecond) -} - -func (c *ChatClient) sendEncryptedMessage(message string) error { - if c.gcm == nil || c.conn == nil { - return fmt.Errorf("not connected") - } - - randomDelay() - - paddedMessage := padMessage([]byte(message)) - - nonce := make([]byte, 12) - if _, err := io.ReadFull(cryptorand.Reader, nonce); err != nil { - return fmt.Errorf("nonce generation failed: %v", err) - } - - encrypted := c.gcm.Seal(nil, nonce, paddedMessage, nil) - data := append(nonce, encrypted...) - - _, err := c.conn.Write(data) - return err -} - -func (c *ChatClient) receiveMessages() { - buf := make([]byte, 8192) - - c.addDebugLog("Starting receive loop") - - for c.connected { - c.conn.SetReadDeadline(time.Time{}) - - n, err := c.conn.Read(buf) - if err != nil { - if err == io.EOF { - c.addDebugLog("Server closed connection") - } else { - c.addDebugLog("Read error: " + err.Error()) - } - if c.connected { - c.addMessage("System", "Connection lost") - c.disconnect() - } - return - } - - if n == 0 { - continue - } - - c.addDebugLog(fmt.Sprintf("Received %d bytes", n)) - - message, err := c.decryptMessage(buf[:n]) - if err != nil { - c.addDebugLog("Decrypt error: " + err.Error()) - continue - } - - var userListMsg UserListMessage - if err := json.Unmarshal([]byte(message), &userListMsg); err == nil && userListMsg.Type == "user_list" { - c.onlineUsers = userListMsg.Users - fyne.Do(func() { - c.userList.Refresh() - }) - c.addMessage("System", fmt.Sprintf("Online: %d users", len(userListMsg.Users))) - continue - } - - var msg Message - if err := json.Unmarshal([]byte(message), &msg); err != nil { - c.addMessage("Server", message) - } else { - switch msg.Type { - case "system": - c.addMessage("System", msg.Content) - case "private": - c.addMessage("PM-"+msg.From, msg.Content) - case "error": - c.addMessage("System", "Error: "+msg.Content) - default: - c.addMessage(msg.From, msg.Content) - } - } - } - - c.addDebugLog("Receive loop ended") -} - -func (c *ChatClient) decryptMessage(data []byte) (string, error) { - if len(data) < 12 { - return "", fmt.Errorf("message too short") - } - - nonce := data[:12] - ciphertext := data[12:] - - paddedPlaintext, err := c.gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - return "", fmt.Errorf("decryption failed: %v", err) - } - - plaintext, err := unpadMessage(paddedPlaintext) - if err != nil { - return "", fmt.Errorf("unpad failed: %v", err) - } - - return string(plaintext), nil -} - -func (c *ChatClient) onUserClick(username string) { - fyne.Do(func() { - c.input.SetText("/pm " + username + " ") - c.input.CursorColumn = len(c.input.Text) - c.window.Canvas().Focus(c.input) - }) - c.addDebugLog("Ready to PM " + username) -} - -func (c *ChatClient) addMessage(sender, message string) { - timestamp := time.Now().Format("15:04:05") - formatted := fmt.Sprintf("[%s] %s: %s", timestamp, sender, message) - - c.messageList = append(c.messageList, formatted) - fyne.Do(func() { - c.messages.Refresh() - c.messages.ScrollToBottom() - }) -} - -func (c *ChatClient) showError(title, message string) { - fyne.Do(func() { - dialog.ShowError(fmt.Errorf(message), c.window) - }) - c.addMessage("System", title+": "+message) - c.addDebugLog(title + ": " + message) -} - -func init() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - go func() { - <-sigChan - fmt.Printf("\nShutting down\n") - memguard.Purge() - os.Exit(0) - }() -} |
