feat: add SESSION_COMMAND_USER_IDS for admin session command access
Allow configured user IDs to execute session commands (/clear etc.) in non-main groups. Adds isAdminSender check alongside isFromMe.
This commit is contained in:
@@ -394,7 +394,12 @@ export async function runAgentProcess(
|
|||||||
stdout += chunk.slice(0, remaining);
|
stdout += chunk.slice(0, remaining);
|
||||||
stdoutTruncated = true;
|
stdoutTruncated = true;
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ group: group.name, chatJid: input.chatJid, runId: input.runId, size: stdout.length },
|
{
|
||||||
|
group: group.name,
|
||||||
|
chatJid: input.chatJid,
|
||||||
|
runId: input.runId,
|
||||||
|
size: stdout.length,
|
||||||
|
},
|
||||||
'Agent stdout truncated due to size limit',
|
'Agent stdout truncated due to size limit',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -424,7 +429,12 @@ export async function runAgentProcess(
|
|||||||
outputChain = outputChain.then(() => onOutput(parsed));
|
outputChain = outputChain.then(() => onOutput(parsed));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ group: group.name, chatJid: input.chatJid, runId: input.runId, error: err },
|
{
|
||||||
|
group: group.name,
|
||||||
|
chatJid: input.chatJid,
|
||||||
|
runId: input.runId,
|
||||||
|
error: err,
|
||||||
|
},
|
||||||
'Failed to parse streamed output chunk',
|
'Failed to parse streamed output chunk',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -440,7 +450,12 @@ export async function runAgentProcess(
|
|||||||
const killOnTimeout = () => {
|
const killOnTimeout = () => {
|
||||||
timedOut = true;
|
timedOut = true;
|
||||||
logger.error(
|
logger.error(
|
||||||
{ group: group.name, chatJid: input.chatJid, runId: input.runId, processName },
|
{
|
||||||
|
group: group.name,
|
||||||
|
chatJid: input.chatJid,
|
||||||
|
runId: input.runId,
|
||||||
|
processName,
|
||||||
|
},
|
||||||
'Agent timeout, sending SIGTERM',
|
'Agent timeout, sending SIGTERM',
|
||||||
);
|
);
|
||||||
proc.kill('SIGTERM');
|
proc.kill('SIGTERM');
|
||||||
@@ -640,7 +655,12 @@ export async function runAgentProcess(
|
|||||||
resolve(output);
|
resolve(output);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{ group: group.name, chatJid: input.chatJid, runId: input.runId, error: err },
|
{
|
||||||
|
group: group.name,
|
||||||
|
chatJid: input.chatJid,
|
||||||
|
runId: input.runId,
|
||||||
|
error: err,
|
||||||
|
},
|
||||||
'Failed to parse agent output',
|
'Failed to parse agent output',
|
||||||
);
|
);
|
||||||
resolve({
|
resolve({
|
||||||
@@ -654,7 +674,13 @@ export async function runAgentProcess(
|
|||||||
proc.on('error', (err) => {
|
proc.on('error', (err) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
logger.error(
|
logger.error(
|
||||||
{ group: group.name, chatJid: input.chatJid, runId: input.runId, processName, error: err },
|
{
|
||||||
|
group: group.name,
|
||||||
|
chatJid: input.chatJid,
|
||||||
|
runId: input.runId,
|
||||||
|
processName,
|
||||||
|
error: err,
|
||||||
|
},
|
||||||
'Agent spawn error',
|
'Agent spawn error',
|
||||||
);
|
);
|
||||||
resolve({
|
resolve({
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import path from 'path';
|
|||||||
|
|
||||||
import { readEnvFile } from './env.js';
|
import { readEnvFile } from './env.js';
|
||||||
|
|
||||||
const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER']);
|
const envConfig = readEnvFile([
|
||||||
|
'ASSISTANT_NAME',
|
||||||
|
'ASSISTANT_HAS_OWN_NUMBER',
|
||||||
|
'SESSION_COMMAND_USER_IDS',
|
||||||
|
]);
|
||||||
|
|
||||||
export const ASSISTANT_NAME =
|
export const ASSISTANT_NAME =
|
||||||
process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy';
|
process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy';
|
||||||
@@ -15,6 +19,14 @@ export const SERVICE_AGENT_TYPE: 'claude-code' | 'codex' =
|
|||||||
export const ASSISTANT_HAS_OWN_NUMBER =
|
export const ASSISTANT_HAS_OWN_NUMBER =
|
||||||
(process.env.ASSISTANT_HAS_OWN_NUMBER ||
|
(process.env.ASSISTANT_HAS_OWN_NUMBER ||
|
||||||
envConfig.ASSISTANT_HAS_OWN_NUMBER) === 'true';
|
envConfig.ASSISTANT_HAS_OWN_NUMBER) === 'true';
|
||||||
|
export const SESSION_COMMAND_USER_IDS = new Set(
|
||||||
|
(process.env.SESSION_COMMAND_USER_IDS ||
|
||||||
|
envConfig.SESSION_COMMAND_USER_IDS ||
|
||||||
|
'')
|
||||||
|
.split(',')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
export const POLL_INTERVAL = 2000;
|
export const POLL_INTERVAL = 2000;
|
||||||
export const SCHEDULER_POLL_INTERVAL = 60000;
|
export const SCHEDULER_POLL_INTERVAL = 60000;
|
||||||
|
|
||||||
@@ -72,3 +84,7 @@ export const USAGE_UPDATE_INTERVAL = 300000; // 5 minutes
|
|||||||
// Uses system timezone by default
|
// Uses system timezone by default
|
||||||
export const TIMEZONE =
|
export const TIMEZONE =
|
||||||
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
|
export function isSessionCommandSenderAllowed(senderId: string): boolean {
|
||||||
|
return SESSION_COMMAND_USER_IDS.has(senderId);
|
||||||
|
}
|
||||||
|
|||||||
@@ -245,7 +245,12 @@ export class GroupQueue {
|
|||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ groupJid, runId: state.currentRunId, groupFolder: state.groupFolder, err },
|
{
|
||||||
|
groupJid,
|
||||||
|
runId: state.currentRunId,
|
||||||
|
groupFolder: state.groupFolder,
|
||||||
|
err,
|
||||||
|
},
|
||||||
'Failed to queue follow-up message for active agent',
|
'Failed to queue follow-up message for active agent',
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@@ -311,7 +316,10 @@ export class GroupQueue {
|
|||||||
let outcome: 'success' | 'retry_scheduled' | 'error' = 'success';
|
let outcome: 'success' | 'retry_scheduled' | 'error' = 'success';
|
||||||
try {
|
try {
|
||||||
if (this.processMessagesFn) {
|
if (this.processMessagesFn) {
|
||||||
const success = await this.processMessagesFn(groupJid, { runId, reason });
|
const success = await this.processMessagesFn(groupJid, {
|
||||||
|
runId,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
if (success) {
|
if (success) {
|
||||||
state.retryCount = 0;
|
state.retryCount = 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -321,7 +329,10 @@ export class GroupQueue {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
outcome = 'error';
|
outcome = 'error';
|
||||||
logger.error({ groupJid, runId, err }, 'Error processing messages for group');
|
logger.error(
|
||||||
|
{ groupJid, runId, err },
|
||||||
|
'Error processing messages for group',
|
||||||
|
);
|
||||||
this.scheduleRetry(groupJid, state, runId);
|
this.scheduleRetry(groupJid, state, runId);
|
||||||
} finally {
|
} finally {
|
||||||
const durationMs = state.startedAt ? Date.now() - state.startedAt : null;
|
const durationMs = state.startedAt ? Date.now() - state.startedAt : null;
|
||||||
|
|||||||
@@ -400,11 +400,7 @@ export async function processTaskIpc(
|
|||||||
await deps.syncGroups(true);
|
await deps.syncGroups(true);
|
||||||
// Write updated snapshot immediately
|
// Write updated snapshot immediately
|
||||||
const availableGroups = deps.getAvailableGroups();
|
const availableGroups = deps.getAvailableGroups();
|
||||||
deps.writeGroupsSnapshot(
|
deps.writeGroupsSnapshot(sourceGroup, true, availableGroups);
|
||||||
sourceGroup,
|
|
||||||
true,
|
|
||||||
availableGroups,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ sourceGroup },
|
{ sourceGroup },
|
||||||
|
|||||||
@@ -12,12 +12,10 @@ import {
|
|||||||
getMessagesSince,
|
getMessagesSince,
|
||||||
getNewMessages,
|
getNewMessages,
|
||||||
} from './db.js';
|
} from './db.js';
|
||||||
|
import { isSessionCommandSenderAllowed } from './config.js';
|
||||||
import { GroupQueue, GroupRunContext } from './group-queue.js';
|
import { GroupQueue, GroupRunContext } from './group-queue.js';
|
||||||
import { findChannel, formatMessages } from './router.js';
|
import { findChannel, formatMessages } from './router.js';
|
||||||
import {
|
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
||||||
isTriggerAllowed,
|
|
||||||
loadSenderAllowlist,
|
|
||||||
} from './sender-allowlist.js';
|
|
||||||
import {
|
import {
|
||||||
extractSessionCommand,
|
extractSessionCommand,
|
||||||
handleSessionCommand,
|
handleSessionCommand,
|
||||||
@@ -154,13 +152,19 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const registeredGroups = deps.getRegisteredGroups();
|
const registeredGroups = deps.getRegisteredGroups();
|
||||||
const group = registeredGroups[chatJid];
|
const group = registeredGroups[chatJid];
|
||||||
if (!group) {
|
if (!group) {
|
||||||
logger.warn({ chatJid, runId, reason }, 'Registered group missing for queued run');
|
logger.warn(
|
||||||
|
{ chatJid, runId, reason },
|
||||||
|
'Registered group missing for queued run',
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = findChannel(deps.channels, chatJid);
|
const channel = findChannel(deps.channels, chatJid);
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
logger.warn({ chatJid, runId, reason }, 'No channel owns JID, skipping messages');
|
logger.warn(
|
||||||
|
{ chatJid, runId, reason },
|
||||||
|
'No channel owns JID, skipping messages',
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +179,13 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
if (missedMessages.length === 0) {
|
if (missedMessages.length === 0) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ chatJid, group: group.name, groupFolder: group.folder, runId, reason },
|
{
|
||||||
|
chatJid,
|
||||||
|
group: group.name,
|
||||||
|
groupFolder: group.folder,
|
||||||
|
runId,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
'No pending messages for queued run',
|
'No pending messages for queued run',
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -218,6 +228,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
deps.saveState();
|
deps.saveState();
|
||||||
},
|
},
|
||||||
formatMessages,
|
formatMessages,
|
||||||
|
isAdminSender: (msg) => isSessionCommandSenderAllowed(msg.sender),
|
||||||
canSenderInteract: (msg) => {
|
canSenderInteract: (msg) => {
|
||||||
const hasTrigger = deps.triggerPattern.test(msg.content.trim());
|
const hasTrigger = deps.triggerPattern.test(msg.content.trim());
|
||||||
const requiresTrigger =
|
const requiresTrigger =
|
||||||
@@ -227,11 +238,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
!requiresTrigger ||
|
!requiresTrigger ||
|
||||||
(hasTrigger &&
|
(hasTrigger &&
|
||||||
(msg.is_from_me ||
|
(msg.is_from_me ||
|
||||||
isTriggerAllowed(
|
isTriggerAllowed(chatJid, msg.sender, loadSenderAllowlist())))
|
||||||
chatJid,
|
|
||||||
msg.sender,
|
|
||||||
loadSenderAllowlist(),
|
|
||||||
)))
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -293,40 +300,48 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
await channel.setTyping?.(chatJid, true);
|
await channel.setTyping?.(chatJid, true);
|
||||||
|
|
||||||
const output = await runAgent(group, prompt, chatJid, runId, async (result) => {
|
const output = await runAgent(
|
||||||
if (result.result) {
|
group,
|
||||||
const raw =
|
prompt,
|
||||||
typeof result.result === 'string'
|
chatJid,
|
||||||
? result.result
|
runId,
|
||||||
: JSON.stringify(result.result);
|
async (result) => {
|
||||||
const text = raw.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
|
if (result.result) {
|
||||||
logger.info(
|
const raw =
|
||||||
{
|
typeof result.result === 'string'
|
||||||
chatJid,
|
? result.result
|
||||||
group: group.name,
|
: JSON.stringify(result.result);
|
||||||
groupFolder: group.folder,
|
const text = raw
|
||||||
runId,
|
.replace(/<internal>[\s\S]*?<\/internal>/g, '')
|
||||||
resultStatus: result.status,
|
.trim();
|
||||||
},
|
logger.info(
|
||||||
`Agent output: ${raw.slice(0, 200)}`,
|
{
|
||||||
);
|
chatJid,
|
||||||
if (text) {
|
group: group.name,
|
||||||
await channel.sendMessage(chatJid, text);
|
groupFolder: group.folder,
|
||||||
outputSentToUser = true;
|
runId,
|
||||||
|
resultStatus: result.status,
|
||||||
|
},
|
||||||
|
`Agent output: ${raw.slice(0, 200)}`,
|
||||||
|
);
|
||||||
|
if (text) {
|
||||||
|
await channel.sendMessage(chatJid, text);
|
||||||
|
outputSentToUser = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await channel.setTyping?.(chatJid, false);
|
await channel.setTyping?.(chatJid, false);
|
||||||
resetIdleTimer();
|
resetIdleTimer();
|
||||||
|
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
deps.queue.notifyIdle(chatJid, runId);
|
deps.queue.notifyIdle(chatJid, runId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.status === 'error') {
|
if (result.status === 'error') {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await channel.setTyping?.(chatJid, false);
|
await channel.setTyping?.(chatJid, false);
|
||||||
|
|
||||||
@@ -408,7 +423,10 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
const channel = findChannel(deps.channels, chatJid);
|
const channel = findChannel(deps.channels, chatJid);
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
logger.warn({ chatJid }, 'No channel owns JID, skipping messages');
|
logger.warn(
|
||||||
|
{ chatJid },
|
||||||
|
'No channel owns JID, skipping messages',
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,8 +438,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const lastHuman = getLastHumanMessageTimestamp(chatJid);
|
const lastHuman = getLastHumanMessageTimestamp(chatJid);
|
||||||
if (
|
if (
|
||||||
!lastHuman ||
|
!lastHuman ||
|
||||||
Date.now() - new Date(lastHuman).getTime() >
|
Date.now() - new Date(lastHuman).getTime() > 12 * 60 * 60 * 1000
|
||||||
12 * 60 * 60 * 1000
|
|
||||||
) {
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ chatJid, lastHuman },
|
{ chatJid, lastHuman },
|
||||||
@@ -433,7 +450,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
const loopCmdMsg = groupMessages.find(
|
const loopCmdMsg = groupMessages.find(
|
||||||
(msg) =>
|
(msg) =>
|
||||||
extractSessionCommand(msg.content, deps.triggerPattern) !== null,
|
extractSessionCommand(msg.content, deps.triggerPattern) !==
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loopCmdMsg) {
|
if (loopCmdMsg) {
|
||||||
@@ -441,9 +459,12 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
isSessionCommandAllowed(
|
isSessionCommandAllowed(
|
||||||
isMainGroup,
|
isMainGroup,
|
||||||
loopCmdMsg.is_from_me === true,
|
loopCmdMsg.is_from_me === true,
|
||||||
|
isSessionCommandSenderAllowed(loopCmdMsg.sender),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
deps.queue.closeStdin(chatJid);
|
deps.queue.closeStdin(chatJid, {
|
||||||
|
reason: 'session-command-detected',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
deps.queue.enqueueMessageCheck(chatJid);
|
deps.queue.enqueueMessageCheck(chatJid);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -47,19 +47,23 @@ describe('extractSessionCommand', () => {
|
|||||||
|
|
||||||
describe('isSessionCommandAllowed', () => {
|
describe('isSessionCommandAllowed', () => {
|
||||||
it('allows main group regardless of sender', () => {
|
it('allows main group regardless of sender', () => {
|
||||||
expect(isSessionCommandAllowed(true, false)).toBe(true);
|
expect(isSessionCommandAllowed(true, false, false)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows trusted/admin sender (is_from_me) in non-main group', () => {
|
it('allows trusted/admin sender (is_from_me) in non-main group', () => {
|
||||||
expect(isSessionCommandAllowed(false, true)).toBe(true);
|
expect(isSessionCommandAllowed(false, true, false)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows configured admin sender in non-main group', () => {
|
||||||
|
expect(isSessionCommandAllowed(false, false, true)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('denies untrusted sender in non-main group', () => {
|
it('denies untrusted sender in non-main group', () => {
|
||||||
expect(isSessionCommandAllowed(false, false)).toBe(false);
|
expect(isSessionCommandAllowed(false, false, false)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows trusted sender in main group', () => {
|
it('allows trusted sender in main group', () => {
|
||||||
expect(isSessionCommandAllowed(true, true)).toBe(true);
|
expect(isSessionCommandAllowed(true, true, false)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,6 +93,7 @@ function makeDeps(
|
|||||||
clearSession: vi.fn(),
|
clearSession: vi.fn(),
|
||||||
advanceCursor: vi.fn(),
|
advanceCursor: vi.fn(),
|
||||||
formatMessages: vi.fn().mockReturnValue('<formatted>'),
|
formatMessages: vi.fn().mockReturnValue('<formatted>'),
|
||||||
|
isAdminSender: vi.fn().mockReturnValue(false),
|
||||||
canSenderInteract: vi.fn().mockReturnValue(true),
|
canSenderInteract: vi.fn().mockReturnValue(true),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
@@ -228,6 +233,25 @@ describe('handleSessionCommand', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows configured admin sender in non-main group', async () => {
|
||||||
|
const deps = makeDeps({
|
||||||
|
isAdminSender: vi.fn().mockReturnValue(true),
|
||||||
|
});
|
||||||
|
const result = await handleSessionCommand({
|
||||||
|
missedMessages: [makeMsg('/clear', { is_from_me: false, sender: 'discord-user-1' })],
|
||||||
|
isMainGroup: false,
|
||||||
|
groupName: 'test',
|
||||||
|
triggerPattern: trigger,
|
||||||
|
timezone: 'UTC',
|
||||||
|
deps,
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ handled: true, success: true });
|
||||||
|
expect(deps.clearSession).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.sendMessage).toHaveBeenCalledWith(
|
||||||
|
'Current session cleared. The next message will start a new conversation.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('reports failure when command-stage runAgent returns error without streamed status', async () => {
|
it('reports failure when command-stage runAgent returns error without streamed status', async () => {
|
||||||
// runAgent resolves 'error' but callback never gets status: 'error'
|
// runAgent resolves 'error' but callback never gets status: 'error'
|
||||||
const deps = makeDeps({
|
const deps = makeDeps({
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ export function extractSessionCommand(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a session command sender is authorized.
|
* Check if a session command sender is authorized.
|
||||||
* Allowed: main group (any sender), or trusted/admin sender (is_from_me) in any group.
|
* Allowed: main group (any sender), or trusted/admin sender in any group.
|
||||||
*/
|
*/
|
||||||
export function isSessionCommandAllowed(
|
export function isSessionCommandAllowed(
|
||||||
isMainGroup: boolean,
|
isMainGroup: boolean,
|
||||||
isFromMe: boolean,
|
isFromMe: boolean,
|
||||||
|
isAdminSender: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
return isMainGroup || isFromMe;
|
return isMainGroup || isFromMe || isAdminSender;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Minimal agent result interface — matches the subset of AgentOutput used here. */
|
/** Minimal agent result interface — matches the subset of AgentOutput used here. */
|
||||||
@@ -45,6 +46,7 @@ export interface SessionCommandDeps {
|
|||||||
clearSession: () => void;
|
clearSession: () => void;
|
||||||
advanceCursor: (timestamp: string) => void;
|
advanceCursor: (timestamp: string) => void;
|
||||||
formatMessages: (msgs: NewMessage[], timezone: string) => string;
|
formatMessages: (msgs: NewMessage[], timezone: string) => string;
|
||||||
|
isAdminSender: (msg: NewMessage) => boolean;
|
||||||
/** Whether the denied sender would normally be allowed to interact (for denial messages). */
|
/** Whether the denied sender would normally be allowed to interact (for denial messages). */
|
||||||
canSenderInteract: (msg: NewMessage) => boolean;
|
canSenderInteract: (msg: NewMessage) => boolean;
|
||||||
}
|
}
|
||||||
@@ -89,7 +91,13 @@ export async function handleSessionCommand(opts: {
|
|||||||
|
|
||||||
if (!command || !cmdMsg) return { handled: false };
|
if (!command || !cmdMsg) return { handled: false };
|
||||||
|
|
||||||
if (!isSessionCommandAllowed(isMainGroup, cmdMsg.is_from_me === true)) {
|
if (
|
||||||
|
!isSessionCommandAllowed(
|
||||||
|
isMainGroup,
|
||||||
|
cmdMsg.is_from_me === true,
|
||||||
|
deps.isAdminSender(cmdMsg),
|
||||||
|
)
|
||||||
|
) {
|
||||||
// DENIED: send denial if the sender would normally be allowed to interact,
|
// DENIED: send denial if the sender would normally be allowed to interact,
|
||||||
// then silently consume the command by advancing the cursor past it.
|
// then silently consume the command by advancing the cursor past it.
|
||||||
// Trade-off: other messages in the same batch are also consumed (cursor is
|
// Trade-off: other messages in the same batch are also consumed (cursor is
|
||||||
|
|||||||
Reference in New Issue
Block a user