Compare commits

...

55 Commits
0.1.0 ... 0.2.2

Author SHA1 Message Date
751f9989eb Merge pull request #27 from warengroup/v0.2.2
Version 0.2.2
2021-08-21 15:19:30 +03:00
6c860b6b23 Fixed #26 in voiceStateUpdate.js 2021-08-21 15:15:32 +03:00
0bfb34c1dd Updated Dependencies 2021-08-21 14:49:07 +03:00
9a84a3c938 Merge pull request #20 from warengroup/develop
Version 0.2.1
2021-08-18 12:46:50 +03:00
510f2c5b50 Updated Dockerfile 2021-08-17 18:54:22 +03:00
e571194eac Updated package.json & Updated dependencies 2021-08-17 18:50:48 +03:00
ca02f95500 Updated Dockerfile 2021-08-17 18:48:13 +03:00
28e7476f70 Added Docker Build workflow (Github Workflows) 2021-08-17 18:38:40 +03:00
95e8b17ddd Updated statisticsUpdate function 2021-08-08 20:06:30 +03:00
6a11482110 Fix typo in datastore.js 2021-08-08 19:20:20 +03:00
202674cb53 Update job name in TypeScript Build workflow (Github) 2021-08-08 17:39:51 +03:00
64f97b990a Added TypeScript Build (Github Workflow) 2021-08-08 03:04:54 +03:00
7dc0a6d3b9 Added .env example 2021-08-08 00:09:20 +03:00
8f685a0676 Updated adapter.ts 2021-08-07 23:56:58 +03:00
ce7d3d633b Updated Dockerfile 2021-08-07 23:56:43 +03:00
177a405f08 Updated Dependencies 2021-08-07 23:56:23 +03:00
40490d80dc Updated stop.js & voiceStateUpdate.js 2021-08-04 17:46:06 +03:00
ef18a08cc6 Updated play 2021-08-04 15:18:14 +03:00
378fe97ea3 Updated voiceStateUpdate 2021-08-04 15:14:12 +03:00
39d7acaa9e Updated voiceStateUpdate 2021-08-04 15:03:58 +03:00
0ef9cad101 Updated voiceStateUpdate 2021-08-04 14:57:02 +03:00
07e45ae54d Updated voiceStateUpdate 2021-08-04 14:47:27 +03:00
f437797bed Updated voiceStateUpdate 2021-08-04 14:42:37 +03:00
e267b06cf1 Updated voiceStateUpdate 2021-08-04 14:40:06 +03:00
a5ffaadf31 Updated voiceStateUpdate 2021-08-04 14:34:41 +03:00
251eb7f5a1 Fix VoiceStateUpdate 2021-08-04 13:52:30 +03:00
b2ae9ef6f0 Stop audioPlayer in maintenance.js 2021-08-04 13:07:48 +03:00
db409ac6d2 Stop audioPlayer in stop.js 2021-08-04 13:07:09 +03:00
fb2c18b9b0 Removed commented code in play.js 2021-08-04 13:06:09 +03:00
5efb68b717 Removed radio.voiceChannel.leave() and replaced with radio.connection.destroy() in maintenance.js 2021-08-04 12:56:08 +03:00
f543cd3267 Removed radio.voiceChannel.leave() and replaced with radio.connection.destroy() 2021-08-04 12:48:59 +03:00
0515f0f5e3 Updated Dependencies 2021-08-04 12:26:52 +03:00
59796ff002 Added libsodium-wrappers to dependencies 2021-08-04 12:25:42 +03:00
bf70276d7f Fix typo 2021-08-04 11:29:49 +03:00
7db92969af Fix embed sending part 3 2021-08-04 11:17:30 +03:00
3520e86f0b Fix embed sending part 2 2021-08-04 11:12:07 +03:00
bb07884ab4 Fix embed sending 2021-08-04 10:52:56 +03:00
2de365770d Updated event message to messageCreate 2021-08-04 10:18:43 +03:00
0c5fdcd651 Updated Dependencies 2021-08-04 10:07:29 +03:00
b01ee3b380 Create datastore folder 2021-08-04 10:07:11 +03:00
308afd9f96 Updated play command 2021-06-24 14:05:30 +03:00
31baeeab45 Made invite URL dynamic 2021-06-23 18:48:55 +03:00
c17900b618 Removed volume command 2021-06-23 18:48:38 +03:00
10c6a8c589 Stopped using dispatcher 2021-06-23 01:25:45 +03:00
284f0e9f0c Updated Dependencies 2021-06-22 23:31:44 +03:00
1f259d95ac Defined devID variable in isDev.js 2021-06-16 04:50:15 +03:00
9b67506558 Removed static directory address in datastore.js 2021-06-16 04:49:45 +03:00
f9ba5a3117 Removed static directory address in Client.ts 2021-06-16 04:49:11 +03:00
f1f16c4f9a Updated Dockerfile 2021-06-16 04:46:46 +03:00
5147c6455f Updated Dockerfile 2021-06-15 21:02:59 +03:00
3b8ae4fd4b Merge pull request #18 from warengroup/discord.js-v13-update
Version 0.2.0
2021-06-15 20:56:29 +03:00
471f345026 Merge branch 'master' into discord.js-v13-update 2021-06-08 12:02:36 +03:00
0d63c1920e Version 0.2.0 2021-06-08 12:02:22 +03:00
3cb07107d5 Updated src 2021-06-08 12:01:56 +03:00
acef9404b2 Updated misc 2021-06-08 12:01:32 +03:00
40 changed files with 7062 additions and 841 deletions

3
.env_example Normal file
View File

@ -0,0 +1,3 @@
DISCORD_TOKEN=
RADIOX_STATIONSLISTURL=https://gitea.cwinfo.org/cwchristerw/radio/raw/branch/master/playlist.json
RADIOX_PREFIX=rx-

13
.eslintrc Normal file
View File

@ -0,0 +1,13 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module"
},
"rules": {}
}

19
.github/workflows/docker-build.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Docker Build
on:
pull_request:
jobs:
buildx:
name: Docker Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v1.5.1
id: buildx
with:
install: true
- name: Build
run: docker build . # will run buildx

22
.github/workflows/typescript-build.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: TypeScript Build
on:
push:
pull_request:
jobs:
tsc:
name: TypeScript Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: install node v16
uses: actions/setup-node@v2.3.1
with:
node-version: 16
- name: npm install -g npm
run: npm install -g npm
- name: npm install
run: npm install
- name: tsc
uses: icrawl/action-tsc@v1

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules/
npm-debug.log
.vscode/
.env
build/

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"printWidth": 100,
"tabWidth": 4,
"semi": true
}

View File

@ -1,15 +1,19 @@
FROM node:14.15.4-alpine
FROM node:16-alpine
#Dependencies
RUN apk add --virtual .build-deps python make g++ gcc
RUN apk add --virtual .build-deps python3 make g++ gcc git
#Dependencies for RadioX Bot
RUN apk add --virtual .radiox-deps ffmpeg
#Code Dependencies
RUN apk add --virtual .code-deps ffmpeg
WORKDIR /usr/src/app
COPY / /usr/src/app/
RUN npm install -g npm
RUN npm install
RUN npm run build
CMD [ "npm", "start" ]

View File

@ -1,62 +0,0 @@
const { Client, Collection } = require('discord.js');
const Discord = require('discord.js');
const fs = require('fs');
const path = require('path');
const events = './events/';
const Datastore = require('./datastore.js');
const GatewayIntents = new Discord.Intents();
GatewayIntents.add(
1 << 0, // GUILDS
1 << 7, // GUILD_VOICE_STATES
1 << 9 // GUILD_MESSAGES
);
module.exports = class extends Client {
constructor() {
super({
disableEveryone: true,
disabledEvents: ['TYPING_START'],
ws: {
intents: GatewayIntents
}
});
this.commands = new Collection();
this.commandAliases = new Collection();
this.radio = new Map();
this.funcs = {};
this.funcs.check = require('./funcs/check.js');
this.funcs.checkFetchStatus = require('./funcs/checkFetchStatus.js');
this.funcs.isDev = require('./funcs/isDev.js');
this.funcs.msToTime = require('./funcs/msToTime.js');
this.funcs.statisticsUpdate = require('./funcs/statisticsUpdate.js');
this.config = require('../config.js');
this.messages = require('./messages.js');
const commandFiles = fs.readdirSync('./client/commands/').filter(f => f.endsWith('.js'));
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
command.uses = 0;
this.commands.set(command.name, command);
this.commandAliases.set(command.alias, command);
}
this.on('ready', () => {
require(`${events}ready`).execute(this, Discord);
this.datastore = new Datastore();
});
this.on('message', (msg) => {
require(`${events}msg`).execute(this, msg, Discord);
});
this.on('voiceStateUpdate', (oldState, newState) => {
require(`${events}voiceStateUpdate`).execute(this, oldState, newState);
});
this.on('error', (error) => {
console.error(error);
});
this.login(this.config.token).catch(err => console.log('Failed to login: ' + err));
}
};

View File

@ -1,158 +0,0 @@
module.exports = {
name: 'play',
alias: 'p',
usage: '<song name>',
description: 'Play some music.',
permission: 'none',
category: 'radio',
async execute(msg, args, client, Discord, command) {
let message = {};
let url = args[1] ? args[1].replace(/<(.+)>/g, "$1") : "";
const radio = client.radio.get(msg.guild.id);
const voiceChannel = msg.member.voice.channel;
if (!radio) {
if (!msg.member.voice.channel) return msg.channel.send(client.messageEmojis["error"] + client.messages.noVoiceChannel);
} else {
if (voiceChannel !== radio.voiceChannel) return msg.channel.send(client.messageEmojis["error"] + client.messages.wrongVoiceChannel);
}
if(!client.stations) {
message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace("%client.config.supportGuild%", client.config.supportGuild);
return msg.channel.send(client.messageEmojis["error"] + message.errorToGetPlaylist);
}
if (!args[1]) return msg.channel.send(client.messages.noQuery);
const permissions = voiceChannel.permissionsFor(msg.client.user);
if (!permissions.has('CONNECT')) {
return msg.channel.send(client.messageEmojis["error"] + client.messages.noPermsConnect);
}
if (!permissions.has('SPEAK')) {
return msg.channel.send(client.messageEmojis["error"] + client.messages.noPermsSpeak);
}
let station;
const number = parseInt(args[1] - 1);
if (url.startsWith('http')) {
return msg.channel.send(client.messageEmojis["error"] + client.messages.errorStationURL);
} else if (!isNaN(number)) {
if (number > client.stations.length - 1) {
return msg.channel.send(client.messageEmojis["error"] + client.messages.wrongStationNumber);
} else {
url = client.stations[number].stream[client.stations[number].stream.default];
station = client.stations[number];
}
} else {
if (args[1].length < 3) return msg.channel.send(client.messageEmojis["error"] + client.messages.tooShortSearch);
const sstation = await searchStation(args.slice(1).join(' '), client);
if (!sstation) return msg.channel.send(client.messageEmojis["error"] + client.messages.noSearchResults);
url = sstation.stream[sstation.stream.default];
station = sstation;
}
if (radio) {
client.funcs.statisticsUpdate(client, msg.guild, radio);
radio.connection.dispatcher.destroy();
radio.station = station;
radio.textChannel = msg.channel;
play(msg.guild, client, url);
return;
}
const construct = {
textChannel: msg.channel,
voiceChannel: voiceChannel,
connection: null,
station: station,
volume: 5,
};
client.radio.set(msg.guild.id, construct);
try {
const connection = await voiceChannel.join();
construct.connection = connection;
let date = new Date();
construct.startTime = date.getTime();
play(msg.guild, client, url);
client.datastore.checkEntry(msg.guild.id);
construct.currentGuild = client.datastore.getEntry(msg.guild.id);
if(!construct.currentGuild.statistics[construct.station.name]){
construct.currentGuild.statistics[construct.station.name] = {};
construct.currentGuild.statistics[construct.station.name].time = 0;
construct.currentGuild.statistics[construct.station.name].used = 0;
client.datastore.updateEntry(msg.guild, construct.currentGuild);
}
} catch (error) {
console.log(error);
client.radio.delete(msg.guild.id);
return msg.channel.send(client.messageEmojis["error"] + `An error occured: ${error}`);
}
}
};
function play(guild, client, url) {
let message = {};
const radio = client.radio.get(guild.id);
const dispatcher = radio.connection
.play(url, { bitrate: "auto", volume: 1 })
.on("finish", () => {
console.log("Stream finished");
client.funcs.statisticsUpdate(client, guild, radio);
radio.voiceChannel.leave();
client.radio.delete(guild.id);
return;
});
dispatcher.on('error', error => {
console.error(error);
radio.voiceChannel.leave();
client.radio.delete(guild.id);
return radio.textChannel.send(client.messages.errorPlaying);
});
dispatcher.setVolume(radio.volume / 10);
message.play = client.messages.play.replace("%radio.station.name%", radio.station.name);
radio.textChannel.send(client.messageEmojis["play"] + message.play);
};
function searchStation(key, client) {
if (client.stations === null) return false;
let foundStations = [];
if (!key) return false;
if (key == 'radio') return false;
if (key.startsWith("radio ")) key = key.slice(6);
const probabilityIncrement = 100 / key.split(' ').length / 2;
for (let i = 0; i < key.split(' ').length; i++) {
client.stations.filter(x => x.name.toUpperCase().includes(key.split(' ')[i].toUpperCase()) || x === key).forEach(x => foundStations.push({ station: x, name: x.name, probability: probabilityIncrement }));
}
if (foundStations.length === 0) return false;
for (let i = 0; i < foundStations.length; i++) {
for (let j = 0; j < foundStations.length; j++) {
if (foundStations[i] === foundStations[j] && i !== j) foundStations.splice(i, 1);
}
}
for (let i = 0; i < foundStations.length; i++) {
if (foundStations[i].name.length > key.length) {
foundStations[i].probability -= (foundStations[i].name.split(' ').length - key.split(' ').length) * (probabilityIncrement * 0.5);
} else if (foundStations[i].name.length === key.length) {
foundStations[i].probability += (probabilityIncrement * 0.9);
}
for (let j = 0; j < key.split(' ').length; j++) {
if (!foundStations[i].name.toUpperCase().includes(key.toUpperCase().split(' ')[j])) {
foundStations[i].probability -= (probabilityIncrement * 0.5);
}
}
}
let highestProbabilityStation;
for (let i = 0; i < foundStations.length; i++) {
if (!highestProbabilityStation || highestProbabilityStation.probability < foundStations[i].probability) highestProbabilityStation = foundStations[i];
if (highestProbabilityStation && highestProbabilityStation.probability === foundStations[i].probability) {
highestProbabilityStation = foundStations[i].station;
}
}
return highestProbabilityStation;
};

View File

@ -1,27 +0,0 @@
module.exports = {
name: 'volume',
description: 'Volume command.',
alias: 'none',
usage: '<volume>',
permission: 'MANAGE_MESSAGES',
category: 'radio',
execute(msg, args, client, Discord, command) {
let message = {};
const radio = client.radio.get(msg.guild.id);
if (!args[1] && radio) {
message.currentVolume = client.messages.currentVolume.replace("%radio.volume%", radio.volume)
return msg.channel.send(message.currentVolume);
}
const volume = parseFloat(args[1]);
if (client.funcs.check(client, msg, command)) {
if (isNaN(volume)) return msg.channel.send(client.messages.invalidVolume);
if (volume > 100) return msg.channel.send(client.messages.maxVolume);
if (volume < 0) return msg.channel.send(client.messages.negativeVolume);
radio.volume = volume;
radio.connection.dispatcher.setVolume(volume / 5);
message.newVolume = client.messages.newVolume.replace("%volume%", volume);
return msg.channel.send(message.newVolume);
}
}
};

View File

@ -1,2 +0,0 @@
const radioClient = require("./client/class.js");
const client = new radioClient();

6451
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,12 @@
{
"name": "eximiabots-radiox",
"version": "0.1.0",
"version": "0.2.2",
"description": "Internet Radio to your Discord guild",
"main": "index.js",
"scripts": {
"start": "node ."
"build": "rimraf ./build && tsc",
"start": "node build/index.js",
"start:dev": "npm run build && node build/index.js"
},
"repository": {
"type": "git",
@ -16,10 +18,32 @@
"url": "https://github.com/warengroup/eximiabots-radiox/issues"
},
"dependencies": {
"@discordjs/opus": "^0.3.3",
"discord.js": "^12.5.3",
"dotenv": "^8.2.0",
"@discordjs/opus": "^0.5.3",
"@discordjs/voice": "^0.6.0",
"discord-api-types": "^0.22.0",
"discord.js": "^13.1.0",
"dotenv": "^10.0.0",
"libsodium-wrappers": "^0.7.9",
"node-fetch": "^2.6.1",
"path": "^0.12.7"
},
"devDependencies": {
"@types/node": "^16.6.1",
"@types/ws": "^7.4.7",
"@typescript-eslint/eslint-plugin": "^4.29.2",
"@typescript-eslint/parser": "^4.29.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"nodemon": "^2.0.12",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"ts-node": "^10.1.0",
"tsc-watch": "^4.4.0",
"typescript": "^4.3.5"
},
"engines": {
"node": ">=16.6.0",
"npm": ">=7.0.0"
}
}

67
src/Client.ts Normal file
View File

@ -0,0 +1,67 @@
import Discord, { Client, Collection } from "discord.js";
import fs from "fs";
const events = "./client/events/";
import Datastore from "./client/datastore.js";
import { command, radio } from "./client/utils/typings.js";
import config from "./config.js";
import messages from "./client/messages.js";
import path from "path"
const GatewayIntents = new Discord.Intents();
GatewayIntents.add(
1 << 0, // GUILDS
1 << 7, // GUILD_VOICE_STATES
1 << 9 // GUILD_MESSAGES
);
class RadioClient extends Client {
readonly commands: Collection<string, command>;
readonly commandAliases: Collection<string, command>;
readonly radio: Map<string, radio>;
public funcs: any;
readonly config = config;
readonly messages = messages;
public datastore: Datastore | null;
constructor() {
super({
intents: GatewayIntents
});
this.commands = new Collection();
this.commandAliases = new Collection();
this.radio = new Map();
this.datastore = null;
this.funcs = {};
this.funcs.check = require("./client/funcs/check.js");
this.funcs.checkFetchStatus = require("./client/funcs/checkFetchStatus.js");
this.funcs.isDev = require("./client/funcs/isDev.js");
this.funcs.msToTime = require("./client/funcs/msToTime.js");
this.funcs.statisticsUpdate = require("./client/funcs/statisticsUpdate.js");
const commandFiles = fs.readdirSync(path.join("./src/client/commands")).filter(f => f.endsWith(".js"));
for (const file of commandFiles) {
const command = require(`./client/commands/${file}`);
command.uses = 0;
this.commands.set(command.name, command);
this.commandAliases.set(command.alias, command);
}
this.on("ready", () => {
require(`${events}ready`).execute(this, Discord);
this.datastore = new Datastore();
});
this.on("messageCreate", msg => {
require(`${events}msg`).execute(this, msg, Discord);
});
this.on("voiceStateUpdate", (oldState, newState) => {
require(`${events}voiceStateUpdate`).execute(this, oldState, newState);
});
this.on("error", error => {
console.error(error);
});
this.login(this.config.token).catch(err => console.log("Failed to login: " + err));
}
}
export default RadioClient

View File

@ -17,7 +17,7 @@ module.exports = {
.setColor(client.config.embedColor)
.setDescription(message.bugDescription)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
msg.channel.send(embed);
msg.channel.send({ embeds: [embed] });
},
}
};

View File

@ -24,7 +24,7 @@ module.exports = {
.setColor(client.config.embedColor)
.setDescription(message.helpCommandDescription)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
msg.channel.send(embed);
msg.channel.send({ embeds: [embed] });
} else {
const categories = [];
for (let i = 0; i < client.commands.size; i++) {
@ -45,7 +45,7 @@ module.exports = {
.setColor(client.config.embedColor)
.setDescription(message.helpDescription)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
msg.channel.send(embed);
msg.channel.send({ embeds: [embed] });
}
}
};

View File

@ -11,8 +11,8 @@ module.exports = {
const embed = new Discord.MessageEmbed()
.setTitle(message.inviteTitle)
.setColor(client.config.embedColor)
.setURL(client.config.invite)
.setURL("https://discordapp.com/api/oauth2/authorize?client_id=" + client.user.id + "&permissions=3427328&scope=bot")
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
return msg.channel.send(embed);
return msg.channel.send({ embeds: [embed] });
}
};

View File

@ -23,6 +23,6 @@ module.exports = {
.setColor(client.config.embedColor)
.setDescription(stations)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
return msg.channel.send(embed);
return msg.channel.send({ embeds: [embed] });
}
};

View File

@ -27,15 +27,15 @@ module.exports = {
if(currentRadio){
client.funcs.statisticsUpdate(client, currentRadio.currentGuild.guild, currentRadio);
currentRadio.connection.dispatcher?.destroy();
currentRadio.voiceChannel.leave();
currentRadio.connection.destroy();
currentRadio.audioPlayer.stop();
const cembed = new Discord.MessageEmbed()
.setTitle(client.messages.maintenanceTitle)
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messageEmojis["maintenance"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor)
.setDescription(client.messages.sendedMaintenanceMessage)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
currentRadio.textChannel.send(cembed);
currentRadio.textChannel.send({ embeds: [cembed] });
client.radio.delete(radio.value);
stoppedRadios += "-" + radio.value + ": " + currentRadio.currentGuild.guild.name + "\n";
}
@ -48,6 +48,6 @@ module.exports = {
.setColor(client.config.embedColor)
.setDescription("Stopped all radios" + "\n" + stoppedRadios)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
return msg.channel.send(embed);
return msg.channel.send({ embeds: [embed] });
}
};

View File

@ -25,6 +25,6 @@ module.exports = {
.setColor(client.config.embedColor)
.setDescription(message.nowplayingDescription)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
return msg.channel.send(embed);
return msg.channel.send({ embeds: [embed] });
}
};

205
src/client/commands/play.js Normal file
View File

@ -0,0 +1,205 @@
const {
createAudioPlayer,
createAudioResource,
getVoiceConnection,
joinVoiceChannel
} = require("@discordjs/voice");
const { createDiscordJSAdapter } = require("../utils/adapter");
module.exports = {
name: "play",
alias: "p",
usage: "<song name>",
description: "Play some music.",
permission: "none",
category: "radio",
async execute(msg, args, client, Discord, command) {
let message = {};
let url = args[1] ? args[1].replace(/<(.+)>/g, "$1") : "";
const radio = client.radio.get(msg.guild.id);
const voiceChannel = msg.member.voice.channel;
if (!radio) {
if (!msg.member.voice.channel)
return msg.channel.send(
client.messageEmojis["error"] + client.messages.noVoiceChannel
);
} else {
if (voiceChannel !== radio.voiceChannel)
return msg.channel.send(
client.messageEmojis["error"] + client.messages.wrongVoiceChannel
);
}
if (!client.stations) {
message.errorToGetPlaylist = client.messages.errorToGetPlaylist.replace(
"%client.config.supportGuild%",
client.config.supportGuild
);
return msg.channel.send(client.messageEmojis["error"] + message.errorToGetPlaylist);
}
if (!args[1]) return msg.channel.send(client.messages.noQuery);
const permissions = voiceChannel.permissionsFor(msg.client.user);
if (!permissions.has("CONNECT")) {
return msg.channel.send(client.messageEmojis["error"] + client.messages.noPermsConnect);
}
if (!permissions.has("SPEAK")) {
return msg.channel.send(client.messageEmojis["error"] + client.messages.noPermsSpeak);
}
let station;
const number = parseInt(args[1] - 1);
if (url.startsWith("http")) {
return msg.channel.send(
client.messageEmojis["error"] + client.messages.errorStationURL
);
} else if (!isNaN(number)) {
if (number > client.stations.length - 1) {
return msg.channel.send(
client.messageEmojis["error"] + client.messages.wrongStationNumber
);
} else {
url = client.stations[number].stream[client.stations[number].stream.default];
station = client.stations[number];
}
} else {
if (args[1].length < 3)
return msg.channel.send(
client.messageEmojis["error"] + client.messages.tooShortSearch
);
const sstation = await searchStation(args.slice(1).join(" "), client);
if (!sstation)
return msg.channel.send(
client.messageEmojis["error"] + client.messages.noSearchResults
);
url = sstation.stream[sstation.stream.default];
station = sstation;
}
if (radio) {
client.funcs.statisticsUpdate(client, msg.guild, radio);
radio.audioPlayer.stop();
radio.station = station;
radio.textChannel = msg.channel;
play(msg.guild, client, url);
return;
}
const construct = {
textChannel: msg.channel,
voiceChannel: voiceChannel,
connection: null,
audioPlayer: createAudioPlayer(),
station: station
};
client.radio.set(msg.guild.id, construct);
try {
const connection =
getVoiceConnection(voiceChannel.guild.id) ??
joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: createDiscordJSAdapter(voiceChannel)
});
construct.connection = connection;
let date = new Date();
construct.startTime = date.getTime();
play(msg.guild, client, url);
client.datastore.checkEntry(msg.guild.id);
construct.currentGuild = client.datastore.getEntry(msg.guild.id);
if (!construct.currentGuild.statistics[construct.station.name]) {
construct.currentGuild.statistics[construct.station.name] = {};
construct.currentGuild.statistics[construct.station.name].time = 0;
construct.currentGuild.statistics[construct.station.name].used = 0;
client.datastore.updateEntry(msg.guild, construct.currentGuild);
}
} catch (error) {
console.log(error);
client.radio.delete(msg.guild.id);
return msg.channel.send(client.messageEmojis["error"] + `An error occured: ${error}`);
}
}
};
function play(guild, client, url) {
let message = {};
const radio = client.radio.get(guild.id);
const resource = createAudioResource(url);
radio.connection.subscribe(radio.audioPlayer);
radio.audioPlayer.play(resource);
resource.playStream
.on("readable", () => {
console.log("Stream started");
})
.on("finish", () => {
console.log("Stream finished");
client.funcs.statisticsUpdate(client, guild, radio);
radio.connection.destroy();
client.radio.delete(guild.id);
return;
})
.on("error", error => {
console.error(error);
radio.connection.destroy();
client.radio.delete(guild.id);
return radio.textChannel.send(client.messages.errorPlaying);
});
message.play = client.messages.play.replace("%radio.station.name%", radio.station.name);
radio.textChannel.send(client.messageEmojis["play"] + message.play);
}
function searchStation(key, client) {
if (client.stations === null) return false;
let foundStations = [];
if (!key) return false;
if (key == "radio") return false;
if (key.startsWith("radio ")) key = key.slice(6);
const probabilityIncrement = 100 / key.split(" ").length / 2;
for (let i = 0; i < key.split(" ").length; i++) {
client.stations
.filter(
x => x.name.toUpperCase().includes(key.split(" ")[i].toUpperCase()) || x === key
)
.forEach(x =>
foundStations.push({ station: x, name: x.name, probability: probabilityIncrement })
);
}
if (foundStations.length === 0) return false;
for (let i = 0; i < foundStations.length; i++) {
for (let j = 0; j < foundStations.length; j++) {
if (foundStations[i] === foundStations[j] && i !== j) foundStations.splice(i, 1);
}
}
for (let i = 0; i < foundStations.length; i++) {
if (foundStations[i].name.length > key.length) {
foundStations[i].probability -=
(foundStations[i].name.split(" ").length - key.split(" ").length) *
(probabilityIncrement * 0.5);
} else if (foundStations[i].name.length === key.length) {
foundStations[i].probability += probabilityIncrement * 0.9;
}
for (let j = 0; j < key.split(" ").length; j++) {
if (!foundStations[i].name.toUpperCase().includes(key.toUpperCase().split(" ")[j])) {
foundStations[i].probability -= probabilityIncrement * 0.5;
}
}
}
let highestProbabilityStation;
for (let i = 0; i < foundStations.length; i++) {
if (
!highestProbabilityStation ||
highestProbabilityStation.probability < foundStations[i].probability
)
highestProbabilityStation = foundStations[i];
if (
highestProbabilityStation &&
highestProbabilityStation.probability === foundStations[i].probability
) {
highestProbabilityStation = foundStations[i].station;
}
}
return highestProbabilityStation;
}

View File

@ -34,6 +34,6 @@ module.exports = {
.setColor(client.config.embedColor)
.setDescription(statistics)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
return msg.channel.send(embed);
return msg.channel.send({ embeds: [embed] });
}
};

View File

@ -21,7 +21,7 @@ module.exports = {
.addField(client.messages.statusField4, client.config.version, true)
.addField(client.messages.statusField5, client.config.hostedBy, true)
.setFooter(client.messages.footerText, "https://cdn.discordapp.com/emojis/" + client.messageEmojis["eximiabots"].replace(/[^0-9]+/g, ''));
msg.channel.send(embed);
msg.channel.send({ embeds: [embed] });
}
};

View File

@ -9,8 +9,8 @@ module.exports = {
const radio = client.radio.get(msg.guild.id);
if (client.funcs.check(client, msg, command)) {
client.funcs.statisticsUpdate(client, msg.guild, radio);
radio.connection.dispatcher.destroy();
radio.voiceChannel.leave();
radio.connection?.destroy();
radio.audioPlayer?.stop();
client.radio.delete(msg.guild.id);
msg.channel.send(client.messageEmojis["stop"] + client.messages.stop);
}

View File

@ -8,11 +8,15 @@ module.exports = class {
}
loadData() {
const dir = path.join(path.dirname(__dirname), '../datastore');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
//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 => f.endsWith('.json'));
for (const file of dataFiles) {
try {
const json = require(`../datastore/${file}`);
const json = require(`../../datastore/${file}`);
this.map.set(json.guild.id, json);
//console.log('[LOADED] ' + file + " (" + json.guild.id + ")");
//console.log(JSON.stringify(json, null, 4));
@ -115,7 +119,7 @@ module.exports = class {
saveEntry(file, data) {
data = JSON.stringify(data, null, 4);
fs.writeFile(path.join(path.dirname(__dirname), 'datastore') + "/" + file + ".json", data, 'utf8', function(err) {
fs.writeFile(path.join(path.dirname(__dirname), '../datastore') + "/" + file + ".json", data, 'utf8', function(err) {
if (err) {
//console.log(err);
}

View File

@ -25,9 +25,9 @@ module.exports = {
client.messageEmojis = {};
for (customEmojiName in customEmojis) {
customEmojiID = customEmojis[customEmojiName].replace(/[^0-9]+/g, '');
customEmoji = client.emojis.cache.get(customEmojiID);
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 {

View File

@ -10,7 +10,7 @@ module.exports = {
client.developers = "";
let user = "";
for (i = 0; i < client.config.devId.length; i++) {
for (let i = 0; i < client.config.devId.length; i++) {
user = await client.users.fetch(client.config.devId[i]);
if (i == client.config.devId.length - 1) {
client.developers += user.tag;

View File

@ -1,3 +1,9 @@
const {
getVoiceConnection,
joinVoiceChannel
} = require("@discordjs/voice");
const { createDiscordJSAdapter } = require("../utils/adapter");
module.exports = {
name: "voiceStateUpdate",
async execute(client, oldState, newState) {
@ -7,8 +13,10 @@ module.exports = {
if (!radio) return;
if (newState.member.id === client.user.id && oldState.member.id === client.user.id) {
if (newState.channel === null) {
client.funcs.statisticsUpdate(client, newState.guild, radio);
radio.audioPlayer?.stop();
return client.radio.delete(newState.guild.id);
}
@ -16,13 +24,20 @@ module.exports = {
if (!newPermissions.has("CONNECT") || !newPermissions.has("SPEAK") || !newPermissions.has("VIEW_CHANNEL")) {
try {
setTimeout(
async () => (radio.connection = await oldState.channel.join()),
async () => (
radio.connection = joinVoiceChannel({
channelId: oldState.channel.id,
guildId: oldState.channel.guild.id,
adapterCreator: createDiscordJSAdapter(oldState.channel)
})
//radio.connection = await oldState.channel.join()
),
1000
);
} catch (error) {
client.funcs.statisticsUpdate(client, newState.guild, radio);
radio.connection.dispatcher.destroy();
radio.voiceChannel.leave();
radio.connection?.destroy();
radio.audioPlayer?.stop();
client.radio.delete(oldState.guild.id);
}
return;
@ -30,16 +45,17 @@ module.exports = {
if (newState.channel !== radio.voiceChannel) {
change = true;
radio.voiceChannel = newState.channel;
radio.connection = await newState.channel.join();
radio.connection = getVoiceConnection(newState.channel.guild.id);
//radio.connection = await newState.channel.join();
}
}
if ((oldState.channel.members.size === 1 && oldState.channel === radio.voiceChannel) || change) {
setTimeout(() => {
if (!radio || !radio.connection.dispatcher || !radio.connection.dispatcher === null) return;
if (!radio || !radio.connection || !radio.connection === null) return;
if (radio.voiceChannel.members.size === 1) {
client.funcs.statisticsUpdate(client, newState.guild, radio);
radio.connection.dispatcher.destroy();
radio.voiceChannel.leave();
radio.connection?.destroy();
radio.audioPlayer?.stop();
client.radio.delete(newState.guild.id);
}
}, 120000);

View File

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

View File

@ -11,14 +11,10 @@ module.exports = function statisticsUpdate(client, guild, radio) {
client.datastore.updateEntry(guild, radio.currentGuild);
}
if(!radio.connection.dispatcher){
let date = new Date();
radio.currentTime = date.getTime();
radio.playTime = parseInt(radio.currentTime)-parseInt(radio.startTime);
radio.currentGuild.statistics[radio.station.name].time = parseInt(radio.currentGuild.statistics[radio.station.name].time)+parseInt(radio.playTime);
} else {
radio.currentGuild.statistics[radio.station.name].time = parseInt(radio.currentGuild.statistics[radio.station.name].time)+parseInt(radio.connection.dispatcher.streamTime.toFixed(0));
}
radio.currentGuild.statistics[radio.station.name].used = parseInt(radio.currentGuild.statistics[radio.station.name].used)+1;
client.datastore.updateEntry(guild, radio.currentGuild);

View File

@ -24,11 +24,6 @@ module.exports = {
errorPlaying: "An error has occured while playing radio!",
play: "Start playing: %radio.station.name%",
stop: "Stopped playback!",
currentVolume: "Current volume: **%radio.volume%**",
maxVolume: "The max volume is `100`!",
invalidVolume: "You need to enter a valid __number__.",
negativeVolume: "The volume needs to be a positive number!",
newVolume: "Volume is now: **%volume%**",
statisticsTitle: "Statistics",
maintenanceTitle: "Maintenance",
errorToGetPlaylist: "You can't use this bot because it has no playlist available. Check more information in our Discord support server %client.config.supportGuild% !",

View File

@ -0,0 +1,70 @@
import { DiscordGatewayAdapterCreator, DiscordGatewayAdapterLibraryMethods } from '@discordjs/voice';
import { VoiceChannel, Snowflake, Client, Constants, WebSocketShard, Guild, StageChannel } from 'discord.js';
import { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v9';
const adapters = new Map<Snowflake, DiscordGatewayAdapterLibraryMethods>();
const trackedClients = new Set<Client>();
/**
* Tracks a Discord.js client, listening to VOICE_SERVER_UPDATE and VOICE_STATE_UPDATE events.
* @param client - The Discord.js Client to track
*/
function trackClient(client: Client) {
if (trackedClients.has(client)) return;
trackedClients.add(client);
client.ws.on(Constants.WSEvents.VOICE_SERVER_UPDATE, (payload: GatewayVoiceServerUpdateDispatchData) => {
adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
});
client.ws.on(Constants.WSEvents.VOICE_STATE_UPDATE, (payload: GatewayVoiceStateUpdateDispatchData) => {
if (payload.guild_id && payload.session_id && payload.user_id === client.user?.id) {
adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
}
});
}
const trackedGuilds = new Map<WebSocketShard, Set<Snowflake>>();
function cleanupGuilds(shard: WebSocketShard) {
const guilds = trackedGuilds.get(shard);
if (guilds) {
for (const guildID of guilds.values()) {
adapters.get(guildID)?.destroy();
}
}
}
function trackGuild(guild: Guild) {
let guilds = trackedGuilds.get(guild.shard);
if (!guilds) {
const cleanup = () => cleanupGuilds(guild.shard);
guild.shard.on('close', cleanup);
guild.shard.on('destroyed', cleanup);
guilds = new Set();
trackedGuilds.set(guild.shard, guilds);
}
guilds.add(guild.id);
}
/**
* Creates an adapter for a Voice Channel
* @param channel - The channel to create the adapter for
*/
export function createDiscordJSAdapter(channel: VoiceChannel | StageChannel): DiscordGatewayAdapterCreator {
return (methods) => {
adapters.set(channel.guild.id, methods);
trackClient(channel.client);
trackGuild(channel.guild);
return {
sendPayload(data) {
if (channel.guild.shard.status === Constants.Status.READY) {
channel.guild.shard.send(data);
return true;
}
return false;
},
destroy() {
return adapters.delete(channel.guild.id);
},
};
};
}

View File

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

View File

@ -17,7 +17,6 @@ module.exports = {
//misc
embedColor: "#88aa00",
invite: "https://discordapp.com/api/oauth2/authorize?client_id=684109535312609409&permissions=3427328&scope=bot",
hostedBy: "[Warén Group](https://waren.io)",
//Settings

3
src/index.js Normal file
View File

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

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["esnext"],
"allowJs": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true
},
"exclude": ["build", "node_modules", "datastore"]
}