Compare commits

..

No commits in common. "master" and "0.5.2" have entirely different histories.

39 changed files with 3443 additions and 1441 deletions

View File

@ -1,5 +1,5 @@
DISCORD_TOKEN=
RADIOX_STATIONSLISTURL=https://eximiabots.waren.io/radiox/stations.json
RADIOX_STATIONSLISTURL=https://git.cwinfo.net/cwchristerw/radio/raw/branch/master/playlist.json
DEV_MODE=false
DEBUG_MODE=false
STREAMER_MODE=manual

16
.github/labeler.yml vendored
View File

@ -1,13 +1,9 @@
dependencies:
- changed-files:
- any-glob-to-any-file:
- package-lock.json
- package-lock.json
documentation:
- changed-files:
- any-glob-to-any-file:
- README.md
- SECURITY.md
- CONTRIBUTING.md
- LICENSE
- .env_example
- README.md
- SECURITY.md
- CONTRIBUTING.md
- LICENSE
- .env_example

View File

@ -43,7 +43,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -54,7 +54,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -68,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2

View File

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

View File

@ -11,7 +11,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/labeler@v5
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true

View File

@ -1,644 +0,0 @@
# CHANGELOG
## 0.5.9 (23.2.2025)
Patch Release
- Updating code, because Discord.js has deprecated few options previously used.
**Package**
- Dependencies Update
**Contributors:**
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.8 (30.9.2024)
Patch Release
**Package**
- Dependencies Update
**Documentation**
- Update radio stations list address and repo in README.md
- Fix versions 0.5.5-0.5.7 release years in CHANGELOG.md
**Contributors:**
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.7 (19.6.2024)
Patch Release
**Package**
- Dependencies Update
**Contributors:**
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.6 (8.6.2024)
Patch Release
**Package**
- Dependencies Update
**Documentation**
- Add CHANGELOG.md
**Contributors:**
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.5 (30.4.2024)
Patch Release
- Avoid refreshing player too often to keep in Discord API quotas.
**Package**
- Dependencies Update
**Miscellaneous:**
- Dockerfile: Use "docker.io/library/node:20-alpine" as upstream to image.
**Documentation**
- Use Podman in instructions.
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.4 (21.12.2023)
Patch Release
- Update new stationlistUrl address
- Change player interval to every 10 seconds in Play function
- Handle application commands better in commands.ts
- Handle DiscordAPIError: unknown interaction in uncaughtException event
- Remove audioPlayer maxMissedFrames in Streamer class
- Remove Bug command
- Remove Invite command
**Package**
- Dependencies Update
**Miscellaneous:**
- Dockerfile
- Github Workflow: Labeler (update)
**Docs**
- .env_example Update
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.3 (29.11.2023)
Patch Release
- Add duration to Play command
- Add RadioPlay playlist support to track info
- Remove Now Playing command
**Package**
- Dependencies Update
- Typescript Typings
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.2 (23.11.2023)
Patch Release
- Display track info in play and nowplaying commands
- Fix idling audioPlayer
**Package**
- Update Dependencies
- Typescript Typings
**Docs**
- Update supported versions list in SECURITY.md
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.1 (13.7.2023)
Patch Release
**Package**
- Update Dependencies
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.5.0 (9.6.2023)
Minor Release
- Create event listeners once in Streamer class.
- Limit commands in maintenance mode.
- Remove unnecessary await in Play command.
- Replace multiple forEach loop to for...of loops.
- Move events and funcs from RadioClient to events.ts and funcs.ts respectively.
- Remove execute functions in events and commands.ts.
- Move emojis into messages.ts.
- Fallback missing version into version 0.0.0.
- Change em dash to dash in Stations class.
- Remove messageCreate event and deprecation messages.
- Converted codebase to Typescript
**Package**
- NodeJS 18
- Use lockfileVersion 3
- Remove node-fetch dependency
- Update Dependencies
**Documentation**
- Removed version 0.4.x support in Security Policy
**Miscellaneous:**
- Dockerfile
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.4.3 (4.6.2023)
Patch Release
**Package**
- Update Dependencies
***Miscellaneous:***
- Github Workflow: Docker Build & TypeScript Build (update)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.4.2 (24.5.2023)
Patch Release
- Fix Status command
- Replaced SelectMenuBuilder (deprecated) with StringSelectMenuBuilder (Discord.js)
**Package**
- Update Dependencies
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.4.1 (29.11.2022)
Patch Release
**Package**
- Update Dependencies
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.4.0 (19.7.2022)
Minor Release
**Package**
- Update Dependencies
**Docs**
- Improviding Docker instructions in README.md
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.20 (7.4.2022)
Patch Release
- Added dashboard link to Statistics command. Preparations to [#24](<https://github.com/warengroup/eximiabots-radiox/issues/24>)
- Minor changes in Ready event and Stations class.
- Fixed multiple bugs [#286](<https://github.com/warengroup/eximiabots-radiox/issues/286>), [#284](<https://github.com/warengroup/eximiabots-radiox/issues/284>), [#283](<https://github.com/warengroup/eximiabots-radiox/issues/283>), [#227](<https://github.com/warengroup/eximiabots-radiox/issues/227>).
**Package**
- Update Dependencies
***Miscellaneous:***
- Github Workflow: Dependabot Auto-Merge (update)
**Docs**
- Improviding Docker instructions in README.md
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.19 (26.2.2022)
Patch Release
**Package**
- Update Dependencies
***Miscellaneous:***
- Github Workflow: Docker Build & TypeScript Build (update)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.18 (26.2.2022)
Patch Release
***Miscellaneous:***
- Github Workflow: Dependabot Auto-Merge (update)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.17 (26.2.2022)
Patch Release
**Package**
- Update Dependencies
***Miscellaneous:***
- Github Workflow: Dependabot Auto-Merge (update)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.16 (24.2.2022)
Patch Release
**Package**
- Update Dependencies
***Miscellaneous:***
- Github Workflow: Dependabot Auto-Merge (update)
**Docs**
- Update year in LICENSE
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.15 (21.2.2022)
Patch Release
**Package**
- Updated Dependencies
***Miscellaneous:***
- Github Workflow: CodeQL Analyze (update)
- Github Workflow: Dependabot Auto-Merge (new)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.14 (1.2.2022)
Patch Release
**Package**
- Updated Dependencies
***Miscellaneous:***
- Github Workflow: Typescript Build (updated)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.13 (21.12.2021)
Patch Release
- Listen function will use play to restart playing station when streamerMode is manual and audioPlayer has no subscribers in Streamer class
- Prevent bot restarting when uncaughtException event is caused by "DiscordAPIError - Unknown interaction" in uncaughtException event.
- Remove Discord.js voice audioResource event listeners in Streamer class
**Package**
- Updated Dependencies
***Miscellaneous:***
- Github Workflow: Typescript Build (updated)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.12 (30.11.2021)
Patch Release
- Add removal feature when station isn't working in Stations class
- Add direct type to search function in Stations class
- Add validation to station at restore function in Radio class
- Add manual mode at play function in Streamer class
- Update audioPlayer idle event in Streamer class
- Update fetch function in Stations class
- Change stationsListURL
- Move previous search function to text type at search function in Stations class
**Package**
- Updated Dependencies
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.11 (18.9.2021)
Patch Release
- Catch errors inside loadEntry method in Datastore class
- Fix memory leak bug in Streamer class
- Dont delete first streamer when refreshing streamers in Streamer class
- Fix maintenance command
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.10 (17.9.2021)
Patch Release
- Fix Stations class bug
- Prevent loadState function updating datastore entries everytime
- Streamlined restore method in Radio class with play command
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.9 (17.9.2021)
Patch Release
- Move Datastore class into classes folder.
- Add loadEntry method to Datastore class.
- Move calculateGlobal method from Datastore to Statistics class.
- Create Radio, Stations, Streamer, Statistics class.
- Commands are now set into map in commands.js.
- Remove application command options in maintenance command.
- Add Streamer Mode Manual and Streamer Mode Auto to selectMenu in maintenance command.
- Small fixes to next, play and prev command.
- Hide owner when its same as station name in nowplaying command.
- Update fields in status command.
- Delete message when using stop command in different textChannel.
- Small fixes to SIGINT, interactionCreate and ready event.
- Delete radio when no members in voiceChannel with excluding bot users in voiceStateUpdate event.
- Small fixes to check, isDev, listStations and logger function.
- Move checkFetchStatus function to Stations class.
- Delete message and send new message when textChannel has changed in play function.
- Hide owner when its same as station name in play function.
- Move restoreRadios function to Radio class.
- Move saveRadios function to Radio class.
- Move searchStation function to Statistics class.
- Move statisticsUpdate function to Statistics class.
- Update statusFields in messages.
- Rename maintenanceMode in config.
- Add Streamer Mode in config.
- Add Dev Mode in config.
**Package**
- Updated Dependencies
**Docs**
- Add new environment variables to .env_example file.
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.8 (10.9.2021)
Patch Release
- Add maintenance mode
- Node-fetch update to 3.0.0 with temporary solution
- Create exit event in Client.ts and added logger.
- Remove logger from SIGINT event
- Handle warnings in event instead of default warnings.
- Add logger to uncaughtException event
- Update login error catcher
**Package**
- Updated Dependencies
***Miscellaneous:***
- VSCode settings
**Docs**
- Contributing Guide CONTRIBUTING.md (new)
- Security Policy SECURITY.md (new)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.7 (7.9.2021)
Patch Release
- Fixed messageCreate event
**Package**
- Updated Dependencies
***Miscellaneous:***
- Github Workflow: Labeler (updated)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.6 (6.9.2021)
Patch Release
- Fixed prev & next command
- Changed forgotten interaction replies to ephemeral in commands.
- Handle uncaughtException event
- Tidied code
**Package**
- Updated Dependencies
***Miscellaneous:***
- Github Workflow: CodeQL Analyze (new), Labeler (new)
**Docs**
- Updated README.md
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.5 (6.9.2021)
Patch Release
- Avoid answering interaction that came from channels that bot has no rights to view.
- Hide decimals from global percent in statistics
- Check if there members when restoring radio instead of returning to empty channel and staying alone.
- Simplified listStations function and decided to hide one channel because it has maximum of 25 items in select menu options.
- Show unknown errors more transparently by using console.error function when needed.
- Moved restoreRadios function to funcs folder
- Created saveRadios function
- Updated SIGINT event: Removed code that was there before saveRadios function was separated into function script
- Added more controls to maintenance command
- Fixed play command
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>) & [Vekki000](<https://github.com/Vekki000>)
## 0.3.4 (5.9.2021)
Patch Release
- Created next & prev command
- Fixed typo in bug command code
- Added loggers to Slash Commands creation process
- Tidied code and moved few functions to funcs folder
- Disabled removing commands when bot is going offline
- Removed deprecated code that may have caused bot to restart unintentionally
**Package**
- Updated Dependencies
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.3 (4.9.2021)
Patch Release
- Changed few replies to ephemeral in nowplaying command.
- Fixed bug command
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.2 (3.9.2021)
Patch Release
- Added station logo to embed thumbnail
- Added empty image to make embeds same size
- Improved mobile user experience by removing unnecessary spaces in messages
- restoreradio.js is now checking that there is stations before continuing.
- Improved Dev bot to remove slash commands during process ending.
***Package:***
- Updated Dependencies
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.1 (3.9.2021)
Patch Release
- Added message command deprecation message
- Updated Invite link
- Added messageDelete event
- Edited play message
- Updated logger
- Updated list command
- Gracefully handling process ending when requested (SIGINT & SIGTERM)
- Update startTime when changing stations
- Remove play message when bot is disconnected from voice channel
- Removed references to prefix
- Removed unnecessary comments & messages
- Removed maintenance message in maintenance command because we will automatically resume playing after restart by saving and loading state.
***Package:***
- Updated Dependencies
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.3.0 (31.8.2021)
Minor Release
- Slash Commands
- Removed Message Commands
- Improved logging with new logger function
- Yle X is now searchable
- Ephemeral replies
- New invite link
- Using play command now gives you dropdown menu when no station id or name is given to command.
- Elapsed time is better shown because bot has improved msToTime function.
- New Emojis
- We may utilize new Discord features because bot can now handle new types of interactions.
- Version number in console
***Package:***
- Updated Dependencies
***Miscellaneous:***
- Dockerfile
- Github Workflow: TypeScript Build
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.2.4 (31.8.2021)
Patch Release
Changed voiceAdapterCreator to Discord.js instead of custom adapter. Should fix #26 indefinitely until major changes coming to Discord.js or Discord.js Voice.
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.2.3 (21.8.2021)
Patch Release
- Fixed help command (#28)
- Nulling connection after bot is disconnected
***Package:***
- Updated Dependencies
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.2.2 (21.8.2021)
Patch Release
Fixed #26 in voiceStateUpdate.js
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.2.1 (18.8.2021)
Patch Release
***Package:***
- Updated Dependencies
***Miscellaneous:***
- Dockerfile
- Github Workflow: Docker Build (new)
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>)
## 0.2.0 (8.8.2021)
Minor Release
***Miscellaneous:***
- eslint
- prettier
- Dockerfile
- TypeScript
__**Contributors:**__
[cwchristerw](<https://github.com/cwchristerw>) & [MatteZ02](<https://github.com/MatteZ02>)
## 0.1.0 (15.6.2021)
\-

View File

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

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2025 EximiaBots by Warén Group
Copyright (c) 2020-2023 EximiaBots by Warén Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,12 +1,8 @@
# RadioX by EximiaBots
Internet Radio to your Discord guild
## [Radio Stations List](https://eximiabots.waren.io/radiox/stations.json)
This bot is getting radio stations from our servers.
https://eximiabots.waren.io/radiox/stations.json
List is generated with cwchristerw's [radio](https://git.waren.io/cwchristerw/radio) repo. This list is currently maintained by Christer Warén. You can use alternative list with same format when using RADIOX_STATIONSLISTURL environment variable.
## [Radio Stations List](https://git.cwinfo.net/cwchristerw/radio)
This bot is using Gitea repo to get radio stations from [playlist.json](https://git.cwinfo.net/cwchristerw/radio/raw/branch/master/playlist.json) file. List is currently maintained by Christer Warén. You can use alternative list with same format when using RADIOX_STATIONSLISTURL environment variable.
## Docker
@ -14,35 +10,36 @@ List is generated with cwchristerw's [radio](https://git.waren.io/cwchristerw/ra
**Production**
```
podman build -t warengroup/eximiabots-radiox:latest . --pull
docker build -t warengroup/eximiabots-radiox:latest . --pull
```
**Beta**
```
podman build -t warengroup/eximiabots-radiox:latest-beta . --pull
docker build -t warengroup/eximiabots-radiox:latest-beta . --pull
```
**Dev**
```
podman build -t warengroup/eximiabots-radiox:latest-dev . --pull
docker build -t warengroup/eximiabots-radiox:latest-dev . --pull
```
### 2. Run Container
**Production**
```
podman run --name radiox -d -e DISCORD_TOKEN= -e STREAMER_MODE=auto -v "$PWD/datastore":/usr/src/app/datastore/ warengroup/eximiabots-radiox:latest
docker run --name radiox --net host -d -e DISCORD_TOKEN= -e STREAMER_MODE=auto -v "$PWD/datastore":/usr/src/app/datastore/ warengroup/eximiabots-radiox:latest
```
**Beta**
```
podman run --name radiox -d -e DISCORD_TOKEN= -e STREAMER_MODE=auto -v "$PWD/datastore":/usr/src/app/datastore/ warengroup/eximiabots-radiox:latest-beta
docker run --name radiox --net host -d -e DISCORD_TOKEN= -e STREAMER_MODE=auto -v "$PWD/datastore":/usr/src/app/datastore/ warengroup/eximiabots-radiox:latest-beta
```
**Dev**
```
podman run --rm --name radiox-dev -e DISCORD_TOKEN= -e DEV_MODE=true -v "$PWD":/usr/src/app/ warengroup/eximiabots-radiox:latest-dev
docker run --rm --name radiox-dev --net host -e DISCORD_TOKEN= -e DEV_MODE=true -v "$PWD":/usr/src/app/ warengroup/eximiabots-radiox:latest-dev
```
## Join our Discord Server
https://discord.gg/rRA65Mn

3519
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "eximiabots-radiox",
"version": "0.5.9",
"version": "0.5.2",
"description": "Internet Radio to your Discord guild",
"main": "index.js",
"scripts": {
@ -18,18 +18,32 @@
"url": "https://github.com/warengroup/eximiabots-radiox/issues"
},
"dependencies": {
"@discordjs/voice": "^0.18.0",
"discord.js": "^14.18.0",
"dotenv": "^16.4.7",
"libsodium-wrappers": "^0.7.15",
"@discordjs/builders": "^1.7.0",
"@discordjs/opus": "^0.9.0",
"@discordjs/rest": "^2.2.0",
"@discordjs/voice": "^0.16.1",
"discord-api-types": "^0.37.63",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"libsodium-wrappers": "^0.7.13",
"path": "^0.12.7"
},
"devDependencies": {
"rimraf": "^6.0.1",
"typescript": "^5.7.3"
"@types/node": "^20.9.4",
"@types/ws": "^8.5.9",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0",
"rimraf": "^5.0.5",
"ts-node": "^10.9.1",
"tsc-watch": "^6.0.4",
"typescript": "^5.2.2"
},
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
"node": ">=18.16.0",
"npm": ">=8.0.0"
}
}

View File

@ -42,7 +42,7 @@ export default class RadioClient extends Client {
console.log('RadioX ' + this.config.version);
console.log('Internet Radio to your Discord guild');
console.log('(c)2020-2024 EximiaBots by Warén Group');
console.log('(c)2020-2022 EximiaBots by Warén Group');
console.log('');
this.funcs.logger("Bot", "Starting");

View File

@ -76,7 +76,7 @@ export default class Datastore {
return this.map.get(id);
}
updateEntry(guild: Guild | { id: string, name?: string }, newData: datastore) {
updateEntry(guild: Guild | { id: string, name: string }, newData: datastore) {
newData.guild.name = guild.name;
let date = new Date();

View File

@ -1,20 +1,19 @@
import { Collection, GuildMember, Message, Guild, OAuth2Guild, TextBasedChannel, VoiceBasedChannel, VoiceChannel } from "discord.js";
import { DiscordGatewayAdapterCreator, getVoiceConnection, joinVoiceChannel, VoiceConnection } from "@discordjs/voice";
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: TextBasedChannel | null,
voiceChannel: VoiceBasedChannel | null,
connection: VoiceConnection | undefined,
message: Message | null,
textChannel: Channel | TextBasedChannel | undefined | null,
voiceChannel: Channel | VoiceBasedChannel | undefined,
connection: VoiceConnection | null,
message: null,
station: station,
datastore?: datastore,
currentTime?: number,
startTime: number,
playTime?: number,
guild?: Guild | { id: string, name?: string }
}
export interface state {
@ -29,7 +28,7 @@ export interface state {
}
}
export default class Radio extends Map<string, radio> {
export default class Radio extends Map {
constructor() {
super();
@ -76,13 +75,12 @@ export default class Radio extends Map<string, radio> {
let date = new Date();
const construct: radio = {
textChannel: client.channels.cache.get(state.channels.text) as TextBasedChannel,
voiceChannel: client.channels.cache.get(state.channels.voice) as VoiceBasedChannel,
connection: undefined,
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(),
guild: guild
startTime: date.getTime()
};
this.set(guild.id, construct);
@ -92,7 +90,7 @@ export default class Radio extends Map<string, radio> {
joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
adapterCreator: voiceChannel.guild.voiceAdapterCreator
});
construct.connection = connection;

View File

@ -8,10 +8,9 @@ export interface station {
[key: string]: string
},
playlist?: {
type: "radioplay" | "supla" | "yle",
type: "supla" | "yle",
address: string | string
}
track?: string;
}
export default class Stations extends Array {

View File

@ -1,4 +1,4 @@
import { Guild, OAuth2Guild } from "discord.js";
import { Guild } from "discord.js";
import RadioClient from "../../Client";
import { radio } from "./Radio";
@ -18,7 +18,7 @@ export default class Statistics {
this.map = new Map();
}
update(client: RadioClient, guild: Guild | { id: string, name?: string } | undefined, radio: radio) {
update(client: RadioClient, guild: Guild | null, radio: radio) {
if(!guild) return;
client.datastore?.checkEntry(guild.id);

View File

@ -51,13 +51,15 @@ export default class Streamer {
if(this.mode == "auto"){
audioPlayer = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play
noSubscriber: NoSubscriberBehavior.Play,
maxMissedFrames: Math.round(5000 / 20),
}
});
} else {
audioPlayer = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Stop
noSubscriber: NoSubscriberBehavior.Stop,
maxMissedFrames: Math.round(5000 / 20),
}
});
}

View File

@ -1,9 +1,12 @@
import { ApplicationCommand, ApplicationCommandManager, BaseGuild, Guild, GuildApplicationCommandManager, OAuth2Guild, Snowflake } from "discord.js";
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";
@ -19,17 +22,35 @@ export interface command {
}
export default async function commands(client: RadioClient) {
const commands1 : command[] = [ help, list, maintenance, next, play, prev, statistics, status, stop ];
const commands2 = await client.application?.commands.fetch();
const commands : command[] = [ bug, help, invite, list, maintenance, next, nowplaying, play, prev, statistics, status, stop ];
for(const command of commands1){
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(commands1){
for(const command of commands1){
await client.application?.commands.create({
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 || []
@ -37,24 +58,15 @@ export default async function commands(client: RadioClient) {
client.funcs.logger('Application Commands', 'Command: ' + command.name);
}
}
if(commands2){
commands2.forEach(async command2 => {
if(commands1.findIndex((command1) => command1.name == command2.name) == -1){
await client.application?.commands.delete(command2.id);
}
});
}
let guilds = await client.guilds.fetch();
guilds.forEach(async (guild: Guild | OAuth2Guild) => {
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

@ -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

@ -10,15 +10,26 @@ export default {
if(!client.user) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
flags: 'Ephemeral'
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.helpTitle)
.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, {
"%client.config.supportGuild%": client.config.supportGuild
"%commands%": commands
}))
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
@ -28,7 +39,7 @@ export default {
interaction.reply({
embeds: [embed],
flags: 'Ephemeral'
ephemeral: true
});
}
};

View File

@ -0,0 +1,32 @@
import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
import RadioClient from "../../Client";
export default {
name: 'invite',
description: 'Invite 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 embed = new EmbedBuilder()
.setTitle(client.messages.replace(client.messages.inviteTitle, {
"%client.user.username%": client.user.username
}))
.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
.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,50 +1,33 @@
import { ApplicationCommandOptionType, ButtonInteraction, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
import { ButtonInteraction, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { station } from "../classes/Stations";
export default {
name: 'list',
description: 'List stations',
options: [
{ type: ApplicationCommandOptionType.String, name: "query", description: "Select list", choices: [{"name": "1", "value": "1"},{"name": "2", "value": "2"}], required: false}
],
description: 'List radio stations',
category: 'radio',
execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient) {
if(client.config.maintenanceMode){
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
flags: 'Ephemeral'
ephemeral: true
});
}
if(!interaction.guild) return;
let query: string | null = null;
if(interaction.isChatInputCommand()){
query = interaction.options?.getString("query");
}
if(interaction.isStringSelectMenu()){
query = interaction.values?.[0];
}
if(!query) query = "1";
if(!client.stations) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
flags: 'Ephemeral'
ephemeral: true
});
}
const radio = client.radio?.get(interaction.guild.id);
const radio = client.radio?.get(interaction.guild?.id);
if(radio && !client.config.maintenanceMode){
client.funcs.listStations(client, interaction, query);
client.funcs.listStations(client, interaction);
} else {
let stations = `${client.stations.map((s: station) => `**#** ${s.name}`).join('\n')}`
const hashs = stations.split('**#**').length;
@ -65,7 +48,7 @@ export default {
interaction.reply({
embeds: [embed],
flags: 'Ephemeral'
ephemeral: true
});
}
}

View File

@ -11,7 +11,7 @@ export default {
if(!client.funcs.isDev(client.config.devIDs, interaction.user.id)) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.notAllowed,
flags: 'Ephemeral'
ephemeral: true
});
let action : number | string | null = null;
@ -107,7 +107,7 @@ export default {
return interaction.reply({
content: "**" + client.messages.maintenanceTitle + "**",
components: [menu],
flags: 'Ephemeral'
ephemeral: true
});
}
@ -124,7 +124,7 @@ export default {
interaction.reply({
embeds: [embed],
flags: 'Ephemeral'
ephemeral: true
});
let guilds = await client.guilds.fetch();
@ -183,7 +183,7 @@ export default {
client.user?.setStatus('idle');
client.radio?.save(client);
let timer : NodeJS.Timeout = setInterval(() => {
setInterval(() => {
if(client.radio?.size == 0 && client.config.streamerMode == "manual" && client.config.maintenanceMode){
client.streamer?.leave(client);
client.streamer = new Streamer();
@ -195,9 +195,9 @@ export default {
}
if(!client.config.maintenanceMode){
clearInterval(timer);
clearInterval(undefined);
}
}, 1000);
}, 500);
break;
case "11":
@ -207,7 +207,7 @@ export default {
client.user?.setStatus('idle');
client.radio?.save(client);
let timer2 : NodeJS.Timeout = setInterval(() => {
setInterval(() => {
if(client.radio?.size == 0 && client.config.streamerMode == "auto" && client.config.maintenanceMode){
client.streamer?.leave(client);
client.streamer = new Streamer();
@ -219,9 +219,9 @@ export default {
}
if(!client.config.maintenanceMode){
clearInterval(timer2);
clearInterval(undefined);
}
}, 1000);
}, 500);
break;
default:

View File

@ -9,14 +9,12 @@ export default {
category: 'radio',
async execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient, command: command) {
if (client.funcs.check(client, interaction, command)) {
if(!interaction.guild) return;
const radio = client.radio?.get(interaction.guild?.id);
if(!radio) return;
if(client.config.maintenanceMode){
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
flags: 'Ephemeral'
ephemeral: true
});
}
@ -25,7 +23,7 @@ export default {
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
flags: 'Ephemeral'
ephemeral: true
});
}
@ -36,7 +34,7 @@ export default {
if(!station) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noSearchResults,
flags: 'Ephemeral'
ephemeral: true
});
client.statistics?.update(client, interaction.guild, radio);

View File

@ -0,0 +1,62 @@
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);
if(radio.station?.playlist?.type == "supla" || radio.station?.playlist?.type == "yle"){
let playlist: any = await fetch(radio.station.playlist.address)
.then((response: Response) => response.json())
.catch(error => {
});
try {
switch(radio.station?.playlist.type){
case "supla":
radio.station.track = "__" + playlist.items[0]?.artist + "__" + "\n" + playlist.items[0]?.song;
break;
case "yle":
radio.station.track = "-";
break;
default:
radio.station.track = "-";
}
} catch(TypeError) {
}
}
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),
"\n\n%radio.station.track%": radio.station.track != undefined ? "\n\n" + radio.station.track : ""
}))
.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,5 +1,5 @@
import { ApplicationCommandOptionType, ChatInputCommandInteraction, GuildMember, PermissionFlagsBits, StringSelectMenuInteraction } from "discord.js";
import { DiscordGatewayAdapterCreator, getVoiceConnection, joinVoiceChannel } from "@discordjs/voice";
import { getVoiceConnection, joinVoiceChannel } from "@discordjs/voice";
import RadioClient from "../../Client";
import { radio } from "../classes/Radio"
@ -13,12 +13,10 @@ export default {
category: "radio",
async execute(interaction: ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient) {
if(!interaction.guild) return;
if(client.config.maintenanceMode){
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
flags: 'Ephemeral'
ephemeral: true
});
}
@ -27,7 +25,7 @@ export default {
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
flags: 'Ephemeral'
ephemeral: true
});
}
@ -42,42 +40,42 @@ export default {
}
if(!query){
return client.funcs.listStations(client, interaction, "1");
return client.funcs.listStations(client, interaction);
}
const radio = client.radio?.get(interaction.guild.id);
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,
flags: 'Ephemeral'
ephemeral: true
});
if (radio) {
if (voiceChannel !== radio.voiceChannel) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.wrongVoiceChannel,
flags: 'Ephemeral'
ephemeral: true
});
}
if (!query) return interaction.reply({
content: client.messages.noQuery,
flags: 'Ephemeral'
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,
flags: 'Ephemeral'
ephemeral: true
});
}
if (!permissions?.has(PermissionFlagsBits.Speak)) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noPermsSpeak,
flags: 'Ephemeral'
ephemeral: true
});
}
let station;
@ -87,7 +85,7 @@ export default {
if(number > client.stations.length - 1) {
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.wrongStationNumber,
flags: 'Ephemeral'
ephemeral: true
});
} else {
station = client.stations[number];
@ -96,7 +94,7 @@ export default {
if(query.length < 3) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.tooShortSearch,
flags: 'Ephemeral'
ephemeral: true
});
let type = "text";
@ -108,7 +106,7 @@ export default {
const sstation = client.stations.search(query, type);
if (!sstation) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noSearchResults,
flags: 'Ephemeral'
ephemeral: true
});
station = sstation;
}
@ -129,11 +127,10 @@ export default {
const construct: radio = {
textChannel: interaction.channel,
voiceChannel: voiceChannel,
connection: undefined,
connection: null,
message: null,
station: station,
startTime: date.getTime(),
guild: interaction.guild
startTime: date.getTime()
};
client.radio?.set(interaction.guild?.id, construct);
@ -143,7 +140,7 @@ export default {
joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild?.voiceAdapterCreator as DiscordGatewayAdapterCreator
adapterCreator: voiceChannel.guild.voiceAdapterCreator
});
construct.connection = connection;
let date = new Date();
@ -155,7 +152,7 @@ export default {
client.radio?.delete(interaction.guild?.id);
return interaction.reply({
content: client.messages.emojis["error"] + `An error occured: ${error}`,
flags: 'Ephemeral'
ephemeral: true
});
}
}

View File

@ -9,14 +9,12 @@ export default {
category: 'radio',
async execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient, command: command) {
if (client.funcs.check(client, interaction, command)) {
if(!interaction.guild) return;
const radio = client.radio?.get(interaction.guild?.id);
if(!radio) return;
if(client.config.maintenanceMode){
return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
flags: 'Ephemeral'
ephemeral: true
});
}
@ -25,7 +23,7 @@ export default {
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
flags: 'Ephemeral'
ephemeral: true
});
}
@ -36,7 +34,7 @@ export default {
if(!station) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.noSearchResults,
flags: 'Ephemeral'
ephemeral: true
});
client.statistics?.update(client, interaction.guild, radio);

View File

@ -10,7 +10,7 @@ export default {
if(!interaction.guild) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
flags: 'Ephemeral'
ephemeral: true
});
let currentGuild = client.datastore?.getEntry(interaction.guild.id);
@ -22,7 +22,7 @@ export default {
content: client.messages.emojis["error"] + client.messages.replace(client.messages.errorToGetPlaylist, {
"%client.config.supportGuild%": client.config.supportGuild
}),
flags: 'Ephemeral'
ephemeral: true
});
}
@ -45,7 +45,7 @@ export default {
interaction.reply({
embeds: [embed],
flags: 'Ephemeral'
ephemeral: true
});
}
};

View File

@ -9,7 +9,7 @@ export default {
if(!client.user) return interaction.reply({
content: client.messages.emojis["error"] + client.messages.maintenance,
flags: 'Ephemeral'
ephemeral: true
});
let uptime = client.funcs.msToTime(client.uptime || 0);
@ -35,7 +35,7 @@ export default {
interaction.reply({
embeds: [embed],
flags: 'Ephemeral'
ephemeral: true
});
}

View File

@ -1,4 +1,4 @@
import { ButtonInteraction, ChannelType, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
import { ButtonInteraction, ChatInputCommandInteraction, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { command } from "../commands";
@ -8,10 +8,7 @@ export default {
category: 'radio',
async execute(interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, client: RadioClient, command: command) {
if (client.funcs.check(client, interaction, command)) {
if(!interaction.guild) return;
const radio = client.radio?.get(interaction.guild?.id);
if(!radio) return;
if(radio.textChannel?.type == ChannelType.DM || radio.textChannel?.type == ChannelType.GroupDM) return;
client.statistics?.update(client, interaction.guild, radio);
radio.connection?.destroy();
client.funcs.logger('Radio', interaction.guild?.id + " / " + 'Stop');
@ -21,7 +18,7 @@ export default {
.setThumbnail("https://cdn.discordapp.com/emojis/" + client.messages.emojis["stop"].replace(/[^0-9]+/g, ''))
.setColor(client.config.embedColor)
.addFields({
name: client.messages.playTitle1,
name: client.messages.nowplayingTitle,
value: "-"
})
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
@ -31,9 +28,9 @@ export default {
});
if(!radio.message){
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [] }) ?? null;
radio.message = radio.textChannel.send({ embeds: [embed], components: [] });
} else {
if(radio.textChannel?.id == radio.message.channel.id){
if(radio.textChannel.id == radio.message.channel.id){
radio.message.edit({ embeds: [embed], components: [] });
} else {
radio.message?.delete();
@ -44,11 +41,11 @@ export default {
await radio.message?.delete();
}, 5000);
client.radio?.delete(interaction.guild.id);
client.radio?.delete(interaction.guild?.id);
interaction.reply({
content: client.messages.emojis["stop"] + client.messages.stop,
flags: 'Ephemeral'
ephemeral: true
});
}
}

View File

@ -3,15 +3,16 @@ 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 || interaction.channel?.type == ChannelType.GroupDM) return;
const permissions = interaction.channel?.permissionsFor(interaction.client.user);
if (!permissions?.has(PermissionFlagsBits.ViewChannel)) 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,
flags: 'Ephemeral'
});
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;
@ -23,7 +24,7 @@ export default function interactionCreate(client: RadioClient, interaction: Inte
} catch (error) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.runningCommandFailed,
flags: 'Ephemeral'
ephemeral: true
});
console.error(error);
}
@ -37,7 +38,7 @@ export default function interactionCreate(client: RadioClient, interaction: Inte
} catch (error) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.runningCommandFailed,
flags: 'Ephemeral'
ephemeral: true
});
console.error(error);
}

View File

@ -5,6 +5,6 @@ export default function uncaughtException(client: RadioClient, error: Error) {
console.log(error.stack);
console.log('');
if(error.name == "DiscordAPIError[10062]" && error.message == "Unknown interaction") return;
if(error.name == "DiscordAPIError" && error.message == "Unknown interaction") return;
process.emit('SIGINT');
}

View File

@ -1,6 +1,9 @@
import { GuildMember, PermissionFlagsBits, VoiceState } from "discord.js";
import RadioClient from "../../Client";
import { DiscordGatewayAdapterCreator, getVoiceConnection, joinVoiceChannel } from "@discordjs/voice";
const {
getVoiceConnection,
joinVoiceChannel
} = require("@discordjs/voice");
export default async function voiceStateUpdate(client: RadioClient, oldState: VoiceState, newState: VoiceState) {
if (oldState.channel === null) return;
@ -24,9 +27,9 @@ export default async function voiceStateUpdate(client: RadioClient, oldState: Vo
setTimeout(
async () => (
radio.connection = joinVoiceChannel({
channelId: oldState.channel?.id as string,
guildId: oldState.channel?.guild.id as string,
adapterCreator: oldState.channel?.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
channelId: oldState.channel?.id,
guildId: oldState.channel?.guild.id,
adapterCreator: oldState.channel?.guild.voiceAdapterCreator
})
),
1000
@ -50,7 +53,7 @@ export default async function voiceStateUpdate(client: RadioClient, oldState: Vo
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) {
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();

View File

@ -4,21 +4,20 @@ import { command } from "../commands";
export default function check(client: RadioClient, interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, command: command) {
if(!interaction.guild) return;
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
}),
flags: 'Ephemeral'
ephemeral: true
});
return false;
}
if (!radio) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.notPlaying,
flags: 'Ephemeral'
ephemeral: true
});
return false;
}
@ -26,7 +25,7 @@ export default function check(client: RadioClient, interaction: ButtonInteractio
if (interaction.member instanceof GuildMember && interaction.member?.voice.channel !== radio.voiceChannel) {
interaction.reply({
content: client.messages.emojis["error"] + client.messages.wrongVoiceChannel,
flags: 'Ephemeral'
ephemeral: true
});
return false;
}

View File

@ -1,12 +1,11 @@
import { ActionRowBuilder, ButtonInteraction, ChatInputCommandInteraction, SelectMenuComponentOptionData, StringSelectMenuBuilder, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
export default function listStations(client: RadioClient, interaction: ButtonInteraction | ChatInputCommandInteraction | StringSelectMenuInteraction, offset: string){
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,
@ -15,17 +14,6 @@ export default function listStations(client: RadioClient, interaction: ButtonInt
});
}
switch(offset){
case "1":
options = options.slice(0,Math.round(options.length/2));
break;
case "2":
options = options.slice(Math.round(options.length/2),options.length-1);
break;
default:
options = options.slice(0,Math.round(options.length/2));
}
const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
.addComponents(
new StringSelectMenuBuilder()
@ -37,6 +25,6 @@ export default function listStations(client: RadioClient, interaction: ButtonInt
return interaction.reply({
content: '**Select station:**',
components: [menu],
flags: 'Ephemeral'
ephemeral: true
});
}

View File

@ -1,4 +1,4 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, EmbedBuilder, Guild, OAuth2Guild, StringSelectMenuInteraction } from "discord.js";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, EmbedBuilder, Guild, OAuth2Guild, StringSelectMenuInteraction } from "discord.js";
import RadioClient from "../../Client";
import { station } from "../classes/Stations";
@ -6,43 +6,29 @@ export default async function play(client: RadioClient, interaction: ChatInputCo
if(!guild) return;
const radio = client.radio?.get(guild.id);
if(!radio) return;
if(radio.textChannel?.type == ChannelType.DM || radio.textChannel?.type == ChannelType.GroupDM) return;
const audioPlayer = client.streamer?.listen(station);
if(!audioPlayer) return;
radio.connection?.subscribe(audioPlayer);
radio.connection.subscribe(audioPlayer);
client.funcs.logger('Radio', guild.id + " / " + "Play" + " / " + radio.station.name);
if(radio.station.playlist){
if(radio.station.playlist.type == "radioplay" || radio.station.playlist.type == "supla" || radio.station.playlist.type == "yle"){
let playlist: any = await fetch(radio.station.playlist.address)
if(radio.station?.playlist?.type == "supla" || radio.station?.playlist?.type == "yle"){
let playlist: any = await fetch(radio.station.playlist.address)
.then((response: Response) => response.json())
.catch(error => {
});
radio.station.track = "-";
if(playlist){
switch(radio.station.playlist.type){
case "radioplay":
if(playlist[0] && playlist[0].stationNowPlaying && playlist[0].stationNowPlaying.nowPlayingArtist && playlist[0].stationNowPlaying.nowPlayingTrack){
radio.station.track = "__" + playlist[0].stationNowPlaying.nowPlayingArtist + "__" + "\n" + playlist[0].stationNowPlaying.nowPlayingTrack;
}
break;
case "supla":
if(playlist.items && playlist.items[0] && playlist.items[0].artist && playlist.items[0].song){
radio.station.track = "__" + playlist.items[0].artist + "__" + "\n" + playlist.items[0].song;
}
break;
case "yle":
if(playlist.data && playlist.data.performer && playlist.data.title){
radio.station.track = "__" + playlist.data.performer + "__" + "\n" + playlist.data.title;
}
break;
default:
radio.station.track = "-";
}
try {
switch(radio.station?.playlist.type){
case "supla":
radio.station.track = "__" + playlist.items[0]?.artist + "__" + "\n" + playlist.items[0]?.song;
break;
case "yle":
radio.station.track = "-";
break;
default:
radio.station.track = "-";
}
} catch(TypeError) {
}
}
@ -62,14 +48,7 @@ export default async function play(client: RadioClient, interaction: ChatInputCo
value: client.messages.replace(client.messages.playDescription2, {
"%radio.station.track%": radio.station.track != undefined ? "\n\n" + radio.station.track : "-"
})
},
{
name: client.messages.playTitle3,
value: client.messages.replace(client.messages.playDescription3, {
"%client.funcs.msToTime(completed)%": "-"
})
})
.setImage('https://waren.io/berriabot-temp-sa7a36a9xm6837br/images/empty-3.png')
.setFooter({
text: client.messages.footerText,
@ -109,110 +88,86 @@ export default async function play(client: RadioClient, interaction: ChatInputCo
);
if(!radio.message){
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [buttons] }) ?? null;
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [buttons] });
} else {
if(radio.textChannel?.id == radio.message.channel.id){
if(radio.textChannel.id == radio.message.channel.id){
radio.message.edit({ embeds: [embed], components: [buttons] });
} else {
radio.message?.delete();
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [buttons] }) ?? null;
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [buttons] });
}
}
const oldRadio = {...radio};
setInterval(async function(){
let changed = false;
let timer : NodeJS.Timeout = setInterval(async function(){
const radio = client.radio?.get(guild.id);
if(!radio || !oldRadio || radio.station.name != oldRadio.station.name || radio.textChannel?.type == ChannelType.DM || radio.textChannel?.type == ChannelType.GroupDM) {
return clearInterval(timer);
}
if(radio.station.playlist){
if(radio.station.playlist.type == "radioplay" || radio.station.playlist.type == "supla" || radio.station.playlist.type == "yle"){
let playlist: any = await fetch(radio.station.playlist.address)
if(radio.station?.playlist?.type == "supla" || radio.station?.playlist?.type == "yle"){
let playlist: any = await fetch(radio.station.playlist.address)
.then((response: Response) => response.json())
.catch(error => {
});
try {
switch(radio.station?.playlist.type){
case "supla":
if(radio.station.track != playlist.items[0].artist + "\n" + playlist.items[0].song){
changed = true;
radio.station.track = "__" + playlist.items[0].artist + "__" + "\n" + playlist.items[0].song;
}
break;
case "yle":
radio.station.track = "-";
break;
default:
radio.station.track = "-";
}
} catch(TypeError) {
radio.station.track = "-";
}
}
if(playlist){
switch(radio.station.playlist?.type){
case "radioplay":
if(playlist[0] && playlist[0].stationNowPlaying && playlist[0].stationNowPlaying.nowPlayingArtist && playlist[0].stationNowPlaying.nowPlayingTrack){
radio.station.track = "__" + playlist[0].stationNowPlaying.nowPlayingArtist + "__" + "\n" + playlist[0].stationNowPlaying.nowPlayingTrack;
}
break;
case "supla":
if(playlist.items && playlist.items[0] && playlist.items[0].artist && playlist.items[0].song){
radio.station.track = "__" + playlist.items[0].artist + "__" + "\n" + playlist.items[0].song;
}
break;
case "yle":
if(playlist.data && playlist.data.performer && playlist.data.title){
radio.station.track = "__" + playlist.data.performer + "__" + "\n" + playlist.data.title;
}
break;
default:
radio.station.track = "-";
}
if(changed == true){
const embed = new EmbedBuilder()
.setTitle(client.user?.username || "-")
.setThumbnail((radio.station.logo || "https://cdn.discordapp.com/emojis/" + client.messages.emojis["play"].replace(/[^0-9]+/g, '')))
.setColor(client.config.embedColor)
.addFields({
name: client.messages.playTitle1,
value: client.messages.replace(client.messages.playDescription1, {
"%radio.station.name%": radio.station.name,
"%radio.station.owner%": radio.station.name != radio.station.owner ? radio.station.owner + "\n" : ""
})
},
{
name: client.messages.playTitle2,
value: client.messages.replace(client.messages.playDescription2, {
"%radio.station.track%": radio.station.track != undefined ? "\n\n" + radio.station.track : "-"
})
})
.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, '')
});
if(!radio.message){
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [buttons] });
} else {
if(radio.textChannel.id == radio.message.channel.id){
radio.message.edit({ embeds: [embed], components: [buttons] });
} else {
radio.message?.delete();
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [buttons] });
}
}
}
let date = new Date();
radio.currentTime = date.getTime();
radio.playTime = radio.currentTime - radio.startTime;
const completed = (radio.playTime);
const embed = new EmbedBuilder()
.setTitle(client.user?.username || "-")
.setThumbnail((radio.station.logo || "https://cdn.discordapp.com/emojis/" + client.messages.emojis["play"].replace(/[^0-9]+/g, '')))
.setColor(client.config.embedColor)
.addFields({
name: client.messages.playTitle1,
value: client.messages.replace(client.messages.playDescription1, {
"%radio.station.name%": radio.station.name,
"%radio.station.owner%": radio.station.name != radio.station.owner ? radio.station.owner + "\n" : ""
})
},
{
name: client.messages.playTitle2,
value: client.messages.replace(client.messages.playDescription2, {
"%radio.station.track%": radio.station.track != undefined ? "\n\n" + radio.station.track : "-"
})
},
{
name: client.messages.playTitle3,
value: client.messages.replace(client.messages.playDescription3, {
"%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, '')
});
if(!radio.message){
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [buttons] }) ?? null;
} else {
if(radio.textChannel?.id == radio.message.channel.id){
radio.message?.edit({ embeds: [embed], components: [buttons] });
} else {
radio.message?.delete();
radio.message = await radio.textChannel?.send({ embeds: [embed], components: [buttons] }) ?? null;
}
}
},30000);
},15000);
interaction?.reply({
content: client.messages.emojis["play"] + client.messages.replace(client.messages.play, {
"%radio.station.name%": radio.station.name
}),
flags: 'Ephemeral'
ephemeral: true
});
}

View File

@ -2,8 +2,8 @@ import { Guild } from "discord.js";
import RadioClient from "../../Client";
import { radio } from "../classes/Radio";
export default function saveState(client: RadioClient, guild: Guild | { id: string, name?: string } | undefined, radio: radio){
if(!client.datastore || !guild) return;
export default function saveState(client: RadioClient, guild: Guild, radio: radio){
if(!client.datastore) return;
client.datastore.checkEntry(guild.id);
let date = new Date();

View File

@ -16,15 +16,18 @@ export const messages = {
notPlaying: "There is nothing playing!",
runningCommandFailed: "Running this command failed!",
noPermsEmbed: "I cannot send embeds (Embed links).",
helpTitle: "Help",
helpDescription: "Join to our support server" + "\n" + "%client.config.supportGuild%",
bugTitle: "Found a bug with %client.user.username%?",
bugDescription: "Join the support server" + "\n" + "%client.config.supportGuild%",
helpTitle: "%client.user.username% help:",
helpDescription: "%commands%",
inviteTitle: "Invite %client.user.username% to your Discord server!",
listTitle: "Radio Stations",
playTitle1: ":radio: Channel",
playDescription1: "__%radio.station.name%__" + "\n" + "%radio.station.owner%",
playTitle2: ":musical_note: Track",
playDescription2: "%radio.station.track%",
playTitle3: ":stopwatch: Duration",
playDescription3: "%client.funcs.msToTime(completed)%",
nowplayingTitle: "Now Playing",
nowplayingDescription: "**%radio.station.name%**" + "\n" + "%radio.station.owner%" + "\n" + "%client.funcs.msToTime(completed)%" + "\n\n" + "%radio.station.track%",
noVoiceChannel: "You need to be in a voice channel to play radio!",
noQuery: "You need to use a number or search for a supported station!",
noPermsConnect: "I cannot connect to your voice channel.",

View File

@ -6,7 +6,7 @@ export default {
token: process.env.DISCORD_TOKEN,
//radio stations
stationslistUrl: process.env.RADIOX_STATIONSLISTURL || "https://eximiabots.waren.io/radiox/stations.json",
stationslistUrl: process.env.RADIOX_STATIONSLISTURL || "https://git.cwinfo.net/cwchristerw/radio/raw/branch/master/playlist.json",
//support
supportGuild: "https://discord.gg/rRA65Mn",