mirror of
https://github.com/documenso/documenso.git
synced 2026-08-17 20:11:49 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73de55ce0a | ||
|
|
53ccd42ee8 | ||
|
|
688ef2fdf3 | ||
|
|
a5e37af3e8 |
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: create-justification
|
||||
description: Create a new justification file in .agents/justifications/ with a unique three-word ID, frontmatter, and formatted title
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
audience: agents
|
||||
workflow: decision-making
|
||||
---
|
||||
|
||||
## What I do
|
||||
|
||||
I help you create new justification files in the `.agents/justifications/` directory. Each justification file gets:
|
||||
|
||||
- A unique three-word identifier (e.g., `swift-emerald-river`)
|
||||
- Frontmatter with the current date and formatted title
|
||||
- Content you provide
|
||||
|
||||
## How to use
|
||||
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "decision-name" "Justification content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "decision-name" << HEREDOC
|
||||
Multi-line
|
||||
justification content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
## File format
|
||||
|
||||
Files are created as: `{three-word-id}-{slug}.md`
|
||||
|
||||
Example: `swift-emerald-river-decision-name.md`
|
||||
|
||||
The file includes frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Decision Name
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this skill when you need to document the reasoning or justification for a decision, approach, or architectural choice. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: create-plan
|
||||
description: Create a new plan file in .agents/plans/ with a unique three-word ID, frontmatter, and formatted title
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
audience: agents
|
||||
workflow: planning
|
||||
---
|
||||
|
||||
## What I do
|
||||
|
||||
I help you create new plan files in the `.agents/plans/` directory. Each plan file gets:
|
||||
|
||||
- A unique three-word identifier (e.g., `happy-blue-moon`)
|
||||
- Frontmatter with the current date and formatted title
|
||||
- Content you provide
|
||||
|
||||
## How to use
|
||||
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "feature-name" "Plan content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "feature-name" << HEREDOC
|
||||
Multi-line
|
||||
plan content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
## File format
|
||||
|
||||
Files are created as: `{three-word-id}-{slug}.md`
|
||||
|
||||
Example: `happy-blue-moon-feature-name.md`
|
||||
|
||||
The file includes frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Feature Name
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this skill when you need to create a new plan document for a feature, task, or project. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: create-scratch
|
||||
description: Create a new scratch file in .agents/scratches/ with a unique three-word ID, frontmatter, and formatted title
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
audience: agents
|
||||
workflow: exploration
|
||||
---
|
||||
|
||||
## What I do
|
||||
|
||||
I help you create new scratch files in the `.agents/scratches/` directory. Each scratch file gets:
|
||||
|
||||
- A unique three-word identifier (e.g., `calm-teal-cloud`)
|
||||
- Frontmatter with the current date and formatted title
|
||||
- Content you provide
|
||||
|
||||
## How to use
|
||||
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "note-name" "Scratch content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "note-name" << HEREDOC
|
||||
Multi-line
|
||||
scratch content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
## File format
|
||||
|
||||
Files are created as: `{three-word-id}-{slug}.md`
|
||||
|
||||
Example: `calm-teal-cloud-note-name.md`
|
||||
|
||||
The file includes frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Note Name
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this skill when you need to create a temporary note, exploration document, or scratch pad for ideas. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err}
|
||||
- POSTGRES_DB=${POSTGRES_DB:?err}
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
volumes:
|
||||
- documenso_database:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
|
||||
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -8,7 +8,7 @@ services:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err}
|
||||
- POSTGRES_DB=${POSTGRES_DB:?err}
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -8,7 +8,7 @@ services:
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=documenso
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U documenso']
|
||||
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
+147
-147
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ const CONTENT_TYPE_MULTIPART = 'multipart/form-data';
|
||||
const getUrlEncodedBody = async (req: Request) => {
|
||||
const params = new URLSearchParams(await req.text());
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
const data: Record<string, string[]> = {};
|
||||
|
||||
for (const key of params.keys()) {
|
||||
data[key] = params.getAll(key);
|
||||
@@ -22,18 +22,20 @@ const getUrlEncodedBody = async (req: Request) => {
|
||||
const getMultipartBody = async (req: Request) => {
|
||||
const formData = await req.formData();
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
const data: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
// !: Handles cases where our generated SDKs send key[] syntax for arrays.
|
||||
const normalizedKey = key.endsWith('[]') ? key.slice(0, -2) : key;
|
||||
|
||||
if (data[normalizedKey] === undefined) {
|
||||
const existing = data[normalizedKey];
|
||||
|
||||
if (existing === undefined) {
|
||||
data[normalizedKey] = value;
|
||||
} else if (Array.isArray(data[normalizedKey])) {
|
||||
data[normalizedKey].push(value);
|
||||
} else if (Array.isArray(existing)) {
|
||||
existing.push(value);
|
||||
} else {
|
||||
data[normalizedKey] = [data[normalizedKey], value];
|
||||
data[normalizedKey] = [existing, value];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +140,10 @@ const createRequestProxy = async (req: Request, url?: string) => {
|
||||
}
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
||||
return (target as unknown as Record<string | number | symbol, unknown>)[prop];
|
||||
// SAFETY: Every property this trap does not special-case is forwarded from the
|
||||
// original Request, so `prop` can only be a key the caller reads off a Request.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return target[prop as keyof Request];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -160,23 +164,25 @@ export const createOpenApiFetchHandler = async <TRouter extends OpenApiRouter>(
|
||||
const url = new URL(opts.req.url.replace(opts.endpoint, ''));
|
||||
const req: Request = await createRequestProxy(opts.req, url.toString());
|
||||
|
||||
// The handler is typed against Node HTTP req/res, but only reads properties our request
|
||||
// proxy and mock response provide, so we declare it against the fetch-based types we pass.
|
||||
// @ts-expect-error Inherited from original fetch handler in `trpc-to-openapi`
|
||||
const openApiHttpHandler = createOpenApiNodeHttpHandler(opts);
|
||||
const openApiHttpHandler: (req: Request, res: ServerResponse) => void = createOpenApiNodeHttpHandler(opts);
|
||||
|
||||
return new Promise<Response>((resolve) => {
|
||||
let statusCode: number;
|
||||
|
||||
// Create a mock ServerResponse object that bridges Node HTTP APIs with Fetch API Response.
|
||||
// This allows the Node HTTP handler to work with Fetch API Request objects.
|
||||
// SAFETY: The Node HTTP handler only calls setHeader/statusCode/end on the response,
|
||||
// which this mock implements to bridge Node HTTP APIs with a Fetch API Response.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const res = {
|
||||
setHeader: (key: string, value: string | readonly string[]) => {
|
||||
if (typeof value === 'string') {
|
||||
resHeaders.set(key, value);
|
||||
} else {
|
||||
setHeader: (key: string, value: string | string[]) => {
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
resHeaders.append(key, v);
|
||||
}
|
||||
} else {
|
||||
resHeaders.set(key, value);
|
||||
}
|
||||
},
|
||||
get statusCode() {
|
||||
@@ -195,14 +201,6 @@ export const createOpenApiFetchHandler = async <TRouter extends OpenApiRouter>(
|
||||
},
|
||||
} as ServerResponse;
|
||||
|
||||
// Type assertions are necessary here for interop between Fetch API Request/Response
|
||||
// and Node HTTP IncomingMessage/ServerResponse types.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const nodeReq = req as unknown as Parameters<typeof openApiHttpHandler>[0];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const nodeRes = res as unknown as Parameters<typeof openApiHttpHandler>[1];
|
||||
|
||||
void openApiHttpHandler(nodeReq, nodeRes);
|
||||
void openApiHttpHandler(req, res);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
BRANDING_LOGO_MAX_SIZE_MB,
|
||||
} from '@documenso/lib/constants/branding';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import type { ZodRawShape } from 'zod';
|
||||
import z from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
@@ -49,10 +48,10 @@ export const zfdBrandingImageFile = () => {
|
||||
* an error. This provides the same functionality as `zfd.formData()` but
|
||||
* can be considered somewhat safer.
|
||||
*/
|
||||
export const zodFormData = <T extends ZodRawShape>(schema: T) => {
|
||||
export const zodFormData = <T extends Parameters<typeof z.object>[0]>(schema: T) => {
|
||||
return z.preprocess((data) => {
|
||||
if (data instanceof FormData) {
|
||||
const formData: Record<string, unknown> = {};
|
||||
const formData: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {};
|
||||
|
||||
for (const key of data.keys()) {
|
||||
const values = data.getAll(key);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { generateId } from './utils/generate-id';
|
||||
|
||||
const JUSTIFICATIONS_DIR = join(process.cwd(), '.agents', 'justifications');
|
||||
|
||||
const main = () => {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: npx tsx scripts/create-justification.ts "file-slug" [content]');
|
||||
console.error(' or: npx tsx scripts/create-justification.ts "file-slug" << HEREDOC');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const slug = args[0];
|
||||
let content = '';
|
||||
|
||||
// Check if content is provided as second argument
|
||||
if (args.length > 1) {
|
||||
content = args.slice(1).join(' ');
|
||||
} else {
|
||||
// Read from stdin (heredoc)
|
||||
try {
|
||||
const stdin = readFileSync(0, 'utf-8');
|
||||
content = stdin.trim();
|
||||
} catch (error) {
|
||||
console.error('Error reading from stdin:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
console.error('Error: No content provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate unique ID
|
||||
const id = generateId();
|
||||
const filename = `${id}-${slug}.md`;
|
||||
const filepath = join(JUSTIFICATIONS_DIR, filename);
|
||||
|
||||
// Format title from slug (kebab-case to Title Case)
|
||||
const title = slug
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
|
||||
// Get current date in ISO format
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Create frontmatter
|
||||
const frontmatter = `---
|
||||
date: ${date}
|
||||
title: ${title}
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
// Ensure directory exists
|
||||
mkdirSync(JUSTIFICATIONS_DIR, { recursive: true });
|
||||
|
||||
// Write file with frontmatter
|
||||
writeFileSync(filepath, frontmatter + content, 'utf-8');
|
||||
|
||||
console.log(`Created justification: ${filepath}`);
|
||||
console.log(`ID: ${id}`);
|
||||
console.log(`Filename: ${filename}`);
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { generateId } from './utils/generate-id';
|
||||
|
||||
const PLANS_DIR = join(process.cwd(), '.agents', 'plans');
|
||||
|
||||
const main = () => {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: npx tsx scripts/create-plan.ts "file-slug" [content]');
|
||||
console.error(' or: npx tsx scripts/create-plan.ts "file-slug" << HEREDOC');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const slug = args[0];
|
||||
let content = '';
|
||||
|
||||
// Check if content is provided as second argument
|
||||
if (args.length > 1) {
|
||||
content = args.slice(1).join(' ');
|
||||
} else {
|
||||
// Read from stdin (heredoc)
|
||||
try {
|
||||
const stdin = readFileSync(0, 'utf-8');
|
||||
content = stdin.trim();
|
||||
} catch (error) {
|
||||
console.error('Error reading from stdin:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
console.error('Error: No content provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate unique ID
|
||||
const id = generateId();
|
||||
const filename = `${id}-${slug}.md`;
|
||||
const filepath = join(PLANS_DIR, filename);
|
||||
|
||||
// Format title from slug (kebab-case to Title Case)
|
||||
const title = slug
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
|
||||
// Get current date in ISO format
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Create frontmatter
|
||||
const frontmatter = `---
|
||||
date: ${date}
|
||||
title: ${title}
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
// Ensure directory exists
|
||||
mkdirSync(PLANS_DIR, { recursive: true });
|
||||
|
||||
// Write file with frontmatter
|
||||
writeFileSync(filepath, frontmatter + content, 'utf-8');
|
||||
|
||||
console.log(`Created plan: ${filepath}`);
|
||||
console.log(`ID: ${id}`);
|
||||
console.log(`Filename: ${filename}`);
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { generateId } from './utils/generate-id';
|
||||
|
||||
const SCRATCHES_DIR = join(process.cwd(), '.agents', 'scratches');
|
||||
|
||||
const main = () => {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: npx tsx scripts/create-scratch.ts "file-slug" [content]');
|
||||
console.error(' or: npx tsx scripts/create-scratch.ts "file-slug" << HEREDOC');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const slug = args[0];
|
||||
let content = '';
|
||||
|
||||
// Check if content is provided as second argument
|
||||
if (args.length > 1) {
|
||||
content = args.slice(1).join(' ');
|
||||
} else {
|
||||
// Read from stdin (heredoc)
|
||||
try {
|
||||
const stdin = readFileSync(0, 'utf-8');
|
||||
content = stdin.trim();
|
||||
} catch (error) {
|
||||
console.error('Error reading from stdin:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
console.error('Error: No content provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate unique ID
|
||||
const id = generateId();
|
||||
const filename = `${id}-${slug}.md`;
|
||||
const filepath = join(SCRATCHES_DIR, filename);
|
||||
|
||||
// Format title from slug (kebab-case to Title Case)
|
||||
const title = slug
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
|
||||
// Get current date in ISO format
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Create frontmatter
|
||||
const frontmatter = `---
|
||||
date: ${date}
|
||||
title: ${title}
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
// Ensure directory exists
|
||||
mkdirSync(SCRATCHES_DIR, { recursive: true });
|
||||
|
||||
// Write file with frontmatter
|
||||
writeFileSync(filepath, frontmatter + content, 'utf-8');
|
||||
|
||||
console.log(`Created scratch: ${filepath}`);
|
||||
console.log(`ID: ${id}`);
|
||||
console.log(`Filename: ${filename}`);
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Generates a unique identifier using three simple words.
|
||||
* Falls back to unix timestamp if word generation fails.
|
||||
*/
|
||||
export const generateId = (): string => {
|
||||
const adjectives = [
|
||||
'happy',
|
||||
'bright',
|
||||
'swift',
|
||||
'calm',
|
||||
'bold',
|
||||
'clever',
|
||||
'gentle',
|
||||
'quick',
|
||||
'sharp',
|
||||
'warm',
|
||||
'cool',
|
||||
'fresh',
|
||||
'solid',
|
||||
'clear',
|
||||
'sweet',
|
||||
'wild',
|
||||
'quiet',
|
||||
'loud',
|
||||
'smooth',
|
||||
];
|
||||
|
||||
const nouns = [
|
||||
'moon',
|
||||
'star',
|
||||
'ocean',
|
||||
'river',
|
||||
'forest',
|
||||
'mountain',
|
||||
'cloud',
|
||||
'wave',
|
||||
'stone',
|
||||
'flower',
|
||||
'bird',
|
||||
'wind',
|
||||
'light',
|
||||
'shadow',
|
||||
'fire',
|
||||
'earth',
|
||||
'sky',
|
||||
'tree',
|
||||
'leaf',
|
||||
'rock',
|
||||
];
|
||||
|
||||
const colors = [
|
||||
'blue',
|
||||
'red',
|
||||
'green',
|
||||
'yellow',
|
||||
'purple',
|
||||
'orange',
|
||||
'pink',
|
||||
'cyan',
|
||||
'amber',
|
||||
'emerald',
|
||||
'violet',
|
||||
'indigo',
|
||||
'coral',
|
||||
'teal',
|
||||
'gold',
|
||||
'silver',
|
||||
'copper',
|
||||
'bronze',
|
||||
'ivory',
|
||||
'jade',
|
||||
];
|
||||
|
||||
try {
|
||||
const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||
|
||||
return `${randomAdjective}-${randomColor}-${randomNoun}`;
|
||||
} catch {
|
||||
// Fallback to unix timestamp if something goes wrong
|
||||
return Date.now().toString();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user