Merge branch 'main' into new-validation
This commit is contained in:
commit
29604a2ed1
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "txtdot",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "txtdot",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/static": "^6.10.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "txtdot",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"main": "dist/app.js",
|
||||
|
34
src/errors/api.ts
Normal file
34
src/errors/api.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export interface IApiError {
|
||||
code: number;
|
||||
name: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const errorSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: "number",
|
||||
description: "HTTP error code",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Exception class name",
|
||||
},
|
||||
message: {
|
||||
type: "string",
|
||||
description: "Exception message",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const errorResponseSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
},
|
||||
error: errorSchema,
|
||||
},
|
||||
};
|
@ -1,14 +1,66 @@
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { NotHtmlMimetypeError } from "./main";
|
||||
import { NotHtmlMimetypeError, TxtDotError } from "./main";
|
||||
import { getFastifyError } from "./validation";
|
||||
|
||||
export default function errorHandler(
|
||||
error: Error,
|
||||
_: FastifyRequest,
|
||||
req: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
if (req.originalUrl.startsWith("/api/")) {
|
||||
return apiErrorHandler(error, reply);
|
||||
}
|
||||
return htmlErrorHandler(error, reply);
|
||||
}
|
||||
|
||||
function apiErrorHandler(error: Error, reply: FastifyReply) {
|
||||
function generateResponse(code: number) {
|
||||
return reply.code(code).send({
|
||||
data: null,
|
||||
error: {
|
||||
code: code,
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof NotHtmlMimetypeError) {
|
||||
return generateResponse(501);
|
||||
}
|
||||
|
||||
if (getFastifyError(error)?.statusCode === 400) {
|
||||
return generateResponse(400);
|
||||
}
|
||||
|
||||
if (error instanceof TxtDotError) {
|
||||
return generateResponse(error.code);
|
||||
}
|
||||
|
||||
return generateResponse(500);
|
||||
}
|
||||
|
||||
function htmlErrorHandler(error: Error, reply: FastifyReply) {
|
||||
if (error instanceof NotHtmlMimetypeError) {
|
||||
return reply.redirect(301, error.url);
|
||||
} else {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (getFastifyError(error)?.statusCode === 400) {
|
||||
return reply.code(400).view("/templates/error.ejs", {
|
||||
code: 400,
|
||||
description: `Invalid parameter specified: ${error.message}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (error instanceof TxtDotError) {
|
||||
return reply.code(error.code).view("/templates/error.ejs", {
|
||||
code: error.code,
|
||||
description: error.description,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).view("/templates/error.ejs", {
|
||||
code: 500,
|
||||
description: `${error.name}: ${error.message}`,
|
||||
});
|
||||
}
|
||||
|
@ -1,10 +1,34 @@
|
||||
export class EngineParseError extends Error {}
|
||||
export class InvalidParameterError extends Error {}
|
||||
export class LocalResourceError extends Error {}
|
||||
export class NotHtmlMimetypeError extends Error {
|
||||
url: string;
|
||||
constructor(params: { url: string }) {
|
||||
super();
|
||||
this.url = params?.url;
|
||||
export abstract class TxtDotError extends Error {
|
||||
code: number;
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
constructor(code: number, name: string, description: string) {
|
||||
super(description);
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineParseError extends TxtDotError {
|
||||
constructor(message: string) {
|
||||
super(422, "EngineParseError", `Parse error: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalResourceError extends TxtDotError {
|
||||
constructor() {
|
||||
super(403, "LocalResourceError", "Proxying local resources is forbidden.");
|
||||
}
|
||||
}
|
||||
|
||||
export class NotHtmlMimetypeError extends Error {
|
||||
name: string = "NotHtmlMimetypeError";
|
||||
url: string;
|
||||
|
||||
constructor(url: string) {
|
||||
super();
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
9
src/errors/validation.ts
Normal file
9
src/errors/validation.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface IFastifyValidationError {
|
||||
statusCode?: number;
|
||||
code?: string;
|
||||
validation?: any;
|
||||
}
|
||||
|
||||
export function getFastifyError(error: Error) {
|
||||
return error as unknown as IFastifyValidationError;
|
||||
}
|
@ -6,16 +6,15 @@ import { DOMWindow } from "jsdom";
|
||||
|
||||
import readability from "./readability";
|
||||
import google from "./google";
|
||||
import stackoverflow from "./stackoverflow/main";
|
||||
|
||||
import { generateProxyUrl } from "../utils/generate";
|
||||
import isLocalResource from "../utils/islocal";
|
||||
|
||||
import {
|
||||
InvalidParameterError,
|
||||
LocalResourceError,
|
||||
NotHtmlMimetypeError,
|
||||
} from "../errors/main";
|
||||
import stackoverflow from "./stackoverflow/main";
|
||||
|
||||
export default async function handlePage(
|
||||
url: string, // remote URL
|
||||
@ -28,21 +27,21 @@ export default async function handlePage(
|
||||
throw new LocalResourceError();
|
||||
}
|
||||
|
||||
if (engine && engineList.indexOf(engine) === -1) {
|
||||
throw new InvalidParameterError("Invalid engine");
|
||||
}
|
||||
|
||||
const response = await axios.get(url);
|
||||
const mime: string | undefined = response.headers["content-type"]?.toString();
|
||||
|
||||
if (mime && mime.indexOf("text/html") === -1) {
|
||||
throw new NotHtmlMimetypeError({ url });
|
||||
throw new NotHtmlMimetypeError(url);
|
||||
}
|
||||
|
||||
const window = new JSDOM(response.data, { url }).window;
|
||||
|
||||
[...window.document.getElementsByTagName("a")].forEach((link) => {
|
||||
try {
|
||||
link.href = generateProxyUrl(requestUrl, link.href, engine);
|
||||
} catch (_err) {
|
||||
// ignore TypeError: Invalid URL
|
||||
}
|
||||
});
|
||||
|
||||
if (engine) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
version: "1.1.0",
|
||||
version: "1.1.1",
|
||||
description:
|
||||
"HTTP proxy that parses only text, links and pictures from pages reducing internet traffic, removing ads and heavy scripts",
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
|
||||
import { GetSchema, IGetSchema } from "../types/requests";
|
||||
import { GetSchema, IGetSchema } from "../types/requests/browser";
|
||||
import handlePage from "../handlers/main";
|
||||
import { generateRequestUrl } from "../utils/generate";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { engineList } from "../handlers/main";
|
||||
import { indexSchema } from "../types/requests";
|
||||
import { indexSchema } from "../types/requests/browser";
|
||||
|
||||
export default async function indexRoute(fastify: FastifyInstance) {
|
||||
fastify.get("/", { schema: indexSchema }, async (_, reply) => {
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { EngineRequest, IParseSchema, parseSchema } from "../types/requests";
|
||||
import { FastifyInstance } from "fastify";
|
||||
|
||||
import { EngineRequest, IParseSchema, parseSchema } from "../types/requests/api";
|
||||
|
||||
import handlePage from "../handlers/main";
|
||||
import { generateRequestUrl } from "../utils/generate";
|
||||
|
||||
@ -8,7 +10,8 @@ export default async function parseRoute(fastify: FastifyInstance) {
|
||||
"/api/parse",
|
||||
{ schema: parseSchema },
|
||||
async (request: EngineRequest) => {
|
||||
return await handlePage(
|
||||
return {
|
||||
data: await handlePage(
|
||||
request.query.url,
|
||||
generateRequestUrl(
|
||||
request.protocol,
|
||||
@ -16,7 +19,9 @@ export default async function parseRoute(fastify: FastifyInstance) {
|
||||
request.originalUrl
|
||||
),
|
||||
request.query.engine
|
||||
);
|
||||
),
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
|
||||
import { GetRequest, IParseSchema, rawHtmlSchema } from "../types/requests";
|
||||
import { IParseSchema, rawHtmlSchema } from "../types/requests/api";
|
||||
import { GetRequest } from "../types/requests/browser";
|
||||
|
||||
import handlePage from "../handlers/main";
|
||||
import { generateRequestUrl } from "../utils/generate";
|
||||
|
||||
|
@ -1,99 +0,0 @@
|
||||
import { FastifyRequest, FastifySchema } from "fastify";
|
||||
import { handlerSchema } from "../handlers/handler.interface";
|
||||
import { engineList } from "../handlers/main";
|
||||
|
||||
export type GetRequest = FastifyRequest<{
|
||||
Querystring: {
|
||||
url: string;
|
||||
format?: string;
|
||||
engine?: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
export interface IGetQuery {
|
||||
url: string;
|
||||
format?: string;
|
||||
engine?: string;
|
||||
}
|
||||
|
||||
export interface IParseQuery {
|
||||
url: string;
|
||||
engine?: string;
|
||||
}
|
||||
|
||||
export interface IGetSchema {
|
||||
Querystring: IGetQuery;
|
||||
}
|
||||
|
||||
export interface IParseSchema {
|
||||
Querystring: IParseQuery;
|
||||
}
|
||||
|
||||
export const indexSchema = {
|
||||
produces: ["text/html"],
|
||||
hide: true
|
||||
};
|
||||
|
||||
export const getQuerySchema = {
|
||||
type: "object",
|
||||
required: ["url"],
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: "URL",
|
||||
},
|
||||
format: {
|
||||
type: "string",
|
||||
enum: ["text", "html", ""],
|
||||
default: "html",
|
||||
},
|
||||
engine: {
|
||||
type: "string",
|
||||
enum: [...engineList, ""],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const parseQuerySchema = {
|
||||
type: "object",
|
||||
required: ["url"],
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: "URL",
|
||||
},
|
||||
engine: {
|
||||
type: "string",
|
||||
enum: [...engineList, ""],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GetSchema: FastifySchema = {
|
||||
description: "Get page",
|
||||
hide: true,
|
||||
querystring: getQuerySchema,
|
||||
produces: ["text/html", "text/plain"],
|
||||
};
|
||||
|
||||
export const parseSchema: FastifySchema = {
|
||||
description: "Parse page",
|
||||
querystring: parseQuerySchema,
|
||||
response: {
|
||||
"2xx": handlerSchema,
|
||||
},
|
||||
produces: ["text/json"],
|
||||
};
|
||||
|
||||
export const rawHtmlSchema: FastifySchema = {
|
||||
description: "Get raw HTML",
|
||||
querystring: parseQuerySchema,
|
||||
produces: ["text/html"],
|
||||
};
|
||||
|
||||
export type EngineRequest = FastifyRequest<{
|
||||
Querystring: {
|
||||
url: string;
|
||||
engine?: string;
|
||||
};
|
||||
}>;
|
66
src/types/requests/api.ts
Normal file
66
src/types/requests/api.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { FastifySchema, FastifyRequest } from "fastify";
|
||||
import { IApiError, errorResponseSchema } from "../../errors/api";
|
||||
import { handlerSchema } from "../../handlers/handler.interface";
|
||||
import { engineList } from "../../handlers/main";
|
||||
|
||||
export interface IApiResponse<T> {
|
||||
data?: T;
|
||||
error?: IApiError;
|
||||
}
|
||||
|
||||
export interface IParseQuery {
|
||||
url: string;
|
||||
engine?: string;
|
||||
}
|
||||
|
||||
export interface IParseSchema {
|
||||
Querystring: IParseQuery;
|
||||
}
|
||||
|
||||
export const parseQuerySchema = {
|
||||
type: "object",
|
||||
required: ["url"],
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: "URL",
|
||||
},
|
||||
engine: {
|
||||
type: "string",
|
||||
enum: [...engineList, ""],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const parseSchema: FastifySchema = {
|
||||
description: "Parse the page and get all data from the engine",
|
||||
querystring: parseQuerySchema,
|
||||
response: {
|
||||
"2xx": {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: handlerSchema,
|
||||
error: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"4xx": errorResponseSchema,
|
||||
"5xx": errorResponseSchema,
|
||||
},
|
||||
produces: ["text/json"],
|
||||
};
|
||||
|
||||
export const rawHtmlSchema: FastifySchema = {
|
||||
description: "Parse the page and get raw HTML from the engine",
|
||||
querystring: parseQuerySchema,
|
||||
produces: ["text/html"],
|
||||
};
|
||||
|
||||
export type EngineRequest = FastifyRequest<{
|
||||
Querystring: {
|
||||
url: string;
|
||||
engine?: string;
|
||||
};
|
||||
}>;
|
48
src/types/requests/browser.ts
Normal file
48
src/types/requests/browser.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { FastifyRequest, FastifySchema } from "fastify";
|
||||
import { engineList } from "../../handlers/main";
|
||||
|
||||
export type GetRequest = FastifyRequest<{
|
||||
Querystring: IGetQuery;
|
||||
}>;
|
||||
|
||||
export interface IGetQuery {
|
||||
url: string;
|
||||
format?: string;
|
||||
engine?: string;
|
||||
}
|
||||
|
||||
export interface IGetSchema {
|
||||
Querystring: IGetQuery;
|
||||
}
|
||||
|
||||
export const indexSchema = {
|
||||
produces: ["text/html"],
|
||||
hide: true
|
||||
};
|
||||
|
||||
export const getQuerySchema = {
|
||||
type: "object",
|
||||
required: ["url"],
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: "URL",
|
||||
},
|
||||
format: {
|
||||
type: "string",
|
||||
enum: ["text", "html", ""],
|
||||
default: "html",
|
||||
},
|
||||
engine: {
|
||||
type: "string",
|
||||
enum: [...engineList, ""],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GetSchema: FastifySchema = {
|
||||
description: "Get page",
|
||||
hide: true,
|
||||
querystring: getQuerySchema,
|
||||
produces: ["text/html", "text/plain"],
|
||||
};
|
@ -9,8 +9,10 @@
|
||||
--bg2: #bbb;
|
||||
--fg2: #333;
|
||||
|
||||
--accent: hsl(207, 100%, 40%);
|
||||
--accent-hl: hsl(207, 100%, 20%);
|
||||
--accent: #0070cc; /* hsl(207, 100%, 40%) */
|
||||
--accent-hl: #003866; /* hsl(207, 100%, 20%) */
|
||||
|
||||
--error: #ff9400;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@ -21,8 +23,8 @@
|
||||
--bg2: #444;
|
||||
--fg2: #bbb;
|
||||
|
||||
--accent: hsl(207, 100%, 60%);
|
||||
--accent-hl: hsl(207, 100%, 80%);
|
||||
--accent: #33a3ff; /* hsl(207, 100%, 60%) */
|
||||
--accent-hl: #99d1ff; /* hsl(207, 100%, 80%) */
|
||||
}
|
||||
}
|
||||
|
||||
|
60
static/form.css
Normal file
60
static/form.css
Normal file
@ -0,0 +1,60 @@
|
||||
.input-grid {
|
||||
display: grid;
|
||||
/* 2 columns: auto width, min-content width */
|
||||
grid-template-columns: auto min-content;
|
||||
|
||||
/* gap: row column */
|
||||
gap: 0.5rem 0.25rem;
|
||||
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#url {
|
||||
width: 100%;
|
||||
height: 100%; /* shrink to #submit height */
|
||||
|
||||
outline: none;
|
||||
border: 0;
|
||||
border-bottom: 0.125rem solid var(--fg2);
|
||||
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-size: 1rem;
|
||||
}
|
||||
#url::placeholder {
|
||||
color: var(--fg2);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#submit {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
select {
|
||||
border: 0;
|
||||
border-bottom: 0.125rem solid var(--accent);
|
||||
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
@ -5,9 +5,10 @@
|
||||
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
border-bottom: 0.125rem solid var(--bg2);
|
||||
font-weight: 600;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -2,73 +2,17 @@ main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
}
|
||||
h1 > span {
|
||||
|
||||
h1 > .dot {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.input-grid {
|
||||
display: grid;
|
||||
/* 2 columns: auto width, min-content width */
|
||||
grid-template-columns: auto min-content;
|
||||
|
||||
/* gap: row column */
|
||||
gap: 0.5rem 0.25rem;
|
||||
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#url {
|
||||
width: 100%;
|
||||
height: 100%; /* shrink to #submit height */
|
||||
|
||||
outline: none;
|
||||
border: 0;
|
||||
border-bottom: 0.125rem solid var(--fg2);
|
||||
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-size: 1rem;
|
||||
}
|
||||
#url::placeholder {
|
||||
color: var(--fg2);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#submit {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
select {
|
||||
border: 0;
|
||||
border-bottom: 0.125rem solid var(--accent);
|
||||
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
h1 > .dot-err {
|
||||
color: var(--error);
|
||||
}
|
||||
|
21
templates/error.ejs
Normal file
21
templates/error.ejs
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>txt. <%= code %></title>
|
||||
<link rel="stylesheet" href="/static/common.css">
|
||||
<link rel="stylesheet" href="/static/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>txt<span class="dot-err">.</span></h1>
|
||||
<p><%= description %></p>
|
||||
</header>
|
||||
<a href="/" class="button secondary">Home</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
@ -15,10 +15,9 @@
|
||||
<a class="button secondary" href="/">Home</a>
|
||||
<a class="button secondary" href="<%= remoteUrl %>">Original page</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<p class="title">
|
||||
<%= parsed.title %>
|
||||
</div>
|
||||
<hr>
|
||||
</p>
|
||||
<%- parsed.content %>
|
||||
</main>
|
||||
</body>
|
||||
|
@ -9,11 +9,12 @@
|
||||
<title>txt. main page</title>
|
||||
<link rel="stylesheet" href="/static/common.css">
|
||||
<link rel="stylesheet" href="/static/index.css">
|
||||
<link rel="stylesheet" href="/static/form.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>txt<span>.</span></h1>
|
||||
<h1>txt<span class="dot">.</span></h1>
|
||||
<p><%= desc %></p>
|
||||
</header>
|
||||
<form action="/get" method="get" class="input-grid">
|
||||
|
Loading…
x
Reference in New Issue
Block a user