mirror of
https://github.com/immich-app/immich.git
synced 2026-06-04 22:05:19 -04:00
feat: yucca integration
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2877,6 +2877,206 @@ export type WorkflowUpdateDto = {
|
||||
name?: string;
|
||||
triggerType?: PluginTriggerType;
|
||||
};
|
||||
export type BackendDto = {
|
||||
error?: string;
|
||||
id: string;
|
||||
isOnline: boolean;
|
||||
"type": BackendType;
|
||||
};
|
||||
export type BackendsResponseDto = {
|
||||
backends: BackendDto[];
|
||||
};
|
||||
export type CreateLocalBackendRequestDto = {
|
||||
path: string;
|
||||
};
|
||||
export type BackendResponseDto = {
|
||||
backend: BackendDto;
|
||||
};
|
||||
export type FilesystemListingItemDto = {
|
||||
isDirectory: boolean;
|
||||
path: string;
|
||||
};
|
||||
export type FilesystemListingResponseDto = {
|
||||
items: FilesystemListingItemDto[];
|
||||
parent: string;
|
||||
path: string;
|
||||
};
|
||||
export type ImmichIntegrationConfigurationDto = {
|
||||
backupConfiguration: boolean;
|
||||
dataFolders: string[];
|
||||
libraries: "all" | string[];
|
||||
};
|
||||
export type ImmichIntegrationDto = {
|
||||
configuration: ImmichIntegrationConfigurationDto;
|
||||
id: string;
|
||||
scheduleId: string;
|
||||
};
|
||||
export type ImmichLibraryDto = {
|
||||
exclusionPatterns: string[];
|
||||
id: string;
|
||||
importPaths: string[];
|
||||
name: string;
|
||||
};
|
||||
export type ImmichStateDto = {
|
||||
dataFolders: string[];
|
||||
dataPath: string;
|
||||
libraries: ImmichLibraryDto[];
|
||||
};
|
||||
export type IntegrationsResponseDto = {
|
||||
immichIntegration?: ImmichIntegrationDto;
|
||||
immichState?: ImmichStateDto;
|
||||
};
|
||||
export type ConfigureImmichIntegrationRequestDto = {
|
||||
backupConfiguration: boolean;
|
||||
cron: string;
|
||||
dataFolders: string[];
|
||||
libraries: "all" | string[];
|
||||
name: string;
|
||||
worm: boolean;
|
||||
};
|
||||
export type OnboardingStatusResponseDto = {
|
||||
hasBackend: boolean;
|
||||
hasBackup: boolean;
|
||||
hasOnboardedKey: boolean;
|
||||
hasSchedule: boolean;
|
||||
hasSkippedExtraConfig: boolean;
|
||||
};
|
||||
export type CurrentRecoveryKeyResponse = {
|
||||
recoveryKey: string;
|
||||
};
|
||||
export type ImportRecoveryKeyRequest = {
|
||||
recoveryKey: string;
|
||||
};
|
||||
export type RepositoryBackendDto = {
|
||||
id: string;
|
||||
online: boolean;
|
||||
"type": BackendType;
|
||||
};
|
||||
export type RepositoryBackendsDto = {
|
||||
primary: RepositoryBackendDto;
|
||||
secondary: RepositoryBackendDto[];
|
||||
};
|
||||
export type RepositoryConfigurationDto = {
|
||||
paths: string[];
|
||||
};
|
||||
export type RepositoryMetricsDto = {
|
||||
lastBackup?: string;
|
||||
lastBackupDuration?: number;
|
||||
lastSuccessfulBackup?: string;
|
||||
sizeBytes: number;
|
||||
};
|
||||
export type LocalRepositoryDto = {
|
||||
backends?: RepositoryBackendsDto;
|
||||
configuration?: RepositoryConfigurationDto;
|
||||
id: string;
|
||||
metrics: RepositoryMetricsDto;
|
||||
name: string;
|
||||
worm: boolean;
|
||||
};
|
||||
export type RepositoryListResponseDto = {
|
||||
repositories: LocalRepositoryDto[];
|
||||
};
|
||||
export type RepositoryCreateRequestDto = {
|
||||
name: string;
|
||||
paths?: string[];
|
||||
worm: boolean;
|
||||
};
|
||||
export type RepositoryCreateResponseDto = {
|
||||
repository: LocalRepositoryDto;
|
||||
};
|
||||
export type SnapshotDto = {
|
||||
id: string;
|
||||
paths: string[];
|
||||
time: string;
|
||||
};
|
||||
export type InspectedLocalRepositoryDto = {
|
||||
backends?: RepositoryBackendsDto;
|
||||
configuration?: RepositoryConfigurationDto;
|
||||
id: string;
|
||||
metrics: RepositoryMetricsDto;
|
||||
name: string;
|
||||
snapshots: SnapshotDto[];
|
||||
worm: boolean;
|
||||
};
|
||||
export type RepositoryInspectResponseDto = {
|
||||
repositories: InspectedLocalRepositoryDto[];
|
||||
};
|
||||
export type RepositoryUpdateRequestDto = {
|
||||
name?: string;
|
||||
paths?: string[];
|
||||
};
|
||||
export type RepositoryUpdateResponseDto = {
|
||||
repository: LocalRepositoryDto;
|
||||
};
|
||||
export type LogResponseDto = {
|
||||
logId: string;
|
||||
};
|
||||
export type RepositoryCheckImportResponseDto = {
|
||||
readable: boolean;
|
||||
};
|
||||
export type RunDto = {
|
||||
end: string;
|
||||
id: string;
|
||||
logFilePath: string;
|
||||
start: string;
|
||||
status: RunStatus;
|
||||
};
|
||||
export type RunHistoryResponseDto = {
|
||||
runs: RunDto[];
|
||||
};
|
||||
export type ListSnapshotsResponseDto = {
|
||||
snapshots: SnapshotDto[];
|
||||
};
|
||||
export type RepositorySnapshotRestoreRequestDto = {
|
||||
include?: string[];
|
||||
target?: string;
|
||||
};
|
||||
export type RepositorySnapshotRestoreFromPointRequestDto = {
|
||||
include?: string[];
|
||||
yuccaConfig?: string;
|
||||
};
|
||||
export type ScheduleDto = {
|
||||
cron: string;
|
||||
id: string;
|
||||
lastFinished?: string;
|
||||
lastRun?: string;
|
||||
name: string;
|
||||
paused: boolean;
|
||||
repositories: string[];
|
||||
};
|
||||
export type ScheduleListResponseDto = {
|
||||
schedules: ScheduleDto[];
|
||||
};
|
||||
export type ScheduleCreateRequestDto = {
|
||||
cron: string;
|
||||
name: string;
|
||||
repositories: string[];
|
||||
};
|
||||
export type ScheduleCreateResponseDto = {
|
||||
schedule: ScheduleDto;
|
||||
};
|
||||
export type ScheduleUpdateRequestDto = {
|
||||
cron?: string;
|
||||
name?: string;
|
||||
paused?: boolean;
|
||||
repositories?: string[];
|
||||
};
|
||||
export type ScheduleUpdateResponseDto = {
|
||||
schedule: ScheduleDto;
|
||||
};
|
||||
export type ActiveScheduleItemDto = {
|
||||
repositoryId: string;
|
||||
status: TaskStatus;
|
||||
};
|
||||
export type RunningTaskDto = {
|
||||
logId?: string;
|
||||
parentId: string;
|
||||
scheduleStatus?: ActiveScheduleItemDto[];
|
||||
"type": TaskType;
|
||||
};
|
||||
export type RunningTaskListResponse = {
|
||||
tasks: RunningTaskDto[];
|
||||
};
|
||||
export type LicenseResponseDto = UserLicense;
|
||||
export type SyncAckV1 = {};
|
||||
export type SyncAlbumDeleteV1 = {
|
||||
@@ -6755,6 +6955,348 @@ export function updateWorkflow({ id, workflowUpdateDto }: {
|
||||
body: workflowUpdateDto
|
||||
})));
|
||||
}
|
||||
export function oidcCallback(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/yucca/auth/oidc/callback", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function oidcAuthorize({ next }: {
|
||||
next: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/yucca/auth/oidc/login${QS.query(QS.explode({
|
||||
next
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getBackends(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: BackendsResponseDto;
|
||||
}>("/yucca/backend", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function createLocalBackend({ createLocalBackendRequestDto }: {
|
||||
createLocalBackendRequestDto: CreateLocalBackendRequestDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: BackendResponseDto;
|
||||
}>("/yucca/backend/local", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: createLocalBackendRequestDto
|
||||
})));
|
||||
}
|
||||
export function resetOrchestrator(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/yucca/debug/reset", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function getFileListing({ path }: {
|
||||
path?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: FilesystemListingResponseDto;
|
||||
}>(`/yucca/fs${QS.query(QS.explode({
|
||||
path
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getIntegrations(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: IntegrationsResponseDto;
|
||||
}>("/yucca/integrations", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function configureImmichIntegration({ configureImmichIntegrationRequestDto }: {
|
||||
configureImmichIntegrationRequestDto: ConfigureImmichIntegrationRequestDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/yucca/integrations/immich", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: configureImmichIntegrationRequestDto
|
||||
})));
|
||||
}
|
||||
export function logStreamSse({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/yucca/logs/${encodeURIComponent(id)}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function onboardingStatus(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: OnboardingStatusResponseDto;
|
||||
}>("/yucca/onboarding", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function currentRecoveryKey(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: CurrentRecoveryKeyResponse;
|
||||
}>("/yucca/onboarding/recovery-key", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function confirmRecoveryKey(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/yucca/onboarding/recovery-key", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function importRecoveryKey({ importRecoveryKeyRequest }: {
|
||||
importRecoveryKeyRequest: ImportRecoveryKeyRequest;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/yucca/onboarding/recovery-key", oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: importRecoveryKeyRequest
|
||||
})));
|
||||
}
|
||||
export function skipOnboardingExtraConfig(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/yucca/onboarding/skip", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function getRepositories(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: RepositoryListResponseDto;
|
||||
}>("/yucca/repository", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function createRepository({ backend, repositoryCreateRequestDto }: {
|
||||
backend?: string;
|
||||
repositoryCreateRequestDto: RepositoryCreateRequestDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: RepositoryCreateResponseDto;
|
||||
}>(`/yucca/repository${QS.query(QS.explode({
|
||||
backend
|
||||
}))}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: repositoryCreateRequestDto
|
||||
})));
|
||||
}
|
||||
export function inspectRepositories(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: RepositoryInspectResponseDto;
|
||||
}>("/yucca/repository/inspect", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function updateRepository({ backend, id, repositoryUpdateRequestDto }: {
|
||||
backend?: string;
|
||||
id: string;
|
||||
repositoryUpdateRequestDto: RepositoryUpdateRequestDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: RepositoryUpdateResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}${QS.query(QS.explode({
|
||||
backend
|
||||
}))}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PATCH",
|
||||
body: repositoryUpdateRequestDto
|
||||
})));
|
||||
}
|
||||
export function createBackup({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: LogResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}`, {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function checkImportRepository({ backend, id }: {
|
||||
backend: string;
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: RepositoryCheckImportResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}/import${QS.query(QS.explode({
|
||||
backend
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function importRepository({ backend, id }: {
|
||||
backend: string;
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: RepositoryCreateResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}/import${QS.query(QS.explode({
|
||||
backend
|
||||
}))}`, {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function getRunHistory({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: RunHistoryResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}/runs`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getSnapshots({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: ListSnapshotsResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}/snapshots`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function forgetSnapshot({ id, snapshot }: {
|
||||
id: string;
|
||||
snapshot: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: ListSnapshotsResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}/snapshots/${encodeURIComponent(snapshot)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function restoreSnapshot({ id, snapshot, repositorySnapshotRestoreRequestDto }: {
|
||||
id: string;
|
||||
snapshot: string;
|
||||
repositorySnapshotRestoreRequestDto: RepositorySnapshotRestoreRequestDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: LogResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}/snapshots/${encodeURIComponent(snapshot)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: repositorySnapshotRestoreRequestDto
|
||||
})));
|
||||
}
|
||||
export function getSnapshotListing({ id, path, snapshot }: {
|
||||
id: string;
|
||||
path?: string;
|
||||
snapshot: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: FilesystemListingResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}/snapshots/${encodeURIComponent(snapshot)}/listing${QS.query(QS.explode({
|
||||
path
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function restoreFromPoint({ backend, id, snapshot, repositorySnapshotRestoreFromPointRequestDto }: {
|
||||
backend: string;
|
||||
id: string;
|
||||
snapshot: string;
|
||||
repositorySnapshotRestoreFromPointRequestDto: RepositorySnapshotRestoreFromPointRequestDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: LogResponseDto;
|
||||
}>(`/yucca/repository/${encodeURIComponent(id)}/snapshots/${encodeURIComponent(snapshot)}/restore-from-point${QS.query(QS.explode({
|
||||
backend
|
||||
}))}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: repositorySnapshotRestoreFromPointRequestDto
|
||||
})));
|
||||
}
|
||||
export function getSchedules(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: ScheduleListResponseDto;
|
||||
}>("/yucca/schedule", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function createSchedule({ scheduleCreateRequestDto }: {
|
||||
scheduleCreateRequestDto: ScheduleCreateRequestDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: ScheduleCreateResponseDto;
|
||||
}>("/yucca/schedule", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: scheduleCreateRequestDto
|
||||
})));
|
||||
}
|
||||
export function removeSchedule({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/yucca/schedule/${encodeURIComponent(id)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function updateSchedule({ id, scheduleUpdateRequestDto }: {
|
||||
id: string;
|
||||
scheduleUpdateRequestDto: ScheduleUpdateRequestDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: ScheduleUpdateResponseDto;
|
||||
}>(`/yucca/schedule/${encodeURIComponent(id)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PATCH",
|
||||
body: scheduleUpdateRequestDto
|
||||
})));
|
||||
}
|
||||
export function removeRepositoryFromSchedule({ id, repositoryId }: {
|
||||
id: string;
|
||||
repositoryId: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/yucca/schedule/${encodeURIComponent(id)}/${encodeURIComponent(repositoryId)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function addRepositoryToSchedule({ id, repositoryId }: {
|
||||
id: string;
|
||||
repositoryId: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/yucca/schedule/${encodeURIComponent(id)}/${encodeURIComponent(repositoryId)}`, {
|
||||
...opts,
|
||||
method: "PUT"
|
||||
}));
|
||||
}
|
||||
export function getRunningTasks(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: RunningTaskListResponse;
|
||||
}>("/yucca/tasks", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export enum ReactionLevel {
|
||||
Album = "album",
|
||||
Asset = "asset"
|
||||
@@ -7319,6 +7861,27 @@ export enum OAuthTokenEndpointAuthMethod {
|
||||
ClientSecretPost = "client_secret_post",
|
||||
ClientSecretBasic = "client_secret_basic"
|
||||
}
|
||||
export enum BackendType {
|
||||
Yucca = "yucca",
|
||||
Local = "local",
|
||||
S3 = "s3"
|
||||
}
|
||||
export enum RunStatus {
|
||||
Incomplete = "incomplete",
|
||||
Complete = "complete",
|
||||
Failed = "failed"
|
||||
}
|
||||
export enum TaskStatus {
|
||||
Incomplete = "incomplete",
|
||||
Complete = "complete",
|
||||
Failed = "failed"
|
||||
}
|
||||
export enum TaskType {
|
||||
Schedule = "schedule",
|
||||
Restore = "restore",
|
||||
Backup = "backup",
|
||||
Forget = "forget"
|
||||
}
|
||||
export enum UserMetadataKey {
|
||||
Preferences = "preferences",
|
||||
License = "license",
|
||||
|
||||
Reference in New Issue
Block a user