mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-29 12:57:50 +08:00
Compare commits
4 Commits
6d5cf11f2b
...
82a9fec46d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82a9fec46d | ||
|
|
94f0de4c90 | ||
|
|
cf7ddefc2e | ||
|
|
3287661318 |
33
README.md
33
README.md
@@ -5,6 +5,7 @@ A collection of advanced utilities and libraries for Minecraft ComputerCraft, wr
|
||||
## Features
|
||||
|
||||
### 1. Access Control System
|
||||
|
||||
A comprehensive system for managing player access to a specific area. It uses a `playerDetector` to monitor for players in range and a `chatBox` to interact with them and administrators.
|
||||
|
||||
- **Player Detection:** Monitors a configurable range for players.
|
||||
@@ -15,6 +16,7 @@ A comprehensive system for managing player access to a specific area. It uses a
|
||||
- **Logging:** Detailed logging of events, viewable with the included `logviewer` program.
|
||||
|
||||
### 2. AutoCraft System
|
||||
|
||||
An automated crafting solution designed to work with the Create mod's packaged recipes.
|
||||
|
||||
- **Automated Crafting:** Detects cardboard packages in a chest and automatically crafts the recipes they contain.
|
||||
@@ -22,6 +24,7 @@ An automated crafting solution designed to work with the Create mod's packaged r
|
||||
- **Inventory Management:** Manages pulling ingredients from a source inventory and pushing crafted items to a destination.
|
||||
|
||||
### 3. ccTUI Framework
|
||||
|
||||
A declarative, reactive TUI (Terminal User Interface) framework inspired by [SolidJS](https://www.solidjs.com/) for building complex and interactive interfaces in ComputerCraft.
|
||||
|
||||
- **Declarative Syntax:** Build UIs with simple, composable functions like `div`, `label`, `button`, and `input`.
|
||||
@@ -31,6 +34,7 @@ A declarative, reactive TUI (Terminal User Interface) framework inspired by [Sol
|
||||
- **Component-Based:** Structure your UI into reusable components. See `src/tuiExample/main.ts` for a demo.
|
||||
|
||||
### 4. ccCLI Framework
|
||||
|
||||
A lightweight, functional-style framework for building command-line interfaces (CLIs) within CC:Tweaked. It supports nested commands, arguments, options, and automatic help generation. See the [ccCLI Documentation](./docs/ccCLI.md) for more details.
|
||||
|
||||
- **Declarative API:** Define commands, arguments, and options using a simple, object-based structure.
|
||||
@@ -40,6 +44,7 @@ A lightweight, functional-style framework for building command-line interfaces (
|
||||
- **Type-Safe:** Built with TypeScript for robust development.
|
||||
|
||||
### 5. Core Libraries
|
||||
|
||||
- **`ChatManager`:** A powerful manager for `chatBox` peripherals that handles message queuing, cooldowns, and asynchronous sending/receiving. See the [ChatManager Documentation](./docs/ChatManager.md) for more details.
|
||||
- **`ccStructLog`:** A modern, structured logging library inspired by Python's `structlog`. It provides a flexible, extensible framework based on processors, renderers, and streams, designed for CC:Tweaked. See the [ccStructLog Documentation](./docs/ccStructLog.md) for more details.
|
||||
- **`PeripheralManager`:** A utility for easily finding and requiring peripherals by name or type.
|
||||
@@ -54,6 +59,7 @@ A lightweight, functional-style framework for building command-line interfaces (
|
||||
## Setup & Installation
|
||||
|
||||
1. **Clone the repository:**
|
||||
|
||||
```sh
|
||||
git clone <repository-url>
|
||||
cd cc-utils
|
||||
@@ -69,6 +75,7 @@ A lightweight, functional-style framework for building command-line interfaces (
|
||||
This project uses `just` to manage build tasks. The compiled Lua files will be placed in the `build/` directory.
|
||||
|
||||
- **Build all modules:**
|
||||
|
||||
```sh
|
||||
just build
|
||||
```
|
||||
@@ -105,44 +112,30 @@ To deploy the built programs to your in-game computer, you need to configure the
|
||||
### Access Control
|
||||
|
||||
- **Start the system:**
|
||||
|
||||
```sh
|
||||
accesscontrol start
|
||||
```
|
||||
|
||||
- **Open the configuration TUI:**
|
||||
|
||||
```sh
|
||||
accesscontrol config
|
||||
```
|
||||
Alternatively, press `c` while the main program is running.
|
||||
|
||||
- **View logs:**
|
||||
```sh
|
||||
logviewer accesscontrol.log
|
||||
```
|
||||
Alternatively, press `c` while the main program is running.
|
||||
|
||||
- **Admin Commands (in-game chat):**
|
||||
```
|
||||
@AC /help
|
||||
@AC /add user Notch
|
||||
@AC /list
|
||||
@AC help
|
||||
@AC add user Notch
|
||||
@AC list
|
||||
```
|
||||
|
||||
### AutoCraft
|
||||
|
||||
The autocraft program runs in the background. Simply run it on a turtle with the correct peripheral setup (see `src/autocraft/main.ts`). It will automatically process packages placed in the designated chest.
|
||||
|
||||
```sh
|
||||
autocraft
|
||||
```
|
||||
|
||||
### TUI Example
|
||||
|
||||
Run the example program to see a demonstration of the `ccTUI` framework.
|
||||
|
||||
```sh
|
||||
tuiExample
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
- **Lint and format the code:**
|
||||
|
||||
@@ -11,16 +11,37 @@ A modern, structured logging library for CC:Tweaked, inspired by Python's struct
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { createDevLogger } from "@/lib/ccStructLog";
|
||||
The easiest way to get started is to create a `Logger` instance and configure it with processors, a renderer, and streams.
|
||||
|
||||
// Create a development logger
|
||||
const logger = createDevLogger();
|
||||
Here's a simple example of a logger that prints colored, human-readable messages to the console:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Logger,
|
||||
LogLevel,
|
||||
processor,
|
||||
textRenderer,
|
||||
ConsoleStream,
|
||||
} from "@/lib/ccStructLog";
|
||||
|
||||
// Create a logger
|
||||
const logger = new Logger({
|
||||
processors: [
|
||||
processor.addTimestamp({ format: "%T" }), // Add HH:MM:SS timestamp
|
||||
processor.filterByLevel(LogLevel.Info), // Log Info and higher
|
||||
processor.addSource("MyApp"),
|
||||
],
|
||||
renderer: textRenderer,
|
||||
streams: [new ConsoleStream()],
|
||||
});
|
||||
|
||||
// Log messages with context
|
||||
logger.info("Server started", { port: 8080, version: "1.0.0" });
|
||||
logger.warn("Low disk space", { available: 1024, threshold: 2048 });
|
||||
logger.error("Connection failed", { host: "example.com", retries: 3 });
|
||||
|
||||
// This debug message will be filtered out by `filterByLevel`
|
||||
logger.debug("This is a debug message.");
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
@@ -46,48 +67,69 @@ export enum LogLevel {
|
||||
4. **Render**: The final event is converted to a string by a renderer (e.g., `textRenderer`, `jsonRenderer`).
|
||||
5. **Output**: The string is sent to one or more streams (e.g., console, file).
|
||||
|
||||
## Pre-configured Loggers
|
||||
## Common Configurations
|
||||
|
||||
### Development Logger
|
||||
Optimized for development and debugging with human-readable console output.
|
||||
A typical development logger is configured for human-readable console output with timestamps and colors.
|
||||
|
||||
```typescript
|
||||
import { createDevLogger, LogLevel } from "@/lib/ccStructLog";
|
||||
import {
|
||||
Logger,
|
||||
processor,
|
||||
textRenderer,
|
||||
ConsoleStream,
|
||||
} from "@/lib/ccStructLog";
|
||||
|
||||
const logger = createDevLogger({
|
||||
source: "MyApp",
|
||||
includeComputerId: true,
|
||||
const devLogger = new Logger({
|
||||
processors: [
|
||||
processor.addTimestamp({ format: "%F %T" }), // YYYY-MM-DD HH:MM:SS
|
||||
processor.addSource("DevApp"),
|
||||
processor.addComputerId(),
|
||||
],
|
||||
renderer: textRenderer,
|
||||
streams: [new ConsoleStream()],
|
||||
});
|
||||
|
||||
logger.debug("This is a debug message.");
|
||||
devLogger.debug("This is a debug message.", { user: "dev" });
|
||||
```
|
||||
|
||||
### Production Logger
|
||||
Optimized for production with JSON-formatted file output and daily rotation.
|
||||
|
||||
```typescript
|
||||
import { createProdLogger, DAY } from "@/lib/ccStructLog";
|
||||
|
||||
const logger = createProdLogger("app.log", {
|
||||
source: "MyApp",
|
||||
rotationInterval: DAY, // Rotate daily
|
||||
includeConsole: false, // Don't log to console
|
||||
});
|
||||
|
||||
logger.info("Application is running in production.");
|
||||
```
|
||||
|
||||
## Custom Configuration
|
||||
|
||||
You can create a logger with a completely custom setup.
|
||||
A production logger is often configured to write machine-readable JSON logs to a file with daily rotation.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Logger,
|
||||
LogLevel,
|
||||
addFullTimestamp,
|
||||
addComputerId,
|
||||
addSource,
|
||||
processor,
|
||||
jsonRenderer,
|
||||
FileStream,
|
||||
DAY,
|
||||
} from "@/lib/ccStructLog";
|
||||
|
||||
const prodLogger = new Logger({
|
||||
processors: [
|
||||
processor.addTimestamp(), // Default format is %F %T
|
||||
processor.filterByLevel(LogLevel.Info),
|
||||
processor.addSource("ProdApp"),
|
||||
processor.addComputerId(),
|
||||
],
|
||||
renderer: jsonRenderer,
|
||||
streams: [
|
||||
new FileStream("app.log", DAY), // Rotate daily
|
||||
],
|
||||
});
|
||||
|
||||
prodLogger.info("Application is running in production.");
|
||||
```
|
||||
|
||||
## Custom Configuration
|
||||
|
||||
You can create a logger with any combination of processors, renderers, and streams.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Logger,
|
||||
processor,
|
||||
jsonRenderer,
|
||||
FileStream,
|
||||
ConsoleStream,
|
||||
@@ -96,9 +138,9 @@ import {
|
||||
|
||||
const logger = new Logger({
|
||||
processors: [
|
||||
addFullTimestamp(),
|
||||
addComputerId(),
|
||||
addSource("MyApplication"),
|
||||
processor.addTimestamp(),
|
||||
processor.addComputerId(),
|
||||
processor.addSource("MyApplication"),
|
||||
],
|
||||
renderer: jsonRenderer,
|
||||
streams: [
|
||||
@@ -112,31 +154,40 @@ logger.info("Custom logger reporting for duty.", { user: "admin" });
|
||||
|
||||
## Processors
|
||||
|
||||
Processors are functions that modify, enrich, or filter log events before they are rendered.
|
||||
Processors are functions that modify, enrich, or filter log events before they are rendered. They are all available under the `processor` namespace.
|
||||
|
||||
### Built-in Processors
|
||||
```typescript
|
||||
import {
|
||||
addTimestamp, // Add structured timestamp
|
||||
addFormattedTimestamp, // Add HH:MM:SS string
|
||||
addFullTimestamp, // Add YYYY-MM-DD HH:MM:SS string
|
||||
filterByLevel, // Filter by minimum level
|
||||
filterBy, // Filter based on a custom predicate
|
||||
addSource, // Add source/logger name
|
||||
addComputerId, // Add computer ID
|
||||
addComputerLabel, // Add computer label
|
||||
addStaticFields, // Add static fields to all events
|
||||
transformField, // Transform a specific field's value
|
||||
removeFields, // Remove sensitive fields
|
||||
} from "@/lib/ccStructLog";
|
||||
import { Logger, LogLevel, processor } from "@/lib/ccStructLog";
|
||||
|
||||
// Usage example
|
||||
const logger = new Logger({
|
||||
processors: [
|
||||
addTimestamp(),
|
||||
addSource("MyApp"),
|
||||
filterByLevel(LogLevel.Warn), // Only allow Warn, Error, Fatal
|
||||
removeFields(["password", "token"]),
|
||||
// Adds a timestamp. Format is compatible with os.date().
|
||||
// Default: "%F %T" (e.g., "2023-10-27 15:30:00")
|
||||
processor.addTimestamp({ format: "%T" }), // e.g., "15:30:00"
|
||||
|
||||
// Filter by minimum level
|
||||
processor.filterByLevel(LogLevel.Warn), // Only allow Warn, Error, Fatal
|
||||
|
||||
// Filter based on a custom predicate
|
||||
processor.filterBy((event) => event.get("user") === "admin"),
|
||||
|
||||
// Add source/logger name
|
||||
processor.addSource("MyApp"),
|
||||
|
||||
// Add computer ID or label
|
||||
processor.addComputerId(),
|
||||
processor.addComputerLabel(),
|
||||
|
||||
// Add static fields to all events
|
||||
processor.addStaticFields({ env: "production", version: "1.2.3" }),
|
||||
|
||||
// Transform a specific field's value
|
||||
processor.transformField("user_id", (id) => `user_${id}`),
|
||||
|
||||
// Remove sensitive fields
|
||||
processor.removeFields(["password", "token"]),
|
||||
],
|
||||
// ... other config
|
||||
});
|
||||
@@ -172,10 +223,10 @@ Renderers convert the final `LogEvent` object into a string.
|
||||
import { textRenderer, jsonRenderer } from "@/lib/ccStructLog";
|
||||
|
||||
// textRenderer: Human-readable, colored output for the console.
|
||||
// Example: 15:30:45 [INFO] Message key=value
|
||||
// Example: [15:30:45] [INFO] Message key=value
|
||||
|
||||
// jsonRenderer: Machine-readable JSON output.
|
||||
// Example: {"level":"info","message":"Message","key":"value","timestamp":"..."}
|
||||
// Example: {"level":2,"message":"Message","key":"value","timestamp":"15:30:45"}
|
||||
```
|
||||
|
||||
## Streams
|
||||
@@ -185,12 +236,14 @@ Streams handle the final output destination. You can use multiple streams to sen
|
||||
### Built-in Streams
|
||||
```typescript
|
||||
import {
|
||||
ConsoleStream, // Output to CC:Tweaked terminal with colors
|
||||
FileStream, // Output to file with rotation support
|
||||
BufferStream, // Store in an in-memory buffer
|
||||
NullStream, // Discard all output
|
||||
ConsoleStream,
|
||||
FileStream,
|
||||
BufferStream,
|
||||
NullStream,
|
||||
ConditionalStream,
|
||||
LogLevel,
|
||||
DAY,
|
||||
} from "@/lib/ccStructLog";
|
||||
import { ConditionalStream } from "@/lib/ccStructLog/streams"; // Note direct import
|
||||
|
||||
// File stream with daily rotation
|
||||
const fileStream = new FileStream("app.log", DAY);
|
||||
@@ -207,7 +260,7 @@ const errorStream = new ConditionalStream(
|
||||
|
||||
## File Rotation
|
||||
|
||||
`FileStream` supports automatic file rotation based on time intervals.
|
||||
`FileStream` supports automatic file rotation based on time intervals. The rotation interval is specified in seconds as the second argument to the constructor.
|
||||
|
||||
```typescript
|
||||
import { FileStream, HOUR, DAY, WEEK } from "@/lib/ccStructLog";
|
||||
@@ -221,7 +274,7 @@ const dailyLog = new FileStream("app_daily.log", DAY);
|
||||
// Rotate weekly
|
||||
const weeklyLog = new FileStream("app_weekly.log", WEEK);
|
||||
|
||||
// No rotation
|
||||
// No rotation (pass 0 or undefined)
|
||||
const permanentLog = new FileStream("permanent.log", 0);
|
||||
```
|
||||
|
||||
@@ -243,15 +296,22 @@ const permanentLog = new FileStream("permanent.log", 0);
|
||||
- `error`: Errors that affect a single operation but not the whole app.
|
||||
- `fatal`: Critical errors that require the application to shut down.
|
||||
|
||||
3. **Use a `source`**: Identify which component generated the log.
|
||||
3. **Use a `source`**: Identify which component generated the log using `processor.addSource`.
|
||||
```typescript
|
||||
const logger = createDevLogger({ source: "UserService" });
|
||||
import { Logger, processor } from "@/lib/ccStructLog";
|
||||
|
||||
const logger = new Logger({
|
||||
processors: [processor.addSource("UserService")],
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
4. **Sanitize Sensitive Data**: Use a processor to remove passwords, API keys, etc.
|
||||
```typescript
|
||||
import { Logger, processor } from "@/lib/ccStructLog";
|
||||
|
||||
const secureLogger = new Logger({
|
||||
processors: [ removeFields(["password", "token"]) ],
|
||||
processors: [ processor.removeFields(["password", "token"]) ],
|
||||
//...
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CCLog, DAY, LogLevel } from "@/lib/ccLog";
|
||||
import { ToastConfig, UserGroupConfig, loadConfig } from "./config";
|
||||
import { createAccessControlCli } from "./cli";
|
||||
import { launchAccessControlTUI } from "./tui";
|
||||
@@ -7,14 +6,31 @@ import { ReadWriteLock } from "@/lib/mutex/ReadWriteLock";
|
||||
import { ChatManager } from "@/lib/ChatManager";
|
||||
import { gTimerManager } from "@/lib/TimerManager";
|
||||
import { KeyEvent, pullEventAs } from "@/lib/event";
|
||||
import {
|
||||
ConditionalStream,
|
||||
ConsoleStream,
|
||||
DAY,
|
||||
FileStream,
|
||||
Logger,
|
||||
LogLevel,
|
||||
processor,
|
||||
textRenderer,
|
||||
} from "@/lib/ccStructLog";
|
||||
|
||||
const args = [...$vararg];
|
||||
|
||||
// Init Log
|
||||
const logger = new CCLog("accesscontrol.log", {
|
||||
printTerminal: true,
|
||||
logInterval: DAY,
|
||||
outputMinLevel: LogLevel.Info,
|
||||
let isOnConsoleStream = true;
|
||||
const logger = new Logger({
|
||||
processors: [
|
||||
processor.filterByLevel(LogLevel.Info),
|
||||
processor.addTimestamp(),
|
||||
],
|
||||
renderer: textRenderer,
|
||||
streams: [
|
||||
new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream),
|
||||
new FileStream("accesscontrol.log", DAY),
|
||||
],
|
||||
});
|
||||
|
||||
// Load Config
|
||||
@@ -26,7 +42,7 @@ logger.debug(textutils.serialise(config, { allow_repetitions: true }));
|
||||
|
||||
// Peripheral
|
||||
const playerDetector = peripheral.find(
|
||||
"playerDetector",
|
||||
"playerDetector",
|
||||
)[0] as PlayerDetectorPeripheral;
|
||||
const chatBox = peripheral.find("chatBox")[0] as ChatBoxPeripheral;
|
||||
const chatManager: ChatManager = new ChatManager([chatBox]);
|
||||
@@ -37,410 +53,419 @@ let gWatchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
|
||||
let gIsRunning = true;
|
||||
|
||||
interface ParseParams {
|
||||
playerName?: string;
|
||||
groupName?: string;
|
||||
info?: PlayerInfo;
|
||||
playerName?: string;
|
||||
groupName?: string;
|
||||
info?: PlayerInfo;
|
||||
}
|
||||
|
||||
function reloadConfig() {
|
||||
let releaser = configLock.tryAcquireWrite();
|
||||
while (releaser === undefined) {
|
||||
sleep(1);
|
||||
releaser = configLock.tryAcquireWrite();
|
||||
}
|
||||
let releaser = configLock.tryAcquireWrite();
|
||||
while (releaser === undefined) {
|
||||
sleep(1);
|
||||
releaser = configLock.tryAcquireWrite();
|
||||
}
|
||||
|
||||
config = loadConfig(configFilepath)!;
|
||||
gInRangePlayers = [];
|
||||
gWatchPlayersInfo = [];
|
||||
releaser.release();
|
||||
logger.info("Reload config successfully!");
|
||||
config = loadConfig(configFilepath)!;
|
||||
gInRangePlayers = [];
|
||||
gWatchPlayersInfo = [];
|
||||
releaser.release();
|
||||
logger.info("Reload config successfully!");
|
||||
}
|
||||
|
||||
function safeParseTextComponent(
|
||||
component: MinecraftTextComponent,
|
||||
params?: ParseParams,
|
||||
component: MinecraftTextComponent,
|
||||
params?: ParseParams,
|
||||
): MinecraftTextComponent {
|
||||
const newComponent = deepCopy(component);
|
||||
const newComponent = deepCopy(component);
|
||||
|
||||
if (newComponent.text == undefined) {
|
||||
newComponent.text = "Wrong text, please contanct with admin";
|
||||
} else if (newComponent.text.includes("%")) {
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%playerName%",
|
||||
params?.playerName ?? "UnknowPlayer",
|
||||
);
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%groupName%",
|
||||
params?.groupName ?? "UnknowGroup",
|
||||
);
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%playerPosX%",
|
||||
params?.info?.x.toString() ?? "UnknowPosX",
|
||||
);
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%playerPosY%",
|
||||
params?.info?.y.toString() ?? "UnknowPosY",
|
||||
);
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%playerPosZ%",
|
||||
params?.info?.z.toString() ?? "UnknowPosZ",
|
||||
);
|
||||
}
|
||||
return newComponent;
|
||||
if (newComponent.text == undefined) {
|
||||
newComponent.text = "Wrong text, please contanct with admin";
|
||||
} else if (newComponent.text.includes("%")) {
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%playerName%",
|
||||
params?.playerName ?? "UnknowPlayer",
|
||||
);
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%groupName%",
|
||||
params?.groupName ?? "UnknowGroup",
|
||||
);
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%playerPosX%",
|
||||
params?.info?.x.toString() ?? "UnknowPosX",
|
||||
);
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%playerPosY%",
|
||||
params?.info?.y.toString() ?? "UnknowPosY",
|
||||
);
|
||||
newComponent.text = newComponent.text.replace(
|
||||
"%playerPosZ%",
|
||||
params?.info?.z.toString() ?? "UnknowPosZ",
|
||||
);
|
||||
}
|
||||
return newComponent;
|
||||
}
|
||||
|
||||
function sendMessage(
|
||||
toastConfig: ToastConfig,
|
||||
targetPlayer: string,
|
||||
params: ParseParams,
|
||||
toastConfig: ToastConfig,
|
||||
targetPlayer: string,
|
||||
params: ParseParams,
|
||||
) {
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
|
||||
chatManager.sendMessage({
|
||||
message: safeParseTextComponent(
|
||||
toastConfig.msg ?? config.welcomeToastConfig.msg,
|
||||
params,
|
||||
),
|
||||
prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
|
||||
brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
|
||||
bracketColor:
|
||||
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
|
||||
targetPlayer: targetPlayer,
|
||||
utf8Support: true,
|
||||
});
|
||||
chatManager.sendMessage({
|
||||
message: safeParseTextComponent(
|
||||
toastConfig.msg ?? config.welcomeToastConfig.msg,
|
||||
params,
|
||||
),
|
||||
prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
|
||||
brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
|
||||
bracketColor:
|
||||
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
|
||||
targetPlayer: targetPlayer,
|
||||
utf8Support: true,
|
||||
});
|
||||
|
||||
releaser.release();
|
||||
releaser.release();
|
||||
}
|
||||
|
||||
function sendToast(
|
||||
toastConfig: ToastConfig,
|
||||
targetPlayer: string,
|
||||
params: ParseParams,
|
||||
toastConfig: ToastConfig,
|
||||
targetPlayer: string,
|
||||
params: ParseParams,
|
||||
) {
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
|
||||
chatManager.sendToast({
|
||||
message: safeParseTextComponent(
|
||||
toastConfig.msg ?? config.welcomeToastConfig.msg,
|
||||
params,
|
||||
),
|
||||
title: safeParseTextComponent(
|
||||
toastConfig.title ?? config.welcomeToastConfig.title,
|
||||
params,
|
||||
),
|
||||
prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
|
||||
brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
|
||||
bracketColor:
|
||||
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
|
||||
targetPlayer: targetPlayer,
|
||||
utf8Support: true,
|
||||
});
|
||||
releaser.release();
|
||||
chatManager.sendToast({
|
||||
message: safeParseTextComponent(
|
||||
toastConfig.msg ?? config.welcomeToastConfig.msg,
|
||||
params,
|
||||
),
|
||||
title: safeParseTextComponent(
|
||||
toastConfig.title ?? config.welcomeToastConfig.title,
|
||||
params,
|
||||
),
|
||||
prefix: toastConfig.prefix ?? config.welcomeToastConfig.prefix,
|
||||
brackets: toastConfig.brackets ?? config.welcomeToastConfig.brackets,
|
||||
bracketColor:
|
||||
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
|
||||
targetPlayer: targetPlayer,
|
||||
utf8Support: true,
|
||||
});
|
||||
releaser.release();
|
||||
}
|
||||
|
||||
function sendNotice(player: string, playerInfo?: PlayerInfo) {
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
|
||||
const onlinePlayers = playerDetector.getOnlinePlayers();
|
||||
const noticeTargetPlayers = config.adminGroupConfig.groupUsers.concat(
|
||||
config.usersGroups
|
||||
.filter((value) => value.isNotice)
|
||||
.flatMap((value) => value.groupUsers ?? []),
|
||||
);
|
||||
logger.debug(`noticeTargetPlayers: ${noticeTargetPlayers.join(", ")}`);
|
||||
const onlinePlayers = playerDetector.getOnlinePlayers();
|
||||
const noticeTargetPlayers = config.adminGroupConfig.groupUsers.concat(
|
||||
config.usersGroups
|
||||
.filter((value) => value.isNotice)
|
||||
.flatMap((value) => value.groupUsers ?? []),
|
||||
);
|
||||
logger.debug(`noticeTargetPlayers: ${noticeTargetPlayers.join(", ")}`);
|
||||
|
||||
for (const targetPlayer of noticeTargetPlayers) {
|
||||
if (!onlinePlayers.includes(targetPlayer)) continue;
|
||||
sendToast(config.noticeToastConfig, targetPlayer, {
|
||||
playerName: player,
|
||||
info: playerInfo,
|
||||
});
|
||||
sleep(1);
|
||||
}
|
||||
releaser.release();
|
||||
for (const targetPlayer of noticeTargetPlayers) {
|
||||
if (!onlinePlayers.includes(targetPlayer)) continue;
|
||||
sendToast(config.noticeToastConfig, targetPlayer, {
|
||||
playerName: player,
|
||||
info: playerInfo,
|
||||
});
|
||||
sleep(1);
|
||||
}
|
||||
releaser.release();
|
||||
}
|
||||
|
||||
function sendWarn(player: string) {
|
||||
const warnMsg = `Not Allowed Player ${player} Break in Home `;
|
||||
logger.warn(warnMsg);
|
||||
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
|
||||
sendToast(config.warnToastConfig, player, { playerName: player });
|
||||
chatManager.sendMessage({
|
||||
message: safeParseTextComponent(config.warnToastConfig.msg, {
|
||||
playerName: player,
|
||||
}),
|
||||
targetPlayer: player,
|
||||
prefix: "AccessControl",
|
||||
brackets: "[]",
|
||||
utf8Support: true,
|
||||
});
|
||||
releaser.release();
|
||||
}
|
||||
|
||||
function watchLoop() {
|
||||
while (gIsRunning) {
|
||||
const releaser = configLock.tryAcquireRead();
|
||||
if (releaser === undefined) {
|
||||
os.sleep(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const watchPlayerNames = gWatchPlayersInfo.flatMap((value) => value.name);
|
||||
logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`);
|
||||
for (const player of gWatchPlayersInfo) {
|
||||
const playerInfo = playerDetector.getPlayerPos(player.name);
|
||||
if (gInRangePlayers.includes(player.name)) {
|
||||
// Notice
|
||||
if (player.hasNoticeTimes < config.noticeTimes) {
|
||||
sendNotice(player.name, playerInfo);
|
||||
player.hasNoticeTimes += 1;
|
||||
}
|
||||
|
||||
// Warn
|
||||
if (config.isWarn) sendWarn(player.name);
|
||||
|
||||
// Record
|
||||
logger.warn(
|
||||
`Stranger ${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
} else {
|
||||
// Get rid of player from list
|
||||
gWatchPlayersInfo = gWatchPlayersInfo.filter(
|
||||
(value) => value.name != player.name,
|
||||
);
|
||||
logger.info(
|
||||
`Stranger ${player.name} has left the range at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
}
|
||||
os.sleep(1);
|
||||
}
|
||||
|
||||
releaser.release();
|
||||
os.sleep(config.watchInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function mainLoop() {
|
||||
while (gIsRunning) {
|
||||
const releaser = configLock.tryAcquireRead();
|
||||
if (releaser === undefined) {
|
||||
os.sleep(0.1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const players = playerDetector.getPlayersInRange(config.detectRange);
|
||||
const playersList = "[ " + players.join(",") + " ]";
|
||||
logger.debug(`Detected ${players.length} players: ${playersList}`);
|
||||
|
||||
for (const player of players) {
|
||||
if (gInRangePlayers.includes(player)) continue;
|
||||
|
||||
// Get player Info
|
||||
const playerInfo = playerDetector.getPlayerPos(player);
|
||||
|
||||
if (config.adminGroupConfig.groupUsers.includes(player)) {
|
||||
logger.info(
|
||||
`Admin ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
if (config.adminGroupConfig.isWelcome)
|
||||
sendMessage(config.welcomeToastConfig, player, {
|
||||
playerName: player,
|
||||
groupName: "Admin",
|
||||
info: playerInfo,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// New player appear
|
||||
let groupConfig: UserGroupConfig = {
|
||||
groupName: "Unfamiliar",
|
||||
groupUsers: [],
|
||||
isAllowed: false,
|
||||
isNotice: false,
|
||||
isWelcome: false,
|
||||
};
|
||||
|
||||
// Get user group config
|
||||
for (const userGroupConfig of config.usersGroups) {
|
||||
if (userGroupConfig.groupUsers == undefined) continue;
|
||||
if (!userGroupConfig.groupUsers.includes(player)) continue;
|
||||
|
||||
groupConfig = userGroupConfig;
|
||||
logger.info(
|
||||
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
if (userGroupConfig.isWelcome)
|
||||
sendMessage(config.welcomeToastConfig, player, {
|
||||
playerName: player,
|
||||
groupName: groupConfig.groupName,
|
||||
info: playerInfo,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (groupConfig.isAllowed) continue;
|
||||
|
||||
logger.warn(
|
||||
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
if (config.isWelcome)
|
||||
sendMessage(config.welcomeToastConfig, player, {
|
||||
playerName: player,
|
||||
groupName: groupConfig.groupName,
|
||||
info: playerInfo,
|
||||
});
|
||||
if (config.isWarn) sendWarn(player);
|
||||
gWatchPlayersInfo = [
|
||||
...gWatchPlayersInfo,
|
||||
{ name: player, hasNoticeTimes: 0 },
|
||||
];
|
||||
}
|
||||
|
||||
gInRangePlayers = players;
|
||||
releaser.release();
|
||||
os.sleep(config.detectInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardLoop() {
|
||||
while (gIsRunning) {
|
||||
const event = pullEventAs(KeyEvent, "key");
|
||||
if (event === undefined) continue;
|
||||
|
||||
if (event.key === keys.c) {
|
||||
logger.info("Launching Access Control TUI...");
|
||||
try {
|
||||
logger.setInTerminal(false);
|
||||
launchAccessControlTUI();
|
||||
logger.info("TUI closed, resuming normal operation");
|
||||
} catch (error) {
|
||||
logger.error(`TUI error: ${textutils.serialise(error as object)}`);
|
||||
} finally {
|
||||
logger.setInTerminal(true);
|
||||
reloadConfig();
|
||||
}
|
||||
} else if (event.key === keys.r) {
|
||||
reloadConfig();
|
||||
}
|
||||
// else if (event.key === keys.q) {
|
||||
// gIsRunning = false;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
function cliLoop() {
|
||||
let printTargetPlayer: string | undefined;
|
||||
const cli = createAccessControlCli({
|
||||
configFilepath: configFilepath,
|
||||
reloadConfig: () => reloadConfig(),
|
||||
logger: logger,
|
||||
print: (msg) =>
|
||||
chatManager.sendMessage({
|
||||
message: msg,
|
||||
targetPlayer: printTargetPlayer,
|
||||
prefix: "Access Control System",
|
||||
brackets: "[]",
|
||||
utf8Support: true,
|
||||
}),
|
||||
});
|
||||
|
||||
while (gIsRunning) {
|
||||
const result = chatManager.getReceivedMessage();
|
||||
if (result.isErr()) {
|
||||
sleep(0.5);
|
||||
continue;
|
||||
}
|
||||
logger.debug(`Received message: ${result.value.message}`);
|
||||
|
||||
const ev = result.value;
|
||||
const warnMsg = `Not Allowed Player ${player} Break in Home `;
|
||||
logger.warn(warnMsg);
|
||||
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
|
||||
const isAdmin = config.adminGroupConfig.groupUsers.includes(ev.username);
|
||||
|
||||
sendToast(config.warnToastConfig, player, { playerName: player });
|
||||
chatManager.sendMessage({
|
||||
message: safeParseTextComponent(config.warnToastConfig.msg, {
|
||||
playerName: player,
|
||||
}),
|
||||
targetPlayer: player,
|
||||
prefix: "AccessControl",
|
||||
brackets: "[]",
|
||||
utf8Support: true,
|
||||
});
|
||||
releaser.release();
|
||||
if (!isAdmin) continue;
|
||||
if (!ev.message.startsWith("@AC")) continue;
|
||||
}
|
||||
|
||||
printTargetPlayer = ev.username;
|
||||
logger.info(
|
||||
`Received command "${ev.message}" from admin ${printTargetPlayer}`,
|
||||
);
|
||||
function watchLoop() {
|
||||
while (gIsRunning) {
|
||||
const releaser = configLock.tryAcquireRead();
|
||||
if (releaser === undefined) {
|
||||
os.sleep(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const commandArgs = ev.message
|
||||
.substring(3)
|
||||
.split(" ")
|
||||
.filter((s) => s.length > 0);
|
||||
logger.debug(`Command arguments: ${commandArgs.join(", ")}`);
|
||||
const watchPlayerNames = gWatchPlayersInfo.flatMap(
|
||||
(value) => value.name,
|
||||
);
|
||||
logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`);
|
||||
for (const player of gWatchPlayersInfo) {
|
||||
const playerInfo = playerDetector.getPlayerPos(player.name);
|
||||
if (gInRangePlayers.includes(player.name)) {
|
||||
// Notice
|
||||
if (player.hasNoticeTimes < config.noticeTimes) {
|
||||
sendNotice(player.name, playerInfo);
|
||||
player.hasNoticeTimes += 1;
|
||||
}
|
||||
|
||||
cli(commandArgs);
|
||||
printTargetPlayer = undefined;
|
||||
}
|
||||
// Warn
|
||||
if (config.isWarn) sendWarn(player.name);
|
||||
|
||||
// Record
|
||||
logger.warn(
|
||||
`Stranger ${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
} else {
|
||||
// Get rid of player from list
|
||||
gWatchPlayersInfo = gWatchPlayersInfo.filter(
|
||||
(value) => value.name != player.name,
|
||||
);
|
||||
logger.info(
|
||||
`Stranger ${player.name} has left the range at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
}
|
||||
os.sleep(1);
|
||||
}
|
||||
|
||||
releaser.release();
|
||||
os.sleep(config.watchInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function mainLoop() {
|
||||
while (gIsRunning) {
|
||||
const releaser = configLock.tryAcquireRead();
|
||||
if (releaser === undefined) {
|
||||
os.sleep(0.1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const players = playerDetector.getPlayersInRange(config.detectRange);
|
||||
const playersList = "[ " + players.join(",") + " ]";
|
||||
logger.debug(`Detected ${players.length} players: ${playersList}`);
|
||||
|
||||
for (const player of players) {
|
||||
if (gInRangePlayers.includes(player)) continue;
|
||||
|
||||
// Get player Info
|
||||
const playerInfo = playerDetector.getPlayerPos(player);
|
||||
|
||||
if (config.adminGroupConfig.groupUsers.includes(player)) {
|
||||
logger.info(
|
||||
`Admin ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
if (config.adminGroupConfig.isWelcome)
|
||||
sendMessage(config.welcomeToastConfig, player, {
|
||||
playerName: player,
|
||||
groupName: "Admin",
|
||||
info: playerInfo,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// New player appear
|
||||
let groupConfig: UserGroupConfig = {
|
||||
groupName: "Unfamiliar",
|
||||
groupUsers: [],
|
||||
isAllowed: false,
|
||||
isNotice: false,
|
||||
isWelcome: false,
|
||||
};
|
||||
|
||||
// Get user group config
|
||||
for (const userGroupConfig of config.usersGroups) {
|
||||
if (userGroupConfig.groupUsers == undefined) continue;
|
||||
if (!userGroupConfig.groupUsers.includes(player)) continue;
|
||||
|
||||
groupConfig = userGroupConfig;
|
||||
logger.info(
|
||||
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
if (userGroupConfig.isWelcome)
|
||||
sendMessage(config.welcomeToastConfig, player, {
|
||||
playerName: player,
|
||||
groupName: groupConfig.groupName,
|
||||
info: playerInfo,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (groupConfig.isAllowed) continue;
|
||||
|
||||
logger.warn(
|
||||
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
if (config.isWelcome)
|
||||
sendMessage(config.welcomeToastConfig, player, {
|
||||
playerName: player,
|
||||
groupName: groupConfig.groupName,
|
||||
info: playerInfo,
|
||||
});
|
||||
if (config.isWarn) sendWarn(player);
|
||||
gWatchPlayersInfo = [
|
||||
...gWatchPlayersInfo,
|
||||
{ name: player, hasNoticeTimes: 0 },
|
||||
];
|
||||
}
|
||||
|
||||
gInRangePlayers = players;
|
||||
releaser.release();
|
||||
os.sleep(config.detectInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardLoop() {
|
||||
while (gIsRunning) {
|
||||
const event = pullEventAs(KeyEvent, "key");
|
||||
if (event === undefined) continue;
|
||||
|
||||
if (event.key === keys.c) {
|
||||
logger.info("Launching Access Control TUI...");
|
||||
try {
|
||||
isOnConsoleStream = false;
|
||||
launchAccessControlTUI();
|
||||
logger.info("TUI closed, resuming normal operation");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`TUI error: ${textutils.serialise(error as object)}`,
|
||||
);
|
||||
} finally {
|
||||
isOnConsoleStream = true;
|
||||
reloadConfig();
|
||||
}
|
||||
} else if (event.key === keys.r) {
|
||||
reloadConfig();
|
||||
}
|
||||
// else if (event.key === keys.q) {
|
||||
// gIsRunning = false;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
function cliLoop() {
|
||||
let printTargetPlayer: string | undefined;
|
||||
const cli = createAccessControlCli({
|
||||
configFilepath: configFilepath,
|
||||
reloadConfig: () => reloadConfig(),
|
||||
logger: logger,
|
||||
print: (msg) =>
|
||||
chatManager.sendMessage({
|
||||
message: msg,
|
||||
targetPlayer: printTargetPlayer,
|
||||
prefix: "Access Control System",
|
||||
brackets: "[]",
|
||||
utf8Support: true,
|
||||
}),
|
||||
});
|
||||
|
||||
while (gIsRunning) {
|
||||
const result = chatManager.getReceivedMessage();
|
||||
if (result.isErr()) {
|
||||
sleep(0.5);
|
||||
continue;
|
||||
}
|
||||
logger.debug(`Received message: ${result.value.message}`);
|
||||
|
||||
const ev = result.value;
|
||||
|
||||
let releaser = configLock.tryAcquireRead();
|
||||
while (releaser === undefined) {
|
||||
sleep(0.1);
|
||||
releaser = configLock.tryAcquireRead();
|
||||
}
|
||||
|
||||
const isAdmin = config.adminGroupConfig.groupUsers.includes(
|
||||
ev.username,
|
||||
);
|
||||
|
||||
releaser.release();
|
||||
if (!isAdmin) continue;
|
||||
if (!ev.message.startsWith("@AC")) continue;
|
||||
|
||||
printTargetPlayer = ev.username;
|
||||
logger.info(
|
||||
`Received command "${ev.message}" from admin ${printTargetPlayer}`,
|
||||
);
|
||||
|
||||
const commandArgs = ev.message
|
||||
.substring(3)
|
||||
.split(" ")
|
||||
.filter((s) => s.length > 0);
|
||||
logger.debug(`Command arguments: ${commandArgs.join(", ")}`);
|
||||
|
||||
cli(commandArgs);
|
||||
printTargetPlayer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function main(args: string[]) {
|
||||
logger.info("Starting access control system, get args: " + args.join(", "));
|
||||
if (args.length == 1) {
|
||||
if (args[0] == "start") {
|
||||
const tutorial: string[] = [];
|
||||
tutorial.push("Access Control System started.");
|
||||
tutorial.push("\tPress 'c' to open configuration TUI.");
|
||||
tutorial.push("\tPress 'r' to reload configuration.");
|
||||
print(tutorial.join("\n"));
|
||||
parallel.waitForAll(
|
||||
() => mainLoop(),
|
||||
() => gTimerManager.run(),
|
||||
() => cliLoop(),
|
||||
() => watchLoop(),
|
||||
() => keyboardLoop(),
|
||||
() => chatManager.run(),
|
||||
);
|
||||
logger.info("Starting access control system, get args: " + args.join(", "));
|
||||
if (args.length == 1) {
|
||||
if (args[0] == "start") {
|
||||
const tutorial: string[] = [];
|
||||
tutorial.push("Access Control System started.");
|
||||
tutorial.push("\tPress 'c' to open configuration TUI.");
|
||||
tutorial.push("\tPress 'r' to reload configuration.");
|
||||
print(tutorial.join("\n"));
|
||||
parallel.waitForAll(
|
||||
() => mainLoop(),
|
||||
() => gTimerManager.run(),
|
||||
() => cliLoop(),
|
||||
() => watchLoop(),
|
||||
() => keyboardLoop(),
|
||||
() => chatManager.run(),
|
||||
);
|
||||
|
||||
return;
|
||||
} else if (args[0] == "config") {
|
||||
logger.info("Launching Access Control TUI...");
|
||||
logger.setInTerminal(false);
|
||||
try {
|
||||
launchAccessControlTUI();
|
||||
} catch (error) {
|
||||
logger.error(`TUI error: ${textutils.serialise(error as object)}`);
|
||||
}
|
||||
return;
|
||||
return;
|
||||
} else if (args[0] == "config") {
|
||||
logger.info("Launching Access Control TUI...");
|
||||
isOnConsoleStream = false;
|
||||
|
||||
try {
|
||||
launchAccessControlTUI();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`TUI error: ${textutils.serialise(error as object)}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print(`Usage: accesscontrol start | config`);
|
||||
print(" start - Start the access control system with monitoring");
|
||||
print(" config - Open configuration TUI");
|
||||
print(`Usage: accesscontrol start | config`);
|
||||
print(" start - Start the access control system with monitoring");
|
||||
print(" config - Open configuration TUI");
|
||||
}
|
||||
|
||||
try {
|
||||
main(args);
|
||||
main(args);
|
||||
} catch (error: unknown) {
|
||||
logger.error(textutils.serialise(error as object));
|
||||
logger.error(textutils.serialise(error as object));
|
||||
} finally {
|
||||
logger.close();
|
||||
logger.close();
|
||||
}
|
||||
|
||||
@@ -1,215 +1,262 @@
|
||||
import {
|
||||
CraftManager,
|
||||
CraftRecipe,
|
||||
CreatePackageTag,
|
||||
CraftManager,
|
||||
CraftRecipe,
|
||||
CreatePackageTag,
|
||||
} from "@/lib/CraftManager";
|
||||
import { CCLog, LogLevel } from "@/lib/ccLog";
|
||||
import { Queue } from "@/lib/datatype/Queue";
|
||||
import {
|
||||
ConsoleStream,
|
||||
Logger,
|
||||
LogLevel,
|
||||
processor,
|
||||
textRenderer,
|
||||
} from "@/lib/ccStructLog";
|
||||
|
||||
const logger = new CCLog("autocraft.log", { outputMinLevel: LogLevel.Info });
|
||||
const logger = new Logger({
|
||||
processors: [
|
||||
processor.filterByLevel(LogLevel.Info),
|
||||
processor.addTimestamp(),
|
||||
],
|
||||
renderer: textRenderer,
|
||||
streams: [new ConsoleStream()],
|
||||
});
|
||||
|
||||
const peripheralsNames = {
|
||||
// packsInventory: "minecraft:chest_14",
|
||||
// itemsInventory: "minecraft:chest_15",
|
||||
// packageExtractor: "create:packager_3",
|
||||
blockReader: "bottom",
|
||||
wiredModem: "right",
|
||||
redstone: "left",
|
||||
packsInventory: "minecraft:chest_1121",
|
||||
itemsInventory: "minecraft:chest_1120",
|
||||
packageExtractor: "create:packager_0",
|
||||
// packsInventory: "minecraft:chest_14",
|
||||
// itemsInventory: "minecraft:chest_15",
|
||||
// packageExtractor: "create:packager_3",
|
||||
blockReader: "bottom",
|
||||
wiredModem: "right",
|
||||
redstone: "left",
|
||||
packsInventory: "minecraft:chest_1121",
|
||||
itemsInventory: "minecraft:chest_1120",
|
||||
packageExtractor: "create:packager_0",
|
||||
};
|
||||
|
||||
const packsInventory = peripheral.wrap(
|
||||
peripheralsNames.packsInventory,
|
||||
) as InventoryPeripheral;
|
||||
const itemsInventory = peripheral.wrap(
|
||||
peripheralsNames.itemsInventory,
|
||||
) as InventoryPeripheral;
|
||||
const packageExtractor = peripheral.wrap(
|
||||
peripheralsNames.packageExtractor,
|
||||
) as InventoryPeripheral;
|
||||
const blockReader = peripheral.wrap(
|
||||
peripheralsNames.blockReader,
|
||||
) as BlockReaderPeripheral;
|
||||
const wiredModem = peripheral.wrap(
|
||||
peripheralsNames.wiredModem,
|
||||
) as WiredModemPeripheral;
|
||||
const turtleLocalName = wiredModem.getNameLocal();
|
||||
let packsInventory: InventoryPeripheral;
|
||||
let itemsInventory: InventoryPeripheral;
|
||||
let packageExtractor: InventoryPeripheral;
|
||||
let blockReader: BlockReaderPeripheral;
|
||||
let wiredModem: WiredModemPeripheral;
|
||||
let turtleLocalName: string;
|
||||
|
||||
enum State {
|
||||
IDLE,
|
||||
READ_RECIPE,
|
||||
CRAFT_OUTPUT,
|
||||
IDLE,
|
||||
READ_RECIPE,
|
||||
CRAFT_OUTPUT,
|
||||
}
|
||||
|
||||
function main() {
|
||||
const craftManager = new CraftManager(turtleLocalName, itemsInventory);
|
||||
const recipesQueue = new Queue<CraftRecipe>();
|
||||
const recipesWaitingMap = new Map<number, CraftRecipe[] | CraftRecipe>();
|
||||
let currentState = State.IDLE;
|
||||
let nextState = State.IDLE;
|
||||
let hasPackage = redstone.getInput(peripheralsNames.redstone);
|
||||
while (hasPackage) {
|
||||
hasPackage = redstone.getInput(peripheralsNames.redstone);
|
||||
logger.warn("redstone activated when init, please clear inventory");
|
||||
sleep(1);
|
||||
}
|
||||
while (true) {
|
||||
try {
|
||||
packsInventory = peripheral.wrap(
|
||||
peripheralsNames.packsInventory,
|
||||
) as InventoryPeripheral;
|
||||
itemsInventory = peripheral.wrap(
|
||||
peripheralsNames.itemsInventory,
|
||||
) as InventoryPeripheral;
|
||||
packageExtractor = peripheral.wrap(
|
||||
peripheralsNames.packageExtractor,
|
||||
) as InventoryPeripheral;
|
||||
blockReader = peripheral.wrap(
|
||||
peripheralsNames.blockReader,
|
||||
) as BlockReaderPeripheral;
|
||||
wiredModem = peripheral.wrap(
|
||||
peripheralsNames.wiredModem,
|
||||
) as WiredModemPeripheral;
|
||||
turtleLocalName = wiredModem.getNameLocal();
|
||||
|
||||
logger.info("AutoCraft init finished...");
|
||||
while (true) {
|
||||
// Switch state
|
||||
switch (currentState) {
|
||||
case State.IDLE: {
|
||||
nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
|
||||
break;
|
||||
}
|
||||
case State.READ_RECIPE: {
|
||||
nextState = hasPackage ? State.READ_RECIPE : State.CRAFT_OUTPUT;
|
||||
break;
|
||||
}
|
||||
case State.CRAFT_OUTPUT: {
|
||||
nextState =
|
||||
recipesQueue.size() > 0
|
||||
? State.CRAFT_OUTPUT
|
||||
: hasPackage
|
||||
? State.READ_RECIPE
|
||||
: State.IDLE;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.error(`Unknown state`);
|
||||
nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
|
||||
break;
|
||||
}
|
||||
logger.info("Peripheral initialization complete...");
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Peripheral initialization failed for ${String(error)}, try again...`,
|
||||
);
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
// State logic
|
||||
switch (currentState) {
|
||||
case State.IDLE: {
|
||||
if (!hasPackage) os.pullEvent("redstone");
|
||||
const craftManager = new CraftManager(turtleLocalName, itemsInventory);
|
||||
const recipesQueue = new Queue<CraftRecipe>();
|
||||
const recipesWaitingMap = new Map<number, CraftRecipe[] | CraftRecipe>();
|
||||
|
||||
let currentState = State.IDLE;
|
||||
let nextState = State.IDLE;
|
||||
let hasPackage = redstone.getInput(peripheralsNames.redstone);
|
||||
while (hasPackage) {
|
||||
hasPackage = redstone.getInput(peripheralsNames.redstone);
|
||||
break;
|
||||
}
|
||||
|
||||
case State.READ_RECIPE: {
|
||||
logger.info(`Package detected`);
|
||||
const packagesInfoRecord = packsInventory.list();
|
||||
for (const key in packagesInfoRecord) {
|
||||
const slotNum = parseInt(key);
|
||||
packsInventory.pushItems(turtleLocalName, slotNum);
|
||||
|
||||
// Get package NBT
|
||||
logger.debug(
|
||||
`Turtle:\n${textutils.serialise(blockReader.getBlockData()!, { allow_repetitions: true })}`,
|
||||
);
|
||||
const packageDetailInfo = blockReader.getBlockData()?.Items[1];
|
||||
if (packageDetailInfo === undefined) {
|
||||
logger.error(`Package detail info not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get OrderId and isFinal
|
||||
const packageOrderId = (packageDetailInfo.tag as CreatePackageTag)
|
||||
.Fragment.OrderId;
|
||||
const packageIsFinal =
|
||||
(packageDetailInfo.tag as CreatePackageTag).Fragment.IsFinal > 0
|
||||
? true
|
||||
: false;
|
||||
|
||||
// Get recipe
|
||||
const packageRecipes =
|
||||
CraftManager.getPackageRecipe(packageDetailInfo);
|
||||
if (packageRecipes.isSome()) {
|
||||
if (packageIsFinal) recipesQueue.enqueue(packageRecipes.value);
|
||||
else recipesWaitingMap.set(packageOrderId, packageRecipes.value);
|
||||
} else {
|
||||
if (packageIsFinal && recipesWaitingMap.has(packageOrderId)) {
|
||||
recipesQueue.enqueue(recipesWaitingMap.get(packageOrderId)!);
|
||||
recipesWaitingMap.delete(packageOrderId);
|
||||
} else {
|
||||
logger.debug(`No recipe, just pass`);
|
||||
}
|
||||
}
|
||||
packageExtractor.pullItems(turtleLocalName, 1);
|
||||
}
|
||||
|
||||
if (
|
||||
currentState === State.READ_RECIPE &&
|
||||
nextState === State.CRAFT_OUTPUT
|
||||
) {
|
||||
craftManager.initItemsMap();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case State.CRAFT_OUTPUT: {
|
||||
// Check recipe
|
||||
const recipe = recipesQueue.dequeue();
|
||||
if (recipe === undefined) break;
|
||||
|
||||
let restCraftCnt = recipe.Count;
|
||||
let maxSignleCraftCnt = restCraftCnt;
|
||||
|
||||
let craftItemDetail: ItemDetail | undefined = undefined;
|
||||
do {
|
||||
// Clear workbench
|
||||
craftManager.clearTurtle();
|
||||
|
||||
logger.info(`Pull items according to a recipe`);
|
||||
const craftCnt = craftManager
|
||||
.pullItemsWithRecipe(recipe, maxSignleCraftCnt)
|
||||
.unwrapOrElse((error) => {
|
||||
logger.error(error.message);
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (craftCnt == 0) break;
|
||||
if (craftCnt < maxSignleCraftCnt) maxSignleCraftCnt = craftCnt;
|
||||
const craftRet = craftManager.craft(maxSignleCraftCnt);
|
||||
craftItemDetail ??= craftRet;
|
||||
logger.info(`Craft ${craftCnt} times`);
|
||||
restCraftCnt -= craftCnt;
|
||||
} while (restCraftCnt > 0);
|
||||
|
||||
// Finally output
|
||||
if (restCraftCnt > 0) {
|
||||
logger.warn(
|
||||
`Only craft ${recipe.Count - restCraftCnt}x ${craftItemDetail?.name ?? "UnknownItem"}`,
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Finish craft ${recipe.Count}x ${craftItemDetail?.name ?? "UnknownItem"}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Clear workbench and inventory
|
||||
const turtleItemSlots = Object.values(
|
||||
blockReader.getBlockData()!.Items,
|
||||
).map((val) => val.Slot + 1);
|
||||
craftManager.clearTurtle(turtleItemSlots);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
logger.warn("redstone activated when init, please clear inventory");
|
||||
sleep(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check packages
|
||||
hasPackage = redstone.getInput(peripheralsNames.redstone);
|
||||
// State update
|
||||
currentState = nextState;
|
||||
}
|
||||
logger.info("AutoCraft init finished...");
|
||||
while (true) {
|
||||
// Switch state
|
||||
switch (currentState) {
|
||||
case State.IDLE: {
|
||||
nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
|
||||
break;
|
||||
}
|
||||
case State.READ_RECIPE: {
|
||||
nextState = hasPackage ? State.READ_RECIPE : State.CRAFT_OUTPUT;
|
||||
break;
|
||||
}
|
||||
case State.CRAFT_OUTPUT: {
|
||||
nextState =
|
||||
recipesQueue.size() > 0
|
||||
? State.CRAFT_OUTPUT
|
||||
: hasPackage
|
||||
? State.READ_RECIPE
|
||||
: State.IDLE;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.error(`Unknown state`);
|
||||
nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// State logic
|
||||
switch (currentState) {
|
||||
case State.IDLE: {
|
||||
if (!hasPackage) os.pullEvent("redstone");
|
||||
hasPackage = redstone.getInput(peripheralsNames.redstone);
|
||||
break;
|
||||
}
|
||||
|
||||
case State.READ_RECIPE: {
|
||||
logger.info(`Package detected`);
|
||||
const packagesInfoRecord = packsInventory.list();
|
||||
for (const key in packagesInfoRecord) {
|
||||
const slotNum = parseInt(key);
|
||||
packsInventory.pushItems(turtleLocalName, slotNum);
|
||||
|
||||
// Get package NBT
|
||||
logger.debug(
|
||||
`Turtle:\n${textutils.serialise(blockReader.getBlockData()!, { allow_repetitions: true })}`,
|
||||
);
|
||||
const packageDetailInfo =
|
||||
blockReader.getBlockData()?.Items[1];
|
||||
if (packageDetailInfo === undefined) {
|
||||
logger.error(`Package detail info not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get OrderId and isFinal
|
||||
const packageOrderId = (
|
||||
packageDetailInfo.tag as CreatePackageTag
|
||||
).Fragment.OrderId;
|
||||
const packageIsFinal =
|
||||
(packageDetailInfo.tag as CreatePackageTag).Fragment
|
||||
.IsFinal > 0
|
||||
? true
|
||||
: false;
|
||||
|
||||
// Get recipe
|
||||
const packageRecipes =
|
||||
CraftManager.getPackageRecipe(packageDetailInfo);
|
||||
if (packageRecipes.isSome()) {
|
||||
if (packageIsFinal)
|
||||
recipesQueue.enqueue(packageRecipes.value);
|
||||
else
|
||||
recipesWaitingMap.set(
|
||||
packageOrderId,
|
||||
packageRecipes.value,
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
packageIsFinal &&
|
||||
recipesWaitingMap.has(packageOrderId)
|
||||
) {
|
||||
recipesQueue.enqueue(
|
||||
recipesWaitingMap.get(packageOrderId)!,
|
||||
);
|
||||
recipesWaitingMap.delete(packageOrderId);
|
||||
} else {
|
||||
logger.debug(`No recipe, just pass`);
|
||||
}
|
||||
}
|
||||
packageExtractor.pullItems(turtleLocalName, 1);
|
||||
}
|
||||
|
||||
if (
|
||||
currentState === State.READ_RECIPE &&
|
||||
nextState === State.CRAFT_OUTPUT
|
||||
) {
|
||||
craftManager.initItemsMap();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case State.CRAFT_OUTPUT: {
|
||||
// Check recipe
|
||||
const recipe = recipesQueue.dequeue();
|
||||
if (recipe === undefined) break;
|
||||
|
||||
let restCraftCnt = recipe.Count;
|
||||
let maxSignleCraftCnt = restCraftCnt;
|
||||
|
||||
let craftItemDetail: ItemDetail | undefined = undefined;
|
||||
do {
|
||||
// Clear workbench
|
||||
craftManager.clearTurtle();
|
||||
|
||||
logger.info(`Pull items according to a recipe`);
|
||||
const craftCnt = craftManager
|
||||
.pullItemsWithRecipe(recipe, maxSignleCraftCnt)
|
||||
.unwrapOrElse((error) => {
|
||||
logger.error(error.message);
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (craftCnt == 0) break;
|
||||
if (craftCnt < maxSignleCraftCnt)
|
||||
maxSignleCraftCnt = craftCnt;
|
||||
const craftRet = craftManager.craft(maxSignleCraftCnt);
|
||||
craftItemDetail ??= craftRet;
|
||||
logger.info(`Craft ${craftCnt} times`);
|
||||
restCraftCnt -= craftCnt;
|
||||
} while (restCraftCnt > 0);
|
||||
|
||||
// Finally output
|
||||
if (restCraftCnt > 0) {
|
||||
logger.warn(
|
||||
`Only craft ${recipe.Count - restCraftCnt}x ${craftItemDetail?.name ?? "UnknownItem"}`,
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Finish craft ${recipe.Count}x ${craftItemDetail?.name ?? "UnknownItem"}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Clear workbench and inventory
|
||||
const turtleItemSlots = Object.values(
|
||||
blockReader.getBlockData()!.Items,
|
||||
).map((val) => val.Slot + 1);
|
||||
craftManager.clearTurtle(turtleItemSlots);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
sleep(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check packages
|
||||
hasPackage = redstone.getInput(peripheralsNames.redstone);
|
||||
// State update
|
||||
currentState = nextState;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
main();
|
||||
} catch (error: unknown) {
|
||||
logger.error(textutils.serialise(error as object));
|
||||
logger.error(textutils.serialise(error as object));
|
||||
} finally {
|
||||
logger.close();
|
||||
logger.close();
|
||||
}
|
||||
|
||||
@@ -7,150 +7,14 @@
|
||||
*/
|
||||
|
||||
// Re-export all core types and classes
|
||||
export {
|
||||
LogLevel,
|
||||
LogEvent,
|
||||
Processor,
|
||||
Renderer,
|
||||
Stream,
|
||||
LoggerOptions,
|
||||
ILogger,
|
||||
} from "./types";
|
||||
export { Logger } from "./Logger";
|
||||
export * from "./types";
|
||||
export * from "./Logger";
|
||||
|
||||
// Re-export all processors
|
||||
export {
|
||||
addTimestamp,
|
||||
addFormattedTimestamp,
|
||||
addFullTimestamp,
|
||||
filterByLevel,
|
||||
addSource,
|
||||
addComputerId,
|
||||
addComputerLabel,
|
||||
filterBy,
|
||||
transformField,
|
||||
removeFields,
|
||||
addStaticFields,
|
||||
} from "./processors";
|
||||
export * from "./processors";
|
||||
|
||||
// Re-export all renderers
|
||||
export { jsonRenderer, textRenderer } from "./renderers";
|
||||
export * from "./renderers";
|
||||
|
||||
// Re-export all streams
|
||||
export {
|
||||
ConsoleStream,
|
||||
FileStream,
|
||||
BufferStream,
|
||||
NullStream,
|
||||
SECOND,
|
||||
MINUTE,
|
||||
HOUR,
|
||||
DAY,
|
||||
WEEK,
|
||||
} from "./streams";
|
||||
|
||||
import { Logger } from "./Logger";
|
||||
import { LogLevel, LogEvent } from "./types";
|
||||
import {
|
||||
addFormattedTimestamp,
|
||||
addFullTimestamp,
|
||||
addComputerId,
|
||||
} from "./processors";
|
||||
import { textRenderer, jsonRenderer } from "./renderers";
|
||||
import { ConsoleStream, FileStream, DAY } from "./streams";
|
||||
|
||||
/**
|
||||
* Create a development logger with console output and colored formatting.
|
||||
*
|
||||
* This logger is optimized for development and debugging, with:
|
||||
* - Debug level and above
|
||||
* - Formatted timestamps
|
||||
* - Computer ID tracking
|
||||
* - Human-readable console output with colors
|
||||
*
|
||||
* @param options - Optional configuration to override defaults
|
||||
* @returns A configured Logger instance for development
|
||||
*/
|
||||
export function createDevLogger(
|
||||
options: {
|
||||
level?: LogLevel;
|
||||
source?: string;
|
||||
includeComputerId?: boolean;
|
||||
} = {},
|
||||
): Logger {
|
||||
const processors = [addFormattedTimestamp()];
|
||||
|
||||
if (options.includeComputerId !== false) {
|
||||
processors.push(addComputerId());
|
||||
}
|
||||
|
||||
if (options.source) {
|
||||
processors.push((event: LogEvent) => {
|
||||
event.set("source", options.source);
|
||||
return event;
|
||||
});
|
||||
}
|
||||
|
||||
return new Logger({
|
||||
processors,
|
||||
renderer: textRenderer,
|
||||
streams: [new ConsoleStream()],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a production logger with file output and JSON formatting.
|
||||
*
|
||||
* This logger is optimized for production environments, with:
|
||||
* - Info level and above
|
||||
* - Full timestamps
|
||||
* - Computer ID and label tracking
|
||||
* - JSON output for machine processing
|
||||
* - Daily file rotation
|
||||
*
|
||||
* @param filename - Base filename for log files
|
||||
* @param options - Optional configuration to override defaults
|
||||
* @returns A configured Logger instance for production
|
||||
*/
|
||||
export function createProdLogger(
|
||||
filename: string,
|
||||
options: {
|
||||
level?: LogLevel;
|
||||
source?: string;
|
||||
rotationInterval?: number;
|
||||
includeConsole?: boolean;
|
||||
} = {},
|
||||
): Logger {
|
||||
const processors = [
|
||||
addFullTimestamp(),
|
||||
addComputerId(),
|
||||
(event: LogEvent) => {
|
||||
const label = os.getComputerLabel();
|
||||
if (label) {
|
||||
event.set("computer_label", label);
|
||||
}
|
||||
return event;
|
||||
},
|
||||
];
|
||||
|
||||
if (options.source) {
|
||||
processors.push((event: LogEvent) => {
|
||||
event.set("source", options.source);
|
||||
return event;
|
||||
});
|
||||
}
|
||||
|
||||
const streams: Array<ConsoleStream | FileStream> = [
|
||||
new FileStream(filename, options.rotationInterval ?? DAY),
|
||||
];
|
||||
|
||||
if (options.includeConsole) {
|
||||
streams.push(new ConsoleStream());
|
||||
}
|
||||
|
||||
return new Logger({
|
||||
processors,
|
||||
renderer: jsonRenderer,
|
||||
streams,
|
||||
});
|
||||
}
|
||||
export * from "./streams";
|
||||
|
||||
@@ -8,213 +8,196 @@
|
||||
|
||||
import { LogEvent, Processor, LogLevel } from "./types";
|
||||
|
||||
/**
|
||||
* Adds a timestamp to the log event.
|
||||
*
|
||||
* This processor adds the current time as a structured timestamp object
|
||||
* using CC:Tweaked's os.date() function. The timestamp includes year,
|
||||
* month, day, hour, minute, and second components.
|
||||
*
|
||||
* Performance note: os.date() is relatively expensive, so this should
|
||||
* typically be placed early in the processor chain and used only once.
|
||||
*
|
||||
* @param event - The log event to process
|
||||
* @returns The event with timestamp added
|
||||
*/
|
||||
export function addTimestamp(): Processor {
|
||||
return (event) => {
|
||||
const timestamp = os.date("!*t") as LuaDate;
|
||||
event.set("timestamp", timestamp);
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a human-readable timestamp string to the log event.
|
||||
*
|
||||
* This processor adds a formatted timestamp string that's easier to read
|
||||
* in log output. The format is "HH:MM:SS" in UTC time.
|
||||
*
|
||||
* @param event - The log event to process
|
||||
* @returns The event with formatted timestamp added
|
||||
*/
|
||||
export function addFormattedTimestamp(): Processor {
|
||||
return (event) => {
|
||||
const timestamp = os.date("!*t") as LuaDate;
|
||||
const timeStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`;
|
||||
event.set("time", timeStr);
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a full ISO-like timestamp string to the log event.
|
||||
*
|
||||
* This processor adds a complete timestamp in YYYY-MM-DD HH:MM:SS format
|
||||
* which is useful for file logging and structured output.
|
||||
*
|
||||
* @param event - The log event to process
|
||||
* @returns The event with full timestamp added
|
||||
*/
|
||||
export function addFullTimestamp(): Processor {
|
||||
return (event) => {
|
||||
const timestamp = os.date("!*t") as LuaDate;
|
||||
const fullTimeStr = `${timestamp.year}-${string.format("%02d", timestamp.month)}-${string.format("%02d", timestamp.day)} ${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`;
|
||||
event.set("datetime", fullTimeStr);
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters log events by minimum level.
|
||||
*
|
||||
* This processor drops log events that are below the specified minimum level.
|
||||
* Note: The Logger class already does early filtering for performance, but
|
||||
* this processor can be useful for dynamic filtering or when you need
|
||||
* different levels for different streams.
|
||||
*
|
||||
* @param minLevel - The minimum log level to allow through
|
||||
* @returns A processor function that filters by level
|
||||
*/
|
||||
export function filterByLevel(minLevel: LogLevel): Processor {
|
||||
return (event) => {
|
||||
const eventLevel = event.get("level") as LogLevel | undefined;
|
||||
if (eventLevel === undefined) {
|
||||
return event; // Pass through if no level is set
|
||||
}
|
||||
|
||||
if (eventLevel !== undefined && eventLevel < minLevel) {
|
||||
return undefined; // Drop the log event
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a logger name/source to the log event.
|
||||
*
|
||||
* This processor is useful when you have multiple loggers in your application
|
||||
* and want to identify which component generated each log entry.
|
||||
*
|
||||
* @param name - The name/source to add to log events
|
||||
* @returns A processor function that adds the source name
|
||||
*/
|
||||
export function addSource(name: string): Processor {
|
||||
return (event) => {
|
||||
event.set("source", name);
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the current computer ID to the log event.
|
||||
*
|
||||
* In CC:Tweaked environments, this can help identify which computer
|
||||
* generated the log when logs are aggregated from multiple sources.
|
||||
*
|
||||
* @param event - The log event to process
|
||||
* @returns The event with computer ID added
|
||||
*/
|
||||
export function addComputerId(): Processor {
|
||||
return (event) => {
|
||||
event.set("computer_id", os.getComputerID());
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the current computer label to the log event.
|
||||
*
|
||||
* If the computer has a label set, this adds it to the log event.
|
||||
* This can be more human-readable than the computer ID.
|
||||
*
|
||||
* @param event - The log event to process
|
||||
* @returns The event with computer label added (if available)
|
||||
*/
|
||||
export function addComputerLabel(): Processor {
|
||||
return (event) => {
|
||||
const label = os.getComputerLabel();
|
||||
if (label !== undefined && label !== null) {
|
||||
event.set("computer_label", label);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out events that match a specific condition.
|
||||
*
|
||||
* This is a generic processor that allows you to filter events based on
|
||||
* any custom condition. The predicate function should return true to keep
|
||||
* the event and false to drop it.
|
||||
*
|
||||
* @param predicate - Function that returns true to keep the event
|
||||
* @returns A processor function that filters based on the predicate
|
||||
*/
|
||||
export function filterBy(predicate: (event: LogEvent) => boolean): Processor {
|
||||
return (event) => {
|
||||
return predicate(event) ? event : undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a specific field in the log event.
|
||||
*
|
||||
* This processor allows you to modify the value of a specific field
|
||||
* using a transformation function.
|
||||
*
|
||||
* @param fieldName - The name of the field to transform
|
||||
* @param transformer - Function to transform the field value
|
||||
* @returns A processor function that transforms the specified field
|
||||
*/
|
||||
export function transformField(
|
||||
fieldName: string,
|
||||
transformer: (value: unknown) => unknown,
|
||||
): Processor {
|
||||
return (event) => {
|
||||
if (event.has(fieldName)) {
|
||||
const currentValue = event.get(fieldName);
|
||||
const newValue = transformer(currentValue);
|
||||
event.set(fieldName, newValue);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes specified fields from the log event.
|
||||
*
|
||||
* This processor can be used to strip sensitive or unnecessary information
|
||||
* from log events before they are rendered and output.
|
||||
*
|
||||
* @param fieldNames - Array of field names to remove
|
||||
* @returns A processor function that removes the specified fields
|
||||
*/
|
||||
export function removeFields(fieldNames: string[]): Processor {
|
||||
return (event) => {
|
||||
for (const fieldName of fieldNames) {
|
||||
event.delete(fieldName);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds static fields to every log event.
|
||||
*
|
||||
* This processor adds the same set of fields to every log event that
|
||||
* passes through it. Useful for adding application name, version,
|
||||
* environment, etc.
|
||||
*
|
||||
* @param fields - Object containing the static fields to add
|
||||
* @returns A processor function that adds the static fields
|
||||
*/
|
||||
export function addStaticFields(fields: Record<string, unknown>): Processor {
|
||||
return (event) => {
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
event.set(key, value);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
export namespace processor {
|
||||
/**
|
||||
* Configuration options for the timestamp processor.
|
||||
*/
|
||||
interface TimestampConfig {
|
||||
/**
|
||||
* The format string takes the same formats as C's strftime function.
|
||||
*/
|
||||
format?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a timestamp to each log event.
|
||||
*
|
||||
* This processor adds a "time" field to each log event with the current
|
||||
* timestamp. The timestamp format can be customized using the `format`
|
||||
* option.
|
||||
*
|
||||
* @param config - Configuration options for the timestamp processor.
|
||||
* @returns A processor function that adds a timestamp to each log event.
|
||||
*/
|
||||
export function addTimestamp(config: TimestampConfig = {}): Processor {
|
||||
return (event) => {
|
||||
let time: string;
|
||||
if (config.format === undefined) {
|
||||
time = os.date("%F %T") as string;
|
||||
} else {
|
||||
time = os.date(config.format) as string;
|
||||
}
|
||||
|
||||
event.set("timestamp", time);
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters log events by minimum level.
|
||||
*
|
||||
* This processor drops log events that are below the specified minimum level.
|
||||
* Note: The Logger class already does early filtering for performance, but
|
||||
* this processor can be useful for dynamic filtering or when you need
|
||||
* different levels for different streams.
|
||||
*
|
||||
* @param minLevel - The minimum log level to allow through
|
||||
* @returns A processor function that filters by level
|
||||
*/
|
||||
export function filterByLevel(minLevel: LogLevel): Processor {
|
||||
return (event) => {
|
||||
const eventLevel = event.get("level") as LogLevel | undefined;
|
||||
if (eventLevel === undefined) {
|
||||
return event; // Pass through if no level is set
|
||||
}
|
||||
|
||||
if (eventLevel !== undefined && eventLevel < minLevel) {
|
||||
return undefined; // Drop the log event
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a logger name/source to the log event.
|
||||
*
|
||||
* This processor is useful when you have multiple loggers in your application
|
||||
* and want to identify which component generated each log entry.
|
||||
*
|
||||
* @param name - The name/source to add to log events
|
||||
* @returns A processor function that adds the source name
|
||||
*/
|
||||
export function addSource(name: string): Processor {
|
||||
return (event) => {
|
||||
event.set("source", name);
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the current computer ID to the log event.
|
||||
*
|
||||
* In CC:Tweaked environments, this can help identify which computer
|
||||
* generated the log when logs are aggregated from multiple sources.
|
||||
*
|
||||
* @param event - The log event to process
|
||||
* @returns The event with computer ID added
|
||||
*/
|
||||
export function addComputerId(): Processor {
|
||||
return (event) => {
|
||||
event.set("computer_id", os.getComputerID());
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the current computer label to the log event.
|
||||
*
|
||||
* If the computer has a label set, this adds it to the log event.
|
||||
* This can be more human-readable than the computer ID.
|
||||
*
|
||||
* @param event - The log event to process
|
||||
* @returns The event with computer label added (if available)
|
||||
*/
|
||||
export function addComputerLabel(): Processor {
|
||||
return (event) => {
|
||||
const label = os.getComputerLabel();
|
||||
if (label !== undefined && label !== null) {
|
||||
event.set("computer_label", label);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out events that match a specific condition.
|
||||
*
|
||||
* This is a generic processor that allows you to filter events based on
|
||||
* any custom condition. The predicate function should return true to keep
|
||||
* the event and false to drop it.
|
||||
*
|
||||
* @param predicate - Function that returns true to keep the event
|
||||
* @returns A processor function that filters based on the predicate
|
||||
*/
|
||||
export function filterBy(
|
||||
predicate: (event: LogEvent) => boolean,
|
||||
): Processor {
|
||||
return (event) => {
|
||||
return predicate(event) ? event : undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a specific field in the log event.
|
||||
*
|
||||
* This processor allows you to modify the value of a specific field
|
||||
* using a transformation function.
|
||||
*
|
||||
* @param fieldName - The name of the field to transform
|
||||
* @param transformer - Function to transform the field value
|
||||
* @returns A processor function that transforms the specified field
|
||||
*/
|
||||
export function transformField(
|
||||
fieldName: string,
|
||||
transformer: (value: unknown) => unknown,
|
||||
): Processor {
|
||||
return (event) => {
|
||||
if (event.has(fieldName)) {
|
||||
const currentValue = event.get(fieldName);
|
||||
const newValue = transformer(currentValue);
|
||||
event.set(fieldName, newValue);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes specified fields from the log event.
|
||||
*
|
||||
* This processor can be used to strip sensitive or unnecessary information
|
||||
* from log events before they are rendered and output.
|
||||
*
|
||||
* @param fieldNames - Array of field names to remove
|
||||
* @returns A processor function that removes the specified fields
|
||||
*/
|
||||
export function removeFields(fieldNames: string[]): Processor {
|
||||
return (event) => {
|
||||
for (const fieldName of fieldNames) {
|
||||
event.delete(fieldName);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds static fields to every log event.
|
||||
*
|
||||
* This processor adds the same set of fields to every log event that
|
||||
* passes through it. Useful for adding application name, version,
|
||||
* environment, etc.
|
||||
*
|
||||
* @param fields - Object containing the static fields to add
|
||||
* @returns A processor function that adds the static fields
|
||||
*/
|
||||
export function addStaticFields(
|
||||
fields: Record<string, unknown>,
|
||||
): Processor {
|
||||
return (event) => {
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
event.set(key, value);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* different output formats (JSON, console-friendly, etc.).
|
||||
*/
|
||||
|
||||
import { Renderer } from "./types";
|
||||
import { LogLevel, Renderer } from "./types";
|
||||
|
||||
/**
|
||||
* Renders log events as JSON strings.
|
||||
@@ -43,44 +43,30 @@ export const jsonRenderer: Renderer = (event) => {
|
||||
* timestamp, level, message, and additional context fields formatted
|
||||
* in a readable way.
|
||||
*
|
||||
* Format: [HH:MM:SS] [LEVEL] message { key=value, key2=value2 }
|
||||
* Format: [YYYY-MM-DD HH:MM:SS] [LEVEL] message key=value, key2=value2
|
||||
*
|
||||
* @param event - The log event to render
|
||||
* @returns Human-readable string representation
|
||||
*/
|
||||
export const textRenderer: Renderer = (event) => {
|
||||
// Extract core components
|
||||
const timestamp = event.get("timestamp") as LuaDate | undefined;
|
||||
const timeStr = event.get("time") as string | undefined;
|
||||
const level = (event.get("level") as string)?.toUpperCase() ?? "UNKNOWN";
|
||||
const timeStr = event.get("timestamp") as string | undefined;
|
||||
const level: string | undefined = LogLevel[event.get("level") as LogLevel];
|
||||
const message = (event.get("message") as string) ?? "";
|
||||
|
||||
// Format timestamp
|
||||
let timestampStr = "";
|
||||
if (timeStr) {
|
||||
timestampStr = timeStr;
|
||||
} else if (timestamp) {
|
||||
timestampStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`;
|
||||
}
|
||||
|
||||
// Start building the output
|
||||
let output = `[${timestampStr}] [${level}] ${message}`;
|
||||
let output = `[${timeStr}] [${level}] ${message} \t`;
|
||||
|
||||
// Add context fields (excluding the core fields we already used)
|
||||
const contextFields: string[] = [];
|
||||
for (const [key, value] of event.entries()) {
|
||||
if (
|
||||
key !== "timestamp" &&
|
||||
key !== "time" &&
|
||||
key !== "level" &&
|
||||
key !== "message"
|
||||
) {
|
||||
if (key !== "timestamp" && key !== "level" && key !== "message") {
|
||||
contextFields.push(`${key}=${tostring(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (contextFields.length > 0) {
|
||||
output += ` { ${contextFields.join(", ")} }`;
|
||||
output += contextFields.join(", ");
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* implements the Stream interface and handles its own output logic.
|
||||
*/
|
||||
|
||||
import { Stream, LogEvent } from "./types";
|
||||
import { LogLevel, Stream, LogEvent } from "./types";
|
||||
|
||||
/**
|
||||
* Console stream that outputs to the CC:Tweaked terminal.
|
||||
@@ -16,14 +16,14 @@ import { Stream, LogEvent } from "./types";
|
||||
* color after writing each message.
|
||||
*/
|
||||
export class ConsoleStream implements Stream {
|
||||
private levelColors: Map<string, number> = new Map([
|
||||
["trace", colors.lightGray],
|
||||
["debug", colors.gray],
|
||||
["info", colors.white],
|
||||
["warn", colors.orange],
|
||||
["error", colors.red],
|
||||
["fatal", colors.red],
|
||||
]);
|
||||
private levelColors: { [key: string]: number } = {
|
||||
Trace: colors.lightGray,
|
||||
Debug: colors.gray,
|
||||
Info: colors.green,
|
||||
Warn: colors.orange,
|
||||
Error: colors.red,
|
||||
Fatal: colors.red,
|
||||
};
|
||||
|
||||
/**
|
||||
* Write a formatted log message to the terminal.
|
||||
@@ -32,8 +32,9 @@ export class ConsoleStream implements Stream {
|
||||
* @param event - The original log event for context (used for level-based coloring)
|
||||
*/
|
||||
public write(message: string, event: LogEvent): void {
|
||||
const level = event.get("level") as string | undefined;
|
||||
const color = level ? this.levelColors.get(level) : undefined;
|
||||
const level: string | undefined =
|
||||
LogLevel[event.get("level") as LogLevel];
|
||||
const color = level !== undefined ? this.levelColors[level] : undefined;
|
||||
|
||||
if (color !== undefined) {
|
||||
const originalColor = term.getTextColor();
|
||||
|
||||
@@ -7,56 +7,16 @@
|
||||
|
||||
import {
|
||||
Logger,
|
||||
createDevLogger,
|
||||
createProdLogger,
|
||||
|
||||
// Processors
|
||||
addTimestamp,
|
||||
addFormattedTimestamp,
|
||||
addFullTimestamp,
|
||||
addSource,
|
||||
addComputerId,
|
||||
addStaticFields,
|
||||
transformField,
|
||||
|
||||
// Renderers
|
||||
textRenderer,
|
||||
jsonRenderer,
|
||||
|
||||
// Streams
|
||||
processor,
|
||||
ConsoleStream,
|
||||
FileStream,
|
||||
BufferStream,
|
||||
DAY,
|
||||
ConditionalStream,
|
||||
HOUR,
|
||||
jsonRenderer,
|
||||
LogLevel,
|
||||
textRenderer,
|
||||
} from "../lib/ccStructLog";
|
||||
import { ConditionalStream } from "@/lib/ccStructLog/streams";
|
||||
|
||||
// =============================================================================
|
||||
// Basic Usage Examples
|
||||
// =============================================================================
|
||||
|
||||
print("=== Basic Usage Examples ===");
|
||||
|
||||
// 1. Quick start with pre-configured loggers
|
||||
const devLog = createDevLogger();
|
||||
devLog.info("Application started", { version: "1.0.0", port: 8080 });
|
||||
devLog.debug("Debug information", { userId: 123, action: "login" });
|
||||
devLog.error("Something went wrong", {
|
||||
error: "Connection failed",
|
||||
retries: 3,
|
||||
});
|
||||
|
||||
// 2. Production logging to file
|
||||
const prodLog = createProdLogger("app.log", {
|
||||
source: "MyApplication",
|
||||
rotationInterval: DAY,
|
||||
includeConsole: true,
|
||||
});
|
||||
|
||||
prodLog.info("User action", { userId: 456, action: "purchase", amount: 29.99 });
|
||||
prodLog.warn("Low disk space", { available: 1024, threshold: 2048 });
|
||||
|
||||
// =============================================================================
|
||||
// Custom Logger Configurations
|
||||
@@ -67,10 +27,10 @@ print("\n=== Custom Logger Configurations ===");
|
||||
// 4. Custom logger with specific processors and renderer
|
||||
const customLogger = new Logger({
|
||||
processors: [
|
||||
addFullTimestamp(),
|
||||
addComputerId(),
|
||||
addSource("CustomApp"),
|
||||
addStaticFields({
|
||||
processor.addTimestamp(),
|
||||
processor.addComputerId(),
|
||||
processor.addSource("CustomApp"),
|
||||
processor.addStaticFields({
|
||||
environment: "development",
|
||||
version: "2.1.0",
|
||||
}),
|
||||
@@ -109,10 +69,10 @@ const sanitizePasswords = (event: Map<string, unknown>) => {
|
||||
|
||||
const secureLogger = new Logger({
|
||||
processors: [
|
||||
addTimestamp(),
|
||||
processor.addTimestamp(),
|
||||
addRequestId,
|
||||
sanitizePasswords,
|
||||
transformField("message", (msg) => `[SECURE] ${msg}`),
|
||||
processor.transformField("message", (msg) => `[SECURE] ${msg}`),
|
||||
],
|
||||
renderer: jsonRenderer,
|
||||
streams: [new ConsoleStream()],
|
||||
@@ -133,7 +93,7 @@ print("\n=== Stream Examples ===");
|
||||
// 11. Buffer stream for batch processing
|
||||
const bufferStream = new BufferStream(100); // Keep last 100 messages
|
||||
const bufferLogger = new Logger({
|
||||
processors: [addFormattedTimestamp()],
|
||||
processors: [processor.addTimestamp()],
|
||||
renderer: textRenderer,
|
||||
streams: [
|
||||
new ConditionalStream(new ConsoleStream(), (msg, event) => {
|
||||
@@ -162,7 +122,7 @@ for (const msg of bufferedMessages) {
|
||||
|
||||
// 12. Multi-stream with different formats
|
||||
const multiFormatLogger = new Logger({
|
||||
processors: [addFullTimestamp(), addComputerId()],
|
||||
processors: [processor.addTimestamp(), processor.addComputerId()],
|
||||
renderer: (event) => "default", // This won't be used
|
||||
streams: [
|
||||
// Console with human-readable format
|
||||
@@ -196,7 +156,7 @@ print("\n=== Error Handling Examples ===");
|
||||
// 13. Robust error handling
|
||||
const robustLogger = new Logger({
|
||||
processors: [
|
||||
addTimestamp(),
|
||||
processor.addTimestamp(),
|
||||
// Processor that might fail
|
||||
(event) => {
|
||||
try {
|
||||
@@ -231,7 +191,7 @@ print("\n=== Cleanup Examples ===");
|
||||
|
||||
// 14. Proper cleanup
|
||||
const fileLogger = new Logger({
|
||||
processors: [addTimestamp()],
|
||||
processors: [processor.addTimestamp()],
|
||||
renderer: jsonRenderer,
|
||||
streams: [new FileStream("temp.log")],
|
||||
});
|
||||
@@ -249,37 +209,3 @@ print("- all.log (complete log)");
|
||||
print("- debug.log (detailed debug info)");
|
||||
print("- structured.log (JSON format)");
|
||||
print("- temp.log (temporary file, now closed)");
|
||||
|
||||
// =============================================================================
|
||||
// Performance Comparison (commented out to avoid noise)
|
||||
// =============================================================================
|
||||
|
||||
/*
|
||||
print("\n=== Performance Comparison ===");
|
||||
|
||||
const iterations = 1000;
|
||||
|
||||
// Test simple console logging
|
||||
const startTime1 = os.clock();
|
||||
const simpleLogger = createMinimalLogger();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
simpleLogger.info(`Simple message ${i}`);
|
||||
}
|
||||
const endTime1 = os.clock();
|
||||
print(`Simple Console Logger: ${endTime1 - startTime1} seconds`);
|
||||
|
||||
// Test complex processor chain
|
||||
const startTime2 = os.clock();
|
||||
const complexLogger = createDetailedLogger("perf_test.log", {
|
||||
source: "PerfTest"
|
||||
});
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
complexLogger.info(`Complex message ${i}`, {
|
||||
iteration: i,
|
||||
data: { nested: { value: i * 2 } }
|
||||
});
|
||||
}
|
||||
complexLogger.close();
|
||||
const endTime2 = os.clock();
|
||||
print(`Complex Processor Chain: ${endTime2 - startTime2} seconds`);
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user