mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-12-20 13:37:49 +08:00
feature: ChatManager
feature: - multi chatboxes manage - chatbox message queue
This commit is contained in:
@@ -1,16 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* Example CLI application demonstrating the ccCLI framework
|
* Example CLI application demonstrating the ccCLI framework
|
||||||
* This example shows how to create a calculator CLI with global context injection
|
* This example shows how to create a calculator CLI with global context injection
|
||||||
|
* and ChatManager integration for Minecraft chat functionality
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Command, createCli, CliError } from "../lib/ccCLI/index";
|
import { Command, createCli, CliError } from "../lib/ccCLI/index";
|
||||||
import { Ok, Result } from "../lib/thirdparty/ts-result-es";
|
import { Ok, Result } from "../lib/thirdparty/ts-result-es";
|
||||||
|
import { ChatManager, ChatMessage, ChatToast } from "../lib/ChatManager";
|
||||||
|
|
||||||
// 1. Define global context type
|
// 1. Define global context type
|
||||||
interface AppContext {
|
interface AppContext {
|
||||||
appName: string;
|
appName: string;
|
||||||
log: (message: string) => void;
|
log: (message: string) => void;
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
|
chatManager?: ChatManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Define individual commands
|
// 2. Define individual commands
|
||||||
@@ -153,10 +156,346 @@ const configCommand: Command<AppContext> = {
|
|||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ChatManager commands
|
||||||
|
const chatSendCommand: Command<AppContext> = {
|
||||||
|
name: "send",
|
||||||
|
description: "Send a chat message",
|
||||||
|
args: [
|
||||||
|
{ name: "message", description: "The message to send", required: true },
|
||||||
|
],
|
||||||
|
options: new Map([
|
||||||
|
[
|
||||||
|
"player",
|
||||||
|
{
|
||||||
|
name: "player",
|
||||||
|
shortName: "p",
|
||||||
|
description: "Target player for private message",
|
||||||
|
defaultValue: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"prefix",
|
||||||
|
{
|
||||||
|
name: "prefix",
|
||||||
|
description: "Message prefix",
|
||||||
|
defaultValue: "CC",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
action: ({ args, options, context }): Result<void, CliError> => {
|
||||||
|
if (!context.chatManager) {
|
||||||
|
print(
|
||||||
|
"Error: ChatManager not initialized. No chatbox peripherals found.",
|
||||||
|
);
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: ChatMessage = {
|
||||||
|
message: args.message as string,
|
||||||
|
targetPlayer: options.player as string | undefined,
|
||||||
|
prefix: options.prefix as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = context.chatManager.sendMessage(message);
|
||||||
|
if (result.isOk()) {
|
||||||
|
print(`Message queued: "${String(args.message)}"`);
|
||||||
|
|
||||||
|
const targetPlayer = options.player;
|
||||||
|
if (
|
||||||
|
targetPlayer !== undefined &&
|
||||||
|
targetPlayer !== null &&
|
||||||
|
typeof targetPlayer === "string"
|
||||||
|
) {
|
||||||
|
print(`Target: ${targetPlayer}`);
|
||||||
|
} else {
|
||||||
|
print("Target: Global chat");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print(`Failed to queue message: ${result.error.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok.EMPTY;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatToastCommand: Command<AppContext> = {
|
||||||
|
name: "toast",
|
||||||
|
description: "Send a toast notification to a player",
|
||||||
|
args: [
|
||||||
|
{ name: "player", description: "Target player username", required: true },
|
||||||
|
{ name: "title", description: "Toast title", required: true },
|
||||||
|
{ name: "message", description: "Toast message", required: true },
|
||||||
|
],
|
||||||
|
options: new Map([
|
||||||
|
[
|
||||||
|
"prefix",
|
||||||
|
{
|
||||||
|
name: "prefix",
|
||||||
|
description: "Message prefix",
|
||||||
|
defaultValue: "CC",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
action: ({ args, options, context }): Result<void, CliError> => {
|
||||||
|
if (!context.chatManager) {
|
||||||
|
print(
|
||||||
|
"Error: ChatManager not initialized. No chatbox peripherals found.",
|
||||||
|
);
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast: ChatToast = {
|
||||||
|
username: args.player as string,
|
||||||
|
title: args.title as string,
|
||||||
|
message: args.message as string,
|
||||||
|
prefix: options.prefix as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = context.chatManager.sendToast(toast);
|
||||||
|
if (result.isOk()) {
|
||||||
|
print(
|
||||||
|
`Toast queued for ${String(args.player)}: "${String(args.title)}" - "${String(args.message)}"`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print(`Failed to queue toast: ${result.error.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok.EMPTY;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatStatusCommand: Command<AppContext> = {
|
||||||
|
name: "status",
|
||||||
|
description: "Show ChatManager status and queue information",
|
||||||
|
action: ({ context }): Result<void, CliError> => {
|
||||||
|
if (!context.chatManager) {
|
||||||
|
print("ChatManager: Not initialized (no chatbox peripherals found)");
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
print("=== ChatManager Status ===");
|
||||||
|
print(`Pending messages: ${context.chatManager.getPendingMessageCount()}`);
|
||||||
|
print(`Pending toasts: ${context.chatManager.getPendingToastCount()}`);
|
||||||
|
print(
|
||||||
|
`Buffered received: ${context.chatManager.getBufferedMessageCount()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const chatboxStatus = context.chatManager.getChatboxStatus();
|
||||||
|
print(`Chatboxes: ${chatboxStatus.length} total`);
|
||||||
|
|
||||||
|
for (let i = 0; i < chatboxStatus.length; i++) {
|
||||||
|
const status = chatboxStatus[i] ? "idle" : "busy";
|
||||||
|
print(` Chatbox ${i + 1}: ${status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok.EMPTY;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatReceiveCommand: Command<AppContext> = {
|
||||||
|
name: "receive",
|
||||||
|
description: "Check for received chat messages",
|
||||||
|
options: new Map([
|
||||||
|
[
|
||||||
|
"count",
|
||||||
|
{
|
||||||
|
name: "count",
|
||||||
|
shortName: "c",
|
||||||
|
description: "Number of messages to retrieve",
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
action: ({ options, context }): Result<void, CliError> => {
|
||||||
|
if (!context.chatManager) {
|
||||||
|
print(
|
||||||
|
"Error: ChatManager not initialized. No chatbox peripherals found.",
|
||||||
|
);
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = tonumber(options.count as string) ?? 1;
|
||||||
|
let retrieved = 0;
|
||||||
|
|
||||||
|
print("=== Received Messages ===");
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const result = context.chatManager.getReceivedMessage();
|
||||||
|
if (result.isOk()) {
|
||||||
|
const event = result.value;
|
||||||
|
print(`[${event.username}]: ${event.message}`);
|
||||||
|
if (event.uuid !== undefined) {
|
||||||
|
print(` UUID: ${event.uuid}`);
|
||||||
|
}
|
||||||
|
retrieved++;
|
||||||
|
} else {
|
||||||
|
// Buffer is empty
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retrieved === 0) {
|
||||||
|
print("No messages in buffer");
|
||||||
|
} else {
|
||||||
|
print(`Retrieved ${retrieved} message(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok.EMPTY;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatSendImmediateCommand: Command<AppContext> = {
|
||||||
|
name: "send-immediate",
|
||||||
|
description: "Send a chat message immediately (bypass queue)",
|
||||||
|
args: [
|
||||||
|
{ name: "message", description: "The message to send", required: true },
|
||||||
|
],
|
||||||
|
options: new Map([
|
||||||
|
[
|
||||||
|
"player",
|
||||||
|
{
|
||||||
|
name: "player",
|
||||||
|
shortName: "p",
|
||||||
|
description: "Target player for private message",
|
||||||
|
defaultValue: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"prefix",
|
||||||
|
{
|
||||||
|
name: "prefix",
|
||||||
|
description: "Message prefix",
|
||||||
|
defaultValue: "CC",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
action: ({ args, options, context }): Result<void, CliError> => {
|
||||||
|
if (!context.chatManager) {
|
||||||
|
print(
|
||||||
|
"Error: ChatManager not initialized. No chatbox peripherals found.",
|
||||||
|
);
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: ChatMessage = {
|
||||||
|
message: args.message as string,
|
||||||
|
targetPlayer: options.player as string | undefined,
|
||||||
|
prefix: options.prefix as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = context.chatManager.sendMessageImmediate(message);
|
||||||
|
if (result.isOk()) {
|
||||||
|
print(`Message sent immediately: "${String(args.message)}"`);
|
||||||
|
} else {
|
||||||
|
print(`Failed to send message: ${result.error.reason}`);
|
||||||
|
if (result.error.kind === "NoIdleChatbox") {
|
||||||
|
print("All chatboxes are currently busy. Try queuing instead.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok.EMPTY;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatStopCommand: Command<AppContext> = {
|
||||||
|
name: "stop",
|
||||||
|
description: "Stop the ChatManager",
|
||||||
|
action: ({ context }): Result<void, CliError> => {
|
||||||
|
if (!context.chatManager) {
|
||||||
|
print("Error: ChatManager not initialized.");
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = context.chatManager.stop();
|
||||||
|
if (result.isOk()) {
|
||||||
|
print("ChatManager stopped successfully.");
|
||||||
|
} else {
|
||||||
|
print(`Failed to stop ChatManager: ${result.error.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok.EMPTY;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatClearCommand: Command<AppContext> = {
|
||||||
|
name: "clear",
|
||||||
|
description: "Clear queues and buffer",
|
||||||
|
options: new Map([
|
||||||
|
[
|
||||||
|
"queues",
|
||||||
|
{
|
||||||
|
name: "queues",
|
||||||
|
shortName: "q",
|
||||||
|
description: "Clear message and toast queues",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"buffer",
|
||||||
|
{
|
||||||
|
name: "buffer",
|
||||||
|
shortName: "b",
|
||||||
|
description: "Clear received message buffer",
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
action: ({ options, context }): Result<void, CliError> => {
|
||||||
|
if (!context.chatManager) {
|
||||||
|
print("Error: ChatManager not initialized.");
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearQueues = options.queues as boolean;
|
||||||
|
const clearBuffer = options.buffer as boolean;
|
||||||
|
|
||||||
|
if (!clearQueues && !clearBuffer) {
|
||||||
|
print("Specify --queues or --buffer (or both) to clear.");
|
||||||
|
return Ok.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
if (clearQueues) {
|
||||||
|
const result = context.chatManager.clearQueues();
|
||||||
|
if (result.isOk()) {
|
||||||
|
results.push("Queues cleared");
|
||||||
|
} else {
|
||||||
|
results.push(`Failed to clear queues: ${result.error.reason}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearBuffer) {
|
||||||
|
const result = context.chatManager.clearBuffer();
|
||||||
|
if (result.isOk()) {
|
||||||
|
results.push("Buffer cleared");
|
||||||
|
} else {
|
||||||
|
results.push(`Failed to clear buffer: ${result.error.reason}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach((msg) => print(msg));
|
||||||
|
return Ok.EMPTY;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatCommand: Command<AppContext> = {
|
||||||
|
name: "chat",
|
||||||
|
description: "Chat management commands using ChatManager",
|
||||||
|
subcommands: new Map([
|
||||||
|
["send", chatSendCommand],
|
||||||
|
["send-immediate", chatSendImmediateCommand],
|
||||||
|
["toast", chatToastCommand],
|
||||||
|
["status", chatStatusCommand],
|
||||||
|
["receive", chatReceiveCommand],
|
||||||
|
["stop", chatStopCommand],
|
||||||
|
["clear", chatClearCommand],
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
// 3. Define root command
|
// 3. Define root command
|
||||||
const rootCommand: Command<AppContext> = {
|
const rootCommand: Command<AppContext> = {
|
||||||
name: "calculator",
|
name: "calculator",
|
||||||
description: "A feature-rich calculator program",
|
description: "A feature-rich calculator and chat management program",
|
||||||
options: new Map([
|
options: new Map([
|
||||||
[
|
[
|
||||||
"debug",
|
"debug",
|
||||||
@@ -172,6 +511,7 @@ const rootCommand: Command<AppContext> = {
|
|||||||
["math", mathCommand],
|
["math", mathCommand],
|
||||||
["greet", greetCommand],
|
["greet", greetCommand],
|
||||||
["config", configCommand],
|
["config", configCommand],
|
||||||
|
["chat", chatCommand],
|
||||||
]),
|
]),
|
||||||
action: ({ options, context }): Result<void, CliError> => {
|
action: ({ options, context }): Result<void, CliError> => {
|
||||||
// Update debug mode from command line option
|
// Update debug mode from command line option
|
||||||
@@ -183,20 +523,58 @@ const rootCommand: Command<AppContext> = {
|
|||||||
|
|
||||||
print(`Welcome to ${context.appName}!`);
|
print(`Welcome to ${context.appName}!`);
|
||||||
print("Use --help to see available commands");
|
print("Use --help to see available commands");
|
||||||
|
|
||||||
|
if (context.chatManager) {
|
||||||
|
print("ChatManager initialized and ready!");
|
||||||
|
} else {
|
||||||
|
print("Note: No chatbox peripherals found - chat commands unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
return Ok.EMPTY;
|
return Ok.EMPTY;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Create global context instance
|
// 4. Initialize ChatManager if chatbox peripherals are available
|
||||||
|
function initializeChatManager(): ChatManager | undefined {
|
||||||
|
// Find all available chatbox peripherals
|
||||||
|
const peripheralNames = peripheral.getNames();
|
||||||
|
const chatboxPeripherals: ChatBoxPeripheral[] = [];
|
||||||
|
|
||||||
|
for (const name of peripheralNames) {
|
||||||
|
const peripheralType = peripheral.getType(name);
|
||||||
|
if (peripheralType[0] === "chatBox") {
|
||||||
|
const chatbox = peripheral.wrap(name) as ChatBoxPeripheral;
|
||||||
|
chatboxPeripherals.push(chatbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatboxPeripherals.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatManager = new ChatManager(chatboxPeripherals);
|
||||||
|
|
||||||
|
// Start ChatManager in async mode so it doesn't block the CLI
|
||||||
|
const runResult = chatManager.runAsync();
|
||||||
|
if (runResult.isErr()) {
|
||||||
|
print(`Warning: Failed to start ChatManager: ${runResult.error.reason}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Create global context instance
|
||||||
const appContext: AppContext = {
|
const appContext: AppContext = {
|
||||||
appName: "MyAwesomeCalculator",
|
appName: "MyAwesome Calculator & Chat Manager",
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
log: (message) => {
|
log: (message) => {
|
||||||
print(`[LOG] ${message}`);
|
print(`[LOG] ${message}`);
|
||||||
},
|
},
|
||||||
|
chatManager: initializeChatManager(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. Create and export CLI handler
|
// 6. Create and export CLI handler
|
||||||
const cli = createCli(rootCommand, { globalContext: appContext });
|
const cli = createCli(rootCommand, { globalContext: appContext });
|
||||||
const args = [...$vararg];
|
const args = [...$vararg];
|
||||||
cli(args);
|
cli(args);
|
||||||
@@ -215,11 +593,24 @@ cli(['greet', '-n', 'World', '-t', '3']); // Output: Hello, World! (3 times)
|
|||||||
cli(['config', 'show']); // Shows current config
|
cli(['config', 'show']); // Shows current config
|
||||||
cli(['config', 'set', 'theme', 'dark']); // Sets config
|
cli(['config', 'set', 'theme', 'dark']); // Sets config
|
||||||
|
|
||||||
|
// Chat management (requires chatbox peripherals)
|
||||||
|
cli(['chat', 'status']); // Shows ChatManager status
|
||||||
|
cli(['chat', 'send', 'Hello World!']); // Sends global message (queued)
|
||||||
|
cli(['chat', 'send', 'Hi there!', '--player', 'Steve']); // Private message (queued)
|
||||||
|
cli(['chat', 'send-immediate', 'Urgent!', '--player', 'Admin']); // Immediate send
|
||||||
|
cli(['chat', 'toast', 'Steve', 'Alert', 'Server restart in 5 minutes']); // Toast notification
|
||||||
|
cli(['chat', 'receive', '--count', '5']); // Check for received messages
|
||||||
|
cli(['chat', 'clear', '--queues']); // Clear pending queues
|
||||||
|
cli(['chat', 'clear', '--buffer']); // Clear received buffer
|
||||||
|
cli(['chat', 'stop']); // Stop ChatManager
|
||||||
|
|
||||||
// Help examples
|
// Help examples
|
||||||
cli(['--help']); // Shows root help
|
cli(['--help']); // Shows root help
|
||||||
cli(['math', '--help']); // Shows math command help
|
cli(['math', '--help']); // Shows math command help
|
||||||
cli(['config', 'set', '--help']); // Shows config set help
|
cli(['chat', '--help']); // Shows chat command help
|
||||||
|
cli(['chat', 'send', '--help']); // Shows chat send help
|
||||||
|
|
||||||
// Debug mode
|
// Debug mode
|
||||||
cli(['--debug', 'math', 'add', '1', '2']); // Enables debug logging
|
cli(['--debug', 'math', 'add', '1', '2']); // Enables debug logging
|
||||||
|
cli(['--debug', 'chat', 'status']); // Debug mode with chat status
|
||||||
*/
|
*/
|
||||||
|
|||||||
637
src/lib/ChatManager.ts
Normal file
637
src/lib/ChatManager.ts
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
import { Queue } from "./datatype/Queue";
|
||||||
|
import { ChatBoxEvent, pullEventAs } from "./event";
|
||||||
|
import { Result, Ok, Err } from "./thirdparty/ts-result-es";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat manager error types
|
||||||
|
*/
|
||||||
|
export interface ChatManagerError {
|
||||||
|
kind: "ChatManager";
|
||||||
|
reason: string;
|
||||||
|
chatboxIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoIdleChatboxError {
|
||||||
|
kind: "NoIdleChatbox";
|
||||||
|
reason: "All chatboxes are busy";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendFailureError {
|
||||||
|
kind: "SendFailure";
|
||||||
|
reason: string;
|
||||||
|
chatboxIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmptyBufferError {
|
||||||
|
kind: "EmptyBuffer";
|
||||||
|
reason: "No messages in buffer";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatError =
|
||||||
|
| ChatManagerError
|
||||||
|
| NoIdleChatboxError
|
||||||
|
| SendFailureError
|
||||||
|
| EmptyBufferError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base interface for chat messages and toasts
|
||||||
|
*/
|
||||||
|
interface ChatBasicMessage {
|
||||||
|
message: string | MinecraftTextComponent;
|
||||||
|
prefix?: string;
|
||||||
|
brackets?: string;
|
||||||
|
bracketColor?: string;
|
||||||
|
range?: number;
|
||||||
|
utf8Support?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for chat toast notifications
|
||||||
|
*/
|
||||||
|
export interface ChatToast extends ChatBasicMessage {
|
||||||
|
/** Target player username to send the toast to */
|
||||||
|
username: string;
|
||||||
|
/** Title of the toast notification */
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for regular chat messages
|
||||||
|
*/
|
||||||
|
export interface ChatMessage extends ChatBasicMessage {
|
||||||
|
/** Optional target player username for private messages */
|
||||||
|
targetPlayer?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatManager class for managing multiple ChatBox peripherals
|
||||||
|
* Handles message queuing, sending with cooldown management, and event receiving
|
||||||
|
* Uses Result types for robust error handling
|
||||||
|
*/
|
||||||
|
export class ChatManager {
|
||||||
|
/** Array of all available ChatBox peripherals */
|
||||||
|
private chatboxes: ChatBoxPeripheral[];
|
||||||
|
|
||||||
|
/** Queue for pending chat messages */
|
||||||
|
private messageQueue = new Queue<ChatMessage>();
|
||||||
|
|
||||||
|
/** Queue for pending toast notifications */
|
||||||
|
private toastQueue = new Queue<ChatToast>();
|
||||||
|
|
||||||
|
/** Buffer for received chat events */
|
||||||
|
private chatBuffer = new Queue<ChatBoxEvent>();
|
||||||
|
|
||||||
|
/** Array tracking which chatboxes are currently idle (not in cooldown) */
|
||||||
|
private idleChatboxes: boolean[];
|
||||||
|
|
||||||
|
/** Flag
|
||||||
|
to control the running state of loops */
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor - initializes the ChatManager with available ChatBox peripherals
|
||||||
|
* @param peripherals Array of ChatBox peripherals to manage
|
||||||
|
*/
|
||||||
|
constructor(peripherals: ChatBoxPeripheral[]) {
|
||||||
|
if (peripherals.length === 0) {
|
||||||
|
throw new Error("ChatManager requires at least one ChatBox peripheral");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chatboxes = peripherals;
|
||||||
|
// Initially all chatboxes are idle
|
||||||
|
this.idleChatboxes = peripherals.map(() => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a chat message to the sending queue
|
||||||
|
* @param message The chat message to send
|
||||||
|
* @returns Result indicating success or failure
|
||||||
|
*/
|
||||||
|
public sendMessage(message: ChatMessage): Result<void, ChatManagerError> {
|
||||||
|
try {
|
||||||
|
this.messageQueue.enqueue(message);
|
||||||
|
return new Ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: `Failed to enqueue message: ${String(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a toast notification to the sending queue
|
||||||
|
* @param toast The toast notification to send
|
||||||
|
* @returns Result indicating success or failure
|
||||||
|
*/
|
||||||
|
public sendToast(toast: ChatToast): Result<void, ChatManagerError> {
|
||||||
|
try {
|
||||||
|
this.toastQueue.enqueue(toast);
|
||||||
|
return new Ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: `Failed to enqueue toast: ${String(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves and removes the next received chat event from the buffer
|
||||||
|
* @returns Result containing the chat event or an error if buffer is empty
|
||||||
|
*/
|
||||||
|
public getReceivedMessage(): Result<ChatBoxEvent, EmptyBufferError> {
|
||||||
|
const event = this.chatBuffer.dequeue();
|
||||||
|
if (event === undefined) {
|
||||||
|
return new Err({
|
||||||
|
kind: "EmptyBuffer",
|
||||||
|
reason: "No messages in buffer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Ok(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the first available (idle) chatbox
|
||||||
|
* @returns Result containing chatbox index or error if none available
|
||||||
|
*/
|
||||||
|
private findIdleChatbox(): Result<number, NoIdleChatboxError> {
|
||||||
|
for (let i = 0; i < this.idleChatboxes.length; i++) {
|
||||||
|
if (this.idleChatboxes[i]) {
|
||||||
|
return new Ok(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Err({
|
||||||
|
kind: "NoIdleChatbox",
|
||||||
|
reason: "All chatboxes are busy",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a chatbox as busy and sets up a timer to mark it as idle after cooldown
|
||||||
|
* @param chatboxIndex Index of the chatbox to mark as busy
|
||||||
|
* @returns Result indicating success or failure
|
||||||
|
*/
|
||||||
|
private setChatboxBusy(chatboxIndex: number): Result<void, ChatManagerError> {
|
||||||
|
if (chatboxIndex < 0 || chatboxIndex >= this.idleChatboxes.length) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: "Invalid chatbox index",
|
||||||
|
chatboxIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.idleChatboxes[chatboxIndex] = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Set timer to mark chatbox as idle after 1 second cooldown
|
||||||
|
const timerId = os.startTimer(1);
|
||||||
|
|
||||||
|
// Start a coroutine to wait for the timer and mark chatbox as idle
|
||||||
|
coroutine.resume(
|
||||||
|
coroutine.create(() => {
|
||||||
|
while (true) {
|
||||||
|
const [_eventName, id] = os.pullEvent("timer");
|
||||||
|
if (id === timerId) {
|
||||||
|
this.idleChatboxes[chatboxIndex] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
// Revert chatbox state if timer setup fails
|
||||||
|
this.idleChatboxes[chatboxIndex] = true;
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: `Failed to set chatbox timer: ${String(error)}`,
|
||||||
|
chatboxIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to send a chat message using an available chatbox
|
||||||
|
* @param message The message to send
|
||||||
|
* @returns Result indicating success or failure with error details
|
||||||
|
*/
|
||||||
|
private trySendMessage(message: ChatMessage): Result<void, ChatError> {
|
||||||
|
const chatboxResult = this.findIdleChatbox();
|
||||||
|
if (chatboxResult.isErr()) {
|
||||||
|
return chatboxResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatboxIndex = chatboxResult.value;
|
||||||
|
const chatbox = this.chatboxes[chatboxIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
let success: boolean;
|
||||||
|
let errorMsg: string | undefined;
|
||||||
|
|
||||||
|
// Determine the appropriate sending method based on message properties
|
||||||
|
if (message.targetPlayer !== undefined) {
|
||||||
|
// Send private message to specific player
|
||||||
|
if (typeof message.message === "string") {
|
||||||
|
[success, errorMsg] = chatbox.sendMessageToPlayer(
|
||||||
|
message.message,
|
||||||
|
message.targetPlayer,
|
||||||
|
message.prefix,
|
||||||
|
message.brackets,
|
||||||
|
message.bracketColor,
|
||||||
|
message.range,
|
||||||
|
message.utf8Support,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Handle MinecraftTextComponent for private message
|
||||||
|
[success, errorMsg] = chatbox.sendFormattedMessageToPlayer(
|
||||||
|
JSON.stringify(message.message),
|
||||||
|
message.targetPlayer,
|
||||||
|
message.prefix,
|
||||||
|
message.brackets,
|
||||||
|
message.bracketColor,
|
||||||
|
message.range,
|
||||||
|
message.utf8Support,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Send global message
|
||||||
|
if (typeof message.message === "string") {
|
||||||
|
[success, errorMsg] = chatbox.sendMessage(
|
||||||
|
message.message,
|
||||||
|
message.prefix,
|
||||||
|
message.brackets,
|
||||||
|
message.bracketColor,
|
||||||
|
message.range,
|
||||||
|
message.utf8Support,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Handle MinecraftTextComponent for global message
|
||||||
|
[success, errorMsg] = chatbox.sendFormattedMessage(
|
||||||
|
JSON.stringify(message.message),
|
||||||
|
message.prefix,
|
||||||
|
message.brackets,
|
||||||
|
message.bracketColor,
|
||||||
|
message.range,
|
||||||
|
message.utf8Support,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Mark chatbox as busy for cooldown period
|
||||||
|
const busyResult = this.setChatboxBusy(chatboxIndex);
|
||||||
|
if (busyResult.isErr()) {
|
||||||
|
return busyResult;
|
||||||
|
}
|
||||||
|
return new Ok(undefined);
|
||||||
|
} else {
|
||||||
|
return new Err({
|
||||||
|
kind: "SendFailure",
|
||||||
|
reason: errorMsg ?? "Unknown send failure",
|
||||||
|
chatboxIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return new Err({
|
||||||
|
kind: "SendFailure",
|
||||||
|
reason: `Exception during send: ${String(error)}`,
|
||||||
|
chatboxIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to send a toast notification using an available chatbox
|
||||||
|
* @param toast The toast to send
|
||||||
|
* @returns Result indicating success or failure with error details
|
||||||
|
*/
|
||||||
|
private trySendToast(toast: ChatToast): Result<void, ChatError> {
|
||||||
|
const chatboxResult = this.findIdleChatbox();
|
||||||
|
if (chatboxResult.isErr()) {
|
||||||
|
return chatboxResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatboxIndex = chatboxResult.value;
|
||||||
|
const chatbox = this.chatboxes[chatboxIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
let success: boolean;
|
||||||
|
let errorMsg: string | undefined;
|
||||||
|
|
||||||
|
// Send toast notification
|
||||||
|
if (
|
||||||
|
typeof toast.message === "string" &&
|
||||||
|
typeof toast.title === "string"
|
||||||
|
) {
|
||||||
|
[success, errorMsg] = chatbox.sendToastToPlayer(
|
||||||
|
toast.message,
|
||||||
|
toast.title,
|
||||||
|
toast.username,
|
||||||
|
toast.prefix,
|
||||||
|
toast.brackets,
|
||||||
|
toast.bracketColor,
|
||||||
|
toast.range,
|
||||||
|
toast.utf8Support,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Handle MinecraftTextComponent for toast
|
||||||
|
const messageJson =
|
||||||
|
typeof toast.message === "string"
|
||||||
|
? toast.message
|
||||||
|
: JSON.stringify(toast.message);
|
||||||
|
const titleJson =
|
||||||
|
typeof toast.title === "string"
|
||||||
|
? toast.title
|
||||||
|
: JSON.stringify(toast.title);
|
||||||
|
|
||||||
|
[success, errorMsg] = chatbox.sendFormattedToastToPlayer(
|
||||||
|
messageJson,
|
||||||
|
titleJson,
|
||||||
|
toast.username,
|
||||||
|
toast.prefix,
|
||||||
|
toast.brackets,
|
||||||
|
toast.bracketColor,
|
||||||
|
toast.range,
|
||||||
|
toast.utf8Support,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Mark chatbox as busy for cooldown period
|
||||||
|
const busyResult = this.setChatboxBusy(chatboxIndex);
|
||||||
|
if (busyResult.isErr()) {
|
||||||
|
return busyResult;
|
||||||
|
}
|
||||||
|
return new Ok(undefined);
|
||||||
|
} else {
|
||||||
|
return new Err({
|
||||||
|
kind: "SendFailure",
|
||||||
|
reason: errorMsg ?? "Unknown toast send failure",
|
||||||
|
chatboxIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return new Err({
|
||||||
|
kind: "SendFailure",
|
||||||
|
reason: `Exception during toast send: ${String(error)}`,
|
||||||
|
chatboxIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main sending loop - continuously processes message and toast queues
|
||||||
|
* Runs in a separate coroutine to handle sending with proper timing
|
||||||
|
*/
|
||||||
|
private sendLoop(): void {
|
||||||
|
while (this.isRunning) {
|
||||||
|
let sentSomething = false;
|
||||||
|
|
||||||
|
// Try to send a message if queue is not empty
|
||||||
|
if (this.messageQueue.size() > 0) {
|
||||||
|
const message = this.messageQueue.peek();
|
||||||
|
if (message) {
|
||||||
|
const result = this.trySendMessage(message);
|
||||||
|
if (result.isOk()) {
|
||||||
|
this.messageQueue.dequeue(); // Remove from queue only if successfully sent
|
||||||
|
sentSomething = true;
|
||||||
|
} else if (result.error.kind === "SendFailure") {
|
||||||
|
// Log send failures but keep trying
|
||||||
|
print(`Failed to send message: ${result.error.reason}`);
|
||||||
|
this.messageQueue.dequeue(); // Remove failed message to prevent infinite retry
|
||||||
|
}
|
||||||
|
// For NoIdleChatbox errors, we keep the message in queue and try again later
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send a toast if queue is not empty
|
||||||
|
if (this.toastQueue.size() > 0) {
|
||||||
|
const toast = this.toastQueue.peek();
|
||||||
|
if (toast) {
|
||||||
|
const result = this.trySendToast(toast);
|
||||||
|
if (result.isOk()) {
|
||||||
|
this.toastQueue.dequeue(); // Remove from queue only if successfully sent
|
||||||
|
sentSomething = true;
|
||||||
|
} else if (result.error.kind === "SendFailure") {
|
||||||
|
// Log send failures but keep trying
|
||||||
|
print(`Failed to send toast: ${result.error.reason}`);
|
||||||
|
this.toastQueue.dequeue(); // Remove failed toast to prevent infinite retry
|
||||||
|
}
|
||||||
|
// For NoIdleChatbox errors, we keep the toast in queue and try again later
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small sleep to prevent busy waiting and allow other coroutines to run
|
||||||
|
if (!sentSomething) {
|
||||||
|
sleep(0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main receiving loop - continuously listens for chat events
|
||||||
|
* Runs in a separate coroutine to handle incoming messages
|
||||||
|
*/
|
||||||
|
private receiveLoop(): void {
|
||||||
|
while (this.isRunning) {
|
||||||
|
try {
|
||||||
|
// Listen for chatbox_message events (note: event name might be "chat" based on event.ts)
|
||||||
|
const event = pullEventAs(ChatBoxEvent, "chat");
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
// Store received event in buffer for user processing
|
||||||
|
this.chatBuffer.enqueue(event);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Log receive errors but continue running
|
||||||
|
print(`Error in receive loop: ${String(error)}`);
|
||||||
|
sleep(0.1); // Brief pause before retrying
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the ChatManager's main operation loops
|
||||||
|
* Launches both sending and receiving coroutines in parallel
|
||||||
|
* @returns Result indicating success or failure of startup
|
||||||
|
*/
|
||||||
|
public run(): Result<void, ChatManagerError> {
|
||||||
|
if (this.isRunning) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: "ChatManager is already running",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
// Start both send and receive loops in parallel
|
||||||
|
parallel.waitForAll(
|
||||||
|
() => this.sendLoop(),
|
||||||
|
() => this.receiveLoop(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
this.isRunning = false;
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: `Failed to start ChatManager: ${String(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the ChatManager asynchronously without blocking
|
||||||
|
* Useful when you need to run other code alongside the ChatManager
|
||||||
|
* @returns Result indicating success or failure of async startup
|
||||||
|
*/
|
||||||
|
public runAsync(): Result<void, ChatManagerError> {
|
||||||
|
if (this.isRunning) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: "ChatManager is already running",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
// Start the run method in a separate coroutine
|
||||||
|
coroutine.resume(
|
||||||
|
coroutine.create(() => {
|
||||||
|
const result = this.run();
|
||||||
|
if (result.isErr()) {
|
||||||
|
print(`ChatManager async error: ${result.error.reason}`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
this.isRunning = false;
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: `Failed to start ChatManager async: ${String(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the ChatManager loops gracefully
|
||||||
|
* @returns Result indicating success or failure of shutdown
|
||||||
|
*/
|
||||||
|
public stop(): Result<void, ChatManagerError> {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: "ChatManager is not running",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isRunning = false;
|
||||||
|
return new Ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: `Failed to stop ChatManager: ${String(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of pending messages in the queue
|
||||||
|
* @returns Number of pending messages
|
||||||
|
*/
|
||||||
|
public getPendingMessageCount(): number {
|
||||||
|
return this.messageQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of pending toasts in the queue
|
||||||
|
* @returns Number of pending toasts
|
||||||
|
*/
|
||||||
|
public getPendingToastCount(): number {
|
||||||
|
return this.toastQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of received messages in the buffer
|
||||||
|
* @returns Number of buffered received messages
|
||||||
|
*/
|
||||||
|
public getBufferedMessageCount(): number {
|
||||||
|
return this.chatBuffer.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current status of all chatboxes
|
||||||
|
* @returns Array of boolean values indicating which chatboxes are idle
|
||||||
|
*/
|
||||||
|
public getChatboxStatus(): boolean[] {
|
||||||
|
return [...this.idleChatboxes]; // Return a copy to prevent external modification
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the running state of the ChatManager
|
||||||
|
* @returns true if ChatManager is currently running
|
||||||
|
*/
|
||||||
|
public isManagerRunning(): boolean {
|
||||||
|
return this.isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all pending messages and toasts from queues
|
||||||
|
* Does not affect the received message buffer
|
||||||
|
* @returns Result indicating success or failure
|
||||||
|
*/
|
||||||
|
public clearQueues(): Result<void, ChatManagerError> {
|
||||||
|
try {
|
||||||
|
this.messageQueue.clear();
|
||||||
|
this.toastQueue.clear();
|
||||||
|
return new Ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: `Failed to clear queues: ${String(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the received message buffer
|
||||||
|
* @returns Result indicating success or failure
|
||||||
|
*/
|
||||||
|
public clearBuffer(): Result<void, ChatManagerError> {
|
||||||
|
try {
|
||||||
|
this.chatBuffer.clear();
|
||||||
|
return new Ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
return new Err({
|
||||||
|
kind: "ChatManager",
|
||||||
|
reason: `Failed to clear buffer: ${String(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to send a message immediately, bypassing the queue
|
||||||
|
* @param message The message to send immediately
|
||||||
|
* @returns Result indicating success or failure with error details
|
||||||
|
*/
|
||||||
|
public sendMessageImmediate(message: ChatMessage): Result<void, ChatError> {
|
||||||
|
return this.trySendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to send a toast immediately, bypassing the queue
|
||||||
|
* @param toast The toast to send immediately
|
||||||
|
* @returns Result indicating success or failure with error details
|
||||||
|
*/
|
||||||
|
public sendToastImmediate(toast: ChatToast): Result<void, ChatError> {
|
||||||
|
return this.trySendToast(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user