mirror of
https://github.com/ragestudio/relic.git
synced 2025-06-09 10:34:18 +00:00
improve manifest api
This commit is contained in:
parent
98142c8693
commit
e390ea6823
@ -52,23 +52,26 @@ class ElectronApp {
|
|||||||
"get:installations": async () => {
|
"get:installations": async () => {
|
||||||
return await this.pkgManager.getInstallations()
|
return await this.pkgManager.getInstallations()
|
||||||
},
|
},
|
||||||
"bundle:read": async (event, manifest_url) => {
|
"pkg:read": async (event, manifest_url) => {
|
||||||
return JSON.stringify(await readManifest(manifest_url))
|
return JSON.stringify(await readManifest(manifest_url))
|
||||||
},
|
},
|
||||||
"bundle:update": (event, manifest_id) => {
|
"pkg:update": (event, manifest_id) => {
|
||||||
this.pkgManager.update(manifest_id)
|
this.pkgManager.update(manifest_id)
|
||||||
},
|
},
|
||||||
"bundle:exec": (event, manifest_id) => {
|
"pkg:exec": (event, manifest_id) => {
|
||||||
this.pkgManager.execute(manifest_id)
|
this.pkgManager.execute(manifest_id)
|
||||||
},
|
},
|
||||||
"bundle:install": async (event, manifest) => {
|
"pkg:install": async (event, manifest) => {
|
||||||
this.pkgManager.install(manifest)
|
this.pkgManager.install(manifest)
|
||||||
},
|
},
|
||||||
"bundle:uninstall": (event, manifest_id) => {
|
"pkg:uninstall": (event, manifest_id) => {
|
||||||
this.pkgManager.uninstall(manifest_id)
|
this.pkgManager.uninstall(manifest_id)
|
||||||
},
|
},
|
||||||
"bundle:open": (event, manifest_id) => {
|
"pkg:open": (event, manifest_id) => {
|
||||||
this.pkgManager.openBundleFolder(manifest_id)
|
this.pkgManager.openPackageFolder(manifest_id)
|
||||||
|
},
|
||||||
|
"pkg:apply_changes": (event, manifest_id, changes) => {
|
||||||
|
this.pkgManager.applyChanges(manifest_id, changes)
|
||||||
},
|
},
|
||||||
"check:setup": async () => {
|
"check:setup": async () => {
|
||||||
return await setup()
|
return await setup()
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
|
import path from "node:path"
|
||||||
|
import fs from "node:fs"
|
||||||
|
import child_process from "node:child_process"
|
||||||
|
|
||||||
global.OS_USERDATA_PATH = path.resolve(
|
global.OS_USERDATA_PATH = path.resolve(
|
||||||
process.env.APPDATA ||
|
process.env.APPDATA ||
|
||||||
@ -9,9 +12,6 @@ global.TMP_PATH = path.resolve(os.tmpdir(), "rs-bundler")
|
|||||||
global.INSTALLERS_PATH = path.join(global.RUNTIME_PATH, "installations")
|
global.INSTALLERS_PATH = path.join(global.RUNTIME_PATH, "installations")
|
||||||
global.MANIFEST_PATH = path.join(global.RUNTIME_PATH, "manifests")
|
global.MANIFEST_PATH = path.join(global.RUNTIME_PATH, "manifests")
|
||||||
|
|
||||||
import path from "node:path"
|
|
||||||
import fs from "node:fs"
|
|
||||||
|
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { rimraf } from "rimraf"
|
import { rimraf } from "rimraf"
|
||||||
|
|
||||||
@ -19,7 +19,8 @@ import readManifest from "../utils/readManifest"
|
|||||||
import initManifest from "../utils/initManifest"
|
import initManifest from "../utils/initManifest"
|
||||||
|
|
||||||
import ISM_HTTP from "./installs_steps_methods/http"
|
import ISM_HTTP from "./installs_steps_methods/http"
|
||||||
import ISM_GIT from "./installs_steps_methods/git"
|
import ISM_GIT_CLONE from "./installs_steps_methods/git_clone"
|
||||||
|
import ISM_GIT_PULL from "./installs_steps_methods/git_pull"
|
||||||
|
|
||||||
import pkg from "../../../package.json"
|
import pkg from "../../../package.json"
|
||||||
|
|
||||||
@ -30,7 +31,43 @@ const RealmDBDefault = {
|
|||||||
|
|
||||||
const InstallationStepsMethods = {
|
const InstallationStepsMethods = {
|
||||||
http: ISM_HTTP,
|
http: ISM_HTTP,
|
||||||
git: ISM_GIT,
|
git_clone: ISM_GIT_CLONE,
|
||||||
|
git_pull: ISM_GIT_PULL,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processGenericSteps(manifest, steps) {
|
||||||
|
console.log(`Processing steps...`, steps)
|
||||||
|
|
||||||
|
for await (const [stepKey, stepValue] of Object.entries(steps)) {
|
||||||
|
switch (stepKey) {
|
||||||
|
case "http_downloads": {
|
||||||
|
for await (const dl_step of stepValue) {
|
||||||
|
await InstallationStepsMethods.http(manifest, dl_step)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "git_clones":
|
||||||
|
case "git_clones_steps": {
|
||||||
|
for await (const clone_step of stepValue) {
|
||||||
|
await InstallationStepsMethods.git_clone(manifest, clone_step)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "git_pulls":
|
||||||
|
case "git_update":
|
||||||
|
case "git_pulls_steps": {
|
||||||
|
for await (const pull_step of stepValue) {
|
||||||
|
await InstallationStepsMethods.git_pull(manifest, pull_step)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unknown step: ${stepKey}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PkgManager {
|
export default class PkgManager {
|
||||||
@ -100,7 +137,7 @@ export default class PkgManager {
|
|||||||
return db.installations
|
return db.installations
|
||||||
}
|
}
|
||||||
|
|
||||||
async openBundleFolder(manifest_id) {
|
async openPackageFolder(manifest_id) {
|
||||||
const db = await this.readDb()
|
const db = await this.readDb()
|
||||||
|
|
||||||
const index = db.installations.findIndex((i) => i.id === manifest_id)
|
const index = db.installations.findIndex((i) => i.id === manifest_id)
|
||||||
@ -112,6 +149,134 @@ export default class PkgManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseStringVars(str, manifest) {
|
||||||
|
if (!manifest) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
const vars = {
|
||||||
|
id: manifest.id,
|
||||||
|
name: manifest.name,
|
||||||
|
version: manifest.version,
|
||||||
|
install_path: manifest.install_path,
|
||||||
|
remote_url: manifest.remote_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /%([^%]+)%/g
|
||||||
|
|
||||||
|
str = str.replace(regex, (match, varName) => {
|
||||||
|
return vars[varName]
|
||||||
|
})
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChanges(manifest_id, changes) {
|
||||||
|
const db = await this.readDb()
|
||||||
|
|
||||||
|
const index = db.installations.findIndex((i) => i.id === manifest_id)
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error(`Manifest not found for id: [${manifest_id}]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = db.installations[index]
|
||||||
|
|
||||||
|
manifest.status = "installing"
|
||||||
|
|
||||||
|
if (!Array.isArray(manifest.applied_patches)) {
|
||||||
|
manifest.applied_patches = []
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Applying changes for [${manifest_id}]... >`, changes)
|
||||||
|
|
||||||
|
global.sendToRenderer(`installation:done`, {
|
||||||
|
...manifest,
|
||||||
|
statusText: "Applying changes...",
|
||||||
|
})
|
||||||
|
|
||||||
|
const disablePatches = manifest.patches.filter((p) => {
|
||||||
|
return !changes.patches[p.id]
|
||||||
|
})
|
||||||
|
|
||||||
|
const installPatches = manifest.patches.filter((p) => {
|
||||||
|
return changes.patches[p.id]
|
||||||
|
})
|
||||||
|
|
||||||
|
for await (let patch of disablePatches) {
|
||||||
|
console.log(`Removing patch [${patch.id}]...`)
|
||||||
|
|
||||||
|
global.sendToRenderer(`installation:status`, {
|
||||||
|
...manifest,
|
||||||
|
statusText: `Removing patch [${patch.id}]...`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// remove patch additions
|
||||||
|
for await (let addition of patch.additions) {
|
||||||
|
// resolve patch file
|
||||||
|
addition.file = await this.parseStringVars(addition.file, manifest)
|
||||||
|
|
||||||
|
console.log(`Removing addition [${addition.file}]...`)
|
||||||
|
|
||||||
|
if (!fs.existsSync(addition.file)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove addition
|
||||||
|
await fs.promises.unlink(addition.file, { force: true, recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove file patch overrides with original file
|
||||||
|
|
||||||
|
// remove from applied patches
|
||||||
|
manifest.applied_patches = manifest.applied_patches.filter((p) => {
|
||||||
|
return p !== patch.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (let patch of installPatches) {
|
||||||
|
if (manifest.applied_patches.includes(patch.id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Applying patch [${patch.id}]...`)
|
||||||
|
|
||||||
|
global.sendToRenderer(`installation:status`, {
|
||||||
|
...manifest,
|
||||||
|
statusText: `Applying patch [${patch.id}]...`,
|
||||||
|
})
|
||||||
|
|
||||||
|
for await (let addition of patch.additions) {
|
||||||
|
console.log(addition)
|
||||||
|
|
||||||
|
// resolve patch file
|
||||||
|
addition.file = await this.parseStringVars(addition.file, manifest)
|
||||||
|
|
||||||
|
if (fs.existsSync(addition.file)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await processGenericSteps(manifest, addition.steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to applied patches
|
||||||
|
manifest.applied_patches.push(patch.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest.status = "installed"
|
||||||
|
|
||||||
|
global.sendToRenderer(`installation:done`, {
|
||||||
|
...manifest,
|
||||||
|
statusText: "Changes applied!",
|
||||||
|
})
|
||||||
|
|
||||||
|
db.installations[index] = manifest
|
||||||
|
|
||||||
|
console.log(`Patches applied for [${manifest_id}]...`)
|
||||||
|
|
||||||
|
await this.writeDb(db)
|
||||||
|
}
|
||||||
|
|
||||||
async install(manifest) {
|
async install(manifest) {
|
||||||
try {
|
try {
|
||||||
let pendingTasks = []
|
let pendingTasks = []
|
||||||
@ -130,7 +295,7 @@ export default class PkgManager {
|
|||||||
|
|
||||||
manifest.status = "installing"
|
manifest.status = "installing"
|
||||||
|
|
||||||
console.log(`Starting to install ${manifest.pack_name}...`)
|
console.log(`Starting to install ${manifest.name}...`)
|
||||||
console.log(`Installing at >`, manifest.packPath)
|
console.log(`Installing at >`, manifest.packPath)
|
||||||
|
|
||||||
global.sendToRenderer("new:installation", manifest)
|
global.sendToRenderer("new:installation", manifest)
|
||||||
@ -139,25 +304,23 @@ export default class PkgManager {
|
|||||||
|
|
||||||
await this.appendInstallation(manifest)
|
await this.appendInstallation(manifest)
|
||||||
|
|
||||||
if (typeof manifest.on_install === "function") {
|
if (typeof manifest.before_install === "function") {
|
||||||
await manifest.on_install({
|
console.log(`Performing before_install hook...`)
|
||||||
|
|
||||||
|
global.sendToRenderer(`installation:status`, {
|
||||||
|
...manifest,
|
||||||
|
statusText: `Performing before_install hook...`,
|
||||||
|
})
|
||||||
|
|
||||||
|
await manifest.before_install({
|
||||||
manifest: manifest,
|
manifest: manifest,
|
||||||
pack_dir: manifest.packPath,
|
pack_dir: manifest.packPath,
|
||||||
tmp_dir: TMP_PATH,
|
tmp_dir: TMP_PATH,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof manifest.git_clones_steps !== "undefined" && Array.isArray(manifest.git_clones_steps)) {
|
// PROCESS STEPS
|
||||||
for await (const step of manifest.git_clones_steps) {
|
await processGenericSteps(manifest, manifest.install_steps)
|
||||||
await InstallationStepsMethods.git(manifest, step)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof manifest.http_downloads !== "undefined" && Array.isArray(manifest.http_downloads)) {
|
|
||||||
for await (const step of manifest.http_downloads) {
|
|
||||||
await InstallationStepsMethods.http(manifest, step)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pendingTasks.length > 0) {
|
if (pendingTasks.length > 0) {
|
||||||
console.log(`Performing pending tasks...`)
|
console.log(`Performing pending tasks...`)
|
||||||
@ -194,7 +357,14 @@ export default class PkgManager {
|
|||||||
|
|
||||||
await this.appendInstallation(manifest)
|
await this.appendInstallation(manifest)
|
||||||
|
|
||||||
console.log(`Successfully installed ${manifest.pack_name}!`)
|
// process default patches
|
||||||
|
const defaultPatches = manifest.patches.filter((patch) => patch.default)
|
||||||
|
|
||||||
|
this.applyChanges(manifest.id, {
|
||||||
|
patches: Object.fromEntries(defaultPatches.map((patch) => [patch.id, true])),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Successfully installed ${manifest.name}!`)
|
||||||
|
|
||||||
global.sendToRenderer(`installation:done`, {
|
global.sendToRenderer(`installation:done`, {
|
||||||
...manifest,
|
...manifest,
|
||||||
@ -303,17 +473,8 @@ export default class PkgManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof manifest.git_update !== "undefined" && Array.isArray(manifest.git_update)) {
|
// PROCESS STEPS
|
||||||
for await (const step of manifest.git_update) {
|
await processGenericSteps(manifest, manifest.update_steps)
|
||||||
await InstallationStepsMethods.git(manifest, step)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof manifest.http_downloads !== "undefined" && Array.isArray(manifest.http_downloads)) {
|
|
||||||
for await (const step of manifest.http_downloads) {
|
|
||||||
await InstallationStepsMethods.http(manifest, step)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pendingTasks.length > 0) {
|
if (pendingTasks.length > 0) {
|
||||||
console.log(`Performing pending tasks...`)
|
console.log(`Performing pending tasks...`)
|
||||||
@ -349,7 +510,7 @@ export default class PkgManager {
|
|||||||
|
|
||||||
await this.appendInstallation(manifest)
|
await this.appendInstallation(manifest)
|
||||||
|
|
||||||
console.log(`Successfully updated ${manifest.pack_name}!`)
|
console.log(`Successfully updated ${manifest.name}!`)
|
||||||
|
|
||||||
global.sendToRenderer(`installation:done`, {
|
global.sendToRenderer(`installation:done`, {
|
||||||
...manifest,
|
...manifest,
|
||||||
@ -394,21 +555,36 @@ export default class PkgManager {
|
|||||||
|
|
||||||
manifest = await initManifest(manifest)
|
manifest = await initManifest(manifest)
|
||||||
|
|
||||||
if (typeof manifest.execute !== "function") {
|
if (typeof manifest.execute === "string") {
|
||||||
global.sendToRenderer("installation:status", {
|
manifest.execute = this.parseStringVars(manifest.execute, manifest)
|
||||||
status: "execution_failed",
|
|
||||||
...manifest,
|
console.log(`Executing >`, manifest.execute)
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const process = child_process.execFile(manifest.execute, [], {
|
||||||
|
shell: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("exit", resolve)
|
||||||
|
process.on("error", reject)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
if (typeof manifest.execute !== "function") {
|
||||||
|
global.sendToRenderer("installation:status", {
|
||||||
|
status: "execution_failed",
|
||||||
|
...manifest,
|
||||||
|
})
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await manifest.execute({
|
||||||
|
manifest,
|
||||||
|
pack_dir: manifest.install_path,
|
||||||
|
tmp_dir: TMP_PATH
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await manifest.execute({
|
|
||||||
manifest,
|
|
||||||
pack_dir: manifest.install_path,
|
|
||||||
tmp_dir: TMP_PATH
|
|
||||||
})
|
|
||||||
|
|
||||||
global.sendToRenderer("installation:status", {
|
global.sendToRenderer("installation:status", {
|
||||||
status: "installed",
|
status: "installed",
|
||||||
...manifest,
|
...manifest,
|
||||||
|
0
src/main/pkg_mng/installs_steps_methods/drive.js
Normal file
0
src/main/pkg_mng/installs_steps_methods/drive.js
Normal file
@ -2,7 +2,7 @@ import path from "node:path"
|
|||||||
import fs from "node:fs"
|
import fs from "node:fs"
|
||||||
import ChildProcess from "node:child_process"
|
import ChildProcess from "node:child_process"
|
||||||
|
|
||||||
export default async (manifest, step) => {
|
export default async (manifest, step,) => {
|
||||||
const _path = path.resolve(manifest.packPath, step.path)
|
const _path = path.resolve(manifest.packPath, step.path)
|
||||||
|
|
||||||
console.log(`Cloning ${step.url}...`)
|
console.log(`Cloning ${step.url}...`)
|
26
src/main/pkg_mng/installs_steps_methods/git_pull.js
Normal file
26
src/main/pkg_mng/installs_steps_methods/git_pull.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import fs from "node:fs"
|
||||||
|
import ChildProcess from "node:child_process"
|
||||||
|
|
||||||
|
export default async (manifest, step) => {
|
||||||
|
const _path = path.resolve(manifest.packPath, step.path)
|
||||||
|
|
||||||
|
console.log(`Pulling ${step.url}...`)
|
||||||
|
|
||||||
|
sendToRenderer(`installation:status`, {
|
||||||
|
...manifest,
|
||||||
|
statusText: `Pulling ${step.url}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.mkdirSync(_path, { recursive: true })
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const process = ChildProcess.exec(`${global.GIT_PATH ?? "git"} pull`, {
|
||||||
|
cwd: _path,
|
||||||
|
shell: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("exit", resolve)
|
||||||
|
process.on("error", reject)
|
||||||
|
})
|
||||||
|
}
|
@ -30,11 +30,21 @@ export default async (manifest, step) => {
|
|||||||
|
|
||||||
fs.mkdirSync(path.resolve(_path, ".."), { recursive: true })
|
fs.mkdirSync(path.resolve(_path, ".."), { recursive: true })
|
||||||
|
|
||||||
if (step.progress) {
|
if (step.simple) {
|
||||||
|
await streamPipeline(
|
||||||
|
got.stream(step.url),
|
||||||
|
fs.createWriteStream(_path)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
const remoteStream = got.stream(step.url)
|
const remoteStream = got.stream(step.url)
|
||||||
const localStream = fs.createWriteStream(_path)
|
const localStream = fs.createWriteStream(_path)
|
||||||
|
|
||||||
let progress = null
|
let progress = {
|
||||||
|
transferred: 0,
|
||||||
|
total: 0,
|
||||||
|
speed: 0,
|
||||||
|
}
|
||||||
|
|
||||||
let lastTransferred = 0
|
let lastTransferred = 0
|
||||||
|
|
||||||
sendToRenderer(`installation:status`, {
|
sendToRenderer(`installation:status`, {
|
||||||
@ -49,14 +59,14 @@ export default async (manifest, step) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const progressInterval = setInterval(() => {
|
const progressInterval = setInterval(() => {
|
||||||
progress.speed = (progress.transferred - lastTransferred) / 1
|
progress.speed = ((progress.transferred ?? 0) - lastTransferred) / 1
|
||||||
|
|
||||||
lastTransferred = progress.transferred
|
lastTransferred = progress.transferred ?? 0
|
||||||
|
|
||||||
sendToRenderer(`installation:${manifest.id}:status`, {
|
sendToRenderer(`installation:${manifest.id}:status`, {
|
||||||
...manifest,
|
...manifest,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
statusText: `Downloaded ${convertSize(progress.transferred)} / ${convertSize(progress.total)} | ${convertSize(progress.speed)}/s`,
|
statusText: `Downloaded ${convertSize(progress.transferred ?? 0)} / ${convertSize(progress.total)} | ${convertSize(progress.speed)}/s`,
|
||||||
})
|
})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
@ -66,11 +76,6 @@ export default async (manifest, step) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
clearInterval(progressInterval)
|
clearInterval(progressInterval)
|
||||||
} else {
|
|
||||||
await streamPipeline(
|
|
||||||
got.stream(step.url),
|
|
||||||
fs.createWriteStream(_path)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step.extract) {
|
if (step.extract) {
|
||||||
@ -82,7 +87,7 @@ export default async (manifest, step) => {
|
|||||||
|
|
||||||
sendToRenderer(`installation:status`, {
|
sendToRenderer(`installation:status`, {
|
||||||
...manifest,
|
...manifest,
|
||||||
statusText: `Extracting file...`,
|
statusText: `Extracting bundle...`,
|
||||||
})
|
})
|
||||||
|
|
||||||
await extractFile(_path, step.extract)
|
await extractFile(_path, step.extract)
|
||||||
|
0
src/main/pkg_mng/installs_steps_methods/torrent.js
Normal file
0
src/main/pkg_mng/installs_steps_methods/torrent.js
Normal file
Loading…
x
Reference in New Issue
Block a user