apps: support bot relocate

This commit is contained in:
Narasimha-sc
2026-02-12 12:44:53 +02:00
parent d590df4629
commit 0d290eb7b0
16 changed files with 222 additions and 1491 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,143 @@
# SimpleX Chat — Context for AI Assistant
## What is SimpleX Chat?
SimpleX Chat is a private and secure messaging platform. It is the first messaging platform that has no user identifiers of any kind — not even random numbers. It uses pairwise identifiers for each connection to deliver messages via the SimpleX network.
### Core Privacy Guarantees
- **No user identifiers**: No phone numbers, usernames, or account IDs. Users connect via one-time invitation links or QR codes.
- **End-to-end encryption**: All messages use double ratchet protocol with post-quantum key exchange (ML-KEM). Even if encryption keys are compromised in the future, past messages remain secure.
- **No metadata access**: Relay servers cannot correlate senders and receivers — each conversation uses separate unidirectional messaging queues with different addresses on each side.
- **Decentralized**: No central server. Messages are relayed through SMP (SimpleX Messaging Protocol) servers. Users can choose or self-host their own servers.
- **Open source**: All client and server code is available on GitHub under AGPL-3.0 license. The protocol design is published and peer-reviewed.
- **No global identity**: There is no way to discover users on the platform — you can only connect to someone if they share a link or QR code with you.
## Available Platforms
- **Mobile**: iOS (App Store), Android (Google Play, F-Droid, APK)
- **Desktop**: macOS, Windows, Linux (AppImage, deb, Flatpak)
- All platforms support the same features and can be used simultaneously with linked devices
## Key Features
### Messaging
- Text messages with markdown formatting
- Voice messages
- Images and videos
- File sharing (any file type, up to 1GB via XFTP)
- Message reactions and replies
- Message editing and deletion
- Disappearing messages (configurable per contact/group)
- Live messages (recipient sees you typing in real-time)
- Message delivery receipts
### Calls
- End-to-end encrypted audio and video calls
- Calls work peer-to-peer when possible, relayed through TURN servers otherwise
- WebRTC-based
### Groups
- Group chats with roles: owner, admin, moderator, member, observer
- Groups can have hundreds of members
- Group links for easy joining
- Group moderation tools
- Business chat groups for customer support
### Privacy Features
- **Incognito mode**: Use a random profile name per contact — your real profile is never shared
- **Multiple chat profiles**: Maintain separate identities
- **Hidden profiles**: Protect profiles with a password
- **Contact verification**: Verify contacts via security code comparison
- **SimpleX Lock**: App lock with passcode or biometric
- **Private routing**: Route messages through multiple servers to hide your IP from destination servers
- **No tracking or analytics**: The app does not collect or send any telemetry
### Device & Data Management
- **Database export/import**: Migrate to a new device by exporting the database (encrypted or unencrypted)
- **Database passphrase**: Encrypt the local database with a passphrase
- **Linked devices**: Use SimpleX on multiple devices simultaneously (mobile + desktop)
- **Chat archive**: Export and import full chat history
## SimpleX Network Architecture
### SMP (SimpleX Messaging Protocol)
- Asynchronous message delivery via relay servers
- Each conversation uses **separate unidirectional messaging queues**
- Queues have different addresses on sender and receiver sides — servers cannot correlate them
- Messages are end-to-end encrypted; servers only see encrypted blobs
- Servers do not store any user profiles or contact lists
- Messages are deleted from servers once delivered
### XFTP (SimpleX File Transfer Protocol)
- Used for large files (images, videos, documents)
- Files are encrypted, split into chunks, and sent through multiple relay servers
- Temporary file storage — files are deleted after download or expiry
### Server Architecture
- **Preset servers**: SimpleX Chat Inc. operates preset relay servers, but they can be changed
- **Self-hosting**: Users can run their own SMP and XFTP servers
- **No federation**: Servers don't communicate with each other. Each message queue is independent
- **Tor support**: SimpleX supports connecting through Tor for additional IP privacy
## Comparison with Other Messengers
### vs Signal
- SimpleX requires no phone number or any identifier to register
- SimpleX is decentralized — Signal has a central server
- SimpleX relay servers cannot access metadata (who talks to whom) — Signal's server knows your contacts
- Both use strong end-to-end encryption
### vs Telegram
- SimpleX is fully end-to-end encrypted for all chats — Telegram only encrypts "secret chats"
- SimpleX has no phone number requirement
- SimpleX is fully open source (clients and servers) — Telegram server is closed source
- SimpleX collects no metadata
### vs Matrix/Element
- SimpleX has better metadata privacy — Matrix servers see who is in which room
- SimpleX is simpler to use — no server selection or account creation
- SimpleX does not use federated identity
### vs Session
- SimpleX doesn't use a blockchain or cryptocurrency
- SimpleX has better group support and more features
- Both have no phone number requirement
## Common User Questions & Troubleshooting
### Getting Started
- **How do I add contacts?** Create a one-time invitation link (or QR code) and share it with your contact. They open it in their SimpleX app to connect. Links are single-use by default for maximum privacy, but you can create reusable address links.
- **Can I use SimpleX without a phone number?** Yes, SimpleX requires no phone number, email, or any identifier. Just install the app and start chatting.
- **How do I join a group?** Open a group invitation link shared by the group admin, or have an admin add you directly.
### Device Migration
- **How do I move to a new phone?** Go to Settings > Database > Export database. Transfer the file to your new device, install SimpleX, and import the database. Note: you should stop using the old device after export to avoid message duplication.
- **Can I use SimpleX on multiple devices?** Yes, link a desktop app to your mobile app. Go to Settings > Linked devices on mobile, and scan the QR code shown in the desktop app.
### Privacy & Security
- **Can SimpleX servers read my messages?** No. All messages are end-to-end encrypted. Servers only relay encrypted data and cannot decrypt it.
- **Can SimpleX see who I'm talking to?** No. Each conversation uses separate queues with different addresses. Servers cannot correlate senders and receivers.
- **How do I verify my contact?** Open the contact's profile, tap "Verify security code", and compare the code with your contact (in person or via another channel).
- **What is incognito mode?** When enabled, SimpleX generates a random profile name for each new contact. Your real profile name is never shared. Enable it in Settings > Incognito.
### Servers
- **How do I self-host a server?** Follow the guide at https://simplex.chat/docs/server.html. You need a Linux server with a public IP. Install the SMP server package and configure it.
- **How do I change relay servers?** Go to Settings > Network & servers. You can add your own server addresses and disable preset servers.
- **Do I need to use SimpleX's servers?** No. You can use any SMP/XFTP servers, including your own. However, you and your contacts need to be able to reach each other's servers.
### Troubleshooting
- **Messages not delivering?** Check your internet connection. Try switching between WiFi and mobile data. Go to Settings > Network & servers and check server status. You can also try restarting the app.
- **Cannot connect to a contact?** The invitation link may have expired or already been used. Create a new invitation link and share it again.
- **App is slow?** Large databases can slow down the app. Consider archiving old chats or deleting unused contacts/groups.
- **Notifications not working (Android)?** SimpleX needs to run a background service for notifications. Go to Settings > Notifications and enable background service. You may need to disable battery optimization for the app.
- **Notifications not working (iOS)?** Ensure notifications are enabled in iOS Settings > SimpleX Chat. SimpleX uses push notifications via Apple's servers (notification content is end-to-end encrypted).
## Links
- Website: https://simplex.chat
- GitHub: https://github.com/simplex-chat
- Documentation: https://simplex.chat/docs
- Server setup: https://simplex.chat/docs/server.html
- Protocol whitepaper: https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md
- Security audit: https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html

View File

@@ -112,7 +112,7 @@ type ConversationState =
| {type: "welcome"}
| {type: "teamQueue"; userMessages: string[]}
| {type: "grokMode"; grokMemberGId: number; history: GrokMessage[]}
| {type: "teamPending"; teamMemberGId: number; grokMemberGId?: number; history?: GrokMessage[]}
| {type: "teamPending"; teamMemberGId: number}
| {type: "teamLocked"; teamMemberGId: number}
```
@@ -123,12 +123,11 @@ type ConversationState =
welcome ──(1st user msg)──> teamQueue
teamQueue ──(user msg)──> teamQueue (append to userMessages)
teamQueue ──(/grok)──> grokMode (userMessages → initial Grok history)
teamQueue ──(/team)──> teamPending
teamQueue ──(/team)──> teamPending (add team member)
grokMode ──(user msg)──> grokMode (forward to Grok API, append to history)
grokMode ──(/team)──> teamPending (carry grokMemberGId + history)
teamPending ──(team member msg)──> teamLocked (remove Grok if present)
teamPending ──(/grok, grok present)──> teamPending (forward to Grok, still usable)
teamPending ──(/grok, no grok)──> reply "team mode"
grokMode ──(/team)──> teamPending (remove Grok immediately, add team member)
teamPending ──(team member msg)──> teamLocked
teamPending ──(/grok)──> reply "team mode"
teamLocked ──(/grok)──> reply "team mode", stay locked
teamLocked ──(any)──> no action (team sees directly)
```
@@ -227,11 +226,13 @@ await grokChat.startChat()
|-------|---------|--------|
| `acceptingBusinessRequest` | `onBusinessRequest` | `conversations.set(groupInfo.groupId, {type: "welcome"})` |
| `newChatItems` | `onNewChatItems` | For each chatItem: identify sender, extract text, dispatch to routing |
| `leftMember` | `onLeftMember` | If customer left → delete state. If team member left → revert to teamQueue. If Grok left → clear grokMemberGId. |
| `leftMember` | `onLeftMember` | If customer left → delete state. If team member left → revert to teamQueue. If Grok left during grokMode → revert to teamQueue. |
| `deletedMemberUser` | `onDeletedMemberUser` | Bot removed from group → delete state |
| `groupDeleted` | `onGroupDeleted` | Delete state, delete grokGroupMap entry |
| `connectedToGroupMember` | `onMemberConnected` | Log for debugging |
We do NOT use `onMessage`/`onCommands` from `bot.run()` — all routing is done in the `newChatItems` event handler for full control over state-dependent command handling.
**Sender identification in `newChatItems`:**
```typescript
for (const ci of evt.chatItems) {
@@ -248,10 +249,9 @@ for (const ci of evt.chatItems) {
const sender = chatItem.chatDir.groupMember
const isCustomer = sender.memberId === groupInfo.businessChat.customerId
const isTeamMember = state.type === "teamPending" || state.type === "teamLocked"
? sender.groupMemberId === state.teamMemberGId
: false
const isGrok = (state.type === "grokMode" || state.type === "teamPending")
const isTeamMember = (state.type === "teamPending" || state.type === "teamLocked")
&& sender.groupMemberId === state.teamMemberGId
const isGrok = state.type === "grokMode"
&& state.grokMemberGId === sender.groupMemberId
if (isGrok) continue // skip Grok messages (we sent them via grokChat)
@@ -260,12 +260,18 @@ for (const ci of evt.chatItems) {
}
```
**Text extraction:**
**Command detection** — use `util.ciBotCommand()` for `/grok` and `/team`; all other text (including unrecognized `/commands`) is routed as "other text" per spec ("Unrecognized commands: treated as normal messages in the current mode"):
```typescript
function extractText(chatItem: T.ChatItem): string | null {
const text = util.ciContentText(chatItem)
return text?.trim() || null
}
// In onCustomerMessage:
const cmd = util.ciBotCommand(chatItem)
if (cmd?.keyword === "grok") { /* handle /grok */ }
else if (cmd?.keyword === "team") { /* handle /team */ }
else { /* handle as normal text message, including unrecognized /commands */ }
```
## 9. Message Routing Table
@@ -279,10 +285,9 @@ function extractText(chatItem: T.ChatItem): string | null {
| `teamQueue` | `/team` | Add team member | `mainChat.apiAddMember(groupId, teamContactId, "member")` + `mainChat.apiSendTextMessage([Group, groupId], teamAddedMsg)` | `teamPending` |
| `teamQueue` | other text | Forward to team, append to userMessages | `mainChat.apiSendTextMessage([Group, teamGroupId], fwd)` | `teamQueue` |
| `grokMode` | `/grok` | Ignore (already in grok mode) | — | `grokMode` |
| `grokMode` | `/team` | Add team member (keep Grok for now) | `mainChat.apiAddMember(groupId, teamContactId, "member")` + `mainChat.apiSendTextMessage([Group, groupId], teamAddedMsg)` | `teamPending` (carry grokMemberGId + history) |
| `grokMode` | other text | Forward to Grok API + team | Grok API call + `grokChat.apiSendTextMessage([Group, grokLocalGId], response)` + `mainChat.apiSendTextMessage([Group, teamGroupId], fwd)` | `grokMode` (append history) |
| `teamPending` (grok present) | `/grok` | Forward to Grok (still usable) | Grok API call + `grokChat.apiSendTextMessage(...)` | `teamPending` |
| `teamPending` (no grok) | `/grok` | Reply "team mode" | `mainChat.apiSendTextMessage([Group, groupId], teamLockedMsg)` | `teamPending` |
| `grokMode` | `/team` | Remove Grok, add team member | `mainChat.apiRemoveMembers(groupId, [grokMemberGId])` + `mainChat.apiAddMember(groupId, teamContactId, "member")` + `mainChat.apiSendTextMessage([Group, groupId], teamAddedMsg)` | `teamPending` |
| `grokMode` | other text | Forward to Grok API + forward to team | Grok API call + `grokChat.apiSendTextMessage([Group, grokLocalGId], response)` + `mainChat.apiSendTextMessage([Group, teamGroupId], fwd)` | `grokMode` (append history) |
| `teamPending` | `/grok` | Reply "team mode" | `mainChat.apiSendTextMessage([Group, groupId], teamLockedMsg)` | `teamPending` |
| `teamPending` | `/team` | Ignore (already team) | — | `teamPending` |
| `teamPending` | other text | No forwarding (team sees directly in group) | — | `teamPending` |
| `teamLocked` | `/grok` | Reply "team mode" | `mainChat.apiSendTextMessage([Group, groupId], teamLockedMsg)` | `teamLocked` |
@@ -302,13 +307,18 @@ async forwardToTeam(groupId: number, groupInfo: T.GroupInfo, text: string): Prom
}
async activateTeam(groupId: number, state: ConversationState): Promise<void> {
// Remove Grok immediately if present (per spec: "When switching to team mode, Grok is removed")
if (state.type === "grokMode") {
try { await this.mainChat.apiRemoveMembers(groupId, [state.grokMemberGId]) } catch {}
const grokLocalGId = grokGroupMap.get(groupId)
grokGroupMap.delete(groupId)
if (grokLocalGId) reverseGrokMap.delete(grokLocalGId)
}
const teamContactId = this.config.teamMembers[0].id // round-robin or first available
const member = await this.mainChat.apiAddMember(groupId, teamContactId, "member")
this.conversations.set(groupId, {
type: "teamPending",
teamMemberGId: member.groupMemberId,
grokMemberGId: state.type === "grokMode" ? state.grokMemberGId : undefined,
history: state.type === "grokMode" ? state.history : undefined,
})
await this.mainChat.apiSendTextMessage(
[T.ChatType.Group, groupId],
@@ -358,25 +368,20 @@ class GrokApiClient {
## 12. One-Way Gate Logic
Per spec: "When switching to team mode, Grok is removed" and "once the user switches to team mode, /grok command is permanently disabled." Grok removal happens immediately in `activateTeam` (section 10). The one-way gate locks the state after team member engages:
```typescript
async onTeamMemberMessage(groupId: number, state: ConversationState): Promise<void> {
if (state.type !== "teamPending") return
// Remove Grok if present
if (state.grokMemberGId) {
try { await this.mainChat.apiRemoveMembers(groupId, [state.grokMemberGId]) } catch {}
grokGroupMap.delete(groupId)
reverseGrokMap.delete(/* grokLocalGroupId */)
}
this.conversations.set(groupId, {type: "teamLocked", teamMemberGId: state.teamMemberGId})
}
```
Timeline per spec:
1. User sends `/team``apiAddMember` → state = `teamPending` (Grok still usable if present)
2. Team member sends message → `onTeamMemberMessage` → state = `teamLocked`, Grok removed via `apiRemoveMembers`
3. Any `/grok` → reply "You are now in team mode. A team member will reply to your message."
1. User sends `/team`Grok removed immediately (if present) → team member added → state = `teamPending`
2. `/grok` in `teamPending` → reply "team mode" (Grok already gone, command disabled)
3. Team member sends message → `onTeamMemberMessage` → state = `teamLocked`
4. Any subsequent `/grok` → reply "You are now in team mode. A team member will reply to your message."
## 13. Message Templates (verbatim from spec)
@@ -417,7 +422,7 @@ function isWeekend(timezone: string): boolean {
| # | Operation | When | ChatApi Instance | Method | Parameters | Response Type | Error Handling |
|---|-----------|------|-----------------|--------|------------|---------------|----------------|
| 1 | Init main bot | Startup | mainChat | `bot.run()` (wraps `ChatApi.init`) | dbFilePrefix, profile, addressSettings | `[ChatApi, User, UserContactLink]` | Exit on failure |
| 1 | Init main bot | Startup | mainChat | `bot.run()` (wraps `ChatApi.init`) | dbFilePrefix, profile, addressSettings | `[ChatApi, User, UserContactLink \| undefined]` | Exit on failure |
| 2 | Init Grok agent | Startup | grokChat | `ChatApi.init(grokDbPrefix)` | dbFilePrefix | `ChatApi` | Exit on failure |
| 3 | Get/create Grok user | Startup | grokChat | `apiGetActiveUser()` / `apiCreateActiveUser(profile)` | profile: {displayName: "Grok AI"} | `User` | Exit on failure |
| 4 | Start Grok chat | Startup | grokChat | `startChat()` | — | void | Exit on failure |
@@ -427,12 +432,12 @@ function isWeekend(timezone: string): boolean {
| 8 | First-run: connect | First-run | grokChat | `apiConnectActiveUser(invLink)` | connLink | `ConnReqType` | Exit on failure |
| 9 | First-run: wait | First-run | mainChat | `wait("contactConnected", 60000)` | event, timeout | `ChatEvent \| undefined` | Exit on timeout |
| 10 | Send msg to customer | Various | mainChat | `apiSendTextMessage([Group, groupId], text)` | chat, text | `AChatItem[]` | Log error |
| 11 | Forward to team | welcome→teamQueue, teamQueue msg | mainChat | `apiSendTextMessage([Group, teamGroupId], fwd)` | chat, formatted text | `AChatItem[]` | Log error |
| 11 | Forward to team | welcome→teamQueue, teamQueue msg, grokMode msg | mainChat | `apiSendTextMessage([Group, teamGroupId], fwd)` | chat, formatted text | `AChatItem[]` | Log error |
| 12 | Invite Grok to group | /grok in teamQueue | mainChat | `apiAddMember(groupId, grokContactId, "member")` | groupId, contactId, role | `GroupMember` | Send error msg, stay in teamQueue |
| 13 | Grok joins group | receivedGroupInvitation | grokChat | `apiJoinGroup(groupId)` | groupId | `GroupInfo` | Log error |
| 14 | Grok sends response | After Grok API reply | grokChat | `apiSendTextMessage([Group, grokLocalGId], text)` | chat, text | `AChatItem[]` | Send error msg via mainChat |
| 15 | Invite team member | /team | mainChat | `apiAddMember(groupId, teamContactId, "member")` | groupId, contactId, role | `GroupMember` | Send error msg to customer |
| 16 | Remove Grok | Team member msg in teamPending | mainChat | `apiRemoveMembers(groupId, [grokMemberGId])` | groupId, memberIds | `GroupMember[]` | Ignore (may have left) |
| 16 | Remove Grok | /team from grokMode | mainChat | `apiRemoveMembers(groupId, [grokMemberGId])` | groupId, memberIds | `GroupMember[]` | Ignore (may have left) |
| 17 | Update bot profile | Startup (via bot.run) | mainChat | `apiUpdateProfile(userId, profile)` | userId, profile with peerType+commands | `UserProfileUpdateSummary` | Log warning |
| 18 | Set address settings | Startup (via bot.run) | mainChat | `apiSetAddressSettings(userId, settings)` | userId, {businessAddress, autoAccept, welcomeMessage} | void | Exit on failure |
@@ -448,7 +453,7 @@ function isWeekend(timezone: string): boolean {
| Grok join timeout (30s) | `mainChat.apiSendTextMessage` "Grok unavailable", stay in `teamQueue` |
| Customer leaves (`leftMember` where member is customer) | Delete conversation state, delete grokGroupMap entry |
| Group deleted | Delete conversation state, delete grokGroupMap entry |
| Grok leaves during `teamPending` | Clear `grokMemberGId` from state, keep `teamPending` |
| Grok leaves during `grokMode` | Revert to `teamQueue`, delete grokGroupMap entry |
| Team member leaves | Revert to `teamQueue` (accumulate messages again) |
| Bot removed from group (`deletedMemberUser`) | Delete conversation state |
| Grok agent connection lost | Log error; Grok features unavailable until restart |
@@ -481,10 +486,10 @@ function isWeekend(timezone: string): boolean {
- **Verify:** `/grok` → Grok joins as separate participant → Grok responses appear from Grok profile
**Phase 4: Team mode + one-way gate**
- Implement `activateTeam`: add team member
- Implement `onTeamMemberMessage`: detect team msg → lock → remove Grok
- Implement `/grok` rejection in `teamLocked`
- **Verify:** Full flow including: teamQueue → /grok → grokMode → /team → teamPending → team msg → teamLocked → /grok rejected
- Implement `activateTeam`: remove Grok if present, add team member
- Implement `onTeamMemberMessage`: detect team msg → lock state
- Implement `/grok` rejection in `teamPending` and `teamLocked`
- **Verify:** Full flow: teamQueue → /grok → grokMode → /team → Grok removed + teamPending → /grok rejected → team msg → teamLocked
**Phase 5: Polish + first-run**
- Implement `--first-run` auto-contact establishment
@@ -533,9 +538,9 @@ npx ts-node src/index.ts \
2. Send question → verify forwarded to team group with `[CustomerName #groupId]` prefix, queue reply received
3. Send `/grok` → verify Grok joins as separate participant, responses appear from "Grok AI" profile
4. Send text in grokMode → verify Grok response + forwarded to team
5. Send `/team` → verify team member added, team added message
6. Send team member message → verify Grok removed, state locked
7. Send `/grok` after lock → verify "team mode" reply
5. Send `/team` → verify Grok removed, team member added, team added message
6. Send `/grok` after `/team` (before team member message) → verify "team mode" reply
7. Send team member message → verify state locked, `/grok` still rejected
8. Test weekend: set timezone to weekend timezone → verify "48 hours" in messages
9. Customer disconnects → verify state cleanup
10. Grok API failure → verify error message, graceful fallback to teamQueue

View File

@@ -0,0 +1,34 @@
A SimpleX Chat bot that monitors public groups, summarizes conversations using
Grok LLM, moderates content, and forwards important messages to a private
staff group.
Core Features
1. Message Summarization
- Periodically summarizes public group messages using Grok API
- Posts summaries to the group on a configurable schedule (e.g. daily/hourly)
- Summaries capture key topics, decisions, and action items
2. Moderation
- Detects spam, abuse, and policy violations using Grok
- Configurable actions per severity: flag-only, auto-delete, or remove member
- All moderation events are forwarded to the staff group for visibility
3. Important Message Forwarding
- Grok classifies messages by importance (urgency, issues, support requests)
- Forwards important messages to a designated private staff group
- Includes context: sender, group, timestamp, and reason for flagging
Configuration
- GROK_API_KEY — Grok API credentials
- PUBLIC_GROUPS — list of monitored public groups
- STAFF_GROUP — private group for forwarded alerts
- SUMMARY_INTERVAL — how often summaries are generated
- MODERATION_RULES — content policy and action thresholds
Non-Goals
- No interactive Q&A or general chatbot behavior in groups
- No direct user communication from the bot (all escalation goes to staff
group)