Merge pull request #740 from warengroup/develop

Version 0.5.0
This commit is contained in:
Christer Warén 2023-06-09 01:32:44 +03:00 committed by GitHub
commit 4410ba358b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1746 additions and 4027 deletions

View File

@ -14,7 +14,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2.5.0 uses: docker/setup-buildx-action@v2.6.0
id: buildx id: buildx
with: with:
install: true install: true

View File

@ -1,4 +1,4 @@
FROM node:16-alpine FROM node:18-alpine
#Dependencies #Dependencies
RUN apk add --virtual .build-deps python3 make g++ gcc git RUN apk add --virtual .build-deps python3 make g++ gcc git

View File

@ -4,6 +4,7 @@
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 0.5.x | :white_check_mark: |
| 0.4.x | :white_check_mark: | | 0.4.x | :white_check_mark: |
| 0.3.x | :x: | | 0.3.x | :x: |
| 0.2.x | :x: | | 0.2.x | :x: |

2477
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "eximiabots-radiox", "name": "eximiabots-radiox",
"version": "0.4.3", "version": "0.5.0",
"description": "Internet Radio to your Discord guild", "description": "Internet Radio to your Discord guild",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -24,16 +24,15 @@
"@discordjs/voice": "^0.16.0", "@discordjs/voice": "^0.16.0",
"discord-api-types": "^0.37.43", "discord-api-types": "^0.37.43",
"discord.js": "^14.11.0", "discord.js": "^14.11.0",
"dotenv": "^16.1.3", "dotenv": "^16.1.4",
"libsodium-wrappers": "^0.7.11", "libsodium-wrappers": "^0.7.11",
"node-fetch": "^3.3.1",
"path": "^0.12.7" "path": "^0.12.7"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.2.5", "@types/node": "^20.2.5",
"@types/ws": "^8.5.4", "@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.8", "@typescript-eslint/parser": "^5.59.9",
"eslint": "^8.41.0", "eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
@ -44,7 +43,7 @@
"typescript": "^5.1.3" "typescript": "^5.1.3"
}, },
"engines": { "engines": {
"node": ">=16.9.1", "node": ">=18.16.0",
"npm": ">=7.0.0" "npm": ">=8.0.0"
} }
} }

View File

@ -1,16 +1,15 @@
import { Client, Collection, IntentsBitField } from "discord.js"; import { Client, Collection, IntentsBitField } from "discord.js";
import Datastore from "./client/classes/Datastore.js"; import Datastore from "./client/classes/Datastore";
import Radio from "./client/classes/Radio.js"; import Radio from "./client/classes/Radio";
import Stations from "./client/classes/Stations.js"; import Stations from "./client/classes/Stations";
import Streamer from "./client/classes/Streamer.js"; import Streamer from "./client/classes/Streamer";
import Statistics from "./client/classes/Statistics.js"; import Statistics from "./client/classes/Statistics";
import fs from "fs"; import { command } from "./client/commands";
import { command, radio } from "./client/utils/typings.js"; import config from "./config";
import config from "./config.js"; import events from "./client/events"
import messages from "./client/messages.js"; import { funcs } from "./client/funcs";
import path from "path"; import { messages } from "./client/messages";
const events = "./client/events/";
const GatewayIntents = new IntentsBitField(); const GatewayIntents = new IntentsBitField();
GatewayIntents.add( GatewayIntents.add(
@ -19,9 +18,9 @@ GatewayIntents.add(
1 << 9 // GUILD_MESSAGES 1 << 9 // GUILD_MESSAGES
); );
class RadioClient extends Client { export default class RadioClient extends Client {
readonly commands: Collection<string, command>; readonly commands: Collection<string, command>;
public funcs: any; readonly funcs = funcs;
readonly config = config; readonly config = config;
readonly messages = messages; readonly messages = messages;
public datastore: Datastore | null; public datastore: Datastore | null;
@ -29,6 +28,7 @@ class RadioClient extends Client {
public streamer: Streamer | null; public streamer: Streamer | null;
public statistics: Statistics | null; public statistics: Statistics | null;
public radio: Radio | null; public radio: Radio | null;
constructor() { constructor() {
super({ super({
intents: GatewayIntents intents: GatewayIntents
@ -40,16 +40,6 @@ class RadioClient extends Client {
this.statistics = null; this.statistics = null;
this.radio = null; this.radio = null;
this.funcs = {};
this.funcs.check = require("./client/funcs/check.js");
this.funcs.isDev = require("./client/funcs/isDev.js");
this.funcs.logger = require("./client/funcs/logger.js");
this.funcs.msToTime = require("./client/funcs/msToTime.js");
this.funcs.saveState = require("./client/funcs/saveState.js");
this.funcs.loadState = require("./client/funcs/loadState.js");
this.funcs.play = require("./client/funcs/play.js");
this.funcs.listStations = require("./client/funcs/listStations.js");
console.log('RadioX ' + this.config.version); console.log('RadioX ' + this.config.version);
console.log('Internet Radio to your Discord guild'); console.log('Internet Radio to your Discord guild');
console.log('(c)2020-2022 EximiaBots by Warén Group'); console.log('(c)2020-2022 EximiaBots by Warén Group');
@ -60,58 +50,12 @@ class RadioClient extends Client {
this.funcs.logger("Maintenance Mode", "Enabled"); this.funcs.logger("Maintenance Mode", "Enabled");
this.config.maintenanceMode = true; this.config.maintenanceMode = true;
this.on("ready", () => { events(this);
require(`${events}ready`).execute(this);
});
this.on("messageCreate", msg => {
require(`${events}messageCreate`).execute(this, msg);
});
this.on("messageDelete", msg => {
require(`${events}messageDelete`).execute(this, msg);
});
this.on("interactionCreate", interaction => {
require(`${events}interactionCreate`).execute(this, interaction);
});
this.on("voiceStateUpdate", (oldState, newState) => {
require(`${events}voiceStateUpdate`).execute(this, oldState, newState);
});
this.on("error", error => {
this.funcs.logger("Discord Client / Error");
console.error(error);
console.log('');
});
process.on('SIGINT', () => {
require(`${events}SIGINT`).execute(this);
});
process.on('SIGTERM', () => {
require(`${events}SIGTERM`).execute(this);
});
process.on('uncaughtException', (error) => {
require(`${events}uncaughtException`).execute(this, error);
});
process.on('exit', () => {
this.funcs.logger("Bot", "Stopping");
});
process.on('warning', (warning) => {
require(`${events}warning`).execute(this, warning);
});
this.login(this.config.token).catch((err) => { this.login(this.config.token).catch((err) => {
this.funcs.logger("Discord Client / Error"); this.funcs.logger("Discord Client", "Login Error");
console.log(err); console.log(err);
console.log(''); console.log('');
}); });
} }
} }
export default RadioClient

View File

@ -1,7 +1,21 @@
const fs = require('fs'); import { Guild } from 'discord.js';
const path = require('path'); import fs from 'fs';
import path from 'path';
import { state } from './Radio';
import { statistics } from './Statistics';
module.exports = class { export interface datastore {
guild: {
id: string,
name?: string
},
statistics: statistics,
state: state | null,
updated?: string
}
export default class Datastore {
map: Map<string, datastore>;
constructor() { constructor() {
this.map = new Map(); this.map = new Map();
this.loadData(); this.loadData();
@ -13,7 +27,7 @@ module.exports = class {
fs.mkdirSync(dir); fs.mkdirSync(dir);
} }
//console.log(""); //console.log("");
const dataFiles = fs.readdirSync(path.join(path.dirname(__dirname), '../../datastore')).filter(f => f.endsWith('.json')); const dataFiles = fs.readdirSync(path.join(path.dirname(__dirname), '../../datastore')).filter((f: string) => f.endsWith('.json'));
for (const file of dataFiles) { for (const file of dataFiles) {
try { try {
const json = require(`../../../datastore/${file}`); const json = require(`../../../datastore/${file}`);
@ -27,7 +41,8 @@ module.exports = class {
//console.log(""); //console.log("");
} }
checkEntry(id){ checkEntry(id: string | undefined){
if(!id) return;
this.loadEntry(id); this.loadEntry(id);
if(!this.map.has(id)){ if(!this.map.has(id)){
this.createEntry(id); this.createEntry(id);
@ -37,17 +52,19 @@ module.exports = class {
} }
} }
createEntry(id){ createEntry(id: string){
let newData = {}; let newData: datastore = {
newData.guild = {}; guild: {
newData.guild.id = id; id: id,
newData.statistics = {}; },
newData.state = {}; statistics: {},
state: null
};
this.map.set(id, newData); this.map.set(id, newData);
this.saveEntry(id, newData); this.saveEntry(id, newData);
} }
loadEntry(id){ loadEntry(id: string){
try { try {
const json = require(`../../../datastore/` + id + '.json'); const json = require(`../../../datastore/` + id + '.json');
this.map.set(id, json); this.map.set(id, json);
@ -55,11 +72,11 @@ module.exports = class {
} }
} }
getEntry(id){ getEntry(id: string){
return this.map.get(id); return this.map.get(id);
} }
updateEntry(guild, newData) { updateEntry(guild: Guild | { id: string, name: string }, newData: datastore) {
newData.guild.name = guild.name; newData.guild.name = guild.name;
let date = new Date(); let date = new Date();
@ -70,36 +87,14 @@ module.exports = class {
//this.showEntry(this.getEntry(guild.id)); //this.showEntry(this.getEntry(guild.id));
} }
showEntry(data){ showEntry(data : datastore){
console.log(data); console.log(data);
} }
createTestFile () { saveEntry(file: string, data: datastore) {
let newData = { fs.writeFile(path.join(path.dirname(__dirname), '../../datastore') + "/" + file + ".json", JSON.stringify(data, null, 4), 'utf8', function(err: NodeJS.ErrnoException | null) {
"guild": {
"id": "test",
"name": "Test"
},
"statistics": {
"test": {
"time": 0,
"used": 0
}
},
"state": {
}
}
this.updateEntry(newData.guild, newData);
}
saveEntry(file, data) {
data = JSON.stringify(data, null, 4);
fs.writeFile(path.join(path.dirname(__dirname), '../../datastore') + "/" + file + ".json", data, 'utf8', function(err) {
if (err) { if (err) {
//console.log(err);
} }
}); });
} }

View File

@ -1,78 +0,0 @@
const {
getVoiceConnection,
joinVoiceChannel
} = require("@discordjs/voice");
module.exports = class Radio extends Map {
constructor() {
super();
}
save(client) {
let currentRadios = this.keys();
let radio = currentRadios.next();
while (!radio.done) {
let currentRadio = this.get(radio.value);
if (currentRadio) {
currentRadio.guild = client.datastore.getEntry(radio.value).guild;
client.statistics.update(client, currentRadio.guild, currentRadio);
client.funcs.saveState(client, currentRadio.guild, currentRadio);
currentRadio.connection?.destroy();
currentRadio.message?.delete();
this.delete(radio.value);
}
radio = currentRadios.next();
}
}
restore(client, guilds) {
if(!client.stations) return;
guilds.forEach(async guild => {
let state = client.funcs.loadState(client, guild);
if(!state) return;
if(!state.station || !state.channels.voice || !state.channels.text) return;
let voiceChannel = client.channels.cache.get(state.channels.voice);
if(!voiceChannel) return;
if(voiceChannel.members.filter(member => !member.user.bot).size === 0) return;
const sstation = await client.stations.search(state.station.name, "direct");
let station = sstation;
if(!station) return;
const construct = {
textChannel: client.channels.cache.get(state.channels.text),
voiceChannel: client.channels.cache.get(state.channels.voice),
connection: null,
message: null,
station: station
};
this.set(guild.id, construct);
try {
const connection =
getVoiceConnection(guild.id) ??
joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator
});
construct.connection = connection;
let date = new Date();
construct.startTime = date.getTime();
client.datastore.checkEntry(guild.id);
client.funcs.play(client, null, guild, station);
} catch (error) {
console.log(error);
}
});
}
};

108
src/client/classes/Radio.ts Normal file
View File

@ -0,0 +1,108 @@
import { Channel, Collection, GuildMember, OAuth2Guild, TextBasedChannel, VoiceBasedChannel, VoiceChannel } from "discord.js";
import { getVoiceConnection, joinVoiceChannel, VoiceConnection } from "@discordjs/voice";
import RadioClient from "../../Client";
import { station } from "./Stations";
import { datastore } from "./Datastore";
export interface radio {
textChannel: Channel | TextBasedChannel | undefined | null,
voiceChannel: Channel | VoiceBasedChannel | undefined,
connection: VoiceConnection | null,
message: null,
station: station,
datastore?: datastore,
currentTime?: number,
startTime: number,
playTime?: number,
}
export interface state {
channels: {
"text": string | undefined,
"voice": string | undefined
},
date: string,
station: {
name: string,
owner: string
}
}
export default class Radio extends Map {
constructor() {
super();
}
save(client: RadioClient): void {
let currentRadios = this.keys();
let radio = currentRadios.next();
while (!radio.done) {
let currentRadio = this.get(radio.value);
if (currentRadio) {
currentRadio.guild = client.datastore?.getEntry(radio.value)?.guild;
client.statistics?.update(client, currentRadio.guild, currentRadio);
client.funcs.saveState(client, currentRadio.guild, currentRadio);
currentRadio.connection?.destroy();
currentRadio.message?.delete();
this.delete(radio.value);
}
radio = currentRadios.next();
}
}
restore(client: RadioClient, guilds: Collection<string, OAuth2Guild>): void {
if(!client.stations) return;
guilds.forEach(async (guild: OAuth2Guild) => {
let state = client.funcs.loadState(client, guild);
if(!state) return;
if(state.channels?.text === undefined || state.channels?.voice === undefined) return;
let voiceChannel = client.channels.cache.get(state.channels.voice);
if(!voiceChannel || !(voiceChannel instanceof VoiceChannel)) return;
if(voiceChannel.members.filter((member: GuildMember) => !member.user.bot).size === 0) return;
const sstation = client.stations?.search(state.station.name, "direct");
let station = sstation;
if(!station) return;
let date = new Date();
const construct: radio = {
textChannel: client.channels.cache.get(state.channels.text),
voiceChannel: client.channels.cache.get(state.channels.voice),
connection: null,
message: null,
station: station,
startTime: date.getTime()
};
this.set(guild.id, construct);
try {
const connection =
getVoiceConnection(guild.id) ??
joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator
});
construct.connection = connection;
let date = new Date();
construct.startTime = date.getTime();
client.datastore?.checkEntry(guild.id);
client.funcs.play(client, null, guild, station);
} catch (error) {
console.log(error);
}
});
}
};

View File

@ -1,80 +1,59 @@
const _importDynamic = new Function('modulePath', 'return import(modulePath)'); import logger from "../funcs/logger";
const fetch = (...args) => _importDynamic('node-fetch').then(({default: fetch}) => fetch(...args));
export interface station {
name: string,
owner: string,
logo: string,
stream: {
[key: string]: string
}
}
export default class Stations extends Array {
module.exports = class Stations extends Array {
constructor() { constructor() {
super(); super();
this.logger = require("../funcs/logger.js");
} }
async fetch(options){ async fetch(options: { url: string, show?: boolean}){
try { try {
this.logger('Stations', 'Started fetching list ' + options.url); logger('Stations', 'Started fetching list - ' + options.url);
let list = await fetch(options.url) let stations: station[] = await fetch(options.url)
.then(this.checkFetchStatus) .then(this.checkFetchStatus)
.then(response => response.json()); .then((response: Response) => response.json());
if(list){ for(const station of stations){
this.length = 0;
list.forEach(station => {
try {
this.push(station); this.push(station);
if(options.show) logger('Stations', station.name);
}
logger('Stations', 'Successfully fetched list');
} catch (error) { } catch (error) {
logger('Stations', 'Fetching list failed');
}
});
if(options.show){
list.forEach(station => {
this.logger('Stations', station.name);
});
}
list.forEach(async station => {
try {
let stationTest = await fetch(station.stream[station.stream.default]);
if(stationTest.ok === true) return;
this.splice(this.indexOf(station),1);
} catch (error) {
this.splice(this.indexOf(station),1);
}
});
}
this.logger('Stations', 'Successfully fetched list');
} catch (error) {
this.logger('Stations', 'Fetching list failed');
console.error(error + "\n"); console.error(error + "\n");
if(this.length == 0) this.fetch(options); if(this.length == 0) setTimeout( () => {
this.fetch(options)
}, 150 );
} }
} }
checkFetchStatus(response) { checkFetchStatus(response: Response) {
if (response.ok) { // res.status >= 200 && res.status < 300 if (response.ok) {
return response; return response;
} else { } else {
throw new Error(response.status + " " + response.statusText); throw new Error(response.status + " " + response.statusText);
} }
} }
search(key, type) { search(key: string, type: string) {
if (this === null) return false; if (this === null || !key || !type) return null;
if (!key) return false;
if (!type) return false;
if(type == "direct"){ if(type == "direct"){
let foundStation; return this.find(station => station.name === key);
this.forEach(station => {
if(station.name != key) return false;
foundStation = station;
});
return foundStation;
} else { } else {
let foundStations : { station: string, name: string, probability: number }[] = [];
let foundStations = []; if (key == "radio") return null;
if (key == "radio") return false;
this this
.filter( .filter(
@ -95,7 +74,7 @@ module.exports = class Stations extends Array {
foundStations.push({ station: x, name: x.name, probability: probabilityIncrement }) foundStations.push({ station: x, name: x.name, probability: probabilityIncrement })
); );
} }
if (foundStations.length === 0) return false; if (foundStations.length === 0) return null;
for (let i = 0; i < foundStations.length; i++) { for (let i = 0; i < foundStations.length; i++) {
for (let j = 0; j < foundStations.length; j++) { for (let j = 0; j < foundStations.length; j++) {
if (foundStations[i] === foundStations[j] && i !== j) foundStations.splice(i, 1); if (foundStations[i] === foundStations[j] && i !== j) foundStations.splice(i, 1);
@ -116,7 +95,9 @@ module.exports = class Stations extends Array {
} }
} }
} }
let highestProbabilityStation; let highestProbabilityStation : { station: string, name: string, probability: number } | undefined;
let stationName = "";
for (let i = 0; i < foundStations.length; i++) { for (let i = 0; i < foundStations.length; i++) {
if ( if (
!highestProbabilityStation || !highestProbabilityStation ||
@ -127,10 +108,10 @@ module.exports = class Stations extends Array {
highestProbabilityStation && highestProbabilityStation &&
highestProbabilityStation.probability === foundStations[i].probability highestProbabilityStation.probability === foundStations[i].probability
) { ) {
highestProbabilityStation = foundStations[i].station; stationName = foundStations[i].station;
} }
} }
return highestProbabilityStation; return stationName;
} }
} }
}; };

View File

@ -1,70 +0,0 @@
module.exports = class {
constructor() {
this.map = new Map();
}
update(client, guild, radio) {
client.datastore.checkEntry(guild.id);
radio.datastore = client.datastore.getEntry(guild.id);
if(!radio.datastore.statistics[radio.station.name]){
radio.datastore.statistics[radio.station.name] = {};
radio.datastore.statistics[radio.station.name].time = 0;
radio.datastore.statistics[radio.station.name].used = 0;
client.datastore.updateEntry(guild, radio.datastore);
}
let date = new Date();
radio.currentTime = date.getTime();
radio.playTime = parseInt(radio.currentTime)-parseInt(radio.startTime);
radio.datastore.statistics[radio.station.name].time = parseInt(radio.datastore.statistics[radio.station.name].time)+parseInt(radio.playTime);
radio.datastore.statistics[radio.station.name].used = parseInt(radio.datastore.statistics[radio.station.name].used)+1;
client.datastore.updateEntry(guild, radio.datastore);
this.calculateGlobal(client);
}
calculateGlobal(client){
if(!client.stations) return;
if(!client.datastore.map) return;
let guilds = client.datastore.map.keys();
let stations = client.stations;
let statistics = {};
if(!client.stations) return;
let calculation = guilds.next();
while (!calculation.done) {
let currentGuild = client.datastore.getEntry(calculation.value);
if(calculation.value != 'global'){
if(stations){
Object.keys(stations).forEach(function(station) {
if(currentGuild.statistics[stations[station].name] && currentGuild.statistics[stations[station].name].time && parseInt(currentGuild.statistics[stations[station].name].time) != 0 && currentGuild.statistics[stations[station].name].used && parseInt(currentGuild.statistics[stations[station].name].used) != 0){
if(!statistics[stations[station].name]){
statistics[stations[station].name] = {};
statistics[stations[station].name].time = 0;
statistics[stations[station].name].used = 0;
}
statistics[stations[station].name].time = parseInt(statistics[stations[station].name].time)+parseInt(currentGuild.statistics[stations[station].name].time);
statistics[stations[station].name].used = parseInt(statistics[stations[station].name].used)+parseInt(currentGuild.statistics[stations[station].name].used);
}
});
}
}
calculation = guilds.next();
}
let newData = {};
newData.guild = {};
newData.guild.id = "global";
newData.guild.name = "global";
newData.statistics = statistics;
client.datastore.updateEntry(newData.guild, newData);
}
};

View File

@ -0,0 +1,95 @@
import { Guild } from "discord.js";
import RadioClient from "../../Client";
import { radio } from "./Radio";
export interface statistics {
[key: string]: statistic
}
interface statistic {
"time": number,
"used": number
}
export default class Statistics {
map: Map<string, statistics>;
constructor() {
this.map = new Map();
}
update(client: RadioClient, guild: Guild | null, radio: radio) {
if(!guild) return;
client.datastore?.checkEntry(guild.id);
radio.datastore = client.datastore?.getEntry(guild.id);
if(radio.datastore === undefined) return;
if(!radio.datastore.statistics[radio.station.name]){
radio.datastore.statistics[radio.station.name] = {
time: 0,
used: 0
};
client.datastore?.updateEntry(guild, radio.datastore);
}
let date = new Date();
radio.currentTime = date.getTime();
radio.playTime = radio.currentTime - radio.startTime;
radio.datastore.statistics[radio.station.name] = {
time: radio.datastore.statistics[radio.station.name].time + radio.playTime,
used: radio.datastore.statistics[radio.station.name].used + 1
}
client.datastore?.updateEntry(guild, radio.datastore);
this.calculateGlobal(client);
}
calculateGlobal(client: RadioClient){
if(!client.datastore?.map) return;
let guilds = client.datastore.map.keys();
let statistics : statistics = {};
if(!client.stations) return;
let calculation = guilds.next();
while (!calculation.done) {
let currentGuild = client.datastore.getEntry(calculation.value);
if(calculation.value != 'global'){
if(client.stations){
for(const station of client.stations) {
if(!currentGuild) return;
if(currentGuild.statistics[station.name] && currentGuild.statistics[station.name]?.time && currentGuild.statistics[station.name].time != 0 && currentGuild.statistics[station.name].used && currentGuild.statistics[station.name].used != 0){
if(!statistics[station.name]){
statistics[station.name] = {
time: 0,
used: 0
};
}
statistics[station.name] = {
time: statistics[station.name].time + currentGuild.statistics[station.name].time,
used: statistics[station.name].used + currentGuild.statistics[station.name].used
}
}
}
}
}
calculation = guilds.next();
}
let newData = {
guild: {
id: "global",
name: "global"
},
statistics: statistics,
state: null
};
client.datastore.updateEntry(newData.guild, newData);
}
};

View File

@ -1,120 +0,0 @@
const {
createAudioPlayer,
createAudioResource,
AudioPlayerStatus,
NoSubscriberBehavior
} = require("@discordjs/voice");
module.exports = class {
constructor() {
this.map = new Map();
this.mode = null;
this.logger = require("../funcs/logger.js");
}
init(client){
if(!client.config.streamerMode) return;
switch(client.config.streamerMode){
case "manual":
this.mode = "manual";
break;
case "auto":
this.mode = "auto";
break;
default:
this.mode = "manual";
}
if(this.mode == "auto"){
if(!client.stations) return;
client.stations.forEach(station => {
this.play(station);
});
}
}
refresh(client){
this.init(client);
let streamers = this.map.keys();
streamers.forEach(streamer => {
if(client.stations.findIndex(station => station.name == streamer) == -1){
this.stop(streamer);
}
});
}
play(station) {
let audioPlayer = this.map.get(station.name);
if(!audioPlayer) {
if(this.mode == "auto"){
audioPlayer = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
maxMissedFrames: Math.round(5000 / 20),
},
});
}
if(this.mode == "manual"){
audioPlayer = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Stop,
maxMissedFrames: Math.round(5000 / 20),
},
});
}
this.map.set(station.name, audioPlayer);
}
const url = station.stream[station.stream.default];
const resource = createAudioResource(url);
audioPlayer.play(resource);
audioPlayer
.on('playing', () => {
this.logger('Streamer', station.name + " / " + "Playing");
})
.on('idle', () => {
this.logger('Streamer', station.name + " / " + "Idle");
audioPlayer.removeAllListeners();
if(this.mode == "manual" && audioPlayer.subscribers.length == 0) return;
this.play(station);
})
.on('paused', () => {
this.logger('Streamer', station.name + " / " + "Paused");
})
.on('buffering', () => {
this.logger('Streamer', station.name + " / " + "Buffering");
})
.on('autopaused', () => {
this.logger('Streamer', station.name + " / " + "AutoPaused");
})
.on('error', error => {
this.logger('Streamer', station.name + " / " + "Error" + "\n" + error);
});
return audioPlayer;
}
stop(station){
let audioPlayer = this.map.get(station.name);
if(audioPlayer){
this.logger('Streamer', station.name + " / " + "Stop");
audioPlayer.removeAllListeners();
audioPlayer.stop();
}
this.map.delete(station.name);
}
listen(station) {
let audioPlayer = this.map.get(station.name);
if(!audioPlayer || this.mode == "manual" && audioPlayer.subscribers.length == 0) audioPlayer = this.play(station);
return audioPlayer;
}
leave(client) {
if(!client.stations) return;
client.stations.forEach(station => {
this.stop(station);
});
}
};

View File

@ -0,0 +1,116 @@
import logger from "../funcs/logger";
import { AudioPlayer, AudioPlayerStatus, createAudioPlayer, createAudioResource, NoSubscriberBehavior } from "@discordjs/voice";
import RadioClient from "../../Client";
import { station } from "./Stations";
export default class Streamer {
map: Map<string, AudioPlayer>;
mode: "auto" | "manual";
constructor() {
this.map = new Map();
this.mode = "manual";
}
init(client: RadioClient){
if(!client.config.streamerMode) return;
switch(client.config.streamerMode){
case "manual":
this.mode = "manual";
break;
case "auto":
this.mode = "auto";
break;
default:
this.mode = "manual";
}
if(this.mode == "auto"){
if(!client.stations) return;
for(const station of client.stations){
this.play(station);
}
}
}
refresh(client: RadioClient){
this.init(client);
for (const streamer of this.map.keys()){
if(client.stations?.findIndex((station: station) => station.name == streamer) == -1){
this.stop(streamer);
}
}
}
play(station: station) {
let audioPlayer = this.map.get(station.name);
if(!audioPlayer) {
if(this.mode == "auto"){
audioPlayer = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
maxMissedFrames: Math.round(5000 / 20),
}
});
} else {
audioPlayer = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Stop,
maxMissedFrames: Math.round(5000 / 20),
}
});
}
audioPlayer
.on(AudioPlayerStatus.Playing, () => {
logger('Streamer', station.name + " / " + "Playing");
})
.on(AudioPlayerStatus.Idle, () => {
logger('Streamer', station.name + " / " + "Idle");
})
.on(AudioPlayerStatus.Paused, () => {
logger('Streamer', station.name + " / " + "Paused");
})
.on(AudioPlayerStatus.Buffering, () => {
logger('Streamer', station.name + " / " + "Buffering");
})
.on(AudioPlayerStatus.AutoPaused, () => {
logger('Streamer', station.name + " / " + "AutoPaused");
})
this.map.set(station.name, audioPlayer);
}
const url = station.stream[station.stream.default];
const resource = createAudioResource(url);
audioPlayer.play(resource);
return audioPlayer;
}
stop(streamer: string){
let audioPlayer = this.map.get(streamer);
if(audioPlayer){
logger('Streamer', streamer + " / " + "Stop");
audioPlayer.removeAllListeners();
audioPlayer.stop();
}
this.map.delete(streamer);
}
listen(station: station) {
let audioPlayer = this.map.get(station.name);
if(!audioPlayer) audioPlayer = this.play(station);
return audioPlayer;
}
leave(client: RadioClient) {
if(!client.stations) return;
for(const station of client.stations){
this.stop(station.name);
}
}
};

View File

@ -1,83 +0,0 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9');
const fs = require('fs');
const path = require ('path');
module.exports = {
async execute(client) {
const commands = [];
const commandFiles = fs.readdirSync(path.join("./src/client/commands")).filter(f => f.endsWith(".js"));
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
client.commands.set(command.name, command);
command.data = new SlashCommandBuilder()
.setName(command.name)
.setDescription(command.description);
command.data = command.data.toJSON();
if(command.options) {
command.options.forEach(function(option) {
if(option.type == "STRING") option.type = 3;
if(option.type == "NUMBER") option.type = 10;
command.data.options.push(option);
});
}
commands.push(command.data);
}
const rest = new REST({ version: '9' }).setToken(client.config.token);
(async () => {
try {
client.funcs.logger('Slash Commands', 'Started refreshing application (/) commands.');
if(client.config.devMode){
await rest.put(
Routes.applicationCommands(client.user.id),
{ body: [] },
);
let guilds = await client.guilds.fetch();
guilds.forEach(async guild => {
try {
await rest.put(
Routes.applicationGuildCommands(client.user.id, guild.id),
{ body: commands }
);
client.funcs.logger('Slash Commands', 'Guild Applications Successful' + "\n" + guild.id + " / " + guild.name);
} catch (DiscordAPIError) {
client.funcs.logger('Slash Commands', 'Guild Applications Failed' + "\n" + guild.id + " / " + guild.name);
if(DiscordAPIError.name != "DiscordAPIError[50001]") console.error(DiscordAPIError.message + "\n\n");
}
});
} else {
await rest.put(
Routes.applicationCommands(client.user.id),
{ body: commands }
);
let guilds = await client.guilds.fetch();
guilds.forEach(async guild => {
try {
await rest.put(
Routes.applicationGuildCommands(client.user.id, guild.id),
{ body: [] }
);
} catch (DiscordAPIError) {
}
});
}
client.funcs.logger('Slash Commands', 'Successfully reloaded application (/) commands.' + "\n");
} catch (error) {
client.funcs.logger('Slash Commands', 'Reloading application (/) commands failed.' + "\n");
console.error(error);
}
})();
}
}

72
src/client/commands.ts Normal file
View File

@ -0,0 +1,72 @@
import { Snowflake } from "discord.js";
import RadioClient from "../Client";
import bug from "./commands/bug";
import help from "./commands/help";
import invite from "./commands/invite";
import list from "./commands/list";
import maintenance from "./commands/maintenance";
import next from "./commands/next";
import nowplaying from "./commands/nowplaying";
import play from "./commands/play";
import prev from "./commands/prev";
import statistics from "./commands/statistics";
import status from "./commands/status";
import stop from "./commands/stop";
export interface command {
name: string,
description: string,
category: string,
options?: [],
execute: Function
}
export default async function commands(client: RadioClient) {
const commands : command[] = [ bug, help, invite, list, maintenance, next, nowplaying, play, prev, statistics, status, stop ];
for(const command of commands){
client.commands.set(command.name, command);
}
if(!client.application) return;
client.funcs.logger('Application Commands', 'Started refreshing application (/) commands.');
if(client.config.devMode){
client.application.commands.set([]);
for(const command of commands){
let guilds = await client.guilds.fetch();
guilds.forEach(async (guild: { id: Snowflake; name: string; }) => {
try {
if(!client.application) return;
await client.application.commands.create({
name: command.name,
description: command.description,
options: command.options || []
}, guild.id);
client.funcs.logger('Application Commands', 'Guild: ' + guild.id + " (" + guild.name + ") \n" + 'Command: ' + command.name);
} catch(DiscordAPIError) {
client.funcs.logger('Application Commands', 'Guild: ' + guild.id + " (" + guild.name + ") [FAILED] \n" + 'Command: ' + command.name);
}
});
}
} else {
for(const command of commands){
await client.application.commands.create({
name: command.name,
description: command.description,
options: command.options || []
});
client.funcs.logger('Application Commands', 'Command: ' + command.name);
}
let guilds = await client.guilds.fetch();
guilds.forEach(async (guild: { id: Snowflake; }) => {
try {
if(!client.application) return;
await client.application.commands.set([], guild.id);
} catch (DiscordAPIError){
}
});
}
client.funcs.logger('Application Commands', 'Successfully reloaded application (/) commands.' + "\n");
}

View File

@ -1,29 +0,0 @@
import { EmbedBuilder } from "discord.js";
module.exports = {
name: 'bug',
description: 'Report a bug',
category: 'info',
async execute(interaction, client) {
let message = {};
message.bugTitle = client.messages.bugTitle.replace("%client.user.username%", client.user.username);
message.bugDescription = client.messages.bugDescription.replace("%client.config.supportGuild%", client.config.supportGuild);
const embed = new EmbedBuilder()
.setTitle(message.bugTitle)
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messageEmojis["logo"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor)
.setDescription(message.bugDescription)
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '')
});
interaction.reply({
embeds: [embed],
ephemeral: true
});
}
};

View File

@ -0,0 +1,34 @@
import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
import RadioClient from "../../Client";
export default {
name: 'bug',
description: 'Report a bug',
category: 'info',
async execute(interaction: ChatInputCommandInteraction, client: RadioClient) {
if(!client.user) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
ephemeral: true
});
const embed = new EmbedBuilder()
.setTitle(client.messages.replace(client.messages.bugTitle, {
"%client.user.username%": client.user.username
}))
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messages.emojis["logo"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor)
.setDescription(client.messages.replace(client.messages.bugDescription, {
"%client.config.supportGuild%": client.config.supportGuild
}))
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
});
interaction.reply({
embeds: [embed],
ephemeral: true
});
}
};

View File

@ -1,38 +0,0 @@
import { EmbedBuilder } from "discord.js";
module.exports = {
name: 'help',
description: 'Get help using bot',
category: 'info',
execute(interaction, client) {
let message = {};
const categories = [];
for (let i = 0; i < client.commands.size; i++) {
if (!categories.includes([...client.commands.values()][i].category)) categories.push([...client.commands.values()][i].category);
}
let commands = '';
for (let i = 0; i < categories.length; i++) {
commands += `**» ${categories[i].toUpperCase()}**\n${client.commands.filter(x => x.category === categories[i] && !x.omitFromHelp).map(x => `\`${x.name}\``).join(', ')}\n`;
}
message.helpTitle = client.messages.helpTitle.replace("%client.user.username%", client.user.username);
message.helpDescription = client.messages.helpDescription.replace("%commands%", commands);
const embed = new EmbedBuilder()
.setTitle(message.helpTitle)
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messageEmojis["logo"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor)
.setDescription(message.helpDescription)
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '')
});
interaction.reply({
embeds: [embed],
ephemeral: true
});
}
};

View File

@ -0,0 +1,45 @@
import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
import RadioClient from "../../Client";
import { command } from "../commands";
export default {
name: 'help',
description: 'Get help using bot',
category: 'info',
execute(interaction: ChatInputCommandInteraction, client: RadioClient) {
if(!client.user) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
ephemeral: true
});
const categories: string[] = [];
for (let i = 0; i < client.commands.size; i++) {
if (!categories.includes([...client.commands.values()][i].category)) categories.push([...client.commands.values()][i].category);
}
let commands = '';
for (let i = 0; i < categories.length; i++) {
commands += `**» ${categories[i].toUpperCase()}**\n${client.commands.filter(x => x.category === categories[i]).map((x: command) => `\`${x.name}\``).join(', ')}\n`;
}
const embed = new EmbedBuilder()
.setTitle(client.messages.replace(client.messages.helpTitle, {
"%client.user.username%": client.user.username
}))
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messages.emojis["logo"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor)
.setDescription(client.messages.replace(client.messages.helpDescription, {
"%commands%": commands
}))
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
});
interaction.reply({
embeds: [embed],
ephemeral: true
});
}
};

View File

@ -1,20 +1,27 @@
import { EmbedBuilder } from "discord.js"; import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
import RadioClient from "../../Client";
module.exports = { export default {
name: 'invite', name: 'invite',
description: 'Invite Bot', description: 'Invite Bot',
category: 'info', category: 'info',
execute(interaction, client) { execute(interaction: ChatInputCommandInteraction, client: RadioClient) {
let message = {};
message.inviteTitle = client.messages.inviteTitle.replace("%client.user.username%", client.user.username); if(!client.user) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
ephemeral: true
});
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(message.inviteTitle) .setTitle(client.messages.replace(client.messages.inviteTitle, {
"%client.user.username%": client.user.username
}))
.setColor(client.config.embedColor) .setColor(client.config.embedColor)
.setURL("https://discord.com/api/oauth2/authorize?client_id=" + client.user.id + "&permissions=2184465408&scope=applications.commands%20bot") //View Channels, Send Messages, Embed Links, Use External Emojis, Use Slash Commands, Connect, Speak, Use Voice Activity .setURL("https://discord.com/api/oauth2/authorize?client_id=" + client.user.id + "&permissions=2184465408&scope=applications.commands%20bot") //View Channels, Send Messages, Embed Links, Use External Emojis, Use Slash Commands, Connect, Speak, Use Voice Activity
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png') .setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({ .setFooter({
text: client.messages.footerText, text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '') iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
}); });
interaction.reply({ interaction.reply({

View File

@ -1,26 +1,35 @@
import { EmbedBuilder } from "discord.js"; import { ButtonInteraction, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { station } from "../classes/Stations";
module.exports = { export default {
name: 'list', name: 'list',
description: 'List radio stations', description: 'List radio stations',
category: 'radio', category: 'radio',
execute(interaction, client) { execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient) {
let message = {};
if(!client.stations) { if(client.config.maintenanceMode){
message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace("%client.config.supportGuild%", client.config.supportGuild);
return interaction.reply({ return interaction.reply({
content: client.messageEmojis["error"] + message.errorToGetPlaylist, content: client.messages.emojis["error"] + client.messages.maintenance,
ephemeral: true ephemeral: true
}); });
} }
const radio = client.radio.get(interaction.guild.id); if(!client.stations) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
ephemeral: true
});
}
const radio = client.radio?.get(interaction.guild?.id);
if(radio && !client.config.maintenanceMode){ if(radio && !client.config.maintenanceMode){
client.funcs.listStations(client, interaction); client.funcs.listStations(client, interaction);
} else { } else {
let stations = `${client.stations.map(s => `**#** ${s.name}`).join('\n')}` let stations = `${client.stations.map((s: station) => `**#** ${s.name}`).join('\n')}`
const hashs = stations.split('**#**').length; const hashs = stations.split('**#**').length;
for (let i = 0; i < hashs; i++) { for (let i = 0; i < hashs; i++) {
stations = stations.replace('**#**', `**${i + 1}.**`); stations = stations.replace('**#**', `**${i + 1}.**`);
@ -28,13 +37,13 @@ module.exports = {
let embed = new EmbedBuilder() let embed = new EmbedBuilder()
.setTitle(client.messages.listTitle) .setTitle(client.messages.listTitle)
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messageEmojis["list"].replace(/[^0-9]+/g, '')) .setThumbnail("https://cdn.discordapp.com/emojis/" + client.messages.emojis["list"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor) .setColor(client.config.embedColor)
.setDescription(stations) .setDescription(stations)
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png') .setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({ .setFooter({
text: client.messages.footerText, text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '') iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
}); });
interaction.reply({ interaction.reply({

View File

@ -1,200 +0,0 @@
import { ActionRowBuilder, EmbedBuilder, StringSelectMenuBuilder } from "discord.js";
import Streamer from "../classes/Streamer.js";
const _importDynamic = new Function('modulePath', 'return import(modulePath)');
const fetch = (...args) => _importDynamic('node-fetch').then(({default: fetch}) => fetch(...args));
module.exports = {
name: 'maintenance',
description: 'Bot Maintenance',
category: 'info',
async execute(interaction, client) {
let message = {};
if(!client.funcs.isDev(client.config.devId, interaction.user.id)) return interaction.reply({
content: client.messageEmojis["error"] + client.messages.notAllowed,
ephemeral: true
});
let action = interaction.options?.getNumber("action") ?? interaction.values?.[0];
const options = new Array(
{
emoji: "🌀",
label: "Restart Bot",
value: "0"
},
{
emoji: "<:RadioXStop:688541155377414168>",
label: "Save Radios",
value: "4"
},
{
emoji: "<:RadioXPlay:688541155712827458>",
label: "Restore Radios",
value: "5"
},
{
emoji: "#️⃣",
label: "Reload Commands",
value: "6"
},
{
emoji: "<:RadioXList:688541155519889482>",
label: "Reload Stations",
value: "7"
},
{
emoji: "<:dnd:746069698139127831>",
label: "Enable Maintenance Mode",
value: "8"
},
{
emoji: "<:online:746069731836035098>",
label: "Disable Maintenance Mode",
value: "9"
},
{
emoji: "💤",
label: "Streamer Mode Manual",
value: "10"
},
{
emoji: "📡",
label: "Streamer Mode Auto",
value: "11"
}
);
const menu = new ActionRowBuilder()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('maintenance')
.setPlaceholder('Select action')
.addOptions(options)
);
if(!action){
return interaction.reply({
content: "**" + client.messages.maintenanceTitle + "**",
components: [menu],
ephemeral: true
});
}
client.funcs.logger('Maintenance', options.find(option => option.value == action).label);
const embed = new EmbedBuilder()
.setTitle(client.messages.maintenanceTitle)
.setColor(client.config.embedColor)
.setDescription(options.find(option => option.value == action).label)
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '')
});
interaction.reply({
embeds: [embed],
ephemeral: true
});
let guilds = await client.guilds.fetch();
switch(action){
case "0":
client.config.maintenanceMode = true;
process.emit('SIGINT');
break;
case "4":
client.config.maintenanceMode = true;
client.user.setStatus('idle');
client.radio.save(client);
client.user.setStatus('online');
client.config.maintenanceMode = false;
break;
case "5":
client.config.maintenanceMode = true;
client.user.setStatus('idle');
client.radio.restore(client, guilds);
client.user.setStatus('online');
client.config.maintenanceMode = false;
break;
case "6":
client.config.maintenanceMode = true;
client.user.setStatus('idle');
require(`../commands.js`).execute(client);
client.user.setStatus('online');
client.config.maintenanceMode = false;
break;
case "7":
try {
client.stations.fetch({
url: client.config.stationslistUrl
});
client.streamer.refresh(client);
} catch (error) {
}
break;
case "8":
client.user.setStatus('dnd');
client.funcs.logger("Maintenance Mode", "Enabled");
client.config.maintenanceMode = true;
break;
case "9":
client.user.setStatus('online');
client.funcs.logger("Maintenance Mode", "Disabled");
client.config.maintenanceMode = false;
break;
case "10":
client.config.streamerMode = "manual";
client.config.maintenanceMode = true;
client.user.setStatus('idle');
client.radio.save(client);
setInterval(() => {
if(client.radio.size == 0 && client.config.streamerMode == "manual" && client.config.maintenanceMode){
client.streamer.leave(client);
client.streamer = new Streamer();
client.streamer.init(client);
client.radio.restore(client, guilds);
client.user.setStatus('online');
client.config.maintenanceMode = false;
}
if(!client.config.maintenanceMode){
clearInterval();
}
}, 500);
break;
case "11":
client.config.streamerMode = "auto";
client.config.maintenanceMode = true;
client.user.setStatus('idle');
client.radio.save(client);
setInterval(() => {
if(client.radio.size == 0 && client.config.streamerMode == "auto" && client.config.maintenanceMode){
client.streamer.leave(client);
client.streamer = new Streamer();
client.streamer.init(client);
client.radio.restore(client, guilds);
client.user.setStatus('online');
client.config.maintenanceMode = false;
}
if(!client.config.maintenanceMode){
clearInterval();
}
}, 500);
break;
default:
}
}
};

View File

@ -0,0 +1,232 @@
import { ActionRowBuilder, APISelectMenuOption, ButtonInteraction, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import Streamer from "../classes/Streamer";
import commands from "../commands";
export default {
name: 'maintenance',
description: 'Bot Maintenance',
category: 'info',
async execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient) {
if(!client.funcs.isDev(client.config.devIDs, interaction.user.id)) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.notAllowed,
ephemeral: true
});
let action : number | string | null = null;
if(interaction.isChatInputCommand()){
action = interaction.options?.getNumber("action");
}
if(interaction.isStringSelectMenu()){
action = interaction.values?.[0];
}
const options: APISelectMenuOption[] = new Array(
{
emoji: {
"name": "🌀",
},
label: "Restart Bot",
value: "0"
},
{
emoji: {
id: "688541155377414168",
name: "RadioXStop",
},
label: "Save Radios",
value: "4"
},
{
emoji: {
id: "688541155712827458",
name: "RadioXPlay",
},
label: "Restore Radios",
value: "5"
},
{
emoji: {
name: "#️⃣",
},
label: "Reload Commands",
value: "6"
},
{
emoji: {
id: "688541155519889482",
name: "RadioXList",
},
label: "Reload Stations",
value: "7"
},
{
emoji: {
id: "746069698139127831",
name: "dnd",
},
label: "Enable Maintenance Mode",
value: "8"
},
{
emoji: {
id: "746069731836035098",
name: "online",
},
label: "Disable Maintenance Mode",
value: "9"
},
{
emoji: {
name: "💤",
},
label: "Streamer Mode - Manual",
value: "10"
},
{
emoji: {
name: "📡",
},
label: "Streamer Mode - Auto",
value: "11"
}
);
const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('maintenance')
.setPlaceholder('Select action')
.addOptions(options)
);
if(!action){
return interaction.reply({
content: "**" + client.messages.maintenanceTitle + "**",
components: [menu],
ephemeral: true
});
}
client.funcs.logger('Maintenance', options.find((option: APISelectMenuOption) => option.value == action)?.label);
const embed = new EmbedBuilder()
.setTitle(client.messages.maintenanceTitle)
.setColor(client.config.embedColor)
.setDescription(options.find((option: APISelectMenuOption) => option.value == action)?.label || "-")
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
});
interaction.reply({
embeds: [embed],
ephemeral: true
});
let guilds = await client.guilds.fetch();
switch(action){
case "0":
client.config.maintenanceMode = true;
process.emit('SIGINT');
break;
case "4":
client.config.maintenanceMode = true;
client.user?.setStatus('idle');
client.radio?.save(client);
client.user?.setStatus('online');
client.config.maintenanceMode = false;
break;
case "5":
client.config.maintenanceMode = true;
client.user?.setStatus('idle');
client.radio?.restore(client, guilds);
client.user?.setStatus('online');
client.config.maintenanceMode = false;
break;
case "6":
client.config.maintenanceMode = true;
client.user?.setStatus('idle');
commands(client);
client.user?.setStatus('online');
client.config.maintenanceMode = false;
break;
case "7":
try {
client.stations?.fetch({
url: client.config.stationslistUrl
});
client.streamer?.refresh(client);
} catch (error) {
}
break;
case "8":
client.user?.setStatus('dnd');
client.funcs.logger("Maintenance Mode", "Enabled");
client.config.maintenanceMode = true;
break;
case "9":
client.user?.setStatus('online');
client.funcs.logger("Maintenance Mode", "Disabled");
client.config.maintenanceMode = false;
break;
case "10":
client.config.streamerMode = "manual";
client.config.maintenanceMode = true;
client.user?.setStatus('idle');
client.radio?.save(client);
setInterval(() => {
if(client.radio?.size == 0 && client.config.streamerMode == "manual" && client.config.maintenanceMode){
client.streamer?.leave(client);
client.streamer = new Streamer();
client.streamer.init(client);
client.radio?.restore(client, guilds);
client.user?.setStatus('online');
client.config.maintenanceMode = false;
}
if(!client.config.maintenanceMode){
clearInterval(undefined);
}
}, 500);
break;
case "11":
client.config.streamerMode = "auto";
client.config.maintenanceMode = true;
client.user?.setStatus('idle');
client.radio?.save(client);
setInterval(() => {
if(client.radio?.size == 0 && client.config.streamerMode == "auto" && client.config.maintenanceMode){
client.streamer?.leave(client);
client.streamer = new Streamer();
client.streamer.init(client);
client.radio.restore(client, guilds);
client.user?.setStatus('online');
client.config.maintenanceMode = false;
}
if(!client.config.maintenanceMode){
clearInterval(undefined);
}
}, 500);
break;
default:
}
}
};

View File

@ -1,35 +0,0 @@
module.exports = {
name: 'next',
description: 'Next Station',
category: 'radio',
async execute(interaction, client, command) {
if (client.funcs.check(client, interaction, command)) {
const radio = client.radio.get(interaction.guild.id);
let index = client.stations.findIndex(station => station.name == radio.station.name) + 1;
if(index == client.stations.length) index = 0;
let station = client.stations[index];
if(!station) return interaction.reply({
content: client.messageEmojis["error"] + client.messages.noSearchResults,
ephemeral: true
});
client.statistics.update(client, interaction.guild, radio);
let date = new Date();
radio.station = station;
radio.textChannel = interaction.channel;
radio.startTime = date.getTime();
if(interaction.isChatInputCommand()) {
client.funcs.play(client, interaction, interaction.guild, station);
}
if(interaction.isButton()) {
interaction.deferUpdate();
client.funcs.play(client, null, interaction.guild, station);
}
}
}
}

View File

@ -0,0 +1,56 @@
import { ButtonInteraction, ChatInputCommandInteraction, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { station } from "../classes/Stations"
import { command } from "../commands";
export default {
name: 'next',
description: 'Next Station',
category: 'radio',
async execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient, command: command) {
if (client.funcs.check(client, interaction, command)) {
const radio = client.radio?.get(interaction.guild?.id);
if(client.config.maintenanceMode){
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
ephemeral: true
});
}
if(!client.stations) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
ephemeral: true
});
}
let index: number = client.stations.findIndex((station: station) => station.name == radio.station.name) + 1;
if(index == client.stations?.length) index = 0;
let station = client.stations[index];
if(!station) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noSearchResults,
ephemeral: true
});
client.statistics?.update(client, interaction.guild, radio);
let date = new Date();
radio.station = station;
radio.textChannel = interaction.channel;
radio.startTime = date.getTime();
if(interaction.isChatInputCommand()) {
client.funcs.play(client, interaction, interaction.guild, station);
}
if(interaction.isButton()) {
interaction.deferUpdate();
client.funcs.play(client, null, interaction.guild, station);
}
}
}
}

View File

@ -1,38 +0,0 @@
import { EmbedBuilder } from "discord.js";
module.exports = {
name: 'nowplaying',
description: 'Current Radio Station',
category: 'radio',
async execute(interaction, client, command) {
if (client.funcs.check(client, interaction, command)) {
let message = {};
const radio = client.radio.get(interaction.guild.id);
let date = new Date();
radio.currentTime = date.getTime();
radio.playTime = parseInt(radio.currentTime)-parseInt(radio.startTime);
const completed = (radio.playTime);
message.nowplayingDescription = client.messages.nowplayingDescription.replace("%radio.station.name%", radio.station.name);
message.nowplayingDescription = message.nowplayingDescription.replace("%radio.station.owner%" + "\n", radio.station.name != radio.station.owner ? radio.station.owner + "\n" : "");
message.nowplayingDescription = message.nowplayingDescription.replace("%client.funcs.msToTime(completed)%", client.funcs.msToTime(completed));
const embed = new EmbedBuilder()
.setTitle(client.messages.nowplayingTitle)
.setThumbnail((radio.station.logo || "https://cdn.discordapp.com/emojis/" + client.messageEmojis["play"].replace(/[^0-9]+/g, '')))
.setColor(client.config.embedColor)
.setDescription(message.nowplayingDescription)
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '')
});
interaction.reply({
embeds: [embed],
ephemeral: true
});
}
}
};

View File

@ -0,0 +1,40 @@
import { ButtonInteraction, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { command } from "../commands";
export default {
name: 'nowplaying',
description: 'Current Radio Station',
category: 'radio',
async execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient, command: command) {
if(client.funcs.check(client, interaction, command)) {
const radio = client.radio?.get(interaction.guild?.id);
let date = new Date();
radio.currentTime = date.getTime();
radio.playTime = parseInt(radio.currentTime)-parseInt(radio.startTime);
const completed = (radio.playTime);
const embed = new EmbedBuilder()
.setTitle(client.messages.nowplayingTitle)
.setThumbnail((radio.station.logo || "https://cdn.discordapp.com/emojis/" + client.messages.emojis["play"].replace(/[^0-9]+/g, '')))
.setColor(client.config.embedColor)
.setDescription(client.messages.replace(client.messages.nowplayingDescription, {
"%radio.station.name%": radio.station.name,
"%radio.station.owner%\n": radio.station.name != radio.station.owner ? radio.station.owner + "\n" : "",
"%client.funcs.msToTime(completed)%": client.funcs.msToTime(completed)
}))
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
});
interaction.reply({
embeds: [embed],
ephemeral: true
});
}
}
};

View File

@ -1,148 +0,0 @@
import { PermissionFlagsBits } from "discord.js";
const {
getVoiceConnection,
joinVoiceChannel
} = require("@discordjs/voice");
module.exports = {
name: "play",
usage: "<song name>",
description: "Play radio",
options: [
{ type: "STRING", name: "query", description: "Select station", required: false}
],
category: "radio",
async execute(interaction, client) {
let message = {};
if(client.config.maintenanceMode){
return interaction.reply({
content: client.messageEmojis["error"] + client.messages.maintenance,
ephemeral: true
});
}
if(!client.stations) {
message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace("%client.config.supportGuild%", client.config.supportGuild);
return interaction.reply({
content: client.messageEmojis["error"] + message.errorToGetPlaylist,
ephemeral: true
});
}
let query = interaction.options?.getString("query") ?? interaction.values?.[0];
if(!query){
return client.funcs.listStations(client, interaction);
}
let url = query ? query.replace(/<(.+)>/g, "$1") : "";
const radio = client.radio.get(interaction.guild.id);
const voiceChannel = interaction.member.voice.channel;
if (!voiceChannel) return interaction.reply({
content: client.messageEmojis["error"] + client.messages.noVoiceChannel,
ephemeral: true
});
if (radio) {
if (voiceChannel !== radio.voiceChannel) return interaction.reply({
content: client.messageEmojis["error"] + client.messages.wrongVoiceChannel,
ephemeral: true
});
}
if (!query) return interaction.reply({
content: client.messages.noQuery,
ephemeral: true
});
const permissions = voiceChannel.permissionsFor(interaction.client.user);
if (!permissions.has(PermissionFlagsBits.Connect)) {
return interaction.reply({
content: client.messageEmojis["error"] + client.messages.noPermsConnect,
ephemeral: true
});
}
if (!permissions.has(PermissionFlagsBits.Speak)) {
return interaction.reply({
content: client.messageEmojis["error"] + client.messages.noPermsSpeak,
ephemeral: true
});
}
let station;
const number = parseInt(query - 1);
if (url.startsWith("http")) {
return interaction.reply({
content: client.messageEmojis["error"] + client.messages.errorStationURL,
ephemeral: true
});
} else if (!isNaN(number)) {
if (number > client.stations.length - 1) {
return interaction.reply({
content: client.messageEmojis["error"] + client.messages.wrongStationNumber,
ephemeral: true
});
} else {
station = client.stations[number];
}
} else {
if (query.length < 3) return interaction.reply({
content: client.messageEmojis["error"] + client.messages.tooShortSearch,
ephemeral: true
});
let type = "";
if(interaction.values?.[0]){
type = "direct";
} else {
type = "text";
}
const sstation = await client.stations.search(query, type);
if (!sstation) return interaction.reply({
content: client.messageEmojis["error"] + client.messages.noSearchResults,
ephemeral: true
});
station = sstation;
}
if (radio) {
client.statistics.update(client, interaction.guild, radio);
let date = new Date();
radio.station = station;
radio.textChannel = interaction.channel;
radio.startTime = date.getTime();
client.funcs.play(client, interaction, interaction.guild, station);
return;
}
const construct = {
textChannel: interaction.channel,
voiceChannel: voiceChannel,
connection: null,
message: null,
station: station
};
client.radio.set(interaction.guild.id, construct);
try {
const connection =
getVoiceConnection(voiceChannel.guild.id) ??
joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator
});
construct.connection = connection;
let date = new Date();
construct.startTime = date.getTime();
client.datastore.checkEntry(interaction.guild.id);
client.funcs.play(client, interaction, interaction.guild, station);
} catch (error) {
console.log(error);
client.radio.delete(interaction.guild.id);
return interaction.reply({
content: client.messageEmojis["error"] + `An error occured: ${error}`,
ephemeral: true
});
}
}
};

159
src/client/commands/play.ts Normal file
View File

@ -0,0 +1,159 @@
import { ApplicationCommandOptionType, ChatInputCommandInteraction, GuildMember, PermissionFlagsBits, StringSelectMenuInteraction } from "discord.js";
import { getVoiceConnection, joinVoiceChannel } from "@discordjs/voice";
import RadioClient from "../../Client";
import { radio } from "../classes/Radio"
export default {
name: "play",
usage: "<song name>",
description: "Play radio",
options: [
{ type: ApplicationCommandOptionType.String, name: "query", description: "Select station", required: false}
],
category: "radio",
async execute(interaction: ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient) {
if(client.config.maintenanceMode){
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
ephemeral: true
});
}
if(!client.stations) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
ephemeral: true
});
}
let query: string | null = null;
if(interaction.isChatInputCommand()){
query = interaction.options?.getString("query");
}
if(interaction.isStringSelectMenu()){
query = interaction.values?.[0];
}
if(!query){
return client.funcs.listStations(client, interaction);
}
const radio = client.radio?.get(interaction.guild?.id);
if(!(interaction.member instanceof GuildMember)) return;
const voiceChannel = interaction.member?.voice.channel;
if (!voiceChannel) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noVoiceChannel,
ephemeral: true
});
if (radio) {
if (voiceChannel !== radio.voiceChannel) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.wrongVoiceChannel,
ephemeral: true
});
}
if (!query) return interaction.reply({
content: client.messages.noQuery,
ephemeral: true
});
const permissions = voiceChannel.permissionsFor(interaction.client.user);
if (!permissions?.has(PermissionFlagsBits.Connect)) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noPermsConnect,
ephemeral: true
});
}
if (!permissions?.has(PermissionFlagsBits.Speak)) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noPermsSpeak,
ephemeral: true
});
}
let station;
if(!isNaN(parseInt(query) - 1)){
let number = parseInt(query) - 1;
if(number > client.stations.length - 1) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.wrongStationNumber,
ephemeral: true
});
} else {
station = client.stations[number];
}
} else {
if(query.length < 3) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.tooShortSearch,
ephemeral: true
});
let type = "text";
if(interaction.isStringSelectMenu() && interaction.values?.[0]){
type = "direct";
}
const sstation = client.stations.search(query, type);
if (!sstation) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noSearchResults,
ephemeral: true
});
station = sstation;
}
if (radio) {
client.statistics?.update(client, interaction.guild, radio);
let date = new Date();
radio.station = station;
radio.textChannel = interaction.channel;
radio.startTime = date.getTime();
client.funcs.play(client, interaction, interaction.guild, station);
return;
}
let date = new Date();
const construct: radio = {
textChannel: interaction.channel,
voiceChannel: voiceChannel,
connection: null,
message: null,
station: station,
startTime: date.getTime()
};
client.radio?.set(interaction.guild?.id, construct);
try {
const connection =
getVoiceConnection(voiceChannel.guild.id) ??
joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator
});
construct.connection = connection;
let date = new Date();
construct.startTime = date.getTime();
client.datastore?.checkEntry(interaction.guild?.id);
client.funcs.play(client, interaction, interaction.guild, station);
} catch (error) {
console.log(error);
client.radio?.delete(interaction.guild?.id);
return interaction.reply({
content: client.messages.emojis["error"] + `An error occured: ${error}`,
ephemeral: true
});
}
}
};

View File

@ -1,36 +0,0 @@
module.exports = {
name: 'prev',
description: 'Previous Station',
category: 'radio',
async execute(interaction, client, command) {
if (client.funcs.check(client, interaction, command)) {
const radio = client.radio.get(interaction.guild.id);
let index = client.stations.findIndex(station => station.name == radio.station.name) - 1;
if(index == -1) index = client.stations.length - 1;
let station = client.stations[index];
if(!station) return interaction.reply({
content: client.messageEmojis["error"] + client.messages.noSearchResults,
ephemeral: true
});
client.statistics.update(client, interaction.guild, radio);
let date = new Date();
radio.station = station;
radio.textChannel = interaction.channel;
radio.startTime = date.getTime();
if(interaction.isChatInputCommand()) {
client.funcs.play(client, interaction, interaction.guild, station);
}
if(interaction.isButton()) {
interaction.deferUpdate();
client.funcs.play(client, null, interaction.guild, station);
}
}
}
}

View File

@ -0,0 +1,57 @@
import { ButtonInteraction, ChatInputCommandInteraction, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { command } from "../commands";
import { station } from "../classes/Stations"
export default {
name: 'prev',
description: 'Previous Station',
category: 'radio',
async execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient, command: command) {
if (client.funcs.check(client, interaction, command)) {
const radio = client.radio?.get(interaction.guild?.id);
if(client.config.maintenanceMode){
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
ephemeral: true
});
}
if(!client.stations) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
ephemeral: true
});
}
let index = client.stations.findIndex((station: station) => station.name == radio.station.name) - 1;
if(index == -1) index = client.stations.length - 1;
let station = client.stations[index];
if(!station) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noSearchResults,
ephemeral: true
});
client.statistics?.update(client, interaction.guild, radio);
let date = new Date();
radio.station = station;
radio.textChannel = interaction.channel;
radio.startTime = date.getTime();
if(interaction.isChatInputCommand()) {
client.funcs.play(client, interaction, interaction.guild, station);
}
if(interaction.isButton()) {
interaction.deferUpdate();
client.funcs.play(client, null, interaction.guild, station);
}
}
}
}

View File

@ -1,21 +1,27 @@
import { EmbedBuilder } from "discord.js"; import { ButtonInteraction, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
module.exports = { export default {
name: 'statistics', name: 'statistics',
description: 'Show statistics', description: 'Show statistics',
category: 'info', category: 'info',
execute(interaction, client) { execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient) {
let message = {};
let stations = client.stations; if(!interaction.guild) return interaction.reply({
let currentGuild = client.datastore.getEntry(interaction.guild.id); content: client.messages.emojis["error"] + client.messages.maintenance,
let global = client.datastore.getEntry("global"); ephemeral: true
});
let currentGuild = client.datastore?.getEntry(interaction.guild.id);
let global = client.datastore?.getEntry("global");
let statistics = ""; let statistics = "";
if(!client.stations) { if(!client.stations) {
message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace("%client.config.supportGuild%", client.config.supportGuild);
return interaction.reply({ return interaction.reply({
content: client.messageEmojis["error"] + message.errorToGetPlaylist, content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
ephemeral: true ephemeral: true
}); });
} }
@ -28,13 +34,13 @@ module.exports = {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(client.messages.statisticsTitle) .setTitle(client.messages.statisticsTitle)
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messageEmojis["statistics"].replace(/[^0-9]+/g, '')) .setThumbnail("https://cdn.discordapp.com/emojis/" + client.messages.emojis["statistics"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor) .setColor(client.config.embedColor)
.setDescription(statistics) .setDescription(statistics)
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png') .setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({ .setFooter({
text: client.messages.footerText, text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '') iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
}); });
interaction.reply({ interaction.reply({

View File

@ -1,30 +1,36 @@
import { EmbedBuilder } from "discord.js"; import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
import RadioClient from "../../Client";
module.exports = { export default {
name: 'status', name: 'status',
description: 'Bot Status', description: 'Bot Status',
category: 'info', category: 'info',
async execute(interaction, client) { async execute(interaction: ChatInputCommandInteraction, client: RadioClient) {
let message = {};
message.statusTitle = client.messages.statusTitle.replace("%client.user.username%", client.user.username); if(!client.user) return interaction.reply({
let uptime = client.funcs.msToTime(client.uptime); content: client.messages.emojis["error"] + client.messages.maintenance,
ephemeral: true
});
let uptime = client.funcs.msToTime(client.uptime || 0);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(message.statusTitle) .setTitle(client.messages.replace(client.messages.statusTitle, {
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messageEmojis["logo"].replace(/[^0-9]+/g, '')) "%client.user.username%": client.user.username
}))
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messages.emojis["logo"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor) .setColor(client.config.embedColor)
.addFields( .addFields([
{ name: client.messages.statusField1, value: uptime }, { name: client.messages.statusField1, value: uptime },
{ name: client.messages.statusField2, value: client.config.version }, { name: client.messages.statusField2, value: client.config.version },
{ name: client.messages.statusField3, value: Date.now() - interaction.createdTimestamp + "ms" }, { name: client.messages.statusField3, value: Date.now() - interaction.createdTimestamp + "ms" },
{ name: client.messages.statusField4, value: client.ws.ping.toString() }, { name: client.messages.statusField4, value: client.ws.ping.toString() },
{ name: client.messages.statusField5, value: client.config.hostedBy } { name: client.messages.statusField5, value: client.config.hostedBy }
) ])
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png') .setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({ .setFooter({
text: client.messages.footerText, text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '') iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
}); });
interaction.reply({ interaction.reply({

View File

@ -1,19 +1,21 @@
import { EmbedBuilder } from "discord.js"; import { ButtonInteraction, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { command } from "../commands";
module.exports = { export default {
name: 'stop', name: 'stop',
description: 'Stop radio', description: 'Stop radio',
category: 'radio', category: 'radio',
async execute(interaction, client, command) { async execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient, command: command) {
if (client.funcs.check(client, interaction, command)) { if (client.funcs.check(client, interaction, command)) {
const radio = client.radio.get(interaction.guild.id); const radio = client.radio?.get(interaction.guild?.id);
client.statistics.update(client, interaction.guild, radio); client.statistics?.update(client, interaction.guild, radio);
radio.connection?.destroy(); radio.connection?.destroy();
client.funcs.logger('Radio', interaction.guild.id + " / " + 'Stop'); client.funcs.logger('Radio', interaction.guild?.id + " / " + 'Stop');
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(client.user.username) .setTitle(client.user?.username || "-")
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messageEmojis["stop"].replace(/[^0-9]+/g, '')) .setThumbnail("https://cdn.discordapp.com/emojis/" + client.messages.emojis["stop"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor) .setColor(client.config.embedColor)
.addFields({ .addFields({
name: client.messages.nowplayingTitle, name: client.messages.nowplayingTitle,
@ -22,7 +24,7 @@ module.exports = {
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png') .setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({ .setFooter({
text: client.messages.footerText, text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '') iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
}); });
if(!radio.message){ if(!radio.message){
@ -39,10 +41,10 @@ module.exports = {
await radio.message?.delete(); await radio.message?.delete();
}, 5000); }, 5000);
client.radio.delete(interaction.guild.id); client.radio?.delete(interaction.guild?.id);
interaction.reply({ interaction.reply({
content: client.messageEmojis["stop"] + client.messages.stop, content: client.messages.emojis["stop"] + client.messages.stop,
ephemeral: true ephemeral: true
}); });
} }

View File

@ -1,42 +0,0 @@
module.exports = {
name: 'emojis',
async execute(client) {
let customEmojis = {
logo: "<:RadioX:688765708808487072>",
eximiabots: "<:EximiaBots:693277919929303132>",
list: "<:RadioXList:688541155519889482>",
play: "<:RadioXPlay:688541155712827458>",
stop: "<:RadioXStop:688541155377414168>",
statistics: "<:RadioXStatistics:694954485507686421>",
maintenance: "<:RadioXMaintenance:695043843057254493>",
error: "<:RadioXError:688541155792781320>",
prev: "<:RadioXPrev:882153637370023957>",
next: "<:RadioXNext:882153637474893834>"
};
let fallbackEmojis = {
logo: "RadioX",
eximiabots: "EximiaBots",
list: "📜",
play: "▶️",
stop: "⏹️",
statistics: "📊",
maintenance: "🛠️",
error: "❌",
prev: "⏪",
next: "⏩"
};
client.messageEmojis = {};
for (const customEmojiName in customEmojis) {
const customEmojiID = customEmojis[customEmojiName].replace(/[^0-9]+/g, '');
const customEmoji = client.emojis.cache.get(customEmojiID);
if (customEmoji) {
client.messageEmojis[customEmojiName] = customEmojis[customEmojiName];
} else {
client.messageEmojis[customEmojiName] = fallbackEmojis[customEmojiName];
}
}
}
}

53
src/client/events.ts Normal file
View File

@ -0,0 +1,53 @@
import RadioClient from "../Client"
import interactionCreate from "./events/interactionCreate"
import messageDelete from "./events/messageDelete"
import ready from "./events/ready"
import SIGINT from "./events/SIGINT"
import SIGTERM from "./events/SIGTERM"
import uncaughtException from "./events/uncaughtException"
import voiceStateUpdate from "./events/voiceStateUpdate"
import warning from "./events/warning"
export default function events(client: RadioClient) {
client.on("ready", () => {
ready(client);
});
client.on("messageDelete", msg => {
messageDelete(client, msg);
});
client.on("interactionCreate", interaction => {
interactionCreate(client, interaction);
});
client.on("voiceStateUpdate", (oldState, newState) => {
voiceStateUpdate(client, oldState, newState);
});
client.on("error", error => {
client.funcs.logger("Discord Client", "Error");
console.error(error);
console.log('');
});
process.on('SIGINT', () => {
SIGINT(client);
});
process.on('SIGTERM', () => {
SIGTERM(client);
});
process.on('uncaughtException', (error) => {
uncaughtException(client, error);
});
process.on('exit', () => {
client.funcs.logger("Bot", "Stopping");
});
process.on('warning', (error) => {
warning(client, error);
});
}

View File

@ -1,15 +0,0 @@
module.exports = {
name: 'SIGINT',
execute(client) {
client.user.setStatus('dnd');
client.streamer.leave(client);
client.radio.save(client);
setInterval(() => {
if(client.radio.size == 0){
process.exit();
}
}, 500);
}
}

View File

@ -0,0 +1,14 @@
import RadioClient from "../../Client";
export default function SIGINT(client: RadioClient) {
client.user?.setStatus('dnd');
client.streamer?.leave(client);
client.radio?.save(client);
setInterval(() => {
if(client.radio?.size == 0){
process.exit();
}
}, 500);
}

View File

@ -1,6 +0,0 @@
module.exports = {
name: 'SIGTERM',
execute(client) {
process.emit('SIGINT');
}
}

View File

@ -0,0 +1,5 @@
import RadioClient from "../../Client";
export default function SIGTERM(client: RadioClient) {
process.emit('SIGINT');
}

View File

@ -1,45 +0,0 @@
import { PermissionFlagsBits } from "discord.js";
module.exports = {
name: 'interactionCreate',
async execute(client, interaction) {
const permissions = interaction.channel.permissionsFor(interaction.client.user);
if (!permissions.has(PermissionFlagsBits.ViewChannel)) return;
if (!permissions.has(PermissionFlagsBits.EmbedLinks)) return interaction.reply({
content: client.messageEmojis["error"] + client.messages.noPermsEmbed,
ephemeral: true
});
if(interaction.isChatInputCommand()){
const commandName = interaction.commandName;
const command = client.commands.get(commandName);
if (!command) return;
try {
command.execute(interaction, client);
} catch (error) {
interaction.reply({
content: client.messageEmojis["error"] + client.messages.runningCommandFailed,
ephemeral: true
});
console.error(error);
}
} else if (interaction.isStringSelectMenu() || interaction.isButton()){
const commandName = interaction.customId;
const command = client.commands.get(commandName);
if (!command) return;
try {
command.execute(interaction, client, command);
} catch (error) {
interaction.reply({
content: client.messageEmojis["error"] + client.messages.runningCommandFailed,
ephemeral: true
});
console.error(error);
}
}
}
}

View File

@ -0,0 +1,46 @@
import { ChannelType, Interaction, PermissionFlagsBits } from "discord.js";
import RadioClient from "../../Client";
export default function interactionCreate(client: RadioClient, interaction: Interaction) {
if(!(interaction.isButton()) && !(interaction.isChatInputCommand()) && !(interaction.isStringSelectMenu())) return;
if(interaction.channel?.type != ChannelType.DM){
const permissions = interaction.channel?.permissionsFor(interaction.client.user);
if (!permissions?.has(PermissionFlagsBits.ViewChannel)) return;
if (!permissions?.has(PermissionFlagsBits.EmbedLinks)) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noPermsEmbed,
ephemeral: true
});
}
if(interaction.isChatInputCommand()){
const commandName = interaction.commandName;
const command = client.commands.get(commandName);
if (!command) return;
try {
command.execute(interaction, client);
} catch (error) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.runningCommandFailed,
ephemeral: true
});
console.error(error);
}
} else if (interaction.isStringSelectMenu() || interaction.isButton()){
const commandName = interaction.customId;
const command = client.commands.get(commandName);
if (!command) return;
try {
command.execute(interaction, client, command);
} catch (error) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.runningCommandFailed,
ephemeral: true
});
console.error(error);
}
}
}

View File

@ -1,62 +0,0 @@
import { EmbedBuilder, PermissionFlagsBits } from "discord.js";
module.exports = {
name: 'messageCreate',
async execute(client, message) {
if (message.author.bot || !message.guild) return;
let prefix = "rx$";
if(client.user.username == "RadioX"){
prefix = "rx>";
} else if (client.user.username == "RadioX Beta"){
prefix = "rx-";
} else if (client.user.username == "RadioX Dev"){
prefix = "rx$";
} else if(message.mentions.members.first() && message.mentions.members.first().user.id === client.user.id){
prefix = "<@!" + client.user.id + "> ";
} else {
return;
}
const args = message.content.slice(prefix.length).split(' ');
if (!message.content.startsWith(prefix)) return;
if (!args[0]) return;
const commandName = args[0].toLowerCase();
if (commandName === 'none') return;
const command = client.commands.get(commandName) || client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName));
if (!command && message.content !== `${prefix}`) return;
const permissions = message.channel.permissionsFor(message.client.user);
if (!permissions.has(PermissionFlagsBits.EmbedLinks)) return message.channel.send(client.messages.noPermsEmbed);
try {
let newMessage = {};
newMessage.messageCommandsDeprecatedTitle = client.messages.messageCommandsDeprecatedTitle.replace("%client.user.username%", client.user.username);
const embed = new EmbedBuilder()
.setTitle(newMessage.messageCommandsDeprecatedTitle)
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messageEmojis["logo"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor)
.setDescription(client.messages.messageCommandsDeprecatedDescription)
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '')
});
let msg = await message.channel.send({ embeds: [embed] });
setTimeout(async function() {
try {
await msg.delete();
} catch (DiscordAPIError) {
}
}, 30000);
} catch (error) {
message.reply({
content: client.messages.runningCommandFailed,
ephemeral: true
});
console.error(error);
}
}
}

View File

@ -1,11 +0,0 @@
module.exports = {
name: 'messageDelete',
async execute(client, msg) {
if(!msg.author.bot || !msg.guild) return;
const radio = client.radio.get(msg.guild.id);
if(!radio) return;
if(!radio.message) return;
if(msg.id != radio.message.id) return;
radio.message = null;
}
}

View File

@ -0,0 +1,11 @@
import { Message, PartialMessage } from "discord.js";
import RadioClient from "../../Client";
export default function messageDelete(client: RadioClient, msg: Message | PartialMessage){
if(!msg.author?.bot || !msg.guild) return;
const radio = client.radio?.get(msg.guild.id);
if(!radio) return;
if(!radio.message) return;
if(msg.id != radio.message.id) return;
radio.message = null;
}

View File

@ -1,95 +0,0 @@
import Datastore from "../classes/Datastore.js";
import Radio from "../classes/Radio.js";
import Stations from "../classes/Stations.js";
import Streamer from "../classes/Streamer.js";
import Statistics from "../classes/Statistics.js";
module.exports = {
name: 'ready',
async execute(client) {
client.funcs.logger("Bot", "Ready");
/*DATASTORE*/
client.funcs.logger('Datastore', 'Initialize');
client.datastore = new Datastore();
client.datastore.map.forEach(datastore => {
client.funcs.logger('Datastore', datastore.guild.id + " / " + datastore.guild.name);
});
client.funcs.logger('Datastore', 'Ready');
/*DEVELOPERS*/
client.developers = "";
let user = "";
for (let i = 0; i < client.config.devId.length; i++) {
user = await client.users.fetch(client.config.devId[i]);
client.funcs.logger('Developers', user.tag);
if (i == client.config.devId.length - 1) {
client.developers += user.tag;
} else {
client.developers += user.tag + " & ";
}
}
/*STATIONS*/
client.stations = new Stations();
await client.stations.fetch({
url: client.config.stationslistUrl,
show: true
});
/*setInterval(async () => {
await client.stations.fetch({
url: client.config.stationslistUrl,
show: false
});
}, 3600000);*/
client.streamer = new Streamer();
client.streamer.init(client);
if(!client.stations) {
client.user.setStatus('dnd');
}
/*GUILDS*/
client.funcs.logger('Guilds', 'Started fetching list');
let guilds = await client.guilds.fetch();
guilds.forEach(guild => {
client.funcs.logger('Guilds', guild.id + " / " + guild.name);
});
client.funcs.logger('Guilds', 'Successfully fetched list');
/*STATISTICS*/
client.statistics = new Statistics();
client.statistics.calculateGlobal(client);
/*EMOJIS*/
require(`../emojis.js`).execute(client);
/*COMMANDS*/
require(`../commands.js`).execute(client);
/*RADIO*/
client.radio = new Radio();
setTimeout(function () {
/*RESTORE RADIOS*/
client.radio.restore(client, guilds);
}, 5000);
setTimeout(function () {
if(client.stations) {
/*MAINTENANCE MODE*/
client.funcs.logger("Maintenance Mode", "Disabled");
client.config.maintenanceMode = false;
}
}, 10000);
}
}

View File

@ -0,0 +1,77 @@
import RadioClient from "../../Client";
import Datastore, { datastore } from "../classes/Datastore";
import Radio from "../classes/Radio";
import Stations from "../classes/Stations";
import Streamer from "../classes/Streamer";
import Statistics from "../classes/Statistics";
import commands from "../commands";
import { OAuth2Guild } from "discord.js";
export default async function ready(client: RadioClient) {
client.funcs.logger("Bot", "Ready");
/*DATASTORE*/
client.funcs.logger('Datastore', 'Initialize');
client.datastore = new Datastore();
client.datastore.map.forEach((datastore: datastore) => {
client.funcs.logger('Datastore', datastore.guild.id + " / " + datastore.guild.name);
});
client.funcs.logger('Datastore', 'Ready');
/*DEVELOPERS*/
let developers : string[] = [];
for(let devID of client.config.devIDs){
developers.push((await client.users.fetch(devID)).tag);
}
client.funcs.logger('Developers', developers.join(" & "));
/*STATIONS*/
client.stations = new Stations();
await client.stations.fetch({
url: client.config.stationslistUrl,
show: true
});
client.streamer = new Streamer();
client.streamer.init(client);
if(!client.stations) {
client.user?.setStatus('dnd');
}
/*GUILDS*/
client.funcs.logger('Guilds', 'Started fetching list');
let guilds = await client.guilds.fetch();
guilds.forEach((guild: OAuth2Guild) => {
client.funcs.logger('Guilds', guild.id + " / " + guild.name);
});
client.funcs.logger('Guilds', 'Successfully fetched list');
/*STATISTICS*/
client.statistics = new Statistics();
client.statistics.calculateGlobal(client);
/*COMMANDS*/
commands(client);
/*RADIO*/
client.radio = new Radio();
setTimeout(function () {
/*RESTORE RADIOS*/
client.radio?.restore(client, guilds);
}, 5000);
setTimeout(function () {
if(client.stations) {
/*MAINTENANCE MODE*/
client.funcs.logger("Maintenance Mode", "Disabled");
client.config.maintenanceMode = false;
}
}, 10000);
}

View File

@ -1,11 +0,0 @@
module.exports = {
name: 'uncaughtException',
execute(client, error) {
client.funcs.logger("Error");
console.log(error.stack);
console.log('');
if(error.name == "DiscordAPIError" && error.message == "Unknown interaction") return;
process.emit('SIGINT');
}
}

View File

@ -0,0 +1,10 @@
import RadioClient from "../../Client";
export default function uncaughtException(client: RadioClient, error: Error) {
client.funcs.logger("Error");
console.log(error.stack);
console.log('');
if(error.name == "DiscordAPIError" && error.message == "Unknown interaction") return;
process.emit('SIGINT');
}

View File

@ -1,68 +0,0 @@
import { PermissionFlagsBits } from "discord.js";
const {
getVoiceConnection,
joinVoiceChannel
} = require("@discordjs/voice");
module.exports = {
name: "voiceStateUpdate",
async execute(client, oldState, newState) {
if (oldState.channel === null) return;
let change = false;
const radio = client.radio?.get(newState.guild.id);
if (!radio) return;
if (newState.member.id === client.user.id && oldState.member.id === client.user.id) {
if (newState.channel === null) {
client.statistics.update(client, newState.guild, radio);
radio.connection?.destroy();
radio.message?.delete();
client.funcs.logger('Radio', newState.guild.id + " / " + 'Stop');
return client.radio.delete(newState.guild.id);
}
const newPermissions = newState.channel.permissionsFor(newState.client.user);
if (!newPermissions.has(PermissionFlagsBits.Connect) || !newPermissions.has(PermissionFlagsBits.Speak) || !newPermissions.has(PermissionFlagsBits.ViewChannel)) {
try {
setTimeout(
async () => (
radio.connection = joinVoiceChannel({
channelId: oldState.channel.id,
guildId: oldState.channel.guild.id,
adapterCreator: oldState.channel.guild.voiceAdapterCreator
})
//radio.connection = await oldState.channel.join()
),
1000
);
} catch (error) {
client.statistics.update(client, newState.guild, radio);
radio.connection?.destroy();
radio.message?.delete();
client.funcs.logger('Radio', newState.guild.id + " / " + 'Stop');
client.radio.delete(oldState.guild.id);
}
return;
}
if (newState.channel !== radio.voiceChannel) {
change = true;
radio.voiceChannel = newState.channel;
radio.connection = getVoiceConnection(newState.channel.guild.id);
//radio.connection = await newState.channel.join();
}
}
if ((oldState.channel.members.filter(member => !member.user.bot).size === 0 && oldState.channel === radio.voiceChannel) || change) {
setTimeout(() => {
if (!radio || !radio.connection || !radio.connection === null) return;
if (radio.voiceChannel.members.filter(member => !member.user.bot).size === 0) {
client.statistics.update(client, newState.guild, radio);
radio.connection?.destroy();
radio.message?.delete();
client.funcs.logger('Radio', newState.guild.id + " / " + 'Stop');
client.radio.delete(newState.guild.id);
}
}, 5000);
}
},
};

View File

@ -0,0 +1,65 @@
import { GuildMember, PermissionFlagsBits, VoiceState } from "discord.js";
import RadioClient from "../../Client";
const {
getVoiceConnection,
joinVoiceChannel
} = require("@discordjs/voice");
export default async function voiceStateUpdate(client: RadioClient, oldState: VoiceState, newState: VoiceState) {
if (oldState.channel === null) return;
let change = false;
const radio = client.radio?.get(newState.guild.id);
if (!radio) return;
if (newState.member?.id === client.user?.id && oldState.member?.id === client.user?.id) {
if (newState.channel === null) {
client.statistics?.update(client, newState.guild, radio);
radio.connection?.destroy();
radio.message?.delete();
client.funcs.logger('Radio', newState.guild.id + " / " + 'Stop');
return client.radio?.delete(newState.guild.id);
}
const newPermissions = newState.channel.permissionsFor(newState.client.user);
if (!newPermissions?.has(PermissionFlagsBits.Connect) || !newPermissions?.has(PermissionFlagsBits.Speak) || !newPermissions?.has(PermissionFlagsBits.ViewChannel)) {
try {
setTimeout(
async () => (
radio.connection = joinVoiceChannel({
channelId: oldState.channel?.id,
guildId: oldState.channel?.guild.id,
adapterCreator: oldState.channel?.guild.voiceAdapterCreator
})
),
1000
);
} catch (error) {
client.statistics?.update(client, newState.guild, radio);
radio.connection?.destroy();
radio.message?.delete();
client.funcs.logger('Radio', newState.guild.id + " / " + 'Stop');
client.radio?.delete(oldState.guild.id);
}
return;
}
if (newState.channel !== radio.voiceChannel) {
change = true;
radio.voiceChannel = newState.channel;
radio.connection = getVoiceConnection(newState.channel.guild.id);
}
}
if ((oldState.channel.members.filter(member => !member.user.bot).size === 0 && oldState.channel === radio.voiceChannel) || change) {
setTimeout(() => {
if (!radio || !radio.connection || !radio.connection === null) return;
if (radio.voiceChannel.members.filter((member: GuildMember) => !member.user.bot).size === 0) {
client.statistics?.update(client, newState.guild, radio);
radio.connection?.destroy();
radio.message?.delete();
client.funcs.logger('Radio', newState.guild.id + " / " + 'Stop');
client.radio?.delete(newState.guild.id);
}
}, 5000);
}
};

View File

@ -1,12 +0,0 @@
module.exports = {
name: 'warning',
execute(client, warning) {
if(warning.name == "ExperimentalWarning" && warning.message.startsWith("stream/web")) return;
client.funcs.logger("Warning");
console.warn(warning.name);
console.warn(warning.message);
console.warn(warning.stack);
console.log('');
}
}

View File

@ -0,0 +1,11 @@
import RadioClient from "../../Client";
export default function warning(client: RadioClient, warning: Error) {
if(warning.name == "ExperimentalWarning" && warning.message.startsWith("stream/web")) return;
client.funcs.logger("Warning");
console.warn(warning.name);
console.warn(warning.message);
console.warn(warning.stack);
console.log('');
}

12
src/client/funcs.ts Normal file
View File

@ -0,0 +1,12 @@
import check from "./funcs/check";
import isDev from "./funcs/isDev";
import listStations from "./funcs/listStations";
import loadState from "./funcs/loadState";
import logger from "./funcs/logger";
import msToTime from "./funcs/msToTime";
import play from "./funcs/play";
import saveState from "./funcs/saveState";
export const funcs = {
check, isDev, listStations, loadState, logger, msToTime, play, saveState
}

View File

@ -1,35 +0,0 @@
module.exports = function check(client, interaction, command) {
let message = {};
const radio = client.radio.get(interaction.guild.id);
if(client.config.maintenanceMode){
interaction.reply({
content: client.messageEmojis["error"] + client.messages.maintenance,
ephemeral: true
});
return false;
}
if(!client.stations) {
message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace("%client.config.supportGuild%", client.config.supportGuild);
interaction.reply({
content: client.messageEmojis["error"] + message.errorToGetPlaylist,
ephemeral: true
});
return false;
}
if (!radio) {
interaction.reply({
content: client.messageEmojis["error"] + client.messages.notPlaying,
ephemeral: true
});
return false;
}
if (interaction.member.voice.channel !== radio.voiceChannel) {
interaction.reply({
content: client.messageEmojis["error"] + client.messages.wrongVoiceChannel,
ephemeral: true
});
return false;
}
return true;
};

34
src/client/funcs/check.ts Normal file
View File

@ -0,0 +1,34 @@
import { ButtonInteraction, ChatInputCommandInteraction, GuildMember, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { command } from "../commands";
export default function check(client: RadioClient, interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, command: command) {
const radio = client.radio?.get(interaction.guild?.id);
if(!client.stations) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
ephemeral: true
});
return false;
}
if (!radio) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.notPlaying,
ephemeral: true
});
return false;
}
if (interaction.member instanceof GuildMember && interaction.member?.voice.channel !== radio.voiceChannel) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.wrongVoiceChannel,
ephemeral: true
});
return false;
}
return true;
};

View File

@ -1,10 +0,0 @@
module.exports = function isDev(devList, authorID){
let response = false;
Object.keys(devList).forEach(function(oneDev) {
let devID = devList[oneDev];
if(authorID == devID){
response = true;
}
});
return response;
}

View File

@ -0,0 +1,9 @@
import { Snowflake } from "discord.js";
export default function isDev(devIDs : string[], authorID : Snowflake){
for (const devID of devIDs){
if(authorID == devID){
return true;
}
}
}

View File

@ -1,33 +0,0 @@
import { ActionRowBuilder, StringSelectMenuBuilder } from "discord.js";
module.exports = function listStations(client, interaction){
let stations = new Array();
let options = new Array();
stations = client.stations.forEach(station => {
if(station.name == "GrooveFM") return;
station = {
label: station.name,
description: station.owner,
value: station.name
};
options.push(station);
});
const menu = new ActionRowBuilder()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('play')
.setPlaceholder('Nothing selected')
.addOptions(options)
);
stations = null;
options = null;
return interaction.reply({
content: '**Select station:**',
components: [menu],
ephemeral: true
});
}

View File

@ -0,0 +1,30 @@
import { ActionRowBuilder, ButtonInteraction, ChatInputCommandInteraction, SelectMenuComponentOptionData, StringSelectMenuBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
export default function listStations(client: RadioClient, interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction){
if(!client.stations) return;
let options : SelectMenuComponentOptionData[] = new Array();
for (const station of client.stations){
options.push({
label: station.name,
description: station.owner,
value: station.name
});
}
const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('play')
.setPlaceholder('Nothing selected')
.addOptions(options)
);
return interaction.reply({
content: '**Select station:**',
components: [menu],
ephemeral: true
});
}

View File

@ -1,12 +0,0 @@
module.exports = function loadState(client, guild){
let data = client.datastore.getEntry(guild.id);
if(!data) return;
let state;
state = data.state;
if(!state) return;
data.state = {};
client.datastore.updateEntry(guild, data);
return state;
}

View File

@ -0,0 +1,13 @@
import { OAuth2Guild } from "discord.js";
import RadioClient from "../../Client";
export default function loadState(client: RadioClient, guild: OAuth2Guild) {
if(!client.datastore) return;
let data = client.datastore.getEntry(guild.id);
if(!data) return;
let state = data.state;
if(!state) return;
data.state = null;
client.datastore.updateEntry(guild, data);
return state;
}

View File

@ -1,5 +0,0 @@
module.exports = function logger(area, text){
let date = new Date();
console.log('[' + area + '] ' + date.toISOString());
if(text) console.log(text + '\n');
}

View File

@ -0,0 +1,5 @@
export default function logger(area: string, text?: string){
let date = new Date();
console.log('[' + area + '] - ' + date.toISOString());
if(text) console.log(text + '\n');
}

View File

@ -1,4 +1,4 @@
module.exports = function msToTime(duration) { export default function msToTime(duration : number) {
let seconds = Math.floor((duration / 1000) % 60), let seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60), minutes = Math.floor((duration / (1000 * 60)) % 60),
hours = Math.floor((duration / (1000 * 60 * 60)) % 24), hours = Math.floor((duration / (1000 * 60 * 60)) % 24),

View File

@ -1,61 +1,64 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, EmbedBuilder, Guild, OAuth2Guild, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { station } from "../classes/Stations";
module.exports = async function play(client, interaction, guild, station) { export default async function play(client: RadioClient, interaction: ChatInputCommandInteraction | StringSelectMenuInteraction | null, guild: OAuth2Guild | Guild | null, station: station) {
let message = {}; if(!guild) return;
const radio = client.radio.get(guild.id);
const audioPlayer = client.streamer.listen(station); const radio = client.radio?.get(guild.id);
const audioPlayer = client.streamer?.listen(station);
radio.connection.subscribe(audioPlayer); radio.connection.subscribe(audioPlayer);
client.funcs.logger('Radio', guild.id + " / " + "Play" + " / " + radio.station.name); client.funcs.logger('Radio', guild.id + " / " + "Play" + " / " + radio.station.name);
message.nowplayingDescription = client.messages.nowplayingDescription.replace("%radio.station.name%", radio.station.name);
message.nowplayingDescription = message.nowplayingDescription.replace("%radio.station.owner%", radio.station.name != radio.station.owner ? radio.station.owner + "\n" : "");
message.nowplayingDescription = message.nowplayingDescription.replace("%client.funcs.msToTime(completed)%", "");
message.nowplayingDescription = message.nowplayingDescription.replace("**", "");
message.nowplayingDescription = message.nowplayingDescription.replace("**", "");
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(client.user.username) .setTitle(client.user?.username || "-")
.setThumbnail((radio.station.logo || "https://cdn.discordapp.com/emojis/" + client.messageEmojis["play"].replace(/[^0-9]+/g, ''))) .setThumbnail((radio.station.logo || "https://cdn.discordapp.com/emojis/" + client.messages.emojis["play"].replace(/[^0-9]+/g, '')))
.setColor(client.config.embedColor) .setColor(client.config.embedColor)
.addFields({ .addFields({
name: client.messages.nowplayingTitle, name: client.messages.nowplayingTitle,
value: message.nowplayingDescription value: client.messages.replace(client.messages.nowplayingDescription, {
"%radio.station.name%": radio.station.name,
"%radio.station.owner%\n": radio.station.name != radio.station.owner ? radio.station.owner + "\n" : "",
"%client.funcs.msToTime(completed)%": "",
"**": "",
"**:2": ""
})
}) })
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png') .setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({ .setFooter({
text: client.messages.footerText, text: client.messages.footerText,
iconURL: "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, '') iconURL: "https://cdn.discordapp.com/emojis/" + client.messages.emojis["eximiabots"].replace(/[^0-9]+/g, '')
}); });
const buttons = new ActionRowBuilder() const buttons = new ActionRowBuilder<ButtonBuilder>()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId('list') .setCustomId('list')
.setEmoji(client.messageEmojis["list"]) .setEmoji(client.messages.emojis["list"])
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId('prev') .setCustomId('prev')
.setEmoji(client.messageEmojis["prev"]) .setEmoji(client.messages.emojis["prev"])
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId('stop') .setCustomId('stop')
.setEmoji(client.messageEmojis["stop"]) .setEmoji(client.messages.emojis["stop"])
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId('next') .setCustomId('next')
.setEmoji(client.messageEmojis["next"]) .setEmoji(client.messages.emojis["next"])
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId('statistics') .setCustomId('statistics')
.setEmoji(client.messageEmojis["statistics"]) .setEmoji(client.messages.emojis["statistics"])
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
); );
@ -70,10 +73,10 @@ module.exports = async function play(client, interaction, guild, station) {
} }
} }
message.play = client.messages.play.replace("%radio.station.name%", radio.station.name);
interaction?.reply({ interaction?.reply({
content: client.messageEmojis["play"] + message.play, content: client.messages.emojis["play"] + client.messages.replace(client.messages.play, {
"%radio.station.name%": radio.station.name
}),
ephemeral: true ephemeral: true
}); });

View File

@ -1,18 +0,0 @@
module.exports = function saveState(client, guild, radio){
client.datastore.checkEntry(guild.id);
let date = new Date();
let data = client.datastore.getEntry(guild.id);
data.state = {};
data.state.channels = {};
data.state.channels.text = radio.textChannel.id;
data.state.channels.voice = radio.voiceChannel.id;
data.state.date = date.toISOString();
data.state.station = {};
data.state.station.name = radio.station.name;
data.state.station.owner = radio.station.owner;
client.datastore.updateEntry(guild, data);
}

View File

@ -0,0 +1,26 @@
import { Guild } from "discord.js";
import RadioClient from "../../Client";
import { radio } from "../classes/Radio";
export default function saveState(client: RadioClient, guild: Guild, radio: radio){
if(!client.datastore) return;
client.datastore.checkEntry(guild.id);
let date = new Date();
let data = client.datastore.getEntry(guild.id);
if(!data) return;
data.state = {
channels: {
text: radio.textChannel?.id,
voice: radio.voiceChannel?.id
},
date: date.toISOString(),
station: {
name: radio.station.name,
owner: radio.station.owner
}
};
client.datastore.updateEntry(guild, data);
}

View File

@ -1,4 +1,16 @@
module.exports = { export const messages = {
replace(message: string, variables: { [key: string]: string }){
for(let variable in variables){
if(variable.includes('%')){
message = message.replace(variable, variables[variable]);
} else if(variable.includes(':')){
message = message.replace(variable.split(':')[0], variables[variable]);
} else {
message = message.replace(variable, variables[variable]);
}
}
return message;
},
wrongVoiceChannel: "You need to be in the same voice channel as RadioX to use this command!", wrongVoiceChannel: "You need to be in the same voice channel as RadioX to use this command!",
noPerms: "You need the %command.permission% permission to use this command!", noPerms: "You need the %command.permission% permission to use this command!",
notPlaying: "There is nothing playing!", notPlaying: "There is nothing playing!",
@ -35,7 +47,17 @@ module.exports = {
statusField4: ":hourglass: Latency", statusField4: ":hourglass: Latency",
statusField5: ":globe_with_meridians: Hosted by", statusField5: ":globe_with_meridians: Hosted by",
errorStationURL: "Station can't be URL", errorStationURL: "Station can't be URL",
messageCommandsDeprecatedTitle: "%client.user.username%", maintenance: "Shhhh... We are now sleeping and dreaming about new features to implement. Will be back soon.",
messageCommandsDeprecatedDescription: "We recommend you to reauthorize our bot by clicking the invite link down below, because Discord is planning to remove message content from verified bots [Read More](https://support-dev.discord.com/hc/en-us/articles/4404772028055)" + "\n\n" + "**Invite Bot**" + "\n" + "https://wgi.fi/radiox_invite" + "\n\n" + "This bot now supports slash commands, you should start using them instead. Type / into the message box and select the bot you wish to use. Remember to be careful as there are a few bugs here and there on Discord." + "\n\n" + "We will remove this deprecation message in March of 2022 when RadioX 1.0.0 is released.", emojis: {
maintenance: "Shhhh... We are now sleeping and dreaming about new features to implement. Will be back soon." logo: "<:RadioX:688765708808487072>",
eximiabots: "<:EximiaBots:693277919929303132>",
list: "<:RadioXList:688541155519889482>",
play: "<:RadioXPlay:688541155712827458>",
stop: "<:RadioXStop:688541155377414168>",
statistics: "<:RadioXStatistics:694954485507686421>",
maintenance: "<:RadioXMaintenance:695043843057254493>",
error: "<:RadioXError:688541155792781320>",
prev: "<:RadioXPrev:882153637370023957>",
next: "<:RadioXNext:882153637474893834>"
}
}; };

View File

@ -1,3 +0,0 @@
export interface command { }
export interface radio {}

View File

@ -1,6 +1,6 @@
require('dotenv/config'); import { ColorResolvable } from "discord.js";
module.exports = { export default {
//credentials //credentials
token: process.env.DISCORD_TOKEN, token: process.env.DISCORD_TOKEN,
@ -10,17 +10,17 @@ module.exports = {
//support //support
supportGuild: "https://discord.gg/rRA65Mn", supportGuild: "https://discord.gg/rRA65Mn",
devId: [ devIDs: [
"493174343484833802", "493174343484833802",
"360363051792203779" "360363051792203779"
], ],
//misc //misc
embedColor: "#88aa00", embedColor: "#88aa00" as ColorResolvable,
hostedBy: "[Warén Group](https://waren.io)", hostedBy: "[Warén Group](https://waren.io)",
//Settings //Settings
version: process.env.DEV_MODE ? process.env.npm_package_version + "-dev" : process.env.npm_package_version, version: process.env.DEV_MODE ? (process.env.npm_package_version ?? "0.0.0") + "-dev" : process.env.npm_package_version ?? "-",
debug: process.env.DEBUG_MODE || false, debug: process.env.DEBUG_MODE || false,
devMode: process.env.DEV_MODE || false, devMode: process.env.DEV_MODE || false,
maintenanceMode: false, maintenanceMode: false,

View File

@ -1,3 +0,0 @@
const { default: RadioClient } = require("./Client");
const client = new RadioClient();

3
src/index.ts Normal file
View File

@ -0,0 +1,3 @@
import RadioClient from "./Client";
new RadioClient();