@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
260 lines (259 loc) • 14.1 kB
JavaScript
import { RoomStatus, } from "@ably/chat";
import { Args, Flags } from "@oclif/core";
import chalk from "chalk";
import { ChatBaseCommand } from "../../../../chat-base-command.js";
export default class MessagesReactionsSubscribe extends ChatBaseCommand {
static args = {
room: Args.string({
description: "Room to subscribe to message reactions in",
required: true,
}),
};
static description = "Subscribe to message reactions in a chat room";
static examples = [
"$ ably rooms messages reactions subscribe my-room",
"$ ably rooms messages reactions subscribe my-room --raw",
"$ ably rooms messages reactions subscribe my-room --json",
"$ ably rooms messages reactions subscribe my-room --pretty-json",
];
static flags = {
...ChatBaseCommand.globalFlags,
raw: Flags.boolean({
description: "Subscribe to raw individual reaction events instead of summaries",
default: false,
}),
};
chatClient = null;
unsubscribeReactionsFn = null;
unsubscribeRawReactionsFn = null;
unsubscribeStatusFn = null;
async run() {
const { args, flags } = await this.parse(MessagesReactionsSubscribe);
try {
// Create Chat client
this.chatClient = await this.createChatClient(flags);
if (!this.chatClient) {
this.error("Failed to initialize clients");
return;
}
const { room } = args;
// Set up connection state logging
this.setupConnectionStateLogging(this.chatClient.realtime, flags, {
includeUserFriendlyMessages: true,
});
this.logCliEvent(flags, "subscribe", "connecting", `Connecting to Ably and subscribing to message reactions in room ${room}...`);
if (!this.shouldOutputJson(flags)) {
this.log(`Connecting to Ably and subscribing to message reactions in room ${chalk.cyan(room)}...`);
}
// Get the room
this.logCliEvent(flags, "room", "gettingRoom", `Getting room handle for ${room}`);
// Set room options to receive raw reactions if requested
const roomOptions = flags.raw
? {
messages: {
rawMessageReactions: true,
},
}
: {};
const chatRoom = await this.chatClient.rooms.get(room, roomOptions);
this.logCliEvent(flags, "room", "gotRoom", `Got room handle for ${room}`);
// Subscribe to room status changes
this.logCliEvent(flags, "room", "subscribingToStatus", "Subscribing to room status changes");
const { off: unsubscribeStatus } = chatRoom.onStatusChange((statusChange) => {
let reason;
if (statusChange.current === RoomStatus.Failed) {
reason = chatRoom.error; // Get reason from chatRoom.error on failure
}
const reasonMsg = reason instanceof Error ? reason.message : reason;
this.logCliEvent(flags, "room", `status-${statusChange.current}`, `Room status changed to ${statusChange.current}`, { reason: reasonMsg });
switch (statusChange.current) {
case RoomStatus.Attached: {
if (!this.shouldOutputJson(flags)) {
this.log(chalk.green("Successfully connected to Ably"));
this.log(`Listening for message reactions in room ${chalk.cyan(room)}. Press Ctrl+C to exit.`);
}
break;
}
case RoomStatus.Detached: {
if (!this.shouldOutputJson(flags)) {
this.log(chalk.yellow("Disconnected from Ably"));
}
break;
}
case RoomStatus.Failed: {
if (!this.shouldOutputJson(flags)) {
this.error(`${chalk.red("Connection failed:")} ${reasonMsg || "Unknown error"}`);
}
break;
}
// No default
}
});
this.unsubscribeStatusFn = unsubscribeStatus;
this.logCliEvent(flags, "room", "subscribedToStatus", "Successfully subscribed to room status changes");
// Attach to the room
this.logCliEvent(flags, "room", "attaching", `Attaching to room ${room}`);
await chatRoom.attach();
// Successful attach logged by onStatusChange handler
// Subscribe to message reactions based on the flag
if (flags.raw) {
// Subscribe to raw reaction events
this.logCliEvent(flags, "reactions", "subscribingRaw", "Subscribing to raw reaction events");
this.unsubscribeRawReactionsFn =
chatRoom.messages.reactions.subscribeRaw((event) => {
const timestamp = new Date().toISOString();
const eventData = {
type: event.type,
serial: event.reaction.messageSerial,
reaction: event.reaction,
room,
timestamp,
};
this.logCliEvent(flags, "reactions", "rawReceived", "Raw reaction event received", eventData);
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({ success: true, ...eventData }, flags));
}
else {
this.log(`[${chalk.dim(timestamp)}] ${chalk.green("⚡")} ${chalk.blue(event.reaction.clientId || "Unknown")} [${event.reaction.type}] ${event.type}: ${chalk.yellow(event.reaction.name || "unknown")} to message ${chalk.cyan(event.reaction.messageSerial)}`);
}
});
this.logCliEvent(flags, "reactions", "subscribedRaw", "Successfully subscribed to raw reaction events");
}
else {
// Subscribe to reaction summaries
this.logCliEvent(flags, "reactions", "subscribing", "Subscribing to reaction summaries");
this.unsubscribeReactionsFn = chatRoom.messages.reactions.subscribe((event) => {
const timestamp = new Date().toISOString();
// Format the summary for display
const summaryData = event.reactions;
this.logCliEvent(flags, "reactions", "summaryReceived", "Reaction summary received", {
room,
timestamp,
summary: summaryData,
});
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({
success: true,
room,
timestamp,
summary: summaryData,
}, flags));
}
else {
this.log(`[${chalk.dim(timestamp)}] ${chalk.green("📊")} Reaction summary for message ${chalk.cyan(event.messageSerial)}:`);
// Display the summaries by type if they exist
if (event.reactions.unique &&
Object.keys(event.reactions.unique).length > 0) {
this.log(` ${chalk.blue("Unique reactions:")}`);
this.displayReactionSummary(event.reactions.unique, flags);
}
if (event.reactions.distinct &&
Object.keys(event.reactions.distinct).length > 0) {
this.log(` ${chalk.blue("Distinct reactions:")}`);
this.displayReactionSummary(event.reactions.distinct, flags);
}
if (event.reactions.multiple &&
Object.keys(event.reactions.multiple).length > 0) {
this.log(` ${chalk.blue("Multiple reactions:")}`);
this.displayMultipleReactionSummary(event.reactions.multiple, flags);
}
}
});
this.logCliEvent(flags, "reactions", "subscribed", "Successfully subscribed to reaction summaries");
}
this.logCliEvent(flags, "reactions", "listening", "Listening for message reactions...");
// Keep the process running until interrupted
await new Promise((resolve) => {
let cleanupInProgress = false;
const cleanup = async () => {
if (cleanupInProgress)
return;
cleanupInProgress = true;
this.logCliEvent(flags, "reactions", "cleanupInitiated", "Cleanup initiated (Ctrl+C pressed)");
if (!this.shouldOutputJson(flags)) {
this.log(`\n${chalk.yellow("Unsubscribing and closing connection...")}`);
}
// Set a force exit timeout
const forceExitTimeout = setTimeout(() => {
const errorMsg = "Force exiting after timeout during cleanup";
this.logCliEvent(flags, "reactions", "forceExit", errorMsg, {
room,
});
if (!this.shouldOutputJson(flags)) {
this.log(chalk.red("Force exiting after timeout..."));
}
}, 5000);
// Unsubscribe from reactions
if (this.unsubscribeReactionsFn) {
try {
this.logCliEvent(flags, "reactions", "unsubscribing", "Unsubscribing from reaction summaries");
this.unsubscribeReactionsFn.unsubscribe();
this.logCliEvent(flags, "reactions", "unsubscribed", "Unsubscribed from reaction summaries");
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "reactions", "unsubscribeError", `Error unsubscribing from reactions: ${errorMsg}`, { error: errorMsg });
}
}
// Unsubscribe from raw reactions
if (this.unsubscribeRawReactionsFn) {
try {
this.logCliEvent(flags, "reactions", "unsubscribingRaw", "Unsubscribing from raw reaction events");
this.unsubscribeRawReactionsFn.unsubscribe();
this.logCliEvent(flags, "reactions", "unsubscribedRaw", "Unsubscribed from raw reaction events");
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "reactions", "unsubscribeRawError", `Error unsubscribing from raw reactions: ${errorMsg}`, { error: errorMsg });
}
}
// Unsubscribe from status changes
if (this.unsubscribeStatusFn) {
try {
this.logCliEvent(flags, "room", "unsubscribingStatus", "Unsubscribing from room status");
this.unsubscribeStatusFn();
this.logCliEvent(flags, "room", "unsubscribedStatus", "Unsubscribed from room status");
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "room", "unsubscribeStatusError", `Error unsubscribing from status: ${errorMsg}`, { error: errorMsg });
}
}
if (!this.shouldOutputJson(flags)) {
this.log(chalk.green("Successfully disconnected."));
}
clearTimeout(forceExitTimeout);
resolve();
};
process.on("SIGINT", () => void cleanup());
process.on("SIGTERM", () => void cleanup());
});
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "reactions", "fatalError", `Error: ${errorMsg}`, {
error: errorMsg,
room: args.room,
});
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({ error: errorMsg, room: args.room, success: false }, flags));
}
else {
this.error(`Error: ${errorMsg}`);
}
}
}
displayReactionSummary(summary, _flags) {
for (const [reactionName, details] of Object.entries(summary)) {
this.log(` ${chalk.yellow(reactionName)}: ${details.total} (${details.clientIds.join(", ")})`);
}
}
displayMultipleReactionSummary(summary, _flags) {
for (const [reactionName, details] of Object.entries(summary)) {
const clientList = Object.entries(details.clientIds)
.map(([clientId, count]) => `${clientId}(${count})`)
.join(", ");
this.log(` ${chalk.yellow(reactionName)}: ${details.total} (${clientList})`);
}
}
}