Merge branch 'main' into dependabot/npm_and_yarn/array-move-4.0.0

This commit is contained in:
2024-11-24 18:49:10 +08:00
12 changed files with 270 additions and 83 deletions
-7
View File
@@ -1,7 +0,0 @@
contact_links:
- name: ❔ Questions / Help
url: https://github.com/eritislami/evobot/discussions
about: Please use GitHub Discussions for help and questions, do not start an issue.
- name: 📃 Discord.js Guide
url: https://discordjs.guide/
about: The official guide for discord.js
+5 -5
View File
@@ -1,7 +1,7 @@
ARG NODE_VERSION=18.18.2-slim
FROM node:${NODE_VERSION} as base
ENV USER=evobot
ENV USER=Apollo
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 build-essential && \
@@ -9,10 +9,10 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/*
RUN groupadd -r ${USER} && \
useradd --create-home --home /home/evobot -r -g ${USER} ${USER}
useradd --create-home --home /home/apollo -r -g ${USER} ${USER}
USER ${USER}
WORKDIR /home/evobot
WORKDIR /home/apollo
FROM base as build
@@ -26,7 +26,7 @@ RUN rm -rf node_modules && \
FROM node:${NODE_VERSION} as prod
COPY --chown=${USER}:${USER} package*.json ./
COPY --from=build --chown=${USER}:${USER} /home/evobot/node_modules ./node_modules
COPY --from=build --chown=${USER}:${USER} /home/evobot/dist ./dist
COPY --from=build --chown=${USER}:${USER} /home/apollo/node_modules ./node_modules
COPY --from=build --chown=${USER}:${USER} /home/apollo/dist ./dist
CMD [ "node", "./dist/index.js" ]
+2 -25
View File
@@ -1,15 +1,5 @@
![Node build](https://github.com/eritislami/evobot/actions/workflows/node.yml/badge.svg)
![Docker build](https://github.com/eritislami/evobot/actions/workflows/docker.yml/badge.svg)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
![logo](https://repository-images.githubusercontent.com/186841818/8aa95700-7730-11e9-84be-e80f28520325)
# 🤖 EvoBot (Discord Music Bot)
> EvoBot is a Discord Music Bot built with TypeScript, discord.js & uses Command Handler from [discordjs.guide](https://discordjs.guide)
## Requirements
<h1> Hello </h1>
1. Discord Bot Token **[Guide](https://discordjs.guide/preparations/setting-up-a-bot-application.html#creating-your-bot)**
1.1. Enable 'Message Content Intent' in Discord Developer Portal
2. Node.js 16.11.0 or newer
@@ -17,8 +7,6 @@
## 🚀 Getting Started
```sh
git clone https://github.com/eritislami/evobot.git
cd evobot
npm install
```
@@ -43,10 +31,9 @@ Copy or Rename `config.json.example` to `config.json` and fill out the values:
## 🐬 Docker Configuration
For those who would prefer to use our [Docker container](https://hub.docker.com/repository/docker/eritislami/evobot), you may provide values from `config.json` as environment variables.
```shell
docker run -e "TOKEN=<discord-token>" eritislami/evobot
docker run -e "TOKEN=<discord-token>" f04c/apollo
```
## 📝 Features & Commands
@@ -125,13 +112,3 @@ Currently available locales are:
- Vietnamese (vi)
- Check [Contributing](#-contributing) if you wish to help add more languages!
- For languages please use [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) two letter format
## 🤝 Contributing
1. [Fork the repository](https://github.com/eritislami/evobot/fork)
2. Clone your fork: `git clone https://github.com/your-username/evobot.git`
3. Create your feature branch: `git checkout -b my-new-feature`
4. Stage changes `git add .`
5. Commit your changes: `cz` OR `npm run commit` do not use `git commit`
6. Push to the branch: `git push origin my-new-feature`
7. Submit a pull request
+2 -4
View File
@@ -1,7 +1,5 @@
{
"name": "EvoBot",
"description": "🤖 Discord Music Bot built with discord.js",
"repository": "https://github.com/eritislami/evobot",
"logo": "https://i.imgur.com/JFxgbWH.png",
"name": "Apollo",
"description": "Discord Music Bot built with discord.js",
"keywords": ["discord", "discordjs", "typescript", "music", "bot"]
}
+3 -1
View File
@@ -21,7 +21,9 @@ export default {
});
helpEmbed.setTimestamp();
helpEmbed.setFooter({
text: "Made with ❤️ by F04C"
});
return interaction.reply({ embeds: [helpEmbed] }).catch(console.error);
}
};
+1
View File
@@ -5,4 +5,5 @@ export interface Config {
STAY_TIME: number;
DEFAULT_VOLUME: number;
LOCALE: string;
USE_INVIDIOUS_PROXY: boolean;
}
+31 -18
View File
@@ -1,15 +1,17 @@
{
"name": "evobot",
"name": "apollo",
"version": "2.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "evobot",
"name": "apollo",
"version": "2.9.0",
"dependencies": {
"@discordjs/voice": "^0.17.0",
"array-move": "^4.0.0",
"@discordjs/voice": "^0.18.0",
"array-move": "^3.0.1",
"discord.js": "^14.15.3",
"dotenv": "^16.4.5",
"ffmpeg-static": "^4.4.1",
@@ -476,23 +478,28 @@
}
},
"node_modules/@discordjs/voice": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.17.0.tgz",
"integrity": "sha512-hArn9FF5ZYi1IkxdJEVnJi+OxlwLV0NJYWpKXsmNOojtGtAZHxmsELA+MZlu2KW1F/K1/nt7lFOfcMXNYweq9w==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.18.0.tgz",
"integrity": "sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==",
"dependencies": {
"@types/ws": "^8.5.10",
"discord-api-types": "0.37.83",
"@types/ws": "^8.5.12",
"discord-api-types": "^0.37.103",
"prism-media": "^1.3.5",
"tslib": "^2.6.2",
"ws": "^8.16.0"
"tslib": "^2.6.3",
"ws": "^8.18.0"
},
"engines": {
"node": ">=16.11.0"
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/voice/node_modules/discord-api-types": {
"version": "0.37.107",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.107.tgz",
"integrity": "sha512-XOxmxnhtYIRH55kLTrc/JS3nJV1l3wfBtTptFiRGdGDOe2qdCT4DltpxSgskasfDrKfw71Z5quG4tYqTxyPJ7g=="
},
"node_modules/@discordjs/voice/node_modules/opusscript": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz",
@@ -525,6 +532,11 @@
}
}
},
"node_modules/@discordjs/voice/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@discordjs/ws": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.1.1.tgz",
@@ -691,9 +703,9 @@
}
},
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
"dependencies": {
"@types/node": "*"
}
@@ -2618,6 +2630,7 @@
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^3.2.7",
@@ -3606,9 +3619,9 @@
"devOptional": true
},
"node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
+2 -4
View File
@@ -1,13 +1,10 @@
{
"name": "evobot",
"name": "apollo",
"version": "2.9.0",
"description": "Discord music bot built with discord.js",
"main": "index.ts",
"author": "Erit Islami <eritislami@gmail.com>",
"private": true,
"homepage": "https://github.com/eritislami/evobot",
"repository": "github:eritislami/evobot",
"bugs": "https://github.com/eritislami/evobot/issues",
"engines": {
"node": ">=16.11.0"
},
@@ -22,6 +19,7 @@
"dependencies": {
"@discordjs/voice": "^0.17.0",
"array-move": "^4.0.0",
"discord.js": "^14.15.3",
"dotenv": "^16.4.5",
"ffmpeg-static": "^4.4.1",
+184 -18
View File
@@ -1,25 +1,160 @@
import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice";
import youtube from "youtube-sr";
import { i18n } from "../utils/i18n";
import { videoPattern, isURL } from "../utils/patterns";
import { isURL, videoPattern } from "../utils/patterns";
const { stream, video_basic_info } = require("play-dl");
import { Readable } from "stream";
import { extractID } from "../utils/extractor";
import { config } from "../utils/config";
const INVIDIOUS_BASE_URL = "https://inv.nadeko.net";
type InvidiousResponse = {
type: string;
title: string;
videoId: string;
videoThumbnails: Array<{
quality: string;
url: string;
width: number;
height: number;
}>;
storyboards: Array<{
url: string;
templateUrl: string;
width: number;
height: number;
count: number;
interval: number;
storyboardWidth: number;
storyboardHeight: number;
storyboardCount: number;
}>;
description: string;
descriptionHtml: string;
published: number;
publishedText: string;
keywords: Array<string>;
viewCount: number;
likeCount: number;
dislikeCount: number;
paid: boolean;
premium: boolean;
isFamilyFriendly: boolean;
allowedRegions: Array<string>;
genre: string;
genreUrl: any;
author: string;
authorId: string;
authorUrl: string;
authorVerified: boolean;
authorThumbnails: Array<{
url: string;
width: number;
height: number;
}>;
subCountText: string;
lengthSeconds: number;
allowRatings: boolean;
rating: number;
isListed: boolean;
liveNow: boolean;
isPostLiveDvr: boolean;
isUpcoming: boolean;
dashUrl: string;
adaptiveFormats: Array<{
init: string;
index: string;
bitrate: string;
url: string;
itag: string;
type: string;
clen: string;
lmt: string;
projectionType: string;
container: string;
encoding: string;
audioQuality?: string;
audioSampleRate?: number;
audioChannels?: number;
fps?: number;
size?: string;
resolution?: string;
qualityLabel?: string;
colorInfo?: {
primaries: string;
transferCharacteristics: string;
matrixCoefficients: string;
};
}>;
formatStreams: Array<{
url: string;
itag: string;
type: string;
quality: string;
bitrate: string;
fps: number;
size: string;
resolution: string;
qualityLabel: string;
container: string;
encoding: string;
}>;
captions: Array<{
label: string;
language_code: string;
url: string;
}>;
recommendedVideos: Array<{
videoId: string;
title: string;
videoThumbnails: Array<{
quality: string;
url: string;
width: number;
height: number;
}>;
author: string;
authorUrl: string;
authorId: string;
authorVerified: boolean;
lengthSeconds: number;
viewCountText: string;
viewCount: number;
}>;
};
const getBasicVideoInfo = async (url: string) => {
const videoId = extractID(url);
if (!videoId) {
throw new Error("Invalid YouTube URL");
}
const response = await fetch(`https://inv.nadeko.net/api/v1/videos/${videoId}`);
const data = (await response.json()) as InvidiousResponse;
return data;
};
export interface SongData {
url: string;
title: string;
duration: number;
adaptiveFormats?: InvidiousResponse["adaptiveFormats"];
}
export class Song {
public readonly url: string;
public readonly title: string;
public readonly duration: number;
public readonly adaptiveFormats?: InvidiousResponse["adaptiveFormats"];
public constructor({ url, title, duration }: SongData) {
public constructor({ url, title, duration, adaptiveFormats = [] }: SongData) {
this.url = url;
this.title = title;
this.duration = duration;
this.adaptiveFormats = adaptiveFormats;
}
public static async from(url: string = "", search: string = "") {
@@ -28,12 +163,13 @@ export class Song {
let songInfo;
if (isYoutubeUrl) {
songInfo = await video_basic_info(url);
songInfo = await getBasicVideoInfo(url);
return new this({
url: songInfo.video_details.url,
title: songInfo.video_details.title,
duration: parseInt(songInfo.video_details.durationInSec)
url,
title: songInfo.title,
duration: songInfo.lengthSeconds,
adaptiveFormats: songInfo.adaptiveFormats
});
} else {
const result = await youtube.searchOne(search);
@@ -50,31 +186,61 @@ export class Song {
throw err;
}
songInfo = await video_basic_info(`https://youtube.com/watch?v=${result.id}`);
songInfo = await getBasicVideoInfo(`https://youtube.com/watch?v=${result.id}`);
return new this({
url: songInfo.video_details.url,
title: songInfo.video_details.title,
duration: parseInt(songInfo.video_details.durationInSec)
url: `https://youtube.com/watch?v=${result.id}`,
title: songInfo.title,
duration: songInfo.lengthSeconds,
adaptiveFormats: songInfo.adaptiveFormats
});
}
}
public async makeResource(): Promise<AudioResource<Song> | void> {
let playStream;
const format = this.adaptiveFormats
?.sort((a, b) => {
return parseInt(b.bitrate!) - parseInt(a.bitrate!);
})
.find((format) => format.type.startsWith("audio/"));
const source = this.url.includes("youtube") ? "youtube" : "soundcloud";
if (!format) return;
if (source === "youtube") {
playStream = await stream(this.url);
let formatUrl = format.url;
if (config.USE_INVIDIOUS_PROXY) {
const invidiousUrl = new URL(INVIDIOUS_BASE_URL);
const formatUrlObject = new URL(format.url);
formatUrlObject.hostname = invidiousUrl.hostname;
formatUrl = formatUrlObject.toString();
}
if (!stream) return;
const response = await fetch(formatUrl);
return createAudioResource(playStream.stream, { metadata: this, inputType: playStream.type, inlineVolume: true });
const arrayBuffer = await response.arrayBuffer();
const stream = new Readable({
read() {
this.push(Buffer.from(arrayBuffer));
this.push(null);
}
});
const codec = format.encoding;
const type: StreamType =
codec === "opus" && format.container === "webm" ? StreamType.WebmOpus : StreamType.Arbitrary;
return createAudioResource(stream, {
metadata: this,
inputType: type,
inlineVolume: true
});
}
public startMessage() {
return i18n.__mf("play.startedPlaying", { title: this.title, url: this.url });
}
}
}
+2 -1
View File
@@ -12,7 +12,8 @@ try {
PRUNING: process.env.PRUNING === "true" ? true : false,
STAY_TIME: parseInt(process.env.STAY_TIME!) || 30,
DEFAULT_VOLUME: parseInt(process.env.DEFAULT_VOLUME!) || 100,
LOCALE: process.env.LOCALE || "en"
LOCALE: process.env.LOCALE || "en",
USE_INVIDIOUS_PROXY: process.env.USE_INVIDIOUS_PROXY === "true" ? true : false
};
}
+37
View File
@@ -0,0 +1,37 @@
import { videoIdPattern, videoPattern } from "../utils/patterns";
export const extractID = (url: string) => {
const url_ = url.trim();
if (url_.startsWith("https")) {
if (url_.indexOf("list=") === -1) {
const video_id = extractVideoId(url_);
if (!video_id) throw new Error("This is not a YouTube url or videoId or PlaylistID");
return video_id;
} else {
return url_.split("list=")[1].split("&")[0];
}
} else return url_;
};
function extractVideoId(urlOrId: string): string | false {
if (urlOrId.startsWith("https://") && urlOrId.match(videoPattern)) {
let id: string;
if (urlOrId.includes("youtu.be/")) {
id = urlOrId.split("youtu.be/")[1].split(/(\?|\/|&)/)[0];
} else if (urlOrId.includes("youtube.com/embed/")) {
id = urlOrId.split("youtube.com/embed/")[1].split(/(\?|\/|&)/)[0];
} else if (urlOrId.includes("youtube.com/shorts/")) {
id = urlOrId.split("youtube.com/shorts/")[1].split(/(\?|\/|&)/)[0];
} else if (urlOrId.includes("youtube.com/live/")) {
id = urlOrId.split("youtube.com/live/")[1].split(/(\?|\/|&)/)[0];
} else {
id = (urlOrId.split("watch?v=")[1] ?? urlOrId.split("&v=")[1]).split(/(\?|\/|&)/)[0];
}
if (id.match(videoIdPattern)) return id;
} else if (urlOrId.match(videoIdPattern)) {
return urlOrId;
}
return false;
}
+1
View File
@@ -4,3 +4,4 @@ export const scRegex = /^https?:\/\/(soundcloud\.com)\/(.*)$/;
export const mobileScRegex = /^https?:\/\/(soundcloud\.app\.goo\.gl)\/(.*)$/;
export const isURL =
/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
export const videoIdPattern = /^[a-zA-Z\d_-]{11,12}$/;