mirror of
https://github.com/discordeno/discordeno.git
synced 2026-09-17 08:47:22 +00:00
fix: add authorization prop to rest for proxy
This commit is contained in:
+107
-54
@@ -6,6 +6,7 @@ import {
|
||||
camelize,
|
||||
camelToSnakeCase,
|
||||
delay,
|
||||
encode,
|
||||
findFiles,
|
||||
getBotIdFromToken,
|
||||
isGetMessagesAfter,
|
||||
@@ -60,6 +61,7 @@ import type {
|
||||
DiscordVoiceRegion,
|
||||
DiscordWebhook,
|
||||
DiscordWelcomeScreen,
|
||||
FileContent,
|
||||
GetMessagesOptions,
|
||||
GetScheduledEventUsers,
|
||||
MfaLevels,
|
||||
@@ -78,7 +80,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
token: options.token,
|
||||
applicationId: options.applicationId ? BigInt(options.applicationId) : getBotIdFromToken(options.token),
|
||||
version: options.version ?? 10,
|
||||
baseUrl: options.baseUrl ?? 'https://discord.com/api',
|
||||
baseUrl: options.proxy?.baseUrl ?? 'https://discord.com/api',
|
||||
maxRetryCount: Infinity,
|
||||
globallyRateLimited: false,
|
||||
processingRateLimitedPaths: false,
|
||||
@@ -86,6 +88,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
queues: new Map(),
|
||||
rateLimitedPaths: new Map(),
|
||||
invalidBucket: createInvalidRequestBucket({}),
|
||||
authorization: options.proxy?.authorization,
|
||||
|
||||
routes: {
|
||||
webhooks: {
|
||||
@@ -875,11 +878,12 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
logger.debug(`Request to ${url} exceeded the maximum allowed retries.`, 'with payload:', payload)
|
||||
// rest.debug(`[REST - RetriesMaxed] ${JSON.stringify(options)}`)
|
||||
// Remove item from queue to prevent retry
|
||||
return options.reject({
|
||||
options.reject({
|
||||
ok: false,
|
||||
status: response.status,
|
||||
error: 'The options was rate limited and it maxed out the retries limit.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limited, add back to queue
|
||||
@@ -893,13 +897,14 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
return await options.retryRequest?.(options)
|
||||
}
|
||||
|
||||
return options.reject({ ok: false, status: response.status, body: JSON.stringify(await response.json()) })
|
||||
options.reject({ ok: false, status: response.status, body: JSON.stringify(await response.json()) })
|
||||
return
|
||||
}
|
||||
|
||||
const is204 = response.status === 204
|
||||
const json = is204 ? undefined : await response.json()
|
||||
// Discord sometimes sends no response with 204 code
|
||||
return options.resolve({ ok: true, status: response.status, body: JSON.stringify(json) })
|
||||
options.resolve({ ok: true, status: response.status, body: JSON.stringify(json) })
|
||||
},
|
||||
|
||||
// Credits: github.com/abalabahaha/eris lib/rest/RequestHandler.js#L397
|
||||
@@ -951,6 +956,44 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async makeRequest(method, url, body, options) {
|
||||
if (!rest.baseUrl.startsWith('https://discord.com') && url[0] === '/') {
|
||||
// Special handling for sending blobs across http to proxy
|
||||
if (body?.file) {
|
||||
if (!Array.isArray(body.file)) {
|
||||
body.file = [body.file]
|
||||
}
|
||||
// convert blobs to string before sending to proxy
|
||||
body.file = await Promise.all(
|
||||
body.file.map(async (f: FileContent) => {
|
||||
const url = encode(await f.blob.arrayBuffer())
|
||||
|
||||
return { name: f.name, blob: `data:${f.blob.type};base64,${url}` }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const headers: HeadersInit = {
|
||||
Authorization: rest.authorization ?? "",
|
||||
}
|
||||
if (body) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
const result = await fetch(`${rest.baseUrl}${url}`, {
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers,
|
||||
method,
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
const err = await result.json().catch(() => {}) as Record<string, any>;
|
||||
// Legacy Handling to not break old code or when body is missing
|
||||
if (!err?.body) throw new Error(`Error: ${err.message ?? result.statusText}`)
|
||||
throw new Error(JSON.stringify(err));
|
||||
}
|
||||
|
||||
return result.status !== 204 ? await result.json() as any : undefined
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const payload: SendRequestOptions = {
|
||||
url,
|
||||
@@ -960,7 +1003,9 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
retryRequest: async function (options: SendRequestOptions) {
|
||||
rest.processRequest(payload)
|
||||
},
|
||||
resolve: (data) => resolve(data.status !== 204 ? JSON.parse(data.body ?? '{}') : undefined),
|
||||
resolve: (data) => {
|
||||
resolve(data.status !== 204 ? JSON.parse(data.body ?? '{}') : undefined)
|
||||
},
|
||||
reject,
|
||||
options,
|
||||
}
|
||||
@@ -978,7 +1023,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async delete(url: string, body?: Record<string, any>) {
|
||||
return camelize(await rest.makeRequest('DELETE', url, body))
|
||||
camelize(await rest.makeRequest('DELETE', url, body))
|
||||
},
|
||||
|
||||
async patch<T = Record<string, unknown>>(url: string, body?: Record<string, any>) {
|
||||
@@ -992,12 +1037,16 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
async addReaction(channelId, messageId, reaction) {
|
||||
reaction = processReactionString(reaction)
|
||||
|
||||
return await rest.put(rest.routes.channels.reactions.bot(channelId, messageId, reaction))
|
||||
await rest.put(rest.routes.channels.reactions.bot(channelId, messageId, reaction))
|
||||
},
|
||||
|
||||
async addReactions(channelId, messageId, reactions, ordered = false) {
|
||||
if (!ordered) {
|
||||
await Promise.all(reactions.map(async (reaction) => await rest.addReaction(channelId, messageId, reaction)))
|
||||
await Promise.all(
|
||||
reactions.map(async (reaction) => {
|
||||
await rest.addReaction(channelId, messageId, reaction)
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1007,11 +1056,11 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async addRole(guildId, userId, roleId, reason) {
|
||||
return await rest.put(rest.routes.guilds.roles.member(guildId, userId, roleId), { reason })
|
||||
await rest.put(rest.routes.guilds.roles.member(guildId, userId, roleId), { reason })
|
||||
},
|
||||
|
||||
async addThreadMember(channelId, userId) {
|
||||
return await rest.put(rest.routes.channels.threads.user(channelId, userId))
|
||||
await rest.put(rest.routes.channels.threads.user(channelId, userId))
|
||||
},
|
||||
|
||||
async createAutomodRule(guildId, options) {
|
||||
@@ -1086,114 +1135,114 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async deleteAutomodRule(guildId, ruleId, reason) {
|
||||
return await rest.delete(rest.routes.guilds.automod.rule(guildId, ruleId), { reason })
|
||||
await rest.delete(rest.routes.guilds.automod.rule(guildId, ruleId), { reason })
|
||||
},
|
||||
|
||||
async deleteChannel(channelId, reason) {
|
||||
return await rest.delete(rest.routes.channels.channel(channelId), {
|
||||
await rest.delete(rest.routes.channels.channel(channelId), {
|
||||
reason,
|
||||
})
|
||||
},
|
||||
|
||||
async deleteChannelPermissionOverride(channelId, overwriteId, reason) {
|
||||
return await rest.delete(rest.routes.channels.overwrite(channelId, overwriteId), reason ? { reason } : undefined)
|
||||
await rest.delete(rest.routes.channels.overwrite(channelId, overwriteId), reason ? { reason } : undefined)
|
||||
},
|
||||
|
||||
async deleteEmoji(guildId, id, reason) {
|
||||
return await rest.delete(rest.routes.guilds.emoji(guildId, id), { reason })
|
||||
await rest.delete(rest.routes.guilds.emoji(guildId, id), { reason })
|
||||
},
|
||||
|
||||
async deleteFollowupMessage(token, messageId) {
|
||||
return await rest.delete(rest.routes.interactions.responses.message(rest.applicationId, token, messageId))
|
||||
await rest.delete(rest.routes.interactions.responses.message(rest.applicationId, token, messageId))
|
||||
},
|
||||
|
||||
async deleteGlobalApplicationCommand(commandId) {
|
||||
return await rest.delete(rest.routes.interactions.commands.command(rest.applicationId, commandId))
|
||||
await rest.delete(rest.routes.interactions.commands.command(rest.applicationId, commandId))
|
||||
},
|
||||
|
||||
async deleteGuild(guildId) {
|
||||
return await rest.delete(rest.routes.guilds.guild(guildId))
|
||||
await rest.delete(rest.routes.guilds.guild(guildId))
|
||||
},
|
||||
|
||||
async deleteGuildApplicationCommand(commandId, guildId) {
|
||||
return await rest.delete(rest.routes.interactions.commands.guilds.one(rest.applicationId, guildId, commandId))
|
||||
await rest.delete(rest.routes.interactions.commands.guilds.one(rest.applicationId, guildId, commandId))
|
||||
},
|
||||
|
||||
async deleteGuildSticker(guildId, stickerId, reason) {
|
||||
return await rest.delete(rest.routes.guilds.sticker(guildId, stickerId), reason ? { reason } : undefined)
|
||||
await rest.delete(rest.routes.guilds.sticker(guildId, stickerId), reason ? { reason } : undefined)
|
||||
},
|
||||
|
||||
async deleteGuildTemplate(guildId, templateCode) {
|
||||
return await rest.delete(rest.routes.guilds.templates.guild(guildId, templateCode))
|
||||
await rest.delete(rest.routes.guilds.templates.guild(guildId, templateCode))
|
||||
},
|
||||
|
||||
async deleteIntegration(guildId, integrationId) {
|
||||
return await rest.delete(rest.routes.guilds.integration(guildId, integrationId))
|
||||
await rest.delete(rest.routes.guilds.integration(guildId, integrationId))
|
||||
},
|
||||
|
||||
async deleteInvite(inviteCode, reason) {
|
||||
return await rest.delete(rest.routes.guilds.invite(inviteCode), reason ? { reason } : undefined)
|
||||
await rest.delete(rest.routes.guilds.invite(inviteCode), reason ? { reason } : undefined)
|
||||
},
|
||||
|
||||
async deleteMessage(channelId, messageId, reason) {
|
||||
return await rest.delete(rest.routes.channels.message(channelId, messageId), { reason })
|
||||
await rest.delete(rest.routes.channels.message(channelId, messageId), { reason })
|
||||
},
|
||||
|
||||
async deleteMessages(channelId, messageIds, reason) {
|
||||
return await rest.post(rest.routes.channels.bulk(channelId), {
|
||||
await rest.post(rest.routes.channels.bulk(channelId), {
|
||||
messages: messageIds.slice(0, 100).map((id) => id.toString()),
|
||||
reason,
|
||||
})
|
||||
},
|
||||
|
||||
async deleteOriginalInteractionResponse(token) {
|
||||
return await rest.delete(rest.routes.interactions.responses.original(rest.applicationId, token))
|
||||
await rest.delete(rest.routes.interactions.responses.original(rest.applicationId, token))
|
||||
},
|
||||
|
||||
async deleteOwnReaction(channelId, messageId, reaction) {
|
||||
reaction = processReactionString(reaction)
|
||||
|
||||
return await rest.delete(rest.routes.channels.reactions.bot(channelId, messageId, reaction))
|
||||
await rest.delete(rest.routes.channels.reactions.bot(channelId, messageId, reaction))
|
||||
},
|
||||
|
||||
async deleteReactionsAll(channelId, messageId) {
|
||||
return await rest.delete(rest.routes.channels.reactions.all(channelId, messageId))
|
||||
await rest.delete(rest.routes.channels.reactions.all(channelId, messageId))
|
||||
},
|
||||
|
||||
async deleteReactionsEmoji(channelId, messageId, reaction) {
|
||||
reaction = processReactionString(reaction)
|
||||
|
||||
return await rest.delete(rest.routes.channels.reactions.emoji(channelId, messageId, reaction))
|
||||
await rest.delete(rest.routes.channels.reactions.emoji(channelId, messageId, reaction))
|
||||
},
|
||||
|
||||
async deleteRole(guildId, roleId) {
|
||||
return await rest.delete(rest.routes.guilds.roles.one(guildId, roleId))
|
||||
await rest.delete(rest.routes.guilds.roles.one(guildId, roleId))
|
||||
},
|
||||
|
||||
async deleteScheduledEvent(guildId, eventId) {
|
||||
return await rest.delete(rest.routes.guilds.events.event(guildId, eventId))
|
||||
await rest.delete(rest.routes.guilds.events.event(guildId, eventId))
|
||||
},
|
||||
|
||||
async deleteStageInstance(channelId, reason) {
|
||||
return await rest.delete(rest.routes.channels.stage(channelId), reason ? { reason } : undefined)
|
||||
await rest.delete(rest.routes.channels.stage(channelId), reason ? { reason } : undefined)
|
||||
},
|
||||
|
||||
async deleteUserReaction(channelId, messageId, userId, reaction) {
|
||||
reaction = processReactionString(reaction)
|
||||
|
||||
return await rest.delete(rest.routes.channels.reactions.user(channelId, messageId, reaction, userId))
|
||||
await rest.delete(rest.routes.channels.reactions.user(channelId, messageId, reaction, userId))
|
||||
},
|
||||
|
||||
async deleteWebhook(webhookId, reason) {
|
||||
return await rest.delete(rest.routes.webhooks.id(webhookId), { reason })
|
||||
await rest.delete(rest.routes.webhooks.id(webhookId), { reason })
|
||||
},
|
||||
|
||||
async deleteWebhookMessage(webhookId, token, messageId, options) {
|
||||
return await rest.delete(rest.routes.webhooks.message(webhookId, token, messageId, options))
|
||||
await rest.delete(rest.routes.webhooks.message(webhookId, token, messageId, options))
|
||||
},
|
||||
|
||||
async deleteWebhookWithToken(webhookId, token) {
|
||||
return await rest.delete(rest.routes.webhooks.webhook(webhookId, token))
|
||||
await rest.delete(rest.routes.webhooks.webhook(webhookId, token))
|
||||
},
|
||||
|
||||
async editApplicationCommandPermissions(guildId, commandId, bearerToken, options) {
|
||||
@@ -1226,11 +1275,11 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async editChannelPermissionOverrides(channelId, options) {
|
||||
return await rest.put(rest.routes.channels.overwrite(channelId, options.id), options)
|
||||
await rest.put(rest.routes.channels.overwrite(channelId, options.id), options)
|
||||
},
|
||||
|
||||
async editChannelPositions(guildId, channelPositions) {
|
||||
return await rest.patch(rest.routes.guilds.channels(guildId), channelPositions)
|
||||
await rest.patch(rest.routes.guilds.channels(guildId), channelPositions)
|
||||
},
|
||||
|
||||
async editEmoji(guildId, id, options) {
|
||||
@@ -1257,7 +1306,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async editGuildMfaLevel(guildId: BigString, mfaLevel: MfaLevels, reason?: string): Promise<void> {
|
||||
return await rest.post(rest.routes.guilds.mfa(guildId), { level: mfaLevel, reason })
|
||||
await rest.post(rest.routes.guilds.mfa(guildId), { level: mfaLevel, reason })
|
||||
},
|
||||
|
||||
async editGuildSticker(guildId, stickerId, options) {
|
||||
@@ -1284,7 +1333,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async editOwnVoiceState(guildId, options) {
|
||||
return await rest.patch(rest.routes.guilds.voice(guildId), {
|
||||
await rest.patch(rest.routes.guilds.voice(guildId), {
|
||||
channel_id: options.channelId,
|
||||
suppress: options.suppress,
|
||||
request_to_speak_timestamp: options.requestToSpeakTimestamp
|
||||
@@ -1310,7 +1359,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async editUserVoiceState(guildId, options) {
|
||||
return await rest.patch(rest.routes.guilds.voice(guildId, options.userId), {
|
||||
await rest.patch(rest.routes.guilds.voice(guildId, options.userId), {
|
||||
channel_id: options.channelId,
|
||||
suppress: options.suppress,
|
||||
user_id: options.userId,
|
||||
@@ -1596,15 +1645,15 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async joinThread(channelId) {
|
||||
return await rest.put(rest.routes.channels.threads.me(channelId))
|
||||
await rest.put(rest.routes.channels.threads.me(channelId))
|
||||
},
|
||||
|
||||
async leaveGuild(guildId) {
|
||||
return await rest.delete(rest.routes.guilds.leave(guildId))
|
||||
await rest.delete(rest.routes.guilds.leave(guildId))
|
||||
},
|
||||
|
||||
async leaveThread(channelId) {
|
||||
return await rest.delete(rest.routes.channels.threads.me(channelId))
|
||||
await rest.delete(rest.routes.channels.threads.me(channelId))
|
||||
},
|
||||
|
||||
async publishMessage(channelId, messageId) {
|
||||
@@ -1612,11 +1661,11 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async removeRole(guildId, userId, roleId, reason) {
|
||||
return await rest.delete(rest.routes.guilds.roles.member(guildId, userId, roleId), { reason })
|
||||
await rest.delete(rest.routes.guilds.roles.member(guildId, userId, roleId), { reason })
|
||||
},
|
||||
|
||||
async removeThreadMember(channelId, userId) {
|
||||
return await rest.delete(rest.routes.channels.threads.user(channelId, userId))
|
||||
await rest.delete(rest.routes.channels.threads.user(channelId, userId))
|
||||
},
|
||||
|
||||
async sendFollowupMessage(token, options) {
|
||||
@@ -1630,14 +1679,16 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
// TODO: should change to reprocess queue item
|
||||
await rest.sendRequest(options)
|
||||
},
|
||||
resolve: (data) => resolve(data.status !== 204 ? JSON.parse(data.body ?? '{}') : undefined),
|
||||
resolve: (data) => {
|
||||
resolve(data.status !== 204 ? JSON.parse(data.body ?? '{}') : undefined)
|
||||
},
|
||||
reject,
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async sendInteractionResponse(interactionId, token, options) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
rest.sendRequest({
|
||||
url: rest.routes.interactions.responses.callback(interactionId, token),
|
||||
method: 'POST',
|
||||
@@ -1647,7 +1698,9 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
// TODO: should change to reprocess queue item
|
||||
await rest.sendRequest(options)
|
||||
},
|
||||
resolve: (data) => resolve(data.status !== 204 ? JSON.parse(data.body ?? '{}') : undefined),
|
||||
resolve: (data) => {
|
||||
resolve(data.status !== 204 ? JSON.parse(data.body ?? '{}') : undefined)
|
||||
},
|
||||
reject,
|
||||
})
|
||||
})
|
||||
@@ -1670,7 +1723,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async banMember(guildId, userId, options) {
|
||||
return await rest.put<void>(rest.routes.guilds.members.ban(guildId, userId), options)
|
||||
await rest.put<void>(rest.routes.guilds.members.ban(guildId, userId), options)
|
||||
},
|
||||
|
||||
async editBotMember(guildId, options) {
|
||||
@@ -1690,13 +1743,13 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async kickMember(guildId, userId, reason) {
|
||||
return await rest.delete(rest.routes.guilds.members.member(guildId, userId), {
|
||||
await rest.delete(rest.routes.guilds.members.member(guildId, userId), {
|
||||
reason,
|
||||
})
|
||||
},
|
||||
|
||||
async pinMessage(channelId, messageId, reason) {
|
||||
return await rest.put(rest.routes.channels.pin(channelId, messageId), reason ? { reason } : undefined)
|
||||
await rest.put(rest.routes.channels.pin(channelId, messageId), reason ? { reason } : undefined)
|
||||
},
|
||||
|
||||
async pruneMembers(guildId, options) {
|
||||
@@ -1708,15 +1761,15 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
|
||||
},
|
||||
|
||||
async unbanMember(guildId, userId) {
|
||||
return await rest.delete(rest.routes.guilds.members.ban(guildId, userId))
|
||||
await rest.delete(rest.routes.guilds.members.ban(guildId, userId))
|
||||
},
|
||||
|
||||
async unpinMessage(channelId, messageId, reason) {
|
||||
return await rest.delete(rest.routes.channels.pin(channelId, messageId), reason ? { reason } : undefined)
|
||||
await rest.delete(rest.routes.channels.pin(channelId, messageId), reason ? { reason } : undefined)
|
||||
},
|
||||
|
||||
async triggerTypingIndicator(channelId) {
|
||||
return await rest.post(rest.routes.channels.typing(channelId))
|
||||
await rest.post(rest.routes.channels.typing(channelId))
|
||||
},
|
||||
|
||||
async upsertGlobalApplicationCommands(commands) {
|
||||
|
||||
@@ -106,12 +106,17 @@ export interface CreateRestManagerOptions {
|
||||
* @default bot id from token
|
||||
*/
|
||||
applicationId?: BigString
|
||||
/**
|
||||
* The base url to connect to. If you create a proxy rest, that url would go here.
|
||||
* IT SHOULD NOT END WITH A /
|
||||
* @default https://discord.com/api
|
||||
*/
|
||||
baseUrl?: string
|
||||
/** Configuration when using a proxy. */
|
||||
proxy?: {
|
||||
/**
|
||||
* The base url to connect to. If you create a proxy rest, that url would go here.
|
||||
* IT SHOULD NOT END WITH A /
|
||||
* @default https://discord.com/api
|
||||
*/
|
||||
baseUrl: string
|
||||
/** The authorization header to attach when sending requests to the proxy. */
|
||||
authorization: string
|
||||
}
|
||||
/**
|
||||
* The api versions which can be used to make requests.
|
||||
* @default 10
|
||||
@@ -132,6 +137,8 @@ export interface RestManager {
|
||||
* @default https://discord.com/api
|
||||
*/
|
||||
baseUrl: string
|
||||
/** The authorization header to attach when sending requests to the proxy. */
|
||||
authorization?: string
|
||||
/** The maximum amount of times a request should be retried. Defaults to Infinity */
|
||||
maxRetryCount: number
|
||||
/** Whether or not the manager is rate limited globally across all requests. Defaults to false. */
|
||||
|
||||
Reference in New Issue
Block a user