fix: modernize onboarding group settings with unified modes (#296)

This commit is contained in:
Cameron
2026-02-13 17:59:40 -08:00
committed by GitHub
parent c083638be1
commit 6ef987a04f

View File

@@ -37,29 +37,62 @@ export function getChannelHint(id: ChannelId): string {
return getChannelMeta(id).hint; return getChannelMeta(id).hint;
} }
// ============================================================================
// Group ID hints per channel
// ============================================================================
const GROUP_ID_HINTS: Record<ChannelId, string> = {
telegram:
'Group IDs are negative numbers (e.g., -1001234567890).\n' +
'Forward a group message to @userinfobot, or check bot logs.',
discord:
'Enable Developer Mode in Settings > Advanced,\n' +
'then right-click a channel/server > Copy ID.',
slack:
'Right-click channel > Copy link > extract ID,\n' +
'or Channel Details > Copy Channel ID (e.g., C0123456789).',
whatsapp:
'Group JIDs appear in bot logs on first message\n' +
'(e.g., 120363123456@g.us).',
signal:
'Group IDs appear in bot logs on first group message.',
};
// ============================================================================ // ============================================================================
// Setup Functions // Setup Functions
// ============================================================================ // ============================================================================
function parseIdList(input?: string | null): string[] | undefined { type GroupMode = 'open' | 'listen' | 'mention-only' | 'disabled';
if (!input) return undefined;
const ids = input.split(',').map(s => s.trim()).filter(Boolean); /**
return ids.length > 0 ? ids : undefined; * Derive the initial group mode from existing config.
* Reads modern groups config first, falls back to deprecated fields.
*/
function deriveExistingMode(existing?: any): GroupMode | undefined {
// Modern: groups.*.mode
const wildcardMode = existing?.groups?.['*']?.mode;
if (wildcardMode) return wildcardMode as GroupMode;
// Deprecated: listeningGroups implies "listen" was the intent
if (existing?.listeningGroups?.length > 0) return 'listen';
return undefined;
} }
async function promptGroupSettings(existing?: any): Promise<{ async function promptGroupSettings(
channelId: ChannelId,
existing?: any,
): Promise<{
groups?: Record<string, { mode: GroupMode }>;
groupDebounceSec?: number; groupDebounceSec?: number;
groupPollIntervalMin?: number;
instantGroups?: string[];
listeningGroups?: string[];
}> { }> {
const hasExisting = existing?.groupDebounceSec !== undefined const existingMode = deriveExistingMode(existing);
|| existing?.groupPollIntervalMin !== undefined const hasExisting = existingMode !== undefined
|| (existing?.instantGroups && existing.instantGroups.length > 0) || existing?.groupDebounceSec !== undefined
|| (existing?.listeningGroups && existing.listeningGroups.length > 0); || (existing?.groups && Object.keys(existing.groups).length > 0);
const configure = await p.confirm({ const configure = await p.confirm({
message: 'Configure group settings?', message: 'Configure group chat settings?',
initialValue: hasExisting, initialValue: hasExisting,
}); });
if (p.isCancel(configure)) { if (p.isCancel(configure)) {
@@ -68,58 +101,81 @@ async function promptGroupSettings(existing?: any): Promise<{
} }
if (!configure) { if (!configure) {
// Preserve existing config as-is
return { return {
groups: existing?.groups,
groupDebounceSec: existing?.groupDebounceSec, groupDebounceSec: existing?.groupDebounceSec,
groupPollIntervalMin: existing?.groupPollIntervalMin,
instantGroups: existing?.instantGroups,
listeningGroups: existing?.listeningGroups,
}; };
} }
const debounceRaw = await p.text({ // Step 1: Default group mode
message: 'Group debounce seconds (blank = default)', const mode = await p.select({
placeholder: '5', message: 'Default group behavior',
initialValue: existing?.groupDebounceSec !== undefined ? String(existing.groupDebounceSec) : '', options: [
validate: (value) => { { value: 'mention-only', label: 'Mention-only (recommended)', hint: 'Only respond when @mentioned' },
const trimmed = value.trim(); { value: 'listen', label: 'Listen', hint: 'Read all messages, only respond when mentioned' },
if (!trimmed) return undefined; { value: 'open', label: 'Open', hint: 'Respond to all group messages' },
const num = Number(trimmed); { value: 'disabled', label: 'Disabled', hint: 'Ignore all group messages' },
if (!Number.isFinite(num) || num < 0) return 'Enter a non-negative number or leave blank'; ],
return undefined; initialValue: existingMode || 'mention-only',
},
}); });
if (p.isCancel(debounceRaw)) { if (p.isCancel(mode)) {
p.cancel('Cancelled'); p.cancel('Cancelled');
process.exit(0); process.exit(0);
} }
const instantRaw = await p.text({ // Step 2: Debounce (skip for disabled)
message: 'Instant group IDs (comma-separated, optional)', let groupDebounceSec: number | undefined = existing?.groupDebounceSec;
placeholder: '123,456', if (mode !== 'disabled') {
initialValue: Array.isArray(existing?.instantGroups) ? existing.instantGroups.join(',') : '', const debounceRaw = await p.text({
}); message: 'Group debounce seconds (blank = 5s default)',
if (p.isCancel(instantRaw)) { placeholder: '5',
p.cancel('Cancelled'); initialValue: existing?.groupDebounceSec !== undefined ? String(existing.groupDebounceSec) : '',
process.exit(0); validate: (value) => {
const trimmed = value.trim();
if (!trimmed) return undefined;
const num = Number(trimmed);
if (!Number.isFinite(num) || num < 0) return 'Enter a non-negative number or leave blank';
return undefined;
},
});
if (p.isCancel(debounceRaw)) {
p.cancel('Cancelled');
process.exit(0);
}
const debounceValue = typeof debounceRaw === 'string' ? debounceRaw.trim() : '';
groupDebounceSec = debounceValue ? Number(debounceValue) : undefined;
} }
const listeningRaw = await p.text({ // Step 3: Channel-specific hint for finding group IDs
message: 'Listening group IDs (comma-separated, optional)', const hint = GROUP_ID_HINTS[channelId];
placeholder: '123,456', if (hint && mode !== 'disabled') {
initialValue: Array.isArray(existing?.listeningGroups) ? existing.listeningGroups.join(',') : '', p.note(
}); hint + '\n\n' +
if (p.isCancel(listeningRaw)) { 'Tip: Start with this default and check logs for IDs.\n' +
p.cancel('Cancelled'); 'Add per-group overrides in lettabot.yaml later.',
process.exit(0); 'Finding Group IDs'
);
} }
const debounceValue = debounceRaw?.trim() || ''; // Build groups config: set wildcard default, preserve any existing per-group overrides
const groups: Record<string, any> = {};
// Carry over existing per-group entries (non-wildcard)
if (existing?.groups) {
for (const [key, value] of Object.entries(existing.groups)) {
if (key !== '*') {
groups[key] = value;
}
}
}
// Set the wildcard default
groups['*'] = { mode: mode as GroupMode };
return { return {
groupDebounceSec: debounceValue ? Number(debounceValue) : undefined, groups,
groupPollIntervalMin: existing?.groupPollIntervalMin, groupDebounceSec,
instantGroups: parseIdList(instantRaw),
listeningGroups: parseIdList(listeningRaw),
}; };
} }
@@ -172,7 +228,7 @@ export async function setupTelegram(existing?: any): Promise<any> {
} }
} }
const groupSettings = await promptGroupSettings(existing); const groupSettings = await promptGroupSettings('telegram', existing);
return { return {
enabled: true, enabled: true,
@@ -216,7 +272,7 @@ export async function setupSlack(existing?: any): Promise<any> {
}); });
if (result) { if (result) {
const groupSettings = await promptGroupSettings(existing); const groupSettings = await promptGroupSettings('slack', existing);
return { return {
enabled: true, enabled: true,
appToken: result.appToken, appToken: result.appToken,
@@ -266,7 +322,7 @@ export async function setupSlack(existing?: any): Promise<any> {
} }
const allowedUsers = await stepAccessControl(existing?.allowedUsers); const allowedUsers = await stepAccessControl(existing?.allowedUsers);
const groupSettings = await promptGroupSettings(existing); const groupSettings = await promptGroupSettings('slack', existing);
return { return {
enabled: true, enabled: true,
@@ -345,7 +401,7 @@ export async function setupDiscord(existing?: any): Promise<any> {
} }
} }
const groupSettings = await promptGroupSettings(existing); const groupSettings = await promptGroupSettings('discord', existing);
return { return {
enabled: true, enabled: true,
@@ -405,7 +461,7 @@ export async function setupWhatsApp(existing?: any): Promise<any> {
} }
} }
const groupSettings = await promptGroupSettings(existing); const groupSettings = await promptGroupSettings('whatsapp', existing);
p.log.info('Run "lettabot server" to see the QR code and complete pairing.'); p.log.info('Run "lettabot server" to see the QR code and complete pairing.');
@@ -494,7 +550,7 @@ export async function setupSignal(existing?: any): Promise<any> {
} }
} }
const groupSettings = await promptGroupSettings(existing); const groupSettings = await promptGroupSettings('signal', existing);
return { return {
enabled: true, enabled: true,