Integrate plugin with local monitor bridge
This commit is contained in:
15
README.md
15
README.md
@@ -84,13 +84,14 @@ node scripts/install.mjs --verbose
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"harborforge-monitor": {
|
||||
"harbor-forge": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"backendUrl": "https://monitor.hangman-lab.top",
|
||||
"identifier": "my-server-01",
|
||||
"apiKey": "your-api-key-here",
|
||||
"monitorPort": 9100,
|
||||
"reportIntervalSec": 30,
|
||||
"httpFallbackIntervalSec": 60,
|
||||
"logLevel": "info"
|
||||
@@ -115,10 +116,22 @@ openclaw gateway restart
|
||||
| `backendUrl` | string | `https://monitor.hangman-lab.top` | Monitor 后端地址 |
|
||||
| `identifier` | string | 自动检测 hostname | 服务器标识符 |
|
||||
| `apiKey` | string | 必填 | HarborForge Monitor 生成的服务器 API Key |
|
||||
| `monitorPort` | number | `9100`(示例) | 本地桥接端口;插件通过 `127.0.0.1:<monitorPort>` 与 HarborForge.Monitor 通信 |
|
||||
| `reportIntervalSec` | number | `30` | 报告间隔(秒) |
|
||||
| `httpFallbackIntervalSec` | number | `60` | HTTP 回退间隔(秒) |
|
||||
| `logLevel` | string | `"info"` | 日志级别: debug/info/warn/error |
|
||||
|
||||
## Monitor 本地桥接
|
||||
|
||||
当插件配置了 `monitorPort`,并且 HarborForge.Monitor 也以相同的 `MONITOR_PORT` 启动时:
|
||||
|
||||
- Monitor 会在 `127.0.0.1:<monitorPort>` 暴露本地桥接服务
|
||||
- 插件可探测 `GET /health`
|
||||
- 插件工具 `harborforge_monitor_telemetry` 可读取 `GET /telemetry`
|
||||
- 若桥接端口未配置或不可达,插件仍然正常工作,只是不会拿到 Monitor 的宿主机遥测补充数据
|
||||
|
||||
这条链路是**可选增强**,不是插件或 Monitor 心跳上报的前置依赖。
|
||||
|
||||
## 收集的指标
|
||||
|
||||
### 系统指标
|
||||
|
||||
1
plugin/core/live-config.d.ts
vendored
1
plugin/core/live-config.d.ts
vendored
@@ -3,6 +3,7 @@ export interface HarborForgeMonitorConfig {
|
||||
backendUrl?: string;
|
||||
identifier?: string;
|
||||
apiKey?: string;
|
||||
monitorPort?: number;
|
||||
reportIntervalSec?: number;
|
||||
httpFallbackIntervalSec?: number;
|
||||
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"live-config.d.ts","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,qBAAqB,EAC1B,QAAQ,EAAE,wBAAwB,GACjC,wBAAwB,CAqB1B"}
|
||||
{"version":3,"file":"live-config.d.ts","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,qBAAqB,EAC1B,QAAQ,EAAE,wBAAwB,GACjC,wBAAwB,CAqB1B"}
|
||||
@@ -5,7 +5,7 @@ function getLivePluginConfig(api, fallback) {
|
||||
const root = api.config || {};
|
||||
const plugins = root.plugins || {};
|
||||
const entries = plugins.entries || {};
|
||||
const entry = entries['harborforge-monitor'] || {};
|
||||
const entry = entries['harbor-forge'] || {};
|
||||
const cfg = entry.config || {};
|
||||
if (Object.keys(cfg).length > 0 || Object.keys(entry).length > 0) {
|
||||
return {
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"live-config.js","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":";;AAcA,kDAwBC;AAxBD,SAAgB,mBAAmB,CACjC,GAA0B,EAC1B,QAAkC;IAElC,MAAM,IAAI,GAAI,GAAG,CAAC,MAAkC,IAAI,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAI,IAAI,CAAC,OAAmC,IAAI,EAAE,CAAC;IAChE,MAAM,OAAO,GAAI,OAAO,CAAC,OAAmC,IAAI,EAAE,CAAC;IACnE,MAAM,KAAK,GAAI,OAAO,CAAC,qBAAqB,CAA6B,IAAI,EAAE,CAAC;IAChF,MAAM,GAAG,GAAI,KAAK,CAAC,MAAkC,IAAI,EAAE,CAAC;IAE5D,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjE,OAAO;YACL,GAAG,QAAQ;YACX,GAAG,GAAG;YACN,OAAO,EACL,OAAO,GAAG,CAAC,OAAO,KAAK,SAAS;gBAC9B,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;oBAClC,CAAC,CAAC,KAAK,CAAC,OAAO;oBACf,CAAC,CAAC,QAAQ,CAAC,OAAO;SACG,CAAC;IAChC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
||||
{"version":3,"file":"live-config.js","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":";;AAeA,kDAwBC;AAxBD,SAAgB,mBAAmB,CACjC,GAA0B,EAC1B,QAAkC;IAElC,MAAM,IAAI,GAAI,GAAG,CAAC,MAAkC,IAAI,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAI,IAAI,CAAC,OAAmC,IAAI,EAAE,CAAC;IAChE,MAAM,OAAO,GAAI,OAAO,CAAC,OAAmC,IAAI,EAAE,CAAC;IACnE,MAAM,KAAK,GAAI,OAAO,CAAC,cAAc,CAA6B,IAAI,EAAE,CAAC;IACzE,MAAM,GAAG,GAAI,KAAK,CAAC,MAAkC,IAAI,EAAE,CAAC;IAE5D,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjE,OAAO;YACL,GAAG,QAAQ;YACX,GAAG,GAAG;YACN,OAAO,EACL,OAAO,GAAG,CAAC,OAAO,KAAK,SAAS;gBAC9B,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;oBAClC,CAAC,CAAC,KAAK,CAAC,OAAO;oBACf,CAAC,CAAC,QAAQ,CAAC,OAAO;SACG,CAAC;IAChC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
||||
@@ -3,6 +3,7 @@ export interface HarborForgeMonitorConfig {
|
||||
backendUrl?: string;
|
||||
identifier?: string;
|
||||
apiKey?: string;
|
||||
monitorPort?: number;
|
||||
reportIntervalSec?: number;
|
||||
httpFallbackIntervalSec?: number;
|
||||
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
||||
@@ -19,7 +20,7 @@ export function getLivePluginConfig(
|
||||
const root = (api.config as Record<string, unknown>) || {};
|
||||
const plugins = (root.plugins as Record<string, unknown>) || {};
|
||||
const entries = (plugins.entries as Record<string, unknown>) || {};
|
||||
const entry = (entries['harborforge-monitor'] as Record<string, unknown>) || {};
|
||||
const entry = (entries['harbor-forge'] as Record<string, unknown>) || {};
|
||||
const cfg = (entry.config as Record<string, unknown>) || {};
|
||||
|
||||
if (Object.keys(cfg).length > 0 || Object.keys(entry).length > 0) {
|
||||
|
||||
41
plugin/core/monitor-bridge.d.ts
vendored
Normal file
41
plugin/core/monitor-bridge.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Monitor Bridge Client
|
||||
*
|
||||
* Queries the local HarborForge.Monitor bridge endpoint on MONITOR_PORT
|
||||
* to enrich plugin telemetry with host/hardware data.
|
||||
*
|
||||
* If the bridge is unreachable, all methods return null gracefully —
|
||||
* the plugin continues to function without Monitor data.
|
||||
*/
|
||||
export interface MonitorHealth {
|
||||
status: string;
|
||||
monitor_version: string;
|
||||
identifier: string;
|
||||
}
|
||||
export interface MonitorTelemetryResponse {
|
||||
status: string;
|
||||
monitor_version: string;
|
||||
identifier: string;
|
||||
telemetry?: {
|
||||
identifier: string;
|
||||
plugin_version: string;
|
||||
cpu_pct: number;
|
||||
mem_pct: number;
|
||||
disk_pct: number;
|
||||
swap_pct: number;
|
||||
load_avg: number[];
|
||||
uptime_seconds: number;
|
||||
nginx_installed: boolean;
|
||||
nginx_sites: string[];
|
||||
};
|
||||
last_updated?: string;
|
||||
}
|
||||
export declare class MonitorBridgeClient {
|
||||
private baseUrl;
|
||||
private timeoutMs;
|
||||
constructor(port: number, timeoutMs?: number);
|
||||
health(): Promise<MonitorHealth | null>;
|
||||
telemetry(): Promise<MonitorTelemetryResponse | null>;
|
||||
private fetchJson;
|
||||
}
|
||||
//# sourceMappingURL=monitor-bridge.d.ts.map
|
||||
1
plugin/core/monitor-bridge.d.ts.map
Normal file
1
plugin/core/monitor-bridge.d.ts.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"monitor-bridge.d.ts","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC;QACzB,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO;IAKpC,MAAM,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAIvC,SAAS,IAAI,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;YAI7C,SAAS;CAgBxB"}
|
||||
44
plugin/core/monitor-bridge.js
Normal file
44
plugin/core/monitor-bridge.js
Normal file
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Monitor Bridge Client
|
||||
*
|
||||
* Queries the local HarborForge.Monitor bridge endpoint on MONITOR_PORT
|
||||
* to enrich plugin telemetry with host/hardware data.
|
||||
*
|
||||
* If the bridge is unreachable, all methods return null gracefully —
|
||||
* the plugin continues to function without Monitor data.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MonitorBridgeClient = void 0;
|
||||
class MonitorBridgeClient {
|
||||
baseUrl;
|
||||
timeoutMs;
|
||||
constructor(port, timeoutMs = 3000) {
|
||||
this.baseUrl = `http://127.0.0.1:${port}`;
|
||||
this.timeoutMs = timeoutMs;
|
||||
}
|
||||
async health() {
|
||||
return this.fetchJson('/health');
|
||||
}
|
||||
async telemetry() {
|
||||
return this.fetchJson('/telemetry');
|
||||
}
|
||||
async fetchJson(path) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (!response.ok)
|
||||
return null;
|
||||
return (await response.json());
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.MonitorBridgeClient = MonitorBridgeClient;
|
||||
//# sourceMappingURL=monitor-bridge.js.map
|
||||
1
plugin/core/monitor-bridge.js.map
Normal file
1
plugin/core/monitor-bridge.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"monitor-bridge.js","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA2BH,MAAa,mBAAmB;IACtB,OAAO,CAAS;IAChB,SAAS,CAAS;IAE1B,YAAY,IAAY,EAAE,SAAS,GAAG,IAAI;QACxC,IAAI,CAAC,OAAO,GAAG,oBAAoB,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,IAAI,CAAC,SAAS,CAAgB,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,SAAS,CAA2B,YAAY,CAAC,CAAC;IAChE,CAAC;IAEO,KAAK,CAAC,SAAS,CAAI,IAAY;QACrC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBACrD,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YAEtB,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC;YAC9B,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAjCD,kDAiCC"}
|
||||
69
plugin/core/monitor-bridge.ts
Normal file
69
plugin/core/monitor-bridge.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Monitor Bridge Client
|
||||
*
|
||||
* Queries the local HarborForge.Monitor bridge endpoint on MONITOR_PORT
|
||||
* to enrich plugin telemetry with host/hardware data.
|
||||
*
|
||||
* If the bridge is unreachable, all methods return null gracefully —
|
||||
* the plugin continues to function without Monitor data.
|
||||
*/
|
||||
|
||||
export interface MonitorHealth {
|
||||
status: string;
|
||||
monitor_version: string;
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
export interface MonitorTelemetryResponse {
|
||||
status: string;
|
||||
monitor_version: string;
|
||||
identifier: string;
|
||||
telemetry?: {
|
||||
identifier: string;
|
||||
plugin_version: string;
|
||||
cpu_pct: number;
|
||||
mem_pct: number;
|
||||
disk_pct: number;
|
||||
swap_pct: number;
|
||||
load_avg: number[];
|
||||
uptime_seconds: number;
|
||||
nginx_installed: boolean;
|
||||
nginx_sites: string[];
|
||||
};
|
||||
last_updated?: string;
|
||||
}
|
||||
|
||||
export class MonitorBridgeClient {
|
||||
private baseUrl: string;
|
||||
private timeoutMs: number;
|
||||
|
||||
constructor(port: number, timeoutMs = 3000) {
|
||||
this.baseUrl = `http://127.0.0.1:${port}`;
|
||||
this.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
async health(): Promise<MonitorHealth | null> {
|
||||
return this.fetchJson<MonitorHealth>('/health');
|
||||
}
|
||||
|
||||
async telemetry(): Promise<MonitorTelemetryResponse | null> {
|
||||
return this.fetchJson<MonitorTelemetryResponse>('/telemetry');
|
||||
}
|
||||
|
||||
private async fetchJson<T>(path: string): Promise<T | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!response.ok) return null;
|
||||
return (await response.json()) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os';
|
||||
import { getLivePluginConfig, type HarborForgeMonitorConfig } from './core/live-config';
|
||||
import { MonitorBridgeClient } from './core/monitor-bridge';
|
||||
|
||||
interface PluginAPI {
|
||||
logger: {
|
||||
@@ -50,6 +51,16 @@ export default {
|
||||
return getLivePluginConfig(api, baseConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the monitor bridge client if monitorPort is configured.
|
||||
*/
|
||||
function getBridgeClient(): MonitorBridgeClient | null {
|
||||
const live = resolveConfig() as any;
|
||||
const port = live.monitorPort;
|
||||
if (!port || port <= 0) return null;
|
||||
return new MonitorBridgeClient(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect current system telemetry snapshot.
|
||||
* This data is exposed to the Monitor bridge when it queries the plugin.
|
||||
@@ -99,6 +110,16 @@ export default {
|
||||
},
|
||||
async execute() {
|
||||
const live = resolveConfig();
|
||||
const bridgeClient = getBridgeClient();
|
||||
let monitorBridge = null;
|
||||
|
||||
if (bridgeClient) {
|
||||
const health = await bridgeClient.health();
|
||||
monitorBridge = health
|
||||
? { connected: true, ...health }
|
||||
: { connected: false, error: 'Monitor bridge unreachable' };
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: live.enabled !== false,
|
||||
config: {
|
||||
@@ -108,6 +129,7 @@ export default {
|
||||
reportIntervalSec: live.reportIntervalSec,
|
||||
hasApiKey: Boolean(live.apiKey),
|
||||
},
|
||||
monitorBridge,
|
||||
telemetry: collectTelemetry(),
|
||||
};
|
||||
},
|
||||
@@ -126,6 +148,33 @@ export default {
|
||||
},
|
||||
}));
|
||||
|
||||
// Tool: query Monitor bridge for host hardware telemetry
|
||||
api.registerTool(() => ({
|
||||
name: 'harborforge_monitor_telemetry',
|
||||
description: 'Query HarborForge Monitor bridge for host hardware telemetry (CPU, memory, disk, etc.)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
async execute() {
|
||||
const bridgeClient = getBridgeClient();
|
||||
if (!bridgeClient) {
|
||||
return {
|
||||
error: 'Monitor bridge not configured (monitorPort not set or 0)',
|
||||
};
|
||||
}
|
||||
|
||||
const data = await bridgeClient.telemetry();
|
||||
if (!data) {
|
||||
return {
|
||||
error: 'Monitor bridge unreachable',
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
}));
|
||||
|
||||
logger.info('HarborForge plugin registered (id: harbor-forge)');
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user