feat(gitlab): add pipelines and jobs API support, including list/get/trigger/delete/retry/cancel operations, job controls, and related types and list options
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-02 - 2.5.0 - feat(gitlab)
|
||||||
|
add pipelines and jobs API support, including list/get/trigger/delete/retry/cancel operations, job controls, and related types and list options
|
||||||
|
|
||||||
|
- Add new pipeline methods: getPipelines (with filtering, ordering and pagination), getPipeline, triggerPipeline, deletePipeline, retryPipeline, cancelPipeline, getPipelineVariables, getPipelineTestReport
|
||||||
|
- Add new job methods: getPipelineJobs (with scope filtering and pagination), getJob, retryJob, cancelJob, playJob, eraseJob; preserve getJobLog
|
||||||
|
- Introduce IPipelineListOptions and IJobListOptions for richer listing filters
|
||||||
|
- Extend and add interfaces: IGitLabPipeline (additional metadata fields), IGitLabJob (expanded fields including pipeline, runner, artifacts), IGitLabPipelineVariable, IGitLabTestReport, IGitLabTestSuite, IGitLabTestCase and other minor interface reorganizations
|
||||||
|
- Export the new interfaces from the package entry (ts/index.ts)
|
||||||
|
|
||||||
## 2026-03-02 - 2.4.0 - feat(gitlab)
|
## 2026-03-02 - 2.4.0 - feat(gitlab)
|
||||||
add repository branches and tags endpoints and corresponding types
|
add repository branches and tags endpoints and corresponding types
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@apiclient.xyz/gitlab',
|
name: '@apiclient.xyz/gitlab',
|
||||||
version: '2.4.0',
|
version: '2.5.0',
|
||||||
description: 'A TypeScript client for the GitLab API, providing easy access to projects, groups, CI/CD variables, and pipelines.'
|
description: 'A TypeScript client for the GitLab API, providing easy access to projects, groups, CI/CD variables, and pipelines.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import type {
|
|||||||
IGitLabBranch,
|
IGitLabBranch,
|
||||||
IGitLabTag,
|
IGitLabTag,
|
||||||
IGitLabPipeline,
|
IGitLabPipeline,
|
||||||
|
IGitLabPipelineVariable,
|
||||||
|
IGitLabTestReport,
|
||||||
IGitLabJob,
|
IGitLabJob,
|
||||||
ITestConnectionResult,
|
ITestConnectionResult,
|
||||||
IListOptions,
|
IListOptions,
|
||||||
|
IPipelineListOptions,
|
||||||
|
IJobListOptions,
|
||||||
} from './gitlab.interfaces.js';
|
} from './gitlab.interfaces.js';
|
||||||
|
|
||||||
export class GitLabClient {
|
export class GitLabClient {
|
||||||
@@ -335,22 +339,131 @@ export class GitLabClient {
|
|||||||
// Pipelines
|
// Pipelines
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
public async getPipelines(projectId: number | string, opts?: IListOptions): Promise<IGitLabPipeline[]> {
|
/**
|
||||||
|
* List pipelines for a project with optional filters.
|
||||||
|
* Supports status, ref, source, scope, username, date range, ordering.
|
||||||
|
*/
|
||||||
|
public async getPipelines(projectId: number | string, opts?: IPipelineListOptions): Promise<IGitLabPipeline[]> {
|
||||||
const page = opts?.page || 1;
|
const page = opts?.page || 1;
|
||||||
const perPage = opts?.perPage || 30;
|
const perPage = opts?.perPage || 30;
|
||||||
return this.request<IGitLabPipeline[]>(
|
const orderBy = opts?.orderBy || 'updated_at';
|
||||||
|
const sort = opts?.sort || 'desc';
|
||||||
|
let url = `/api/v4/projects/${encodeURIComponent(projectId)}/pipelines?page=${page}&per_page=${perPage}&order_by=${orderBy}&sort=${sort}`;
|
||||||
|
if (opts?.status) url += `&status=${encodeURIComponent(opts.status)}`;
|
||||||
|
if (opts?.ref) url += `&ref=${encodeURIComponent(opts.ref)}`;
|
||||||
|
if (opts?.source) url += `&source=${encodeURIComponent(opts.source)}`;
|
||||||
|
if (opts?.scope) url += `&scope=${encodeURIComponent(opts.scope)}`;
|
||||||
|
if (opts?.username) url += `&username=${encodeURIComponent(opts.username)}`;
|
||||||
|
if (opts?.updatedAfter) url += `&updated_after=${encodeURIComponent(opts.updatedAfter)}`;
|
||||||
|
if (opts?.updatedBefore) url += `&updated_before=${encodeURIComponent(opts.updatedBefore)}`;
|
||||||
|
return this.request<IGitLabPipeline[]>('GET', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single pipeline's full details.
|
||||||
|
*/
|
||||||
|
public async getPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||||
|
return this.request<IGitLabPipeline>(
|
||||||
'GET',
|
'GET',
|
||||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines?page=${page}&per_page=${perPage}&order_by=updated_at&sort=desc`,
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPipelineJobs(projectId: number | string, pipelineId: number): Promise<IGitLabJob[]> {
|
/**
|
||||||
return this.request<IGitLabJob[]>(
|
* Trigger a new pipeline on the given ref, optionally with variables.
|
||||||
'GET',
|
*/
|
||||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs`,
|
public async triggerPipeline(
|
||||||
|
projectId: number | string,
|
||||||
|
ref: string,
|
||||||
|
variables?: { key: string; value: string; variable_type?: string }[],
|
||||||
|
): Promise<IGitLabPipeline> {
|
||||||
|
const body: any = { ref };
|
||||||
|
if (variables && variables.length > 0) {
|
||||||
|
body.variables = variables;
|
||||||
|
}
|
||||||
|
return this.request<IGitLabPipeline>(
|
||||||
|
'POST',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipeline`,
|
||||||
|
body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a pipeline and all its jobs.
|
||||||
|
*/
|
||||||
|
public async deletePipeline(projectId: number | string, pipelineId: number): Promise<void> {
|
||||||
|
await this.request(
|
||||||
|
'DELETE',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get variables used in a specific pipeline run.
|
||||||
|
*/
|
||||||
|
public async getPipelineVariables(projectId: number | string, pipelineId: number): Promise<IGitLabPipelineVariable[]> {
|
||||||
|
return this.request<IGitLabPipelineVariable[]>(
|
||||||
|
'GET',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/variables`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the test report for a pipeline.
|
||||||
|
*/
|
||||||
|
public async getPipelineTestReport(projectId: number | string, pipelineId: number): Promise<IGitLabTestReport> {
|
||||||
|
return this.request<IGitLabTestReport>(
|
||||||
|
'GET',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/test_report`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retryPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||||
|
return this.request<IGitLabPipeline>(
|
||||||
|
'POST',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/retry`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cancelPipeline(projectId: number | string, pipelineId: number): Promise<IGitLabPipeline> {
|
||||||
|
return this.request<IGitLabPipeline>(
|
||||||
|
'POST',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/cancel`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Jobs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List jobs for a pipeline with optional scope filter and pagination.
|
||||||
|
*/
|
||||||
|
public async getPipelineJobs(projectId: number | string, pipelineId: number, opts?: IJobListOptions): Promise<IGitLabJob[]> {
|
||||||
|
const page = opts?.page || 1;
|
||||||
|
const perPage = opts?.perPage || 100;
|
||||||
|
let url = `/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs?page=${page}&per_page=${perPage}`;
|
||||||
|
if (opts?.scope && opts.scope.length > 0) {
|
||||||
|
for (const s of opts.scope) {
|
||||||
|
url += `&scope[]=${encodeURIComponent(s)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.request<IGitLabJob[]>('GET', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single job's full details.
|
||||||
|
*/
|
||||||
|
public async getJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||||
|
return this.request<IGitLabJob>(
|
||||||
|
'GET',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a job's raw log (trace) output.
|
||||||
|
*/
|
||||||
public async getJobLog(projectId: number | string, jobId: number): Promise<string> {
|
public async getJobLog(projectId: number | string, jobId: number): Promise<string> {
|
||||||
return this.requestText(
|
return this.requestText(
|
||||||
'GET',
|
'GET',
|
||||||
@@ -358,17 +471,43 @@ export class GitLabClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async retryPipeline(projectId: number | string, pipelineId: number): Promise<void> {
|
/**
|
||||||
await this.request(
|
* Retry a single job.
|
||||||
|
*/
|
||||||
|
public async retryJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||||
|
return this.request<IGitLabJob>(
|
||||||
'POST',
|
'POST',
|
||||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/retry`,
|
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/retry`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async cancelPipeline(projectId: number | string, pipelineId: number): Promise<void> {
|
/**
|
||||||
|
* Cancel a running job.
|
||||||
|
*/
|
||||||
|
public async cancelJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||||
|
return this.request<IGitLabJob>(
|
||||||
|
'POST',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/cancel`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a manual job (play action).
|
||||||
|
*/
|
||||||
|
public async playJob(projectId: number | string, jobId: number): Promise<IGitLabJob> {
|
||||||
|
return this.request<IGitLabJob>(
|
||||||
|
'POST',
|
||||||
|
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/play`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase a job's trace and artifacts.
|
||||||
|
*/
|
||||||
|
public async eraseJob(projectId: number | string, jobId: number): Promise<void> {
|
||||||
await this.request(
|
await this.request(
|
||||||
'POST',
|
'POST',
|
||||||
`/api/v4/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/cancel`,
|
`/api/v4/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/erase`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,52 @@
|
|||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Common
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface ITestConnectionResult {
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IListOptions {
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pipeline / Job list options
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface IPipelineListOptions extends IListOptions {
|
||||||
|
/** Filter by pipeline status */
|
||||||
|
status?: string;
|
||||||
|
/** Filter by branch or tag ref */
|
||||||
|
ref?: string;
|
||||||
|
/** Filter by trigger source (push, web, trigger, schedule, api, external, pipeline, chat, merge_request_event, …) */
|
||||||
|
source?: string;
|
||||||
|
/** Filter by scope (running, pending, finished, branches, tags) */
|
||||||
|
scope?: string;
|
||||||
|
/** Filter by the user who triggered the pipeline */
|
||||||
|
username?: string;
|
||||||
|
/** Return pipelines updated after this ISO 8601 date */
|
||||||
|
updatedAfter?: string;
|
||||||
|
/** Return pipelines updated before this ISO 8601 date */
|
||||||
|
updatedBefore?: string;
|
||||||
|
/** Order by field (id, status, ref, updated_at, user_id). Default: id */
|
||||||
|
orderBy?: string;
|
||||||
|
/** Sort direction (asc, desc). Default: desc */
|
||||||
|
sort?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJobListOptions extends IListOptions {
|
||||||
|
/** Filter by job scope(s) */
|
||||||
|
scope?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Users
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IGitLabUser {
|
export interface IGitLabUser {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -8,6 +57,10 @@ export interface IGitLabUser {
|
|||||||
state: string;
|
state: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Projects
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IGitLabProject {
|
export interface IGitLabProject {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -20,6 +73,10 @@ export interface IGitLabProject {
|
|||||||
last_activity_at: string;
|
last_activity_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Groups
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IGitLabGroup {
|
export interface IGitLabGroup {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -29,6 +86,10 @@ export interface IGitLabGroup {
|
|||||||
visibility: string;
|
visibility: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Variables
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IGitLabVariable {
|
export interface IGitLabVariable {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -44,32 +105,133 @@ export interface IVariableOptions {
|
|||||||
environment_scope?: string;
|
environment_scope?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Protected Branches
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IGitLabProtectedBranch {
|
export interface IGitLabProtectedBranch {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
allow_force_push: boolean;
|
allow_force_push: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pipelines
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IGitLabPipeline {
|
export interface IGitLabPipeline {
|
||||||
id: number;
|
id: number;
|
||||||
|
iid: number;
|
||||||
project_id: number;
|
project_id: number;
|
||||||
status: string;
|
status: string;
|
||||||
ref: string;
|
ref: string;
|
||||||
sha: string;
|
sha: string;
|
||||||
|
before_sha: string;
|
||||||
|
tag: boolean;
|
||||||
web_url: string;
|
web_url: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
queued_duration: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
started_at: string;
|
||||||
|
finished_at: string;
|
||||||
source: string;
|
source: string;
|
||||||
|
coverage: string;
|
||||||
|
user: IGitLabUser;
|
||||||
|
detailed_status: {
|
||||||
|
icon: string;
|
||||||
|
text: string;
|
||||||
|
label: string;
|
||||||
|
group: string;
|
||||||
|
tooltip: string;
|
||||||
|
has_details: boolean;
|
||||||
|
details_path: string;
|
||||||
|
favicon: string;
|
||||||
|
};
|
||||||
|
yaml_errors: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IGitLabPipelineVariable {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
variable_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGitLabTestReport {
|
||||||
|
total_time: number;
|
||||||
|
total_count: number;
|
||||||
|
success_count: number;
|
||||||
|
failed_count: number;
|
||||||
|
skipped_count: number;
|
||||||
|
error_count: number;
|
||||||
|
test_suites: IGitLabTestSuite[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGitLabTestSuite {
|
||||||
|
name: string;
|
||||||
|
total_time: number;
|
||||||
|
total_count: number;
|
||||||
|
success_count: number;
|
||||||
|
failed_count: number;
|
||||||
|
skipped_count: number;
|
||||||
|
error_count: number;
|
||||||
|
test_cases: IGitLabTestCase[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGitLabTestCase {
|
||||||
|
status: string;
|
||||||
|
name: string;
|
||||||
|
classname: string;
|
||||||
|
execution_time: number;
|
||||||
|
system_output: string;
|
||||||
|
stack_trace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Jobs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IGitLabJob {
|
export interface IGitLabJob {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
stage: string;
|
stage: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
ref: string;
|
||||||
|
tag: boolean;
|
||||||
|
web_url: string;
|
||||||
|
created_at: string;
|
||||||
|
started_at: string;
|
||||||
|
finished_at: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
queued_duration: number;
|
||||||
|
coverage: number;
|
||||||
|
allow_failure: boolean;
|
||||||
|
failure_reason: string;
|
||||||
|
pipeline: {
|
||||||
|
id: number;
|
||||||
|
project_id: number;
|
||||||
|
ref: string;
|
||||||
|
sha: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
user: IGitLabUser;
|
||||||
|
runner: {
|
||||||
|
id: number;
|
||||||
|
description: string;
|
||||||
|
active: boolean;
|
||||||
|
is_shared: boolean;
|
||||||
|
};
|
||||||
|
artifacts: {
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
}[];
|
||||||
|
artifacts_expire_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Branches & Tags
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface IGitLabBranch {
|
export interface IGitLabBranch {
|
||||||
name: string;
|
name: string;
|
||||||
commit: {
|
commit: {
|
||||||
@@ -83,14 +245,3 @@ export interface IGitLabTag {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITestConnectionResult {
|
|
||||||
ok: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IListOptions {
|
|
||||||
search?: string;
|
|
||||||
page?: number;
|
|
||||||
perPage?: number;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ export type {
|
|||||||
IGitLabTag,
|
IGitLabTag,
|
||||||
IVariableOptions,
|
IVariableOptions,
|
||||||
IGitLabPipeline,
|
IGitLabPipeline,
|
||||||
|
IGitLabPipelineVariable,
|
||||||
|
IGitLabTestReport,
|
||||||
|
IGitLabTestSuite,
|
||||||
|
IGitLabTestCase,
|
||||||
IGitLabJob,
|
IGitLabJob,
|
||||||
ITestConnectionResult,
|
ITestConnectionResult,
|
||||||
IListOptions,
|
IListOptions,
|
||||||
|
IPipelineListOptions,
|
||||||
|
IJobListOptions,
|
||||||
} from './gitlab.interfaces.js';
|
} from './gitlab.interfaces.js';
|
||||||
export { commitinfo } from './00_commitinfo_data.js';
|
export { commitinfo } from './00_commitinfo_data.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user