Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: send messages from Koko #24

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions KokoApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { praiseModal } from './modals/PraiseModal';
import { questionModal } from './modals/QuestionModal';
import { valuesModal } from './modals/ValuesModal';
import { settings } from './settings';
import { KokoSend } from './actions/KokoSend';

export class KokoApp extends App implements IUIKitInteractionHandler {
/**
Expand Down Expand Up @@ -116,6 +117,11 @@ export class KokoApp extends App implements IUIKitInteractionHandler {
*/
public kokoWellness: KokoWellness;

/**
* The send message mechanism
*/
public kokoSend: KokoSend;

/**
* Members cache
*/
Expand All @@ -138,6 +144,8 @@ export class KokoApp extends App implements IUIKitInteractionHandler {
return this.kokoQuestion.submit({ context, modify, read, persistence });
case 'values':
return this.kokoValues.submit({ context, modify, read, persistence, http });
case 'send':
return this.kokoSend.submit({ context, modify, read, persistence, http });
}
return {
success: true,
Expand Down Expand Up @@ -178,6 +186,7 @@ export class KokoApp extends App implements IUIKitInteractionHandler {
this.kokoOneOnOne = new KokoOneOnOne(this);
this.kokoWellness = new KokoWellness(this);
this.kokoValues = new KokoValues(this);
this.kokoSend = new KokoSend(this);

await this.extendConfiguration(configurationExtend);
}
Expand Down
138 changes: 138 additions & 0 deletions actions/KokoSend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
IHttp,
IModify,
IPersistence,
IRead,
} from "@rocket.chat/apps-engine/definition/accessors";
import { IRoom } from "@rocket.chat/apps-engine/definition/rooms";
import { UIKitViewSubmitInteractionContext } from "@rocket.chat/apps-engine/definition/uikit";

import { KokoApp } from "../KokoApp";
import { getDirect, sendMessage } from "../lib/helpers";
import { messageSubmittedModal } from "../modals/SendModal";

export class KokoSend {
constructor(private readonly app: KokoApp) {}

/**
* Handles the submit action for the Send Message modal
* Validates input and sends message to the specified room or user
*/
public async submit({
context,
modify,
read,
persistence,
http,
}: {
context: UIKitViewSubmitInteractionContext;
modify: IModify;
read: IRead;
persistence: IPersistence;
http: IHttp;
}) {
const data = context.getInteractionData();
const { send } = data.view.state as any;

// Extract and validate message content
const messageToSend = send?.["message"];
if (!messageToSend?.trim()) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
message: "Please enter a message to send",
},
});
}

// Determine target room (channel or user)
const roomNameInput = send?.["room"];
const roomNameUser = send?.["user"];
const roomName = roomNameInput?.trim() || roomNameUser?.trim();

// Validate room name
if (!roomName) {
return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
room: "Please enter a valid room or user name",
},
});
}

// Find target room
try {
const targetRoom = await this.resolveTargetRoom(
read,
modify,
roomName
);

if (!targetRoom) {
const fieldName = roomName.startsWith("@") ? "user" : "room";
const entityType = roomName.startsWith("@") ? "User" : "Room";

return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
[fieldName]: `${entityType} "${roomName}" not found. Please check the name and try again.`,
},
});
}

// Send the message
await sendMessage(this.app, modify, targetRoom, messageToSend);

// Show confirmation modal
const modal = await messageSubmittedModal({ read, modify, data });
return context
.getInteractionResponder()
.updateModalViewResponse(modal);
} catch (error) {
// Handle errors during room resolution or message sending
this.app
.getLogger()
.error(`Error in send command: ${error.message}`);

return context.getInteractionResponder().viewErrorResponse({
viewId: data.view.id,
errors: {
message:
"An error occurred while sending your message. Please try again.",
},
});
}
}

/**
* Resolves a room name or username to an actual room object
*
* @param {IRead} read - The read accessor
* @param {IModify} modify - The modify accessor
* @param {string} target - The target room name or username (with # or @ prefix)
* @returns {Promise<IRoom|undefined>} The resolved room or undefined if not found
*/
private async resolveTargetRoom(
read: IRead,
modify: IModify,
target: string
): Promise<IRoom | undefined> {
// Handle channel
if (target.startsWith("#")) {
const roomName = target.substring(1); // Remove # prefix
return read.getRoomReader().getByName(roomName);
}

// Handle user
if (target.startsWith("@")) {
const username = target.substring(1); // Remove @ prefix
const user = await read.getUserReader().getByUsername(username);

if (user) {
return getDirect(this.app, read, modify, username);
}
}

return undefined;
}
}
4 changes: 4 additions & 0 deletions commands/KokoCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { processHelpCommand } from './Help';
import { processOneOnOneCommand } from './OneOnOne';
import { processPraiseCommand } from './Praise';
import { processQuestionCommand } from './Question';
import { processSendCommand } from './Send';

export class KokoCommand implements ISlashCommand {
public command = 'koko';
Expand Down Expand Up @@ -56,6 +57,9 @@ export class KokoCommand implements ISlashCommand {
case this.CommandEnum.Cancel:
await processCancelCommand(this.app, context, read, modify, persistence);
break;
case this.CommandEnum.Send:
await processSendCommand(this.app, context, read, modify, persistence, params);
break;
default:
await processHelpCommand(this.app, context, read, modify);
}
Expand Down
61 changes: 61 additions & 0 deletions commands/Send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
IRead,
IModify,
IPersistence,
} from "@rocket.chat/apps-engine/definition/accessors";
import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands";
import { KokoApp } from "../KokoApp";
import { sendModal } from "../modals/SendModal";
import { notifyUser } from "../lib/helpers";

/**
* Process a send command with format: send [roomName] [message]
* @param {string[]} args - Array of command arguments
* @returns {boolean} - True if command was processed successfully
*/
export const processSendCommand = async (
app: KokoApp,
context: SlashCommandContext,
read: IRead,
modify: IModify,
persistence: IPersistence,
args: string[]
) => {
// Check if room name is provided
if (args.length < 1) {
await notifyUser(
app,
modify,
context.getRoom(),
context.getSender(),
"Please provide a room name."
);
return false;
}

const roomName = args[0];

const triggerId = context.getTriggerId();
if (triggerId) {
try {
const modal = await sendModal({
app,
read,
modify,
data: { user: context.getSender(), roomName },
});
await modify
.getUiController()
.openModalView(modal, { triggerId }, context.getSender());
} catch (error) {
console.log(error);
app.getLogger().error(
`Error opening Send modal: ${
error?.message
}, stringify: ${JSON.stringify(error, null, 2)}`
);
}
}

return true;
};
4 changes: 3 additions & 1 deletion i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@
"Koko_Post_Answers_Room_Name": "Answers Room Name",
"Koko_Post_Answers_Room_Name_Description": "The room name for Koko to post answers to",
"AllStars_Webhook": "AllStars Webhook",
"AllStars_Webhook_Description": "Webhook to add Rocket.Chat AllStars points"
"AllStars_Webhook_Description": "Webhook to add Rocket.Chat AllStars points",
"Access_Roles": "Access Roles",
"Access_Roles_Description": "Roles that can manage Koko. Case sensitive, comma separated"
}
128 changes: 128 additions & 0 deletions modals/SendModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { IModify, IRead } from "@rocket.chat/apps-engine/definition/accessors";
import {
BlockElementType,
IMultiStaticSelectElement,
TextObjectType,
} from "@rocket.chat/apps-engine/definition/uikit/blocks";
import { IUIKitModalViewParam } from "@rocket.chat/apps-engine/definition/uikit/UIKitInteractionResponder";

import { KokoApp } from "../KokoApp";
import { getMembers } from "../lib/helpers";

const UI_ID = "send";

export async function sendModal({
app,
data,
read,
modify,
}: {
app: KokoApp;
data;
read: IRead;
modify: IModify;
}): Promise<IUIKitModalViewParam> {
const viewId = UI_ID;
const block = modify.getCreator().getBlockBuilder();
const { roomName }: { roomName: string } = data;

if (roomName.startsWith("#")) {
block.addInputBlock({
blockId: UI_ID,
element: block.newPlainTextInputElement({
actionId: "room",
initialValue: roomName,
multiline: false,
}),
label: {
type: TextObjectType.PLAINTEXT,
text: "Send to Channel",
emoji: true,
},
});
}
if (roomName.startsWith("@")) {
block.addInputBlock({
blockId: UI_ID,
element: block.newPlainTextInputElement({
actionId: "user",
initialValue: roomName,
multiline: false,
}),
label: {
type: TextObjectType.PLAINTEXT,
text: "Send to User",
emoji: true,
},
});
}

block.addInputBlock({
blockId: UI_ID,
element: block.newPlainTextInputElement({
actionId: "message",
multiline: true,
}),
label: {
type: TextObjectType.PLAINTEXT,
text: `What would you like to send?`,
emoji: true,
},
});

return {
id: viewId,
title: {
type: TextObjectType.PLAINTEXT,
text: "Send Message",
},
submit: block.newButtonElement({
text: {
type: TextObjectType.PLAINTEXT,
text: "Send",
},
}),
close: block.newButtonElement({
text: {
type: TextObjectType.PLAINTEXT,
text: "Cancel",
},
}),
blocks: block.getBlocks(),
};
}

export async function messageSubmittedModal({
read,
modify,
data,
}: {
read: IRead;
modify: IModify;
data;
}): Promise<IUIKitModalViewParam> {
const viewId = UI_ID;
const block = modify.getCreator().getBlockBuilder();

block.addSectionBlock({
text: {
type: TextObjectType.PLAINTEXT,
text: "Your message has been sent successfully.",
},
});

return {
id: viewId,
title: {
type: TextObjectType.PLAINTEXT,
text: "Message Sent",
},
close: block.newButtonElement({
text: {
type: TextObjectType.PLAINTEXT,
text: "Close",
},
}),
blocks: block.getBlocks(),
};
}
Loading