diff --git a/README.md b/README.md index e221a92..5a8cf2e 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ Mu is a collection of apps for everyday use. While other platforms monetize your - **Home** - Your personalized dashboard - **Blog** - Thoughtful microblogging -- **Chat** - Discuss topics with AI +- **Chat** - Discuss topics with AI (Web) or federated XMPP chat - **News** - RSS feeds with AI summaries - **Video** - Watch YouTube without ads -- **Mail** - Private messaging & email +- **Mail** - Private messaging & email with SMTP - **Wallet** - Credits and crypto payments Mu runs as a single Go binary on your own server or use the hosted version at [mu.xyz](https://mu.xyz). @@ -31,7 +31,9 @@ Mu runs as a single Go binary on your own server or use the hosted version at [m - [x] Chat - Discussion rooms - [x] News - RSS news feed - [x] Video - YouTube search -- [x] Mail - Private messaging +- [x] Mail - Private messaging +- [x] SMTP - Email server for federation +- [x] XMPP - Chat server for federation - [x] Wallet - Crypto payments - [ ] Services - Marketplace, etc @@ -188,6 +190,7 @@ Full documentation is available in the [docs](docs/) folder and at `/docs` on an **Features** - [Messaging](docs/MESSAGING_SYSTEM.md) - Email and messaging setup +- [XMPP Chat](docs/XMPP_CHAT.md) - Federated chat with XMPP - [Wallet & Credits](docs/WALLET_AND_CREDITS.md) - Credit system for metered usage **Reference** diff --git a/app/status.go b/app/status.go index ec90bf5..9ef166d 100644 --- a/app/status.go +++ b/app/status.go @@ -65,6 +65,9 @@ type MemoryStatus struct { // DKIMStatusFunc is set by main to avoid import cycle var DKIMStatusFunc func() (enabled bool, domain, selector string) +// XMPPStatusFunc is set by main to avoid import cycle +var XMPPStatusFunc func() map[string]interface{} + // StatusHandler handles the /status endpoint func StatusHandler(w http.ResponseWriter, r *http.Request) { // Quick health check endpoint @@ -121,6 +124,31 @@ func buildStatus() StatusResponse { Details: fmt.Sprintf("Port %s", smtpPort), }) + // Check XMPP server + if XMPPStatusFunc != nil { + xmppStatus := XMPPStatusFunc() + enabled, ok := xmppStatus["enabled"].(bool) + if !ok { + enabled = false + } + details := "Not enabled" + if enabled { + domain, domainOk := xmppStatus["domain"].(string) + port, portOk := xmppStatus["port"].(string) + sessions, sessionsOk := xmppStatus["sessions"].(int) + if domainOk && portOk && sessionsOk { + details = fmt.Sprintf("%s:%s (%d sessions)", domain, port, sessions) + } else { + details = "Configuration error" + } + } + services = append(services, StatusCheck{ + Name: "XMPP Server", + Status: enabled, + Details: details, + }) + } + // Check LLM provider llmProvider, llmConfigured := checkLLMConfig() services = append(services, StatusCheck{ diff --git a/chat/chat.go b/chat/chat.go index 222b6be..069abd0 100644 --- a/chat/chat.go +++ b/chat/chat.go @@ -105,6 +105,39 @@ type Client struct { var rooms = make(map[string]*Room) var roomsMutex sync.RWMutex +// ChatMessage represents a direct message between users +type ChatMessage struct { + ID string `json:"id"` + From string `json:"from"` // Sender username + FromID string `json:"from_id"` // Sender account ID + To string `json:"to"` // Recipient username + ToID string `json:"to_id"` // Recipient account ID + Body string `json:"body"` + Read bool `json:"read"` + ReplyTo string `json:"reply_to"` // ID of message this is replying to + ThreadID string `json:"thread_id"` // Root message ID for O(1) thread grouping + CreatedAt time.Time `json:"created_at"` +} + +// ChatThread represents a conversation thread +type ChatThread struct { + Root *ChatMessage + Messages []*ChatMessage + Latest *ChatMessage + HasUnread bool +} + +// ChatInbox organizes messages by thread for a user +type ChatInbox struct { + Threads map[string]*ChatThread // threadID -> Thread + UnreadCount int // Cached unread message count +} + +// stored direct messages +var chatMessages []*ChatMessage +var chatInboxes map[string]*ChatInbox +var chatMessagesMutex sync.RWMutex + // saveRoomMessages persists room messages to disk func saveRoomMessages(roomID string, messages []RoomMessage) { filename := "room_" + strings.ReplaceAll(roomID, "/", "_") + ".json" @@ -1006,6 +1039,9 @@ func Load() { } } + // Load chat messages + loadChatMessages() + // Subscribe to summary generation requests summaryRequestSub := data.Subscribe(data.EventGenerateSummary) go func() { @@ -1217,6 +1253,14 @@ func generateSummaries() { } func Handler(w http.ResponseWriter, r *http.Request) { + // Check mode parameter - "messages" for direct messaging, default is AI chat + mode := r.URL.Query().Get("mode") + + if mode == "messages" { + handleMessagesMode(w, r) + return + } + // Check if this is a room-based chat (e.g., /chat?id=post_123) roomID := r.URL.Query().Get("id") @@ -1331,6 +1375,13 @@ func handleGetChat(w http.ResponseWriter, r *http.Request, roomID string) { roomJSON, _ := json.Marshal(roomData) tmpl := app.RenderHTMLForRequest("Chat", "Chat with AI", fmt.Sprintf(Template, topicTabs), r) + + // Add a link to messages mode + messagesLink := `
💬 New: Direct Messaging - Send messages to other users or chat with @micro (AI assistant)
+