Skip to content
Open
4 changes: 2 additions & 2 deletions commands/TimeOffCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { statusCommand } from './subcommands/Status';

export class TimeOffCommand implements ISlashCommand {
public command = 'time-off';
public i18nParamsExample = '';
public i18nDescription = '';
public i18nParamsExample = 'Time_Off_Command_Params_Example';
public i18nDescription = 'Time_Off_Command_Description';
public providesPreview = false;

constructor(private readonly app: TimeOffApp) {}
Expand Down
11 changes: 6 additions & 5 deletions commands/subcommands/Help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ export async function helpCommand(app: TimeOffApp, context: SlashCommandContext,
const room = context.getRoom();

const message =
`*Time Off App Help*\n\n` +
`*Time-off Help*\n\n` +
`*Commands*\n` +
`• \`/time-off start\` - Start a new time off\n` +
`• \`/time-off end\` - End an existing time off\n` +
`• \`/time-off status\` - Check the current time off status\n` +
`• \`/time-off help\` - Display this help message\n`;
`• \`/time-off start\` starts new time off period\n` +
`• \`/time-off start [message]\` start a new time off period with a custom message\n` +
`• \`/time-off end\` ends current time off period\n` +
`• \`/time-off status\` shows current time off status\n` +
`• \`/time-off help\` shows this list\n`;

const notifier = new AppNotifier(app, read);
await notifier.notifyUser(room, sender, message);
Expand Down
11 changes: 5 additions & 6 deletions commands/subcommands/Start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,22 @@ export async function startCommand(
const room = context.getRoom();
const notifier = new AppNotifier(app, read);

let message = context.getArguments().join(' ').replace(CommandEnum.START, '');
if (!message) {
message = NOTIFICATION_MESSAGES.default_reply_message;
}
const customMessage = context.getArguments().join(' ').replace(CommandEnum.START, '').trim();

const timeOffEntry: ITimeOff = {
coreUserId: currentUser.id,
username: currentUser.username,
status: TimeOffStatus.ON_TIME_OFF,
message: message,
message: customMessage || undefined,
lastNotifiedAtBySenderId: {},
};

const timeOffRepository = new TimeOffRepository(app, read, persistence);
const timeOffService = new TimeOffService(timeOffRepository);
const savedTimeOff = await timeOffService.saveTimeOff(timeOffEntry);

const notificationMessage = !savedTimeOff ? NOTIFICATION_MESSAGES.error : NOTIFICATION_MESSAGES.started;
const notificationMessage = !savedTimeOff
? NOTIFICATION_MESSAGES.error
: NOTIFICATION_MESSAGES.started(customMessage || undefined);
await notifier.notifyUser(room, currentUser, notificationMessage);
}
4 changes: 3 additions & 1 deletion commands/subcommands/Status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export async function statusCommand(
}

if (timeOffEntry.status === TimeOffStatus.ON_TIME_OFF) {
notificationMessage = `${NOTIFICATION_MESSAGES.status_in}${timeOffEntry.message}`;
notificationMessage = timeOffEntry.message
? `${NOTIFICATION_MESSAGES.status_in}${timeOffEntry.message}`
: NOTIFICATION_MESSAGES.status_in_no_message;
} else {
notificationMessage = `${NOTIFICATION_MESSAGES.status_out}`;
}
Expand Down
7 changes: 5 additions & 2 deletions handlers/PostMessageSentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@ export class PostMessageSentHandler {
message: IMessage,
sender: IUser,
receiver: IUser,
timeOffMessage: string,
timeOffMessage?: string,
): Promise<void> {
const formattedMessage = TimeOffMessageFormatter.format(receiver.username, timeOffMessage);
await this.notifier.notifyUser(message.room, sender, timeOffMessage, formattedMessage);
const fallbackText = timeOffMessage

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy-message gap: entries created before this PR have the old default "I am out of office, I will get back to you soon." persisted in message — the previous Start.ts wrote it into the record at save time. For anyone already on time off when this version deploys, this branch still sees a truthy message, so the app keeps sending the fabricated text — now explicitly framed as "They left the following message:", i.e. presented as something the user personally wrote. /time-off status has the same problem, so the PR's headline goal (never put words in the user's mouth) silently fails for exactly those users, with no migration to fix it.

Suggested fix: normalize at read time in TimeOffRepository.findByUserId — if the stored message equals the legacy default string, treat it as unset:

const LEGACY_DEFAULT_REPLY_MESSAGE = 'I am out of office, I will get back to you soon.';

// in findByUserId, before returning:
if (timeOffEntry?.message === LEGACY_DEFAULT_REPLY_MESSAGE) {
	timeOffEntry.message = undefined;
}

A single read-time check covers both this handler and the status command, and it self-heals persistence: the next saveTimeOff (e.g. the cooldown-timestamp update) writes the record back without the legacy message.

? `${receiver.username} is currently on time off. They left the following message: ${timeOffMessage}`
: `${receiver.username} is currently on time off.`;
await this.notifier.notifyUser(message.room, sender, fallbackText, formattedMessage);
}

private shouldSendNotification(timeOffEntry: ITimeOff | undefined, senderId: string): timeOffEntry is ITimeOff {
Expand Down
21 changes: 17 additions & 4 deletions helpers/NotificationMessage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
export const NOTIFICATION_MESSAGES = {
started: `You are marked as Time Off, we will see you when you get back. Use \`/time-off end\` to end your time off.`,
default_reply_message: `I am out of office, I will get back to you soon.`,
started: (customMessage?: string): string => {
const intro = `*Time off on*\nI will automatically reply to any direct messages you receive to let the sender know you're on time off.`;
const reminder = `Remember to use the \`/time-off end\` command when you get back.`;

if (customMessage) {
const quoted = customMessage
.split('\n')
.map((line) => `> ${line}`)
.join('\n');
return `${intro}\n\nI will also include the following custom message:\n${quoted}\n\n${reminder}`;
}

return `${intro}\n\nYou have not set a custom reply message.\n\n${reminder}`;
},
ended: `Welcome back! You are no longer marked as Time Off.`,
not_started: `You are not marked as Time Off. Use \`/time-off start\` to start your time off.`,
not_started: `*Time-off off*\nI will not automatically reply to any direct messages you receive.`,
status_in: `You are currently marked as Time Off. Your message is: `,
status_out: `You are \`not\` marked as Time Off.`,
status_in_no_message: `You are currently marked as Time Off. You have not set a custom reply message.`,
status_out: `*Time-off off*\nI will not automatically reply to any direct messages you receive.`,
error: `An error occurred while updating your Time Off status. Please try again later.`,
};
37 changes: 24 additions & 13 deletions helpers/TimeOffMessageFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { LayoutBlock } from '@rocket.chat/ui-kit';

export class TimeOffMessageFormatter {
public static format(username: string, message: string): LayoutBlock[] {
return [
public static format(username: string, message?: string): LayoutBlock[] {
const headerText = message
? `${username} is currently on time off. They left the following message:`
: `${username} is currently on time off.`;

const blocks: LayoutBlock[] = [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${username} is currently *unavailable* to answer your message, however they left the following message:\n\n`,
text: headerText,
},
},
{
type: 'preview',
title: [],
description: [
{
type: 'mrkdwn',
text: message,
},
],
},
];

if (message) {
const quoted = message
.split('\n')
.map((line) => `> ${line}`)
.join('\n');

blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: quoted,
},
});
}

return blocks;
}
}
4 changes: 4 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"Time_Off_Command_Description": "Manage your time off — start, end, or check your status",
"Time_Off_Command_Params_Example": "start | end | status | help"
}
2 changes: 1 addition & 1 deletion interfaces/ITimeOff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TimeOffStatus } from '../enums/Status';
export interface ITimeOff {
coreUserId: string;
username: string;
message: string;
message?: string;
status: TimeOffStatus;
lastNotifiedAtBySenderId?: Record<string, number>;
}
2 changes: 1 addition & 1 deletion notifiers/AppNotifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LayoutBlock } from '@rocket.chat/ui-kit';
import { TimeOffApp } from '../TimeOffApp';

export class AppNotifier implements IAppNotifier {
private readonly TimeOffName: string = 'TimeOff Bot';
private readonly TimeOffName: string = 'Time-off';

constructor(private readonly app: TimeOffApp, private readonly read: IRead) {}

Expand Down
Loading