mirror of
https://github.com/toast-ts/Daggerbot-TS.git
synced 2024-11-17 00:10:58 -05:00
Compare commits
6 Commits
b42194d13a
...
6bb55fb175
Author | SHA1 | Date | |
---|---|---|---|
|
6bb55fb175 | ||
|
e4fa1abf2f | ||
|
742a9a6e70 | ||
|
d1dfebca97 | ||
|
03f5586dcc | ||
|
0c0216eec6 |
@ -16,7 +16,7 @@
|
|||||||
],
|
],
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64",
|
"x64",
|
||||||
"!arm",
|
"!arm64",
|
||||||
"!mips",
|
"!mips",
|
||||||
"!ia32"
|
"!ia32"
|
||||||
],
|
],
|
||||||
@ -26,7 +26,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "1.4.0",
|
"axios": "1.4.0",
|
||||||
"canvas": "2.11.2",
|
"canvas": "2.11.2",
|
||||||
|
"@discord-player/extractor": "4.3.1",
|
||||||
|
"@ffmpeg-installer/ffmpeg": "1.1.0",
|
||||||
|
"@discordjs/opus": "0.9.0",
|
||||||
|
"discord-player": "6.5.0",
|
||||||
"discord.js": "14.11.0",
|
"discord.js": "14.11.0",
|
||||||
|
"ytdl-core": "4.11.4",
|
||||||
"moment": "2.29.4",
|
"moment": "2.29.4",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"mongoose": "7.3.1",
|
"mongoose": "7.3.1",
|
||||||
|
@ -54,7 +54,8 @@ export default class TClient extends Client {
|
|||||||
GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers,
|
GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers,
|
||||||
GatewayIntentBits.GuildModeration, GatewayIntentBits.GuildInvites,
|
GatewayIntentBits.GuildModeration, GatewayIntentBits.GuildInvites,
|
||||||
GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildPresences,
|
GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildPresences,
|
||||||
GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages
|
GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.GuildVoiceStates
|
||||||
], partials: [
|
], partials: [
|
||||||
Partials.Channel, Partials.Reaction, Partials.Message
|
Partials.Channel, Partials.Reaction, Partials.Message
|
||||||
], allowedMentions: {users:[],roles:[]}
|
], allowedMentions: {users:[],roles:[]}
|
||||||
@ -205,6 +206,15 @@ export default class TClient extends Client {
|
|||||||
(this.channels.resolve(DCChannelID) as Discord.TextChannel).send(`**${YTChannelName}** just uploaded a video!\n${Data.feed.entry[0].link._attributes.href}`)
|
(this.channels.resolve(DCChannelID) as Discord.TextChannel).send(`**${YTChannelName}** just uploaded a video!\n${Data.feed.entry[0].link._attributes.href}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Bytes conversion
|
||||||
|
formatBytes(bytes:number, decimals:number = 2) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WClient extends WebhookClient {
|
export class WClient extends WebhookClient {
|
||||||
|
@ -7,6 +7,7 @@ export default {
|
|||||||
vtcR: ()=>interaction.reply(`You can get the <@&${client.config.mainServer.roles.vtcmember}> role from <#802283932430106624> by clicking :truck: button on a webhook's message\n*VTC skin can also be found in <#801975222609641472> as well.*`),
|
vtcR: ()=>interaction.reply(`You can get the <@&${client.config.mainServer.roles.vtcmember}> role from <#802283932430106624> by clicking :truck: button on a webhook's message\n*VTC skin can also be found in <#801975222609641472> as well.*`),
|
||||||
mpR: ()=>interaction.reply(`You can get the <@&${client.config.mainServer.roles.mpplayer}> role from <#802283932430106624> by clicking :tractor: button on webhook's message`),
|
mpR: ()=>interaction.reply(`You can get the <@&${client.config.mainServer.roles.mpplayer}> role from <#802283932430106624> by clicking :tractor: button on webhook's message`),
|
||||||
ytscam: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Scammers in YouTube comments section').setDescription('If you ever see a comment mentioning a giveaway or anything else, **it\'s a scam!**\nYou should report it to YouTube and move on or ignore it.\nP.S: They\'re on every channel and not just Daggerwin.').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1068078284996345916/image.png')]}),
|
ytscam: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Scammers in YouTube comments section').setDescription('If you ever see a comment mentioning a giveaway or anything else, **it\'s a scam!**\nYou should report it to YouTube and move on or ignore it.\nP.S: They\'re on every channel and not just Daggerwin.').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1068078284996345916/image.png')]}),
|
||||||
|
steamscam: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Steam account report scam').setImage('https://cdn.discordapp.com/attachments/1091300529696673792/1122266621088645130/image.png')]}),
|
||||||
fsShader: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Clearing your shader cache folder').setDescription('If your game keeps crashing shortly after opening your game, then the shaders might be an issue.\nTo resolve this, you can go to `Documents/My Games/FarmingSimulator2022` and delete the folder called `shader_cache`').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1015195687970943016/unknown.png')]}),
|
fsShader: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Clearing your shader cache folder').setDescription('If your game keeps crashing shortly after opening your game, then the shaders might be an issue.\nTo resolve this, you can go to `Documents/My Games/FarmingSimulator2022` and delete the folder called `shader_cache`').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1015195687970943016/unknown.png')]}),
|
||||||
fsLogfile: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Uploading your log file').setDescription('You can find `log.txt` in `Documents/My Games/FarmingSimulator2022` and upload it into <#596989522395398144> along with your issue, so people can assist you further and help you resolve.').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1015195643528101958/unknown.png')]}),
|
fsLogfile: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Uploading your log file').setDescription('You can find `log.txt` in `Documents/My Games/FarmingSimulator2022` and upload it into <#596989522395398144> along with your issue, so people can assist you further and help you resolve.').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1015195643528101958/unknown.png')]}),
|
||||||
fsDevConsole: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Enabling the development console').setDescription('Head over to `game.xml` in `Documents/My Games/FarmingSimulator2022` and find the section that mentions `<controls>false</controls>` inside development section, change it to `true` then you are good to go!\nFYI: The keybind to open console is \``\u200b\` (backtick).').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1097273921444790322/image.png')]})
|
fsDevConsole: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Enabling the development console').setDescription('Head over to `game.xml` in `Documents/My Games/FarmingSimulator2022` and find the section that mentions `<controls>false</controls>` inside development section, change it to `true` then you are good to go!\nFYI: The keybind to open console is \``\u200b\` (backtick).').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1097273921444790322/image.png')]})
|
||||||
@ -22,6 +23,7 @@ export default {
|
|||||||
.addChoices(
|
.addChoices(
|
||||||
{ name: 'Survival Roleplay Map', value: 'srp'},
|
{ name: 'Survival Roleplay Map', value: 'srp'},
|
||||||
{ name: 'Scams in YT comments', value: 'ytscam' },
|
{ name: 'Scams in YT comments', value: 'ytscam' },
|
||||||
|
{ name: 'Steam account report scam', value: 'steamscam' },
|
||||||
{ name: 'VTC Role', value: 'vtcR' },
|
{ name: 'VTC Role', value: 'vtcR' },
|
||||||
{ name: 'MP Role', value: 'mpR' },
|
{ name: 'MP Role', value: 'mpR' },
|
||||||
{ name: '[FS22] Resolve shader_cache issue', value: 'fsShader' },
|
{ name: '[FS22] Resolve shader_cache issue', value: 'fsShader' },
|
||||||
|
83
src/commands/music.ts
Normal file
83
src/commands/music.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import Discord from 'discord.js';
|
||||||
|
import TClient from '../client.js';
|
||||||
|
import {Player,useTimeline,useQueue} from 'discord-player';
|
||||||
|
import {SpotifyExtractor} from '@discord-player/extractor';
|
||||||
|
export default {
|
||||||
|
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
|
||||||
|
if (!client.isStaff(interaction.member) && !client.config.whitelist.includes(interaction.member.id)) return interaction.reply('This command is in early stages of development, some parts may be missing or broken.\nIt has been restricted to staff for time-being.');
|
||||||
|
const player = Player.singleton(client);
|
||||||
|
await player.extractors.register(SpotifyExtractor, {
|
||||||
|
clientId: client.tokens.dontlookatme.client,
|
||||||
|
clientSecret: client.tokens.dontlookatme.secret
|
||||||
|
});
|
||||||
|
const voiceCh = interaction.member.voice.channel;
|
||||||
|
if (!voiceCh) return interaction.reply('Please join a voice channel first to use the command.');
|
||||||
|
player.nodes.create(interaction.guildId, {
|
||||||
|
metadata: {
|
||||||
|
channel: interaction.channel,
|
||||||
|
client: interaction.guild.members.me,
|
||||||
|
requestedBy: interaction.member
|
||||||
|
},
|
||||||
|
selfDeaf: true,
|
||||||
|
volume: 75,
|
||||||
|
skipOnNoStream: true,
|
||||||
|
leaveOnEnd: false,
|
||||||
|
leaveOnEmpty: false,
|
||||||
|
bufferingTimeout: 30000,
|
||||||
|
connectionTimeout: 25000,
|
||||||
|
strategy: 'FIFO'
|
||||||
|
});
|
||||||
|
({
|
||||||
|
play: async()=>{
|
||||||
|
const url = interaction.options.getString('url');
|
||||||
|
if (!url.includes('https://open.spotify.com/')) return interaction.reply('Sorry, I can\'t play that. I can only accept Spotify links that contains `https://open.spotify.com/`');
|
||||||
|
player.play(interaction.member.voice.channel, url);
|
||||||
|
await interaction.reply(`Added the song to the queue.`);
|
||||||
|
},
|
||||||
|
stop: async()=>{
|
||||||
|
player.destroy();
|
||||||
|
await interaction.reply('Player destroyed.')
|
||||||
|
},
|
||||||
|
now_playing: ()=>{
|
||||||
|
const {volume,timestamp,track} = useTimeline(interaction.guildId);
|
||||||
|
interaction.reply({embeds:[
|
||||||
|
new client.embed().setColor(client.config.embedColor).setTitle(`${track.title} - ${track.author}`).setThumbnail(track.thumbnail).addFields(
|
||||||
|
{name: 'Timestamp', value: `**${timestamp.current.label}**/**${timestamp.total.label}**`, inline: true},
|
||||||
|
{name: 'Volume', value: `${volume}%`, inline: true}
|
||||||
|
)
|
||||||
|
]})
|
||||||
|
},
|
||||||
|
volume: ()=>{
|
||||||
|
const vol = interaction.options.getNumber('percentage');
|
||||||
|
const queue = useQueue(interaction.guildId);
|
||||||
|
queue.node.setVolume(vol);
|
||||||
|
interaction.reply(`Successfully adjusted the player's volume to ${vol}%`)
|
||||||
|
}
|
||||||
|
} as any)[interaction.options.getSubcommand()]();
|
||||||
|
},
|
||||||
|
data: new Discord.SlashCommandBuilder()
|
||||||
|
.setName('music')
|
||||||
|
.setDescription('Music module')
|
||||||
|
.addSubcommand(x=>x
|
||||||
|
.setName('play')
|
||||||
|
.setDescription('Play a Spotify song')
|
||||||
|
.addStringOption(x=>x
|
||||||
|
.setName('url')
|
||||||
|
.setDescription('Spotify URL')
|
||||||
|
.setRequired(true)))
|
||||||
|
.addSubcommand(x=>x
|
||||||
|
.setName('stop')
|
||||||
|
.setDescription('Stop playing music and disconnect the bot from voice channel'))
|
||||||
|
.addSubcommand(x=>x
|
||||||
|
.setName('now_playing')
|
||||||
|
.setDescription('Check what song is currently playing'))
|
||||||
|
.addSubcommand(x=>x
|
||||||
|
.setName('volume')
|
||||||
|
.setDescription('Adjust the player\'s volume')
|
||||||
|
.addNumberOption(x=>x
|
||||||
|
.setName('percentage')
|
||||||
|
.setDescription('Volume level to adjust, ranges from 5 to 100, default is 75')
|
||||||
|
.setMaxValue(100)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setRequired(true)))
|
||||||
|
}
|
@ -6,15 +6,6 @@ import os from 'node:os';
|
|||||||
export default {
|
export default {
|
||||||
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
|
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
|
||||||
const waitForData = await interaction.reply({content: '<a:sakjdfsajkfhsdjhjfsa:1065342869428252743>', fetchReply:true})
|
const waitForData = await interaction.reply({content: '<a:sakjdfsajkfhsdjhjfsa:1065342869428252743>', fetchReply:true})
|
||||||
// Bytes conversion
|
|
||||||
function formatBytes(bytes:number, decimals:number = 2) {
|
|
||||||
if (bytes === 0) return '0 Bytes';
|
|
||||||
const k = 1024;
|
|
||||||
const dm = decimals < 0 ? 0 : decimals;
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
||||||
};
|
|
||||||
const cpu = await si.cpu();
|
const cpu = await si.cpu();
|
||||||
const ram = await si.mem();
|
const ram = await si.mem();
|
||||||
const osInfo = await si.osInfo();
|
const osInfo = await si.osInfo();
|
||||||
@ -56,8 +47,8 @@ export default {
|
|||||||
{name: '> __Host__', value: [
|
{name: '> __Host__', value: [
|
||||||
`**Operating System:** ${osInfo.distro + ' ' + osInfo.release}`,
|
`**Operating System:** ${osInfo.distro + ' ' + osInfo.release}`,
|
||||||
`**CPU:** ${cpu.manufacturer} ${cpu.brand}`,
|
`**CPU:** ${cpu.manufacturer} ${cpu.brand}`,
|
||||||
`**Memory:** ${formatBytes(ram.used)}/${formatBytes(ram.total)}`,
|
`**Memory:** ${client.formatBytes(ram.used)}/${client.formatBytes(ram.total)}`,
|
||||||
`**NodeJS:** ${formatBytes(process.memoryUsage().heapUsed)}/${formatBytes(process.memoryUsage().heapTotal)}`,
|
`**NodeJS:** ${client.formatBytes(process.memoryUsage().heapUsed)}/${client.formatBytes(process.memoryUsage().heapTotal)}`,
|
||||||
`**Load Usage:**\nUser: ${currentLoad.currentLoadUser.toFixed(1)}%\nSystem: ${currentLoad.currentLoadSystem.toFixed(1)}%`,
|
`**Load Usage:**\nUser: ${currentLoad.currentLoadUser.toFixed(1)}%\nSystem: ${currentLoad.currentLoadSystem.toFixed(1)}%`,
|
||||||
`**Uptime:**\nHost: ${client.formatTime((os.uptime()*1000), 2, {longNames: true, commas: true})}\nBot: ${client.formatTime(client.uptime as number, 2, {commas: true, longNames: true})}`
|
`**Uptime:**\nHost: ${client.formatTime((os.uptime()*1000), 2, {longNames: true, commas: true})}\nBot: ${client.formatTime(client.uptime as number, 2, {commas: true, longNames: true})}`
|
||||||
].join('\n')}
|
].join('\n')}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"embedColor": "#0052cf",
|
"embedColor": "#0052cf",
|
||||||
"embedColorBackup": "#0052cf",
|
"embedColorBackup": "#0052cf",
|
||||||
"embedColorGreen": "#57f287",
|
"embedColorGreen": "#57f287",
|
||||||
@ -27,7 +27,7 @@
|
|||||||
"botPresence": {
|
"botPresence": {
|
||||||
"activities": [
|
"activities": [
|
||||||
{"name": "the banana factory", "url": "https://www.youtube.com/watch?v=zmHca1rW7B4", "type": 1}
|
{"name": "the banana factory", "url": "https://www.youtube.com/watch?v=zmHca1rW7B4", "type": 1}
|
||||||
],
|
],
|
||||||
"status": "online"
|
"status": "online"
|
||||||
},
|
},
|
||||||
"eval": true,
|
"eval": true,
|
||||||
|
21
src/index.ts
21
src/index.ts
@ -3,6 +3,8 @@ import TClient from './client.js';
|
|||||||
const client = new TClient;
|
const client = new TClient;
|
||||||
client.init();
|
client.init();
|
||||||
import MPLoop from './MPLoop.js';
|
import MPLoop from './MPLoop.js';
|
||||||
|
import {Player} from 'discord-player';
|
||||||
|
const player = Player.singleton(client);
|
||||||
import {writeFileSync, readFileSync} from 'node:fs';
|
import {writeFileSync, readFileSync} from 'node:fs';
|
||||||
|
|
||||||
client.on('ready', async()=>{
|
client.on('ready', async()=>{
|
||||||
@ -25,18 +27,31 @@ client.on('ready', async()=>{
|
|||||||
console.timeEnd('Startup')
|
console.timeEnd('Startup')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle errors
|
// Error handler
|
||||||
function DZ(error:Error, location:string){// Yes, I may have shiternet but I don't need to wake up to like a hundred messages or so.
|
function DZ(error:Error, type:string){// Yes, I may have shiternet but I don't need to wake up to like a hundred messages or so.
|
||||||
if (['ENOTFOUND discord.com', 'EAI_AGAIN discord.com'].includes(error.message)) return;
|
if (['ENOTFOUND discord.com', 'EAI_AGAIN discord.com'].includes(error.message)) return;
|
||||||
//console.error(error);
|
//console.error(error);
|
||||||
const channel = client.channels.resolve(client.config.mainServer.channels.errors) as Discord.TextChannel | null;
|
const channel = client.channels.resolve(client.config.mainServer.channels.errors) as Discord.TextChannel | null;
|
||||||
channel?.send({embeds: [new client.embed().setColor('#420420').setTitle('Error caught!').setFooter({text: location}).setDescription(`**Error:** \`${error.message}\`\n\n**Stack:** \`${`${error.stack}`.slice(0, 2500)}\``)]})
|
// vvv Oh yes, that looks really hot.
|
||||||
|
channel?.send({embeds: [new client.embed().setColor('#560000').setTitle('Error caught!').setFooter({text: 'Error type: ' + type}).setDescription(`**Error:**\n\`\`\`${error.message}\`\`\`**Stack:**\n\`\`\`${`${error.stack}`.slice(0, 2500)}\`\`\``)]})
|
||||||
}
|
}
|
||||||
process.on('unhandledRejection', (error: Error)=>DZ(error, 'unhandledRejection'));
|
process.on('unhandledRejection', (error: Error)=>DZ(error, 'unhandledRejection'));
|
||||||
process.on('uncaughtException', (error: Error)=>DZ(error, 'uncaughtException'));
|
process.on('uncaughtException', (error: Error)=>DZ(error, 'uncaughtException'));
|
||||||
process.on('error', (error: Error)=>DZ(error, 'process-error'));
|
process.on('error', (error: Error)=>DZ(error, 'process-error'));
|
||||||
client.on('error', (error: Error)=>DZ(error, 'client-error'));
|
client.on('error', (error: Error)=>DZ(error, 'client-error'));
|
||||||
|
|
||||||
|
// Audio Player event handling
|
||||||
|
player.events.on('playerStart', (queue,track)=>queue.channel.send({embeds:[new client.embed().setColor(client.config.embedColor).setTitle(`${track.raw.title} - ${track.raw.author}`).setFooter({text:`Playing in ${queue.channel.name}`}).setThumbnail(track.raw.thumbnail)]}));
|
||||||
|
player.events.on('playerFinish', (queue,track)=>{
|
||||||
|
if (queue.tracks.size < 1) return queue.channel.send('There\'s no songs left in the queue, leaving voice channel in 15 seconds.').then(()=>setTimeout(()=>queue.connection.disconnect(), 15000))
|
||||||
|
})
|
||||||
|
player.events.on('audioTrackAdd', (queue,track)=>queue.channel.send({embeds:[new client.embed().setColor(client.config.embedColorGreen).setTitle(`${track.raw.title} - ${track.raw.author}`).setFooter({text:`Added to queue`}).setThumbnail(track.raw.thumbnail)]}));
|
||||||
|
/* player.events.on('debug', (queue,message)=>{
|
||||||
|
console.log(client.logTime(), message)
|
||||||
|
}) */
|
||||||
|
player.events.on('playerError', (queue, error)=>DZ(error, 'playerError'));
|
||||||
|
player.events.on('error', (queue, error)=>DZ(error, 'playerInternalError'));
|
||||||
|
|
||||||
// YouTube Upload notification and Daggerwin MP loop
|
// YouTube Upload notification and Daggerwin MP loop
|
||||||
setInterval(()=>MPLoop(client, client.config.MPStatsLocation.channel, client.config.MPStatsLocation.message, 'Daggerwin'), 60000);
|
setInterval(()=>MPLoop(client, client.config.MPStatsLocation.channel, client.config.MPStatsLocation.message, 'Daggerwin'), 60000);
|
||||||
setInterval(async()=>{
|
setInterval(async()=>{
|
||||||
|
4
src/typings/interfaces.d.ts
vendored
4
src/typings/interfaces.d.ts
vendored
@ -116,6 +116,10 @@ export interface Tokens {
|
|||||||
main: string
|
main: string
|
||||||
beta: string
|
beta: string
|
||||||
toast: string
|
toast: string
|
||||||
|
dontlookatme: {
|
||||||
|
client: string,
|
||||||
|
secret: string
|
||||||
|
}
|
||||||
octokit: string
|
octokit: string
|
||||||
webhook_url: string
|
webhook_url: string
|
||||||
webhook_url_test: string
|
webhook_url_test: string
|
||||||
|
Loading…
Reference in New Issue
Block a user