Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
337
extensions/signal/src/send.ts
Normal file
337
extensions/signal/src/send.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
// Signal plugin module implements send behavior.
|
||||
import {
|
||||
createMessageReceiptFromOutboundResults,
|
||||
type MessageReceipt,
|
||||
type MessageReceiptPartKind,
|
||||
type MessageReceiptSourceResult,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
||||
import { kindFromMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { resolveOutboundAttachmentFromUrl } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { resolveSignalAccount } from "./accounts.js";
|
||||
import { signalRpcRequest } from "./client-adapter.js";
|
||||
import { markdownToSignalText, type SignalTextStyleRange } from "./format.js";
|
||||
import { resolveSignalRpcContext } from "./rpc-context.js";
|
||||
|
||||
export type SignalSendOpts = {
|
||||
cfg: OpenClawConfig;
|
||||
baseUrl?: string;
|
||||
account?: string;
|
||||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
maxBytes?: number;
|
||||
timeoutMs?: number;
|
||||
textMode?: "markdown" | "plain";
|
||||
textStyles?: SignalTextStyleRange[];
|
||||
};
|
||||
|
||||
export type SignalSendResult = {
|
||||
messageId: string;
|
||||
timestamp?: number;
|
||||
receipt: MessageReceipt;
|
||||
};
|
||||
|
||||
export type SignalRpcOpts = Pick<
|
||||
SignalSendOpts,
|
||||
"cfg" | "baseUrl" | "account" | "accountId" | "timeoutMs"
|
||||
>;
|
||||
|
||||
export type SignalReceiptType = "read" | "viewed";
|
||||
|
||||
type SignalTarget =
|
||||
| { type: "recipient"; recipient: string }
|
||||
| { type: "group"; groupId: string }
|
||||
| { type: "username"; username: string };
|
||||
|
||||
async function resolveSignalRpcAccountInfo(opts: SignalRpcOpts) {
|
||||
if (opts.baseUrl?.trim() && opts.account?.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
if (!opts.cfg) {
|
||||
throw new Error(
|
||||
"Signal RPC account resolution requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path.",
|
||||
);
|
||||
}
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Signal RPC account resolution");
|
||||
return resolveSignalAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
}
|
||||
|
||||
function parseTarget(raw: string): SignalTarget {
|
||||
let value = raw.trim();
|
||||
if (!value) {
|
||||
throw new Error("Signal recipient is required");
|
||||
}
|
||||
const lower = normalizeLowercaseStringOrEmpty(value);
|
||||
if (lower.startsWith("signal:")) {
|
||||
value = value.slice("signal:".length).trim();
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(value);
|
||||
if (normalized.startsWith("group:")) {
|
||||
return { type: "group", groupId: value.slice("group:".length).trim() };
|
||||
}
|
||||
if (normalized.startsWith("username:")) {
|
||||
return {
|
||||
type: "username",
|
||||
username: value.slice("username:".length).trim(),
|
||||
};
|
||||
}
|
||||
if (normalized.startsWith("u:")) {
|
||||
return { type: "username", username: value.trim() };
|
||||
}
|
||||
return { type: "recipient", recipient: value };
|
||||
}
|
||||
|
||||
type SignalTargetParams = {
|
||||
recipient?: string[];
|
||||
groupId?: string;
|
||||
username?: string[];
|
||||
};
|
||||
|
||||
type SignalTargetAllowlist = {
|
||||
recipient?: boolean;
|
||||
group?: boolean;
|
||||
username?: boolean;
|
||||
};
|
||||
|
||||
function buildTargetParams(
|
||||
target: SignalTarget,
|
||||
allow: SignalTargetAllowlist,
|
||||
): SignalTargetParams | null {
|
||||
if (target.type === "recipient") {
|
||||
if (!allow.recipient) {
|
||||
return null;
|
||||
}
|
||||
return { recipient: [target.recipient] };
|
||||
}
|
||||
if (target.type === "group") {
|
||||
if (!allow.group) {
|
||||
return null;
|
||||
}
|
||||
return { groupId: target.groupId };
|
||||
}
|
||||
if (target.type === "username") {
|
||||
if (!allow.username) {
|
||||
return null;
|
||||
}
|
||||
return { username: [target.username] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function createSignalSendReceipt(params: {
|
||||
messageId: string;
|
||||
timestamp?: number;
|
||||
target: SignalTarget;
|
||||
kind: MessageReceiptPartKind;
|
||||
}): MessageReceipt {
|
||||
const messageId = params.messageId.trim();
|
||||
const results: MessageReceiptSourceResult[] =
|
||||
messageId && messageId !== "unknown"
|
||||
? [
|
||||
{
|
||||
channel: "signal",
|
||||
messageId,
|
||||
meta: {
|
||||
targetType: params.target.type,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
if (results[0]) {
|
||||
if (params.timestamp != null) {
|
||||
results[0].timestamp = params.timestamp;
|
||||
}
|
||||
if (params.target.type === "group") {
|
||||
results[0].chatId = params.target.groupId;
|
||||
} else if (params.target.type === "recipient") {
|
||||
results[0].toJid = params.target.recipient;
|
||||
} else {
|
||||
results[0].toJid = params.target.username;
|
||||
}
|
||||
}
|
||||
return createMessageReceiptFromOutboundResults({
|
||||
results,
|
||||
kind: params.kind,
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendMessageSignal(
|
||||
to: string,
|
||||
text: string,
|
||||
opts: SignalSendOpts,
|
||||
): Promise<SignalSendResult> {
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Signal send");
|
||||
const apiMode = cfg.channels?.signal?.apiMode;
|
||||
const accountInfo = resolveSignalAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo);
|
||||
const target = parseTarget(to);
|
||||
let message = text ?? "";
|
||||
let messageFromPlaceholder = false;
|
||||
let textStyles: SignalTextStyleRange[] = [];
|
||||
const textMode = opts.textMode ?? "markdown";
|
||||
const maxBytes = (() => {
|
||||
if (typeof opts.maxBytes === "number") {
|
||||
return opts.maxBytes;
|
||||
}
|
||||
if (typeof accountInfo.config.mediaMaxMb === "number") {
|
||||
return accountInfo.config.mediaMaxMb * 1024 * 1024;
|
||||
}
|
||||
if (typeof cfg.agents?.defaults?.mediaMaxMb === "number") {
|
||||
return cfg.agents.defaults.mediaMaxMb * 1024 * 1024;
|
||||
}
|
||||
return 8 * 1024 * 1024;
|
||||
})();
|
||||
|
||||
let attachments: string[] | undefined;
|
||||
if (opts.mediaUrl?.trim()) {
|
||||
const resolved = await resolveOutboundAttachmentFromUrl(opts.mediaUrl.trim(), maxBytes, {
|
||||
mediaAccess: opts.mediaAccess,
|
||||
localRoots: opts.mediaLocalRoots,
|
||||
readFile: opts.mediaReadFile,
|
||||
});
|
||||
attachments = [resolved.path];
|
||||
const kind = kindFromMime(resolved.contentType ?? undefined);
|
||||
if (!message && kind) {
|
||||
// Avoid sending an empty body when only attachments exist.
|
||||
message = kind === "image" ? "<media:image>" : `<media:${kind}>`;
|
||||
messageFromPlaceholder = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.trim() && !messageFromPlaceholder) {
|
||||
if (textMode === "plain") {
|
||||
textStyles = opts.textStyles ?? [];
|
||||
} else {
|
||||
const tableMode = resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "signal",
|
||||
accountId: accountInfo.accountId,
|
||||
});
|
||||
const formatted = markdownToSignalText(message, { tableMode });
|
||||
message = formatted.text;
|
||||
textStyles = formatted.styles;
|
||||
}
|
||||
}
|
||||
|
||||
if (!message.trim() && (!attachments || attachments.length === 0)) {
|
||||
throw new Error("Signal send requires text or media");
|
||||
}
|
||||
|
||||
const params: Record<string, unknown> = { message };
|
||||
if (textStyles.length > 0) {
|
||||
params["text-style"] = textStyles.map(
|
||||
(style) => `${style.start}:${style.length}:${style.style}`,
|
||||
);
|
||||
}
|
||||
if (account) {
|
||||
params.account = account;
|
||||
}
|
||||
if (attachments && attachments.length > 0) {
|
||||
params.attachments = attachments;
|
||||
}
|
||||
|
||||
const targetParams = buildTargetParams(target, {
|
||||
recipient: true,
|
||||
group: true,
|
||||
username: true,
|
||||
});
|
||||
if (!targetParams) {
|
||||
throw new Error("Signal recipient is required");
|
||||
}
|
||||
Object.assign(params, targetParams);
|
||||
|
||||
const result = await signalRpcRequest<{ timestamp?: number }>("send", params, {
|
||||
baseUrl,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
apiMode,
|
||||
});
|
||||
const timestamp = result?.timestamp;
|
||||
const messageId = timestamp ? String(timestamp) : "unknown";
|
||||
return {
|
||||
messageId,
|
||||
timestamp,
|
||||
receipt: createSignalSendReceipt({
|
||||
messageId,
|
||||
target,
|
||||
kind: attachments && attachments.length > 0 ? "media" : "text",
|
||||
...(timestamp != null ? { timestamp } : {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendTypingSignal(
|
||||
to: string,
|
||||
opts: SignalRpcOpts & { stop?: boolean },
|
||||
): Promise<boolean> {
|
||||
const accountInfo = await resolveSignalRpcAccountInfo(opts);
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Signal typing");
|
||||
const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo);
|
||||
const targetParams = buildTargetParams(parseTarget(to), {
|
||||
recipient: true,
|
||||
group: true,
|
||||
});
|
||||
if (!targetParams) {
|
||||
return false;
|
||||
}
|
||||
const params: Record<string, unknown> = { ...targetParams };
|
||||
if (account) {
|
||||
params.account = account;
|
||||
}
|
||||
if (opts.stop) {
|
||||
params.stop = true;
|
||||
}
|
||||
await signalRpcRequest("sendTyping", params, {
|
||||
baseUrl,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
apiMode: cfg.channels?.signal?.apiMode,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function sendReadReceiptSignal(
|
||||
to: string,
|
||||
targetTimestamp: number,
|
||||
opts: SignalRpcOpts & { type?: SignalReceiptType },
|
||||
): Promise<boolean> {
|
||||
if (!Number.isFinite(targetTimestamp) || targetTimestamp <= 0) {
|
||||
return false;
|
||||
}
|
||||
const accountInfo = await resolveSignalRpcAccountInfo(opts);
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Signal read receipt");
|
||||
const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo);
|
||||
const targetParams = buildTargetParams(parseTarget(to), {
|
||||
recipient: true,
|
||||
});
|
||||
if (!targetParams) {
|
||||
return false;
|
||||
}
|
||||
const params: Record<string, unknown> = {
|
||||
...targetParams,
|
||||
targetTimestamp,
|
||||
type: opts.type ?? "read",
|
||||
};
|
||||
if (account) {
|
||||
params.account = account;
|
||||
}
|
||||
await signalRpcRequest("sendReceipt", params, {
|
||||
baseUrl,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
apiMode: cfg.channels?.signal?.apiMode,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user