mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-13 17:13:25 +00:00
Don't change username for existing database
This commit is contained in:
@@ -13,7 +13,8 @@ SimpleX Chat support bot — standalone Node.js app using `simplex-chat-nodejs`
|
||||
│ chat: ChatApi ← ChatApi.init("./data/simplex") │
|
||||
│ Single database, two user profiles │
|
||||
│ │
|
||||
│ mainUserId ← "Ask SimpleX Team" profile │
|
||||
│ mainUserId ← non-Grok user (default name: │
|
||||
│ "Ask SimpleX Team") │
|
||||
│ • Business address, event routing, state mgmt │
|
||||
│ • Controls group membership │
|
||||
│ │
|
||||
@@ -27,7 +28,7 @@ SimpleX Chat support bot — standalone Node.js app using `simplex-chat-nodejs`
|
||||
```
|
||||
|
||||
- Single Node.js process, single `ChatApi` instance via native NAPI
|
||||
- Two user profiles in one database — resolved at startup via `apiListUsers()` by display name
|
||||
- Two user profiles in one database — resolved at startup via `apiListUsers()`: the Grok profile by its known display name (`"Grok"`/`"Grok AI"`), the main profile by exclusion (any non-Grok user). The main profile's displayName is set only on fresh-DB user creation (`"Ask SimpleX Team"`) and is never rewritten by bot code thereafter — `bot.run()` is invoked with `updateProfile: false`. Bot commands (`/grok`, `/team`) are the only profile field the bot pushes at runtime, and only when the desired list differs from what's already in the DB.
|
||||
- `profileMutex` serializes `apiSetActiveUser(userId)` + the subsequent SimpleX API call. Grok HTTP API calls run **outside** the mutex.
|
||||
- Events delivered for all profiles — routed by `event.user` field (main → main handler, Grok → Grok handler)
|
||||
- Business address auto-accept creates a group per customer
|
||||
@@ -472,6 +473,7 @@ const [chat, mainUser, mainAddress] = await bot.run({
|
||||
{type: "command", keyword: "team", label: "Switch to team"},
|
||||
],
|
||||
useBotProfile: true,
|
||||
updateProfile: false, // bot code never rewrites displayName/image/etc.
|
||||
},
|
||||
events: {
|
||||
acceptingBusinessRequest: (evt) => supportBot?.onBusinessRequest(evt),
|
||||
@@ -488,7 +490,7 @@ const [chat, mainUser, mainAddress] = await bot.run({
|
||||
})
|
||||
```
|
||||
|
||||
Note: `/grok` and `/team` registered as customer commands via `bot.run()`. `/join` registered as a team group command separately — after team group is resolved, call `apiUpdateGroupProfile(teamGroupId, groupProfile)` with `groupPreferences` including the `/join` command definition. Customer sending `/join` in a customer group → treated as ordinary message (unrecognized command).
|
||||
Note: `/grok` and `/team` are passed in `options.commands` so `bot.run()` can build the desired `profile.preferences.commands`, but since `updateProfile: false` is set, `bot.run()` never writes the profile. Immediately after `bot.run()`, the bot compares `mainUser.profile.preferences.commands` against the desired list; if they differ, it calls `apiUpdateProfile(mainUser.userId, profile)` with `displayName` copied back from `mainUser.profile` (so core's fast path at `src/Simplex/Chat/Store/Profiles.hs:311` fires — `contact_profiles.preferences.commands` is updated, `contact_profiles.display_name` is rewritten to the same value it already had). `/join` is registered as a team group command separately — after team group is resolved, call `apiUpdateGroupProfile(teamGroupId, groupProfile)` with `groupPreferences` including the `/join` command definition. Customer sending `/join` in a customer group → treated as ordinary message (unrecognized command).
|
||||
|
||||
**Grok profile** — resolved from same ChatApi instance:
|
||||
|
||||
@@ -577,7 +579,13 @@ Cleanup: entries in `customDataMutexes` are bounded by the number of customer gr
|
||||
**Profile images:** Both profiles have base64-encoded JPEG profile pictures (128x128, quality 85, under the 12,500-char data URI limit enforced by iOS/Android clients) set via the `image` field in `T.Profile`. The images are defined as `data:image/jpg;base64,...` string constants in `index.ts`. The main profile image is passed to `bot.run()` which handles update-on-change automatically. The Grok profile image is passed to `apiCreateActiveUser()` on first run; on subsequent runs, the bot compares the current profile against the desired one using `util.fromLocalProfile()` and calls `apiUpdateProfile()` if any field differs — this sends the update to all Grok contacts.
|
||||
|
||||
**Startup sequence:**
|
||||
0. **Active user recovery:** On restart, the active user may be Grok (if the previous run was killed mid-profile-switch). `bot.run()` uses `apiGetActiveUser()` and would rename Grok → `duplicateName` error. Fix: pre-init the DB with a temporary `ChatApi`, check active user, if not "Ask SimpleX Team" then `startChat()` + find the main user via `apiListUsers()` + `apiSetActiveUser()`, then `close()`. This ensures `bot.run()` always finds the correct active user.
|
||||
0. **Active user recovery + name preservation:** Two related safeguards.
|
||||
|
||||
**(a) Active user recovery.** On restart, the active user may be Grok (if the previous run was killed mid-profile-switch). `bot.run()` uses `apiGetActiveUser()` and would then operate against Grok's `userId` as if it were the main user. Fix: pre-init the DB with a temporary `ChatApi`, and if the active user's `displayName` is `"Grok"` or `"Grok AI"`, `apiListUsers()` + `apiSetActiveUser()` to the first non-Grok user. Close the temporary `ChatApi` before `bot.run()` reopens it.
|
||||
|
||||
**(b) Never rewrite the main profile.** The core auto-creates a preset contact named `"Ask SimpleX Team"` in every user's DB (`src/Simplex/Chat/Library/Internal.hs:2749`, exact name from commit `362bdc328` 2025-07-12). That collides with the bot's preferred main-profile displayName within the user's `display_names` namespace (`UNIQUE (user_id, local_display_name)`), so any attempt to rename the main profile to `"Ask SimpleX Team"` fails with `duplicateName`. Worse, `bot.run`'s internal `updateBotUserProfile` (`packages/simplex-chat-nodejs/dist/bot.js:176`) re-syncs image, preferences, and `contactLink` on every startup, and on a DB where `users.local_display_name` has drifted from `contact_profiles.display_name`, the fast path (`src/Simplex/Chat/Store/Profiles.hs:311`) silently rewrites the customer-facing `contact_profiles.display_name`. Fix: pass `options.updateProfile: false` to `bot.run()` so the bot code never calls `apiUpdateProfile` on its own initiative. Whatever displayName the CLI saw is what stays.
|
||||
|
||||
**(c) Commands-only push.** The one profile field we *do* want to push from code is the bot's command list (`/grok`, `/team`). After `bot.run()` returns, compare `mainUser.profile.preferences.commands` (via `util.fromLocalProfile`) against the desired list (using `JSON.stringify` equality, same pattern as team-group-commands diff at `src/index.ts:227`). If equal, skip. If different, call `apiUpdateProfile(mainUser.userId, profile)` where `profile` is a shallow copy of `mainUser.profile` with only `preferences.commands` replaced. Because we pass the profile's own current `displayName` back verbatim, core's fast path writes only `contact_profiles` fields (including the new commands) without any rename attempt. All other profile fields (displayName, image, fullName, contactLink, other preference flags) are preserved byte-for-byte.
|
||||
1. `bot.run()` → init ChatApi, create/resolve main profile (with profile image), business address. Print business address link to stdout.
|
||||
2. Resolve Grok profile via `apiListUsers()` (create with profile image if missing; if existing, compare profile and update via `apiUpdateProfile()` if changed — pushes to contacts)
|
||||
3. Read `{dbPrefix}_state.json` for `teamGroupId` and `grokContactId`
|
||||
|
||||
@@ -57,12 +57,15 @@ export class SupportBot {
|
||||
// Bot's business address link
|
||||
businessAddress: string | null = null
|
||||
|
||||
private commandsSynced = false
|
||||
|
||||
constructor(
|
||||
private chat: api.ChatApi,
|
||||
private grokApi: GrokApiClient | null,
|
||||
private config: Config,
|
||||
private mainUserId: number,
|
||||
private grokUserId: number | null,
|
||||
private desiredCommands: T.ChatBotCommand[],
|
||||
) {
|
||||
this.cards = new CardManager(chat, config, mainUserId, config.cardFlushSeconds * 1000)
|
||||
}
|
||||
@@ -93,10 +96,34 @@ export class SupportBot {
|
||||
private async withMainProfile<R>(fn: () => Promise<R>): Promise<R> {
|
||||
return profileMutex.runExclusive(async () => {
|
||||
await this.chat.apiSetActiveUser(this.mainUserId)
|
||||
await this.syncCommands()
|
||||
return fn()
|
||||
})
|
||||
}
|
||||
|
||||
// Push the bot's command list into the main profile if the DB's
|
||||
// preferences.commands doesn't already match. Passing the profile's own
|
||||
// current displayName back keeps core's updateUserProfile on the fast
|
||||
// path (src/Simplex/Chat/Store/Profiles.hs:311) — no rename.
|
||||
private async syncCommands(): Promise<void> {
|
||||
if (this.commandsSynced) return
|
||||
const user = await this.chat.apiGetActiveUser()
|
||||
if (!user) return
|
||||
const current = JSON.stringify(user.profile.preferences?.commands ?? [])
|
||||
const desired = JSON.stringify(this.desiredCommands)
|
||||
if (current === desired) {
|
||||
this.commandsSynced = true
|
||||
return
|
||||
}
|
||||
const profile = util.fromLocalProfile(user.profile)
|
||||
await this.chat.apiUpdateProfile(user.userId, {
|
||||
...profile,
|
||||
preferences: {...(profile.preferences ?? {}), commands: this.desiredCommands},
|
||||
})
|
||||
log(`Bot commands updated (displayName preserved: "${profile.displayName}")`)
|
||||
this.commandsSynced = true
|
||||
}
|
||||
|
||||
private async withGrokProfile<R>(fn: () => Promise<R>): Promise<R> {
|
||||
if (this.grokUserId === null) throw new Error("Grok is disabled (no GROK_API_KEY)")
|
||||
const grokUserId = this.grokUserId
|
||||
|
||||
@@ -40,24 +40,24 @@ async function main(): Promise<void> {
|
||||
let supportBot: SupportBot | undefined
|
||||
|
||||
// On restart, the active user may be Grok (if the previous run was killed
|
||||
// mid-profile-switch). bot.run() uses apiGetActiveUser() and would then try
|
||||
// to rename Grok to "Ask SimpleX Team" → duplicateName error.
|
||||
// Fix: pre-init the DB, find the main user, set it active, then close.
|
||||
// mid-profile-switch). bot.run() uses apiGetActiveUser() and would then
|
||||
// operate against the Grok userId as if it were the main user. Restore the
|
||||
// main user as active before bot.run(). Main user = any user NOT named
|
||||
// "Grok"/"Grok AI".
|
||||
{
|
||||
const preChat = await api.ChatApi.init(config.dbPrefix)
|
||||
const activeUser = await preChat.apiGetActiveUser()
|
||||
if (activeUser && activeUser.profile.displayName !== "Ask SimpleX Team") {
|
||||
const isGrokName = (n: string) => n === "Grok" || n === "Grok AI"
|
||||
if (activeUser && isGrokName(activeUser.profile.displayName)) {
|
||||
await preChat.startChat()
|
||||
const users = await preChat.apiListUsers()
|
||||
const mainUserInfo = users.find(u => u.user.profile.displayName === "Ask SimpleX Team")
|
||||
const mainUserInfo = users.find(u => !isGrokName(u.user.profile.displayName))
|
||||
if (mainUserInfo) {
|
||||
await preChat.apiSetActiveUser(mainUserInfo.user.userId)
|
||||
log("Restored active user to Ask SimpleX Team")
|
||||
log(`Restored active user to ${mainUserInfo.user.profile.displayName}`)
|
||||
}
|
||||
await preChat.close()
|
||||
} else {
|
||||
await preChat.close()
|
||||
}
|
||||
await preChat.close()
|
||||
}
|
||||
|
||||
// Profile images (base64-encoded JPEG)
|
||||
@@ -80,6 +80,7 @@ async function main(): Promise<void> {
|
||||
{type: "command", keyword: "team", label: "Switch to team"},
|
||||
],
|
||||
useBotProfile: true,
|
||||
updateProfile: false,
|
||||
},
|
||||
events: {
|
||||
acceptingBusinessRequest: (evt) => supportBot?.onBusinessRequest(evt),
|
||||
@@ -96,6 +97,41 @@ async function main(): Promise<void> {
|
||||
})
|
||||
log(`Main bot user: ${mainUser.profile.displayName} (userId=${mainUser.userId})`)
|
||||
|
||||
// Explicit commands-only profile update. bot.run() is configured with
|
||||
// updateProfile: false, so it never rewrites the profile on startup —
|
||||
// which in turn preserves displayName, image, etc. exactly as the CLI
|
||||
// sees them. Here we push *only* the commands list, and only when it
|
||||
// differs from what's already in contact_profiles.preferences.commands.
|
||||
// Pass the current displayName back unchanged so core's updateUserProfile
|
||||
// takes the fast path (src/Simplex/Chat/Store/Profiles.hs:311) — which
|
||||
// does not rename.
|
||||
{
|
||||
const desiredCommands: T.ChatBotCommand[] = [
|
||||
...(grokEnabled ? [{type: "command" as const, keyword: "grok", label: "Ask Grok"}] : []),
|
||||
{type: "command", keyword: "team", label: "Switch to team"},
|
||||
]
|
||||
const currentProfile = util.fromLocalProfile(mainUser.profile)
|
||||
const currentCommands = currentProfile.preferences?.commands ?? []
|
||||
if (JSON.stringify(currentCommands) !== JSON.stringify(desiredCommands)) {
|
||||
const newProfile: T.Profile = {
|
||||
...currentProfile,
|
||||
preferences: {
|
||||
...(currentProfile.preferences ?? {}),
|
||||
commands: desiredCommands,
|
||||
},
|
||||
}
|
||||
log(`Bot commands changed, updating (displayName preserved: "${currentProfile.displayName}")...`)
|
||||
const summary = await chat.apiUpdateProfile(mainUser.userId, newProfile)
|
||||
if (summary) {
|
||||
log(`Bot commands updated: ${summary.updateSuccesses} contact(s), ${summary.updateFailures} failed`)
|
||||
} else {
|
||||
log("Bot commands update: no change reported by core")
|
||||
}
|
||||
} else {
|
||||
log("Bot commands unchanged — skipping profile update")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Resolve Grok profile from same ChatApi instance
|
||||
let grokUser: T.User | null = null
|
||||
if (grokEnabled) {
|
||||
|
||||
Reference in New Issue
Block a user