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:
297
test/scripts/android-release-signing.test.ts
Normal file
297
test/scripts/android-release-signing.test.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
// Android release signing tests cover encrypted signing asset sync and local materialization.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
const SCRIPT = path.join(process.cwd(), "scripts", "android-release-signing.mjs");
|
||||
const MATCH_PASSWORD = "test-match-password";
|
||||
const STORE_PASSWORD = "store_secret_value";
|
||||
const KEY_PASSWORD = "key_secret_value";
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
function makeTempRoot(): string {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-android-signing-"));
|
||||
tempRoots.push(tempRoot);
|
||||
return tempRoot;
|
||||
}
|
||||
|
||||
function runNode(args: string[], env: NodeJS.ProcessEnv = {}) {
|
||||
try {
|
||||
const stdout = execFileSync(process.execPath, [SCRIPT, ...args], {
|
||||
env: { ...process.env, ...env },
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
return { ok: true, stdout, stderr: "" };
|
||||
} catch (error) {
|
||||
const e = error as { stdout?: unknown; stderr?: unknown };
|
||||
return {
|
||||
ok: false,
|
||||
stdout: formatProcessOutput(e.stdout),
|
||||
stderr: formatProcessOutput(e.stderr),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatProcessOutput(value: unknown): string {
|
||||
if (Buffer.isBuffer(value)) {
|
||||
return value.toString("utf8");
|
||||
}
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function runGit(args: string[], cwd?: string, env: NodeJS.ProcessEnv = {}) {
|
||||
execFileSync("git", args, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_AUTHOR_NAME: "OpenClaw Test",
|
||||
GIT_AUTHOR_EMAIL: "test@example.com",
|
||||
GIT_COMMITTER_NAME: "OpenClaw Test",
|
||||
GIT_COMMITTER_EMAIL: "test@example.com",
|
||||
GIT_CONFIG_COUNT: "1",
|
||||
GIT_CONFIG_KEY_0: "commit.gpgsign",
|
||||
GIT_CONFIG_VALUE_0: "false",
|
||||
...env,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
function commandAvailable(command: string): boolean {
|
||||
try {
|
||||
execFileSync(command, ["version"], { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createSigningRepo(tempRoot: string): string {
|
||||
const seed = path.join(tempRoot, "seed");
|
||||
const remote = path.join(tempRoot, "apps-signing.git");
|
||||
fs.mkdirSync(seed, { recursive: true });
|
||||
runGit(["init", "--initial-branch=main"], seed);
|
||||
fs.writeFileSync(path.join(seed, "README.md"), "# signing\n");
|
||||
runGit(["add", "README.md"], seed);
|
||||
runGit(["commit", "-m", "Initial signing repo"], seed);
|
||||
runGit(["clone", "--bare", seed, remote], tempRoot);
|
||||
return remote;
|
||||
}
|
||||
|
||||
function writeManifest(tempRoot: string, signingRepo: string): string {
|
||||
const manifestPath = path.join(tempRoot, "ReleaseSigning.json");
|
||||
fs.writeFileSync(
|
||||
manifestPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
signingRepo,
|
||||
signingBranch: "main",
|
||||
assetPath: "android/openclaw",
|
||||
uploadKeystoreEncryptedFile: "upload-keystore.jks.enc",
|
||||
gradlePropertiesEncryptedFile: "gradle.properties.enc",
|
||||
materializedRoot: "unused-by-test",
|
||||
gradlePropertyNames: [
|
||||
"OPENCLAW_ANDROID_STORE_FILE",
|
||||
"OPENCLAW_ANDROID_STORE_PASSWORD",
|
||||
"OPENCLAW_ANDROID_KEY_ALIAS",
|
||||
"OPENCLAW_ANDROID_KEY_PASSWORD",
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return manifestPath;
|
||||
}
|
||||
|
||||
function writeSigningSources(tempRoot: string) {
|
||||
const keystorePath = path.join(tempRoot, "upload-keystore.jks");
|
||||
const propertiesPath = path.join(tempRoot, "source.properties");
|
||||
fs.writeFileSync(keystorePath, "fake keystore bytes\n");
|
||||
fs.writeFileSync(
|
||||
propertiesPath,
|
||||
[
|
||||
`OPENCLAW_ANDROID_STORE_PASSWORD=${STORE_PASSWORD}`,
|
||||
"OPENCLAW_ANDROID_KEY_ALIAS=openclaw-upload",
|
||||
`OPENCLAW_ANDROID_KEY_PASSWORD=${KEY_PASSWORD}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
return { keystorePath, propertiesPath };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const tempRoot of tempRoots.splice(0)) {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("scripts/android-release-signing.mjs", () => {
|
||||
it.each([
|
||||
["--mode"],
|
||||
["--mode", "--manifest"],
|
||||
["--mode", "-h"],
|
||||
["--manifest"],
|
||||
["--manifest", "-h"],
|
||||
["--workspace", "--mode"],
|
||||
["--workspace", "-h"],
|
||||
["--materialized-dir", "--mode"],
|
||||
["--materialized-dir", "-h"],
|
||||
["--keystore", "--mode"],
|
||||
["--keystore", "-h"],
|
||||
["--properties", "--mode"],
|
||||
["--properties", "-h"],
|
||||
])("rejects missing values for %s before release signing work", (...args) => {
|
||||
const result = runNode(args);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.stderr).toContain(`Missing value for ${args[0]}.`);
|
||||
expect(result.stderr).not.toContain("ENOENT");
|
||||
expect(result.stdout).toBe("");
|
||||
});
|
||||
|
||||
it("documents the canonical Android release signing plan", () => {
|
||||
const result = runNode(["--mode", "plan"]);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.stdout).toContain("Signing repo: git@github.com:openclaw/apps-signing.git");
|
||||
expect(result.stdout).toContain("Signing assets: android/openclaw");
|
||||
expect(result.stdout).toContain("Materialized output: apps/android/build/release-signing");
|
||||
expect(result.stdout).toContain("ORG_GRADLE_PROJECT_*");
|
||||
});
|
||||
|
||||
it.runIf(commandAvailable("openssl"))(
|
||||
"encrypts, pulls, and materializes Android signing assets without printing secrets",
|
||||
() => {
|
||||
const tempRoot = makeTempRoot();
|
||||
const signingRepo = createSigningRepo(tempRoot);
|
||||
const manifestPath = writeManifest(tempRoot, signingRepo);
|
||||
const { keystorePath, propertiesPath } = writeSigningSources(tempRoot);
|
||||
const materializedDir = path.join(tempRoot, "materialized");
|
||||
const workspace = path.join(materializedDir, "apps-signing");
|
||||
const env = {
|
||||
MATCH_PASSWORD,
|
||||
GIT_AUTHOR_NAME: "OpenClaw Test",
|
||||
GIT_AUTHOR_EMAIL: "test@example.com",
|
||||
GIT_COMMITTER_NAME: "OpenClaw Test",
|
||||
GIT_COMMITTER_EMAIL: "test@example.com",
|
||||
GIT_CONFIG_COUNT: "1",
|
||||
GIT_CONFIG_KEY_0: "commit.gpgsign",
|
||||
GIT_CONFIG_VALUE_0: "false",
|
||||
};
|
||||
|
||||
const push = runNode(
|
||||
[
|
||||
"--mode",
|
||||
"sync-push",
|
||||
"--manifest",
|
||||
manifestPath,
|
||||
"--workspace",
|
||||
workspace,
|
||||
"--materialized-dir",
|
||||
materializedDir,
|
||||
"--keystore",
|
||||
keystorePath,
|
||||
"--properties",
|
||||
propertiesPath,
|
||||
],
|
||||
env,
|
||||
);
|
||||
|
||||
expect(push.ok).toBe(true);
|
||||
expect(`${push.stdout}${push.stderr}`).not.toContain(STORE_PASSWORD);
|
||||
expect(`${push.stdout}${push.stderr}`).not.toContain(KEY_PASSWORD);
|
||||
|
||||
const remoteCheck = path.join(tempRoot, "remote-check");
|
||||
runGit(["clone", signingRepo, remoteCheck], tempRoot);
|
||||
const encryptedProperties = fs.readFileSync(
|
||||
path.join(remoteCheck, "android", "openclaw", "gradle.properties.enc"),
|
||||
"utf8",
|
||||
);
|
||||
expect(encryptedProperties).not.toContain(STORE_PASSWORD);
|
||||
expect(encryptedProperties).not.toContain(KEY_PASSWORD);
|
||||
|
||||
fs.mkdirSync(materializedDir, { recursive: true });
|
||||
const stalePropertiesPath = path.join(materializedDir, "gradle.properties");
|
||||
fs.writeFileSync(stalePropertiesPath, "stale=1\n", { mode: 0o644 });
|
||||
fs.chmodSync(stalePropertiesPath, 0o644);
|
||||
|
||||
const pull = runNode(
|
||||
[
|
||||
"--mode",
|
||||
"sync-pull",
|
||||
"--manifest",
|
||||
manifestPath,
|
||||
"--workspace",
|
||||
path.join(materializedDir, "pull-workspace"),
|
||||
"--materialized-dir",
|
||||
materializedDir,
|
||||
],
|
||||
env,
|
||||
);
|
||||
|
||||
expect(pull.ok).toBe(true);
|
||||
expect(`${pull.stdout}${pull.stderr}`).not.toContain(STORE_PASSWORD);
|
||||
expect(`${pull.stdout}${pull.stderr}`).not.toContain(KEY_PASSWORD);
|
||||
expect(fs.readFileSync(path.join(materializedDir, "upload-keystore.jks"), "utf8")).toBe(
|
||||
"fake keystore bytes\n",
|
||||
);
|
||||
|
||||
const materializedProperties = fs.readFileSync(
|
||||
path.join(materializedDir, "gradle.properties"),
|
||||
"utf8",
|
||||
);
|
||||
expect(materializedProperties).toContain(
|
||||
`OPENCLAW_ANDROID_STORE_FILE=${path.join(materializedDir, "upload-keystore.jks")}`,
|
||||
);
|
||||
expect(materializedProperties).toContain(`OPENCLAW_ANDROID_STORE_PASSWORD=${STORE_PASSWORD}`);
|
||||
expect(materializedProperties).toContain("OPENCLAW_ANDROID_KEY_ALIAS=openclaw-upload");
|
||||
expect(materializedProperties).toContain(`OPENCLAW_ANDROID_KEY_PASSWORD=${KEY_PASSWORD}`);
|
||||
if (process.platform !== "win32") {
|
||||
expect(fs.statSync(path.join(materializedDir, "gradle.properties")).mode & 0o777).toBe(
|
||||
0o600,
|
||||
);
|
||||
expect(fs.statSync(path.join(materializedDir, "upload-keystore.jks")).mode & 0o777).toBe(
|
||||
0o600,
|
||||
);
|
||||
}
|
||||
|
||||
const check = runNode([
|
||||
"--mode",
|
||||
"check",
|
||||
"--manifest",
|
||||
manifestPath,
|
||||
"--materialized-dir",
|
||||
materializedDir,
|
||||
]);
|
||||
|
||||
expect(check.ok).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("requires MATCH_PASSWORD before pushing encrypted signing assets", () => {
|
||||
const tempRoot = makeTempRoot();
|
||||
const manifestPath = writeManifest(tempRoot, path.join(tempRoot, "apps-signing.git"));
|
||||
const { keystorePath, propertiesPath } = writeSigningSources(tempRoot);
|
||||
|
||||
const result = runNode([
|
||||
"--mode",
|
||||
"sync-push",
|
||||
"--manifest",
|
||||
manifestPath,
|
||||
"--keystore",
|
||||
keystorePath,
|
||||
"--properties",
|
||||
propertiesPath,
|
||||
]);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.stderr).toContain("MATCH_PASSWORD is required");
|
||||
expect(result.stderr).not.toContain(STORE_PASSWORD);
|
||||
expect(result.stderr).not.toContain(KEY_PASSWORD);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user