refactor(core): consolidate filesystem services (#30447)
This commit is contained in:
@@ -2,16 +2,16 @@ import * as path from "path"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Patch } from "../patch"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import { trimDiff } from "./edit"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import DESCRIPTION from "./apply_patch.txt"
|
||||
import { File } from "../file"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Format } from "../format"
|
||||
import * as Bom from "@/util/bom"
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ApplyPatchTool = Tool.define(
|
||||
"apply_patch",
|
||||
Effect.gen(function* () {
|
||||
const lsp = yield* LSP.Service
|
||||
const afs = yield* AppFileSystem.Service
|
||||
const afs = yield* FSUtil.Service
|
||||
const format = yield* Format.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
|
||||
@@ -253,13 +253,13 @@ export const ApplyPatchTool = Tool.define(
|
||||
if (yield* format.file(edited)) {
|
||||
yield* Bom.syncFile(afs, edited, change.bom)
|
||||
}
|
||||
yield* events.publish(File.Event.Edited, { file: edited })
|
||||
yield* events.publish(FileSystem.Event.Edited, { file: edited })
|
||||
}
|
||||
}
|
||||
|
||||
// Publish file change events
|
||||
for (const update of updates) {
|
||||
yield* events.publish(FileWatcher.Event.Updated, update)
|
||||
yield* events.publish(Watcher.Event.Updated, update)
|
||||
}
|
||||
|
||||
// Notify LSP of file changes and collect diagnostics
|
||||
@@ -286,7 +286,7 @@ export const ApplyPatchTool = Tool.define(
|
||||
for (const change of fileChanges) {
|
||||
if (change.type === "delete") continue
|
||||
const target = change.movePath ?? change.filePath
|
||||
const block = LSP.Diagnostic.report(target, diagnostics[AppFileSystem.normalizePath(target)] ?? [])
|
||||
const block = LSP.Diagnostic.report(target, diagnostics[FSUtil.normalizePath(target)] ?? [])
|
||||
if (!block) continue
|
||||
const rel = path.relative(instance.worktree, target).replaceAll("\\", "/")
|
||||
output += `\n\nLSP errors detected in ${rel}, please fix:\n${block}`
|
||||
|
||||
@@ -9,14 +9,14 @@ import * as Tool from "./tool"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
import DESCRIPTION from "./edit.txt"
|
||||
import { File } from "../file"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Format } from "../format"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import * as Bom from "@/util/bom"
|
||||
|
||||
function normalizeLineEndings(text: string): string {
|
||||
@@ -35,7 +35,7 @@ function convertToLineEnding(text: string, ending: "\n" | "\r\n"): string {
|
||||
const locks = new Map<string, Semaphore.Semaphore>()
|
||||
|
||||
function lock(filePath: string) {
|
||||
const resolvedFilePath = AppFileSystem.resolve(filePath)
|
||||
const resolvedFilePath = FSUtil.resolve(filePath)
|
||||
const hit = locks.get(resolvedFilePath)
|
||||
if (hit) return hit
|
||||
|
||||
@@ -59,7 +59,7 @@ export const EditTool = Tool.define(
|
||||
"edit",
|
||||
Effect.gen(function* () {
|
||||
const lsp = yield* LSP.Service
|
||||
const afs = yield* AppFileSystem.Service
|
||||
const afs = yield* FSUtil.Service
|
||||
const format = yield* Format.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
|
||||
@@ -108,8 +108,8 @@ export const EditTool = Tool.define(
|
||||
if (yield* format.file(filePath)) {
|
||||
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
|
||||
}
|
||||
yield* events.publish(File.Event.Edited, { file: filePath })
|
||||
yield* events.publish(FileWatcher.Event.Updated, {
|
||||
yield* events.publish(FileSystem.Event.Edited, { file: filePath })
|
||||
yield* events.publish(Watcher.Event.Updated, {
|
||||
file: filePath,
|
||||
event: existed ? "change" : "add",
|
||||
})
|
||||
@@ -152,8 +152,8 @@ export const EditTool = Tool.define(
|
||||
if (yield* format.file(filePath)) {
|
||||
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
|
||||
}
|
||||
yield* events.publish(File.Event.Edited, { file: filePath })
|
||||
yield* events.publish(FileWatcher.Event.Updated, {
|
||||
yield* events.publish(FileSystem.Event.Edited, { file: filePath })
|
||||
yield* events.publish(Watcher.Event.Updated, {
|
||||
file: filePath,
|
||||
event: "change",
|
||||
})
|
||||
@@ -192,7 +192,7 @@ export const EditTool = Tool.define(
|
||||
let output = "Edit applied successfully."
|
||||
yield* lsp.touchFile(filePath, "document")
|
||||
const diagnostics = yield* lsp.diagnostics()
|
||||
const normalizedFilePath = AppFileSystem.normalizePath(filePath)
|
||||
const normalizedFilePath = FSUtil.normalizePath(filePath)
|
||||
const block = LSP.Diagnostic.report(filePath, diagnostics[normalizedFilePath] ?? [])
|
||||
if (block) output += `\n\nLSP errors detected in this file, please fix:\n${block}`
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as EffectLogger from "@opencode-ai/core/effect/logger"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import type * as Tool from "./tool"
|
||||
import { containsPath } from "../project/instance-context"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
|
||||
type Kind = "file" | "directory"
|
||||
|
||||
@@ -23,14 +23,14 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
|
||||
if (options?.bypass) return
|
||||
|
||||
const ins = yield* InstanceState.context
|
||||
const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
|
||||
const full = process.platform === "win32" ? FSUtil.normalizePath(target) : target
|
||||
if (containsPath(full, ins)) return
|
||||
|
||||
const kind = options?.kind ?? "file"
|
||||
const dir = kind === "directory" ? full : path.dirname(full)
|
||||
const glob =
|
||||
process.platform === "win32"
|
||||
? AppFileSystem.normalizePathPattern(path.join(dir, "*"))
|
||||
? FSUtil.normalizePathPattern(path.join(dir, "*"))
|
||||
: path.join(dir, "*").replaceAll("\\", "/")
|
||||
|
||||
yield* ctx.ask({
|
||||
|
||||
@@ -2,8 +2,8 @@ import path from "path"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
import * as Tool from "./tool"
|
||||
@@ -20,7 +20,7 @@ export const GlobTool = Tool.define(
|
||||
"glob",
|
||||
Effect.gen(function* () {
|
||||
const rg = yield* Ripgrep.Service
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const reference = yield* Reference.Service
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,8 +2,8 @@ import path from "path"
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Option } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
import * as Tool from "./tool"
|
||||
@@ -24,7 +24,7 @@ export const Parameters = Schema.Struct({
|
||||
export const GrepTool = Tool.define(
|
||||
"grep",
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const rg = yield* Ripgrep.Service
|
||||
const reference = yield* Reference.Service
|
||||
|
||||
@@ -64,7 +64,7 @@ export const GrepTool = Tool.define(
|
||||
kind: requestedInfo?.type === "Directory" ? "directory" : "file",
|
||||
})
|
||||
|
||||
const search = AppFileSystem.resolve(requested)
|
||||
const search = FSUtil.resolve(requested)
|
||||
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
const cwd = info?.type === "Directory" ? search : path.dirname(search)
|
||||
const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
|
||||
@@ -79,9 +79,7 @@ export const GrepTool = Tool.define(
|
||||
if (result.items.length === 0) return empty
|
||||
|
||||
const rows = result.items.map((item) => ({
|
||||
path: AppFileSystem.resolve(
|
||||
path.isAbsolute(item.path.text) ? item.path.text : path.join(cwd, item.path.text),
|
||||
),
|
||||
path: FSUtil.resolve(path.isAbsolute(item.path.text) ? item.path.text : path.join(cwd, item.path.text)),
|
||||
line: item.line_number,
|
||||
text: item.lines.text,
|
||||
}))
|
||||
|
||||
@@ -6,7 +6,7 @@ import DESCRIPTION from "./lsp.txt"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { pathToFileURL } from "url"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
|
||||
const operations = [
|
||||
"goToDefinition",
|
||||
@@ -38,7 +38,7 @@ export const LspTool = Tool.define(
|
||||
"lsp",
|
||||
Effect.gen(function* () {
|
||||
const lsp = yield* LSP.Service
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: Parameters,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Effect, Option, Schema, Scope, Stream } from "effect"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import * as path from "path"
|
||||
import * as Tool from "./tool"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import DESCRIPTION from "./read.txt"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
@@ -39,7 +39,7 @@ export const Parameters = Schema.Struct({
|
||||
export const ReadTool = Tool.define(
|
||||
"read",
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const instruction = yield* Instruction.Service
|
||||
const lsp = yield* LSP.Service
|
||||
const reference = yield* Reference.Service
|
||||
@@ -208,7 +208,7 @@ export const ReadTool = Tool.define(
|
||||
filepath = path.resolve(instance.directory, filepath)
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
filepath = AppFileSystem.normalizePath(filepath)
|
||||
filepath = FSUtil.normalizePath(filepath)
|
||||
}
|
||||
yield* reference.ensure(filepath)
|
||||
const title = path.relative(instance.worktree, filepath)
|
||||
@@ -265,7 +265,7 @@ export const ReadTool = Tool.define(
|
||||
const loaded = yield* instruction.resolve(ctx.messages, filepath, ctx.messageID)
|
||||
const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES)
|
||||
|
||||
const mime = sniffAttachmentMime(sample, AppFileSystem.mimeType(filepath))
|
||||
const mime = sniffAttachmentMime(sample, FSUtil.mimeType(filepath))
|
||||
const isImage = SUPPORTED_IMAGE_MIMES.has(mime)
|
||||
|
||||
if (isImage || isPdfAttachment(mime)) {
|
||||
|
||||
@@ -34,7 +34,7 @@ import { Effect, Layer, Context } from "effect"
|
||||
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
|
||||
import { Format } from "../format"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
@@ -42,7 +42,7 @@ import { Question } from "../question"
|
||||
import { Todo } from "../session/todo"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { Instruction } from "../session/instruction"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Skill } from "../skill"
|
||||
@@ -96,7 +96,7 @@ export const layer: Layer.Layer<
|
||||
| Reference.Service
|
||||
| LSP.Service
|
||||
| Instruction.Service
|
||||
| AppFileSystem.Service
|
||||
| FSUtil.Service
|
||||
| EventV2Bridge.Service
|
||||
| HttpClient.HttpClient
|
||||
| ChildProcessSpawner
|
||||
@@ -380,7 +380,7 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
Layer.provide(Reference.defaultLayer),
|
||||
Layer.provide(LSP.defaultLayer),
|
||||
Layer.provide(Instruction.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
Layer.provide(EventV2Bridge.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(Format.defaultLayer),
|
||||
|
||||
@@ -9,7 +9,7 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { Language, type Node } from "web-tree-sitter"
|
||||
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { fileURLToPath } from "url"
|
||||
import { Config } from "@/config/config"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
@@ -266,7 +266,7 @@ const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boole
|
||||
const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan) {
|
||||
if (scan.dirs.size > 0) {
|
||||
const globs = Array.from(scan.dirs).map((dir) => {
|
||||
if (process.platform === "win32") return AppFileSystem.normalizePathPattern(path.join(dir, "*"))
|
||||
if (process.platform === "win32") return FSUtil.normalizePathPattern(path.join(dir, "*"))
|
||||
return path.join(dir, "*")
|
||||
})
|
||||
yield* ctx.ask({
|
||||
@@ -336,7 +336,7 @@ export const ShellTool = Tool.define(
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const spawner = yield* ChildProcessSpawner
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const trunc = yield* Truncate.Service
|
||||
const plugin = yield* Plugin.Service
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
@@ -348,16 +348,16 @@ export const ShellTool = Tool.define(
|
||||
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
|
||||
const file = lines[0]?.trim()
|
||||
if (!file) return
|
||||
return AppFileSystem.normalizePath(file)
|
||||
return FSUtil.normalizePath(file)
|
||||
})
|
||||
|
||||
const resolvePath = Effect.fn("ShellTool.resolvePath")(function* (text: string, root: string, shell: string) {
|
||||
if (process.platform === "win32") {
|
||||
if (Shell.posix(shell) && text.startsWith("/") && AppFileSystem.windowsPath(text) === text) {
|
||||
if (Shell.posix(shell) && text.startsWith("/") && FSUtil.windowsPath(text) === text) {
|
||||
const file = yield* cygpath(shell, text)
|
||||
if (file) return file
|
||||
}
|
||||
return AppFileSystem.normalizePath(path.resolve(root, AppFileSystem.windowsPath(text)))
|
||||
return FSUtil.normalizePath(path.resolve(root, FSUtil.windowsPath(text)))
|
||||
}
|
||||
return path.resolve(root, text)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
|
||||
import { Skill } from "../skill"
|
||||
import * as Tool from "./tool"
|
||||
import DESCRIPTION from "./skill.txt"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NodePath } from "@effect/platform-node"
|
||||
import { Cause, Duration, Effect, Layer, Option, Schedule, Context } from "effect"
|
||||
import path from "path"
|
||||
import type { Agent } from "../agent/agent"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { evaluate } from "@/permission/evaluate"
|
||||
import { Config } from "@/config/config"
|
||||
import { Identifier } from "../id/id"
|
||||
@@ -50,7 +50,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Tr
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
|
||||
const cleanup = Effect.fn("Truncate.cleanup")(function* () {
|
||||
const cutoff = Identifier.timestamp(
|
||||
@@ -155,6 +155,6 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer))
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(NodePath.layer))
|
||||
|
||||
export * as Truncate from "./truncate"
|
||||
|
||||
@@ -6,10 +6,10 @@ import { LSP } from "@/lsp/lsp"
|
||||
import { createTwoFilesPatch } from "diff"
|
||||
import DESCRIPTION from "./write.txt"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { File } from "../file"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
|
||||
import { Format } from "../format"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { trimDiff } from "./edit"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
@@ -28,7 +28,7 @@ export const WriteTool = Tool.define(
|
||||
"write",
|
||||
Effect.gen(function* () {
|
||||
const lsp = yield* LSP.Service
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const format = yield* Format.Service
|
||||
|
||||
@@ -65,8 +65,8 @@ export const WriteTool = Tool.define(
|
||||
if (yield* format.file(filepath)) {
|
||||
yield* Bom.syncFile(fs, filepath, desiredBom)
|
||||
}
|
||||
yield* events.publish(File.Event.Edited, { file: filepath })
|
||||
yield* events.publish(FileWatcher.Event.Updated, {
|
||||
yield* events.publish(FileSystem.Event.Edited, { file: filepath })
|
||||
yield* events.publish(Watcher.Event.Updated, {
|
||||
file: filepath,
|
||||
event: exists ? "change" : "add",
|
||||
})
|
||||
@@ -74,7 +74,7 @@ export const WriteTool = Tool.define(
|
||||
let output = "Wrote file successfully."
|
||||
yield* lsp.touchFile(filepath, "document")
|
||||
const diagnostics = yield* lsp.diagnostics()
|
||||
const normalizedFilepath = AppFileSystem.normalizePath(filepath)
|
||||
const normalizedFilepath = FSUtil.normalizePath(filepath)
|
||||
let projectDiagnosticsCount = 0
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
const current = file === normalizedFilepath
|
||||
|
||||
Reference in New Issue
Block a user