Compare commits

...

No commits in common. "3234e14a15cf066aa608fa61d05d39b9411eaa0c" and "4680bfecc92c6c9c2fd28c9d51792ac1de598500" have entirely different histories.

3234 changed files with 330580 additions and 2399 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/errors.txt /errors.txt
/unused_extras.ts /unused_extras.ts
/test /test
/node_modules

View file

@ -1 +1 @@
**Permanent Waves** is a Discord music bot based on JasperVanEsveld's [Discordeno Audio Plugin](https://github.com/JasperVanEsveld/discordeno-audio-plugin). It's self-hosted and must be run with Deno's `--unstable` flag since it relies on the unstable DatagramConn interface. **Permanent Waves** is a self-hosted Discord music bot based on [discord.js](https://github.com/discordjs/discord.js/).

View file

@ -1,4 +1,7 @@
import { Bot, Interaction } from "./deps.ts"; import chalk from 'chalk';
import { Interaction } from 'discord.js';
import { MusicSubscription } from './subscription';
import { help } from "./commands/help.ts"; import { help } from "./commands/help.ts";
import { invalidCommand } from "./commands/invalid_command.ts"; import { invalidCommand } from "./commands/invalid_command.ts";
import { leave } from "./commands/leave.ts"; import { leave } from "./commands/leave.ts";
@ -9,53 +12,51 @@ import { play } from "./commands/play.ts";
import { skip } from "./commands/skip.ts"; import { skip } from "./commands/skip.ts";
import { unloop } from "./commands/unloop.ts"; import { unloop } from "./commands/unloop.ts";
import { red } from "https://deno.land/std@0.161.0/fmt/colors.ts"; export async function parseCommand(interaction: Interaction, subscription: MusicSubscription) {
if(!interaction) {
export async function parseCommand(bot: Bot, interaction: Interaction) { console.log(chalk.red("invalid interaction data was passed through somehow:"));
if(!interaction.data) {
console.log(red("invalid interaction data was passed through somehow:"));
console.log(interaction); console.log(interaction);
return; return;
} }
switch(interaction.data.name) { switch(interaction.commandName) {
case "help": { case "help": {
await help(bot, interaction); await help(interaction);
break; break;
} }
case "leave": { case "leave": {
await leave(bot, interaction); await leave(interaction, subscription);
break; break;
} }
case "loop": { case "loop": {
await loop(bot, interaction); await loop(interaction, subscription);
break; break;
} }
case "np": { case "np": {
await np(bot, interaction); await np(interaction, subscription);
break; break;
} }
case "pause": { case "pause": {
await pause(bot, interaction); await pause(interaction, subscription);
break; break;
} }
case "play": { case "play": {
await play(bot, interaction); await play(interaction, subscription);
break; break;
} }
case "skip": { case "skip": {
await skip(bot, interaction); await skip(interaction, subscription);
break; break;
} }
case "stop": { case "stop": {
await pause(bot, interaction); await pause(interaction, subscription);
break; break;
} }
case "unloop": { case "unloop": {
await unloop(bot, interaction); await unloop(interaction, subscription);
break; break;
} }
default: { default: {
await invalidCommand(bot, interaction); await invalidCommand(interaction, subscription);
break; break;
} }
} }

View file

@ -1,63 +1,50 @@
import { import { Interaction } from 'discord.js';
Bot,
Interaction,
sendPrivateInteractionResponse,
type ApplicationCommandOption,
type ApplicationCommandOptionChoice,
type CreateSlashApplicationCommand,
type InteractionResponse
} from "../deps.ts";
const helpChoices = [ const helpChoices = [
<ApplicationCommandOptionChoice>{ {
name: "play", name: "play",
value: "play" value: "play"
} }
]; ];
const helpResponse = <InteractionResponse>{ const helpResponse = {
type: 4, // ChannelMessageWithSource content: `/help: displays this message\n/play: plays a song`,
data: { ephemeral: true
content: `/help: displays this message\n/play: plays a song`
}
} }
const playResponse = <InteractionResponse>{ const playResponse = {
type: 4, // ChannelMessageWithSource content: `/play: Add a song or playlist to the queue and starts the music if it's not already playing
data: {
content: `/play: Add a song or playlist to the queue and starts the music if it's not already playing
**Parameters:** **Parameters:**
url: A URL or video ID of the song or playlist to play` url: A URL or video ID of the song or playlist to play`,
} ephemeral: true
} }
export const helpCommand = <CreateSlashApplicationCommand>{ export const helpCommand = {
name: "help", name: "help",
description: "Lists the bot's commands and describes how to use them", description: "Lists the bot's commands and describes how to use them",
dmPermission: false,
options: [ options: [
<ApplicationCommandOption>{ {
type: 3, // string type: 'STRING' as const,
name: "command", name: "command",
description: "Displays additional info about a particular command", description: "Displays additional info about a particular command",
choices: helpChoices, choices: helpChoices,
required: false required: false
}, },
] ]
}; };
export async function help(bot: Bot, interaction: Interaction) { export async function help(interaction: Interaction) {
if(!interaction.guildId) return; if(!interaction.guildId) return;
if(!interaction.data) return; if(!interaction.data) return;
if(interaction.data.options) { if(interaction.data.options) {
switch(interaction.data.options[0].value) { switch(interaction.data.options[0].value) {
case "play": { case "play": {
await sendPrivateInteractionResponse(bot, interaction.id, interaction.token, playResponse); await interaction.reply(playResponse);
break; break;
} }
} }
} else { } else {
await sendPrivateInteractionResponse(bot, interaction.id, interaction.token, helpResponse); await interaction.reply(helpResponse);
} }
} }

View file

@ -1,18 +1,11 @@
import { import { Interaction } from 'discord.js';
Bot,
Interaction,
sendPrivateInteractionResponse,
type InteractionResponse
} from "../deps.ts";
export async function invalidCommand(bot: Bot, interaction: Interaction) { export async function invalidCommand(interaction: Interaction) {
if (!interaction.guildId) return; if (!interaction.guildId) return;
await sendPrivateInteractionResponse(bot, interaction.id, interaction.token, invalidCommandResponse); await interaction.reply(invalidCommandResponse);
} }
const invalidCommandResponse = <InteractionResponse>{ const invalidCommandResponse = {
type: 4, // ChannelMessageWithSource content: `Either you somehow sent an invalid command or waves didn't understand the command for some reason. Try again or poke sykora about it.`,
data: { ephemeral: true
content: `Either you somehow sent an invalid command or waves didn't understand the command for some reason. Try again or poke sykora about it.`
}
} }

View file

@ -1,34 +1,27 @@
import { import { Interaction } from 'discord.js';
Bot,
editOriginalInteractionResponse,
getConnectionData,
Interaction,
leaveVoiceChannel,
sendInteractionResponse,
type CreateSlashApplicationCommand
} from "../deps.ts";
import { formatCallbackData, waitingForResponse } from "../utils.ts"; import { subscriptions } from "../index";
import { MusicSubscription } from '../subscription';
import { formatCallbackData, waitingForResponse } from "../utils";
const notInVoiceResponse = formatCallbackData(`Permanent Waves isn't currently in a voice channel.`); const notInVoiceResponse = formatCallbackData(`Permanent Waves isn't currently in a voice channel.`);
const leftResponse = formatCallbackData(`Left channel.`); const leftResponse = formatCallbackData(`Left channel.`);
export const leaveCommand = <CreateSlashApplicationCommand>{ export const leaveCommand = {
name: "leave", name: "leave",
description: "Makes the bot leave the current voice channel", description: "Makes the bot leave the current voice channel"
dmPermission: false,
}; };
export async function leave(bot: Bot, interaction: Interaction) { export async function leave(interaction: Interaction, subscription: MusicSubscription) {
if (!interaction.guildId) return; if (!interaction.guildId) return;
const conn = getConnectionData(bot.id, interaction.guildId); await interaction.reply(waitingForResponse);
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse);
if(!conn.connectInfo.endpoint) { if(!subscription) {
await editOriginalInteractionResponse(bot, interaction.token, notInVoiceResponse); await interaction.editReply(notInVoiceResponse);
} else { } else {
await leaveVoiceChannel(bot, interaction.guildId); subscription.voiceConnection.destroy();
await editOriginalInteractionResponse(bot, interaction.token, leftResponse); subscriptions.delete(interaction.guildId);
await interaction.editReply(leftResponse);
} }
} }

View file

@ -1,45 +1,39 @@
import { import { Interaction } from 'discord.js';
Bot,
editOriginalInteractionResponse,
Interaction,
sendInteractionResponse,
type CreateSlashApplicationCommand
} from "../deps.ts";
import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils.ts"; import { subscriptions } from "../index";
import { MusicSubscription } from '../subscription';
import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils";
function alreadyLoopingResponse(bot: Bot, interaction: Interaction) { function alreadyLoopingResponse(interaction: Interaction) {
const player = bot.helpers.getPlayer(interaction.guildId); const subscription = subscriptions.get(interaction.guildId);
return formatCallbackData(`Looping is already enabled. return formatCallbackData(`Looping is already enabled.
Currently playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`); Currently playing: **${subscription.queue[0].title}**, added by ${subscription.queue[0].addedBy}`);
} }
const nothingToLoopResponse = formatCallbackData(`The queue is empty.`); const nothingToLoopResponse = formatCallbackData(`The queue is empty.`);
function loopEnabledResponse(bot: Bot, interaction: Interaction) { function loopEnabledResponse(interaction: Interaction) {
const player = bot.helpers.getPlayer(interaction.guildId); const subscription = subscriptions.get(interaction.guildId);
return formatCallbackData(`Looping has been enabled. return formatCallbackData(`Looping has been enabled.
Currently playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`); Currently playing: **${subscription.queue[0].title}**, added by ${subscription.queue[0].addedBy}`);
} }
export const loopCommand = <CreateSlashApplicationCommand>{ export const loopCommand = {
name: "loop", name: "loop",
description: "Loops the currently playijng song. All other songs remain in the queue", description: "Loops the currently playijng song. All other songs remain in the queue"
dmPermission: false,
}; };
export async function loop(bot: Bot, interaction: Interaction) { export async function loop(interaction: Interaction, subscription: MusicSubscription) {
if (!interaction.guildId) return; if (!interaction.guildId) return;
await ensureVoiceConnection(bot, interaction.guildId); await ensureVoiceConnection(interaction);
const player = bot.helpers.getPlayer(interaction.guildId); await interaction.reply(waitingForResponse);
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse);
if(!player.nowPlaying) { if(subscription.queue.length === 0) {
await editOriginalInteractionResponse(bot, interaction.token, nothingToLoopResponse); await interaction.editReply(nothingToLoopResponse);
} else if(!player.looping){ } else if(!subscription.looping){
await player.loop(true); subscription.looping = true;
await editOriginalInteractionResponse(bot, interaction.token, loopEnabledResponse(bot, interaction)); await interaction.editReply(interaction);
} else { } else {
await editOriginalInteractionResponse(bot, interaction.token, alreadyLoopingResponse(bot, interaction)); await interaction.editReply(interaction);
} }
} }

View file

@ -1,76 +1,48 @@
import { import { Interaction } from 'discord.js';
Bot,
Interaction,
InteractionResponse,
sendInteractionResponse,
sendMessage,
type CreateMessage,
type CreateSlashApplicationCommand,
type Embed,
type InteractionCallbackData
} from "../deps.ts";
import { configs } from "../configs.ts" import { configs } from "../configs"
import { subscriptions } from "../index";
import { MusicSubscription } from '../subscription';
import { formatCallbackData, getAllowedTextChannel } from "../utils";
import { bot } from "../main.ts";
import { getAllowedTextChannel } from "../utils.ts";
import { ConnectionData } from "../discordeno-audio-plugin/mod.ts";
export async function np(bot: Bot, interaction: Interaction) { export async function np(interaction: Interaction) {
await sendInteractionResponse(bot, interaction.id, interaction.token, nowPlayingResponse(bot, interaction)); await interaction.reply(nowPlayingResponse(interaction));
} }
function formatQueue(bot: Bot, interaction: Interaction) { function formatQueue(interaction: Interaction) {
const player = bot.helpers.getPlayer(interaction.guildId);
let formattedText = ""; let formattedText = "";
const subscription = subscriptions.get(interaction.guildId);
if(!player.nowPlaying) { if(subscription.queue.length === 0) {
return "Nothing is currently in the queue."; return "Nothing is currently in the queue.";
} else { } else {
formattedText = `Now playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}` formattedText = `Now playing: [**${subscription.nowPlaying.title}**](${subscription.nowPlaying.url}), added by ${subscription.nowPlaying.addedBy}`
} }
formattedText = formattedText.concat(`\nUp next:`); formattedText = formattedText.concat(`\nUp next:`);
for(let audioSource of player.upcoming()) { subscription.queue.forEach((track) => {
formattedText = formattedText.concat(`\n- **${audioSource.title}**, added by ${audioSource.added_by}`) formattedText = formattedText.concat(`\n- **${track.title}**, added by ${track.addedBy}`)
} });
return formattedText; return formattedText;
} }
function nowPlayingResponse(bot: Bot, interaction: Interaction) { function nowPlayingResponse(interaction: Interaction) {
return <InteractionResponse>{ return formatCallbackData(formatQueue(interaction), "In the queue");
type: 4,
data: <InteractionCallbackData>
{
content: "",
embeds: [<Embed>{
title: "In the queue",
color: configs.embed_color,
description: formatQueue(bot, interaction)
}]
}
}
} }
function nowPlayingMessage(bot: Bot, guildId: BigInt) { function nowPlayingMessage(track: Track) {
const player = bot.helpers.getPlayer(guildId); return formatCallbackData(`Now playing: [**${track.title}**](${track.url}), added by ${track.addedBy}`);
return <CreateMessage>{
embeds: [<Embed>{
color: configs.embed_color,
description: `Now playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`
}]
}
} }
export async function nowPlayingCallback(connectionData: ConnectionData) { export async function nowPlayingCallback(subscription: MusicSubscription) {
const channel = await getAllowedTextChannel(bot, connectionData.guildId); const channel = await getAllowedTextChannel(subscription);
await sendMessage(bot, channel.id, nowPlayingMessage(bot, connectionData.guildId)); await channel.send(nowPlayingMessage(subscription.nowPlaying));
} }
export const npCommand = <CreateSlashApplicationCommand>{ export const npCommand = {
name: "np", name: "np",
description: "Shows the currently-playing song along with the next five songs in the queue", description: "Shows the currently-playing song along with the next five songs in the queue"
dmPermission: false
}; };

View file

@ -1,12 +1,8 @@
import { import { Interaction } from 'discord.js';
Bot,
editOriginalInteractionResponse,
Interaction,
sendInteractionResponse,
type CreateSlashApplicationCommand
} from "../deps.ts";
import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils.ts"; import { subscriptions } from "../index.js";
import { MusicSubscription } from '../subscription';
import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils";
const alreadyPausedResponse = formatCallbackData(`The player is already paused.`); const alreadyPausedResponse = formatCallbackData(`The player is already paused.`);
@ -14,33 +10,32 @@ const emptyQueueResponse = formatCallbackData(`There's nothing in the queue righ
const nowPausedResponse = formatCallbackData(`The player has been paused.`); const nowPausedResponse = formatCallbackData(`The player has been paused.`);
export const pauseCommand = <CreateSlashApplicationCommand>{ export const pauseCommand = {
name: "pause", name: "pause",
description: "Pauses the player", description: "Pauses the player"
dmPermission: false,
}; };
export const stopCommand = <CreateSlashApplicationCommand>{ export const stopCommand = {
name: "stop", name: "stop",
description: "Pauses the player, alias for /pause", description: "Pauses the player, alias for /pause"
dmPermission: false,
}; };
export async function pause(bot: Bot, interaction: Interaction) { export async function pause(interaction: Interaction) {
if (!interaction.guildId) return; if (!interaction.guildId) return;
await ensureVoiceConnection(bot, interaction.guildId); const subscription = subscriptions.get(interaction.guildId);
const player = bot.helpers.getPlayer(interaction.guildId); await ensureVoiceConnection(interaction);
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse); await interaction.reply(waitingForResponse);
if(player.playing && !player.waiting) { if(subscription.playing) {
if(player.nowPlaying) { if(player.nowPlaying) {
await player.pause(); await subscription.audioPlayer.pause();
await editOriginalInteractionResponse(bot, interaction.token, nowPausedResponse); subscription.playing = false;
await interaction.editReply(nowPausedResponse);
} else { } else {
await editOriginalInteractionResponse(bot, interaction.token, emptyQueueResponse); await interaction.editReply(emptyQueueResponse);
} }
} else { } else {
await editOriginalInteractionResponse(bot, interaction.token, alreadyPausedResponse); await interaction.editReply(alreadyPausedResponse);
} }
} }

View file

@ -1,30 +1,24 @@
import { import { Interaction } from 'discord.js';
Bot, import { YouTube } from "youtube-sr";
editOriginalInteractionResponse,
Interaction,
sendInteractionResponse,
type ApplicationCommandOption,
type CreateSlashApplicationCommand
} from "../deps.ts";
import { YouTube } from "../discordeno-audio-plugin/deps.ts"; import { nowPlayingCallback } from './np.ts';
import { subscriptions } from "../index.js";
import { MusicSubscription } from '../subscription';
import { Track } from '../track';
import { ensureVoiceConnection, formatCallbackData, isPlaylist, waitingForResponse } from "../utils";
import { ensureVoiceConnection, formatCallbackData, isPlaylist, waitingForResponse } from "../utils.ts"; function addedPlaylistResponse(interaction: Interaction, url: string, length: number, title: string) {
return formatCallbackData(`${interaction.user.username} added ${length} videos from [**${title}**](${url}) to the queue.`);
async function addedPlaylistResponse(interaction: Interaction, url: string) {
const playlist = await YouTube.getPlaylist(url);
return formatCallbackData(`${interaction.user.username} added ${playlist.videoCount} videos from [**${playlist.title}**](${interaction!.data!.options![0].value}) to the queue.`,
"Added playlist");
} }
function addedSongResponse(interaction: Interaction, title: string) { function addedSongResponse(interaction: Interaction, title: string) {
return formatCallbackData(`${interaction.user.username} added [**${title}**](${interaction!.data!.options![0].value}) to the queue.`, "Added song"); return formatCallbackData(`${interaction.user.username} added [**${title}**](${interaction.options.get('url').value as string}) to the queue.`);
} }
function alreadyPlayingResponse(bot: Bot, interaction: Interaction) { function alreadyPlayingResponse(bot: Bot, interaction: Interaction) {
const player = bot.helpers.getPlayer(interaction.guildId); const subscription = subscriptions.get(interaction.guildId);
return formatCallbackData(`The bot is already playing. return formatCallbackData(`The bot is already playing.
Currently playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`); Currently playing: **${interaction.queue[0].title}**, added by ${interaction.queue[0].addedBy}`);
} }
const badUrlResponse = formatCallbackData(`Bad URL, please enter a URL that starts with https://youtube.com or https://youtu.be.`); const badUrlResponse = formatCallbackData(`Bad URL, please enter a URL that starts with https://youtube.com or https://youtu.be.`);
@ -37,13 +31,13 @@ function nowPlayingResponse(bot: Bot, interaction: Interaction) {
Currently playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`); Currently playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`);
} }
export const playCommand = <CreateSlashApplicationCommand>{ export const playCommand = {
name: "play", name: "play",
description: "Adds a song or playlist to the queue and starts the music if it's not already playing", description: "Adds a song or playlist to the queue and starts the music if it's not already playing",
dmPermission: false, dmPermission: false,
options: [ options: [
<ApplicationCommandOption>{ {
type: 3, // string type: 'STRING' as const,
name: "url", name: "url",
description: "The URL or video ID of the song or playlist to play", description: "The URL or video ID of the song or playlist to play",
required: false required: false
@ -51,55 +45,55 @@ export const playCommand = <CreateSlashApplicationCommand>{
] ]
}; };
export async function play(bot: Bot, interaction: Interaction) { export async function play(interaction: Interaction) {
if (!interaction.guildId) return; if (!interaction.guildId) return;
await ensureVoiceConnection(bot, interaction.guildId); await interaction.reply(waitingForResponse);
const player = bot.helpers.getPlayer(interaction.guildId); await ensureVoiceConnection(interaction);
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse); const subscription = subscriptions.get(interaction.guildId);
let parsed_url; if(interaction.options.get('url') !== undefined) {
if(!interaction) return; const url = interaction.options.get('url')!.value! as string;
if(!interaction.data) return;
if(interaction.data.options) { if(isPlaylist(url)) {
if(!interaction.data.options[0].value) return; const videos = await YouTube.getPlaylist(url, {fetchAll: true});
videos.videos.forEach(async (video) => {
try { const track = await createTrack(interaction, subscription, `https://youtube.com/watch?v=${video.id}`);
parsed_url = new URL(interaction.data.options[0].value.toString()); await subscription.enqueue(track);
} catch { });
await editOriginalInteractionResponse(bot, interaction.token, badUrlResponse); await interaction.editReply(addedPlaylistResponse(interaction, url, videos.videos.length, videos.title));
}
if(!parsed_url) return;
let href;
// remove the timestamp from the query
if(parsed_url.href.indexOf("?t=") !== -1) {
href = parsed_url.href.substring(0, parsed_url.href.indexOf("?"))
} else { } else {
href = parsed_url.href; const track = await createTrack(interaction, subscription, url);
} await subscription.enqueue(track);
await interaction.editReply(addedSongResponse(interaction, track.title))
const result = await player.pushQuery(interaction.guildId, interaction.user.username, href);
if(result && result[0] && parsed_url.href.indexOf("youtube.com") !== -1 || parsed_url.href.indexOf("youtu.be") !== -1 && result[0].title) {
if(isPlaylist(parsed_url.href))
{
await editOriginalInteractionResponse(bot, interaction.token, await addedPlaylistResponse(interaction, parsed_url.href));
} else {
await editOriginalInteractionResponse(bot, interaction.token, addedSongResponse(interaction, result[0].title));
}
} }
} else { } else {
// restart the player if there's no url if(!subscription.playing) {
if(player.waiting || !player.playing) { if(subscription.queue.length === 0) {
if(player.nowPlaying) { await interaction.editReply(emptyQueueResponse);
await player.play();
await editOriginalInteractionResponse(bot, interaction.token, nowPlayingResponse(bot, interaction));
} else { } else {
await editOriginalInteractionResponse(bot, interaction.token, emptyQueueResponse); subscription.audioPlayer.unpause();
await interaction.editReply(nowPlayingResponse(interaction));
} }
} else { } else {
await editOriginalInteractionResponse(bot, interaction.token, alreadyPlayingResponse(bot, interaction)); await interaction.editReply(alreadyPlayingResponse(interaction))
} }
} }
} }
async function createTrack(interaction: Interaction, subscription: MusicSubscription, url: string) {
return await Track.from(url, interaction.user.displayName, {
onStart() {
try {
nowPlayingCallback(subscription);
} catch {
console.warn();
}
},
onFinish() {
},
onError(error) {
console.warn(error);
interaction.followUp({ content: `Error: ${error.message}`, ephemeral: true }).catch(console.warn);
},
});
}

View file

@ -1,33 +1,27 @@
import { import { Interaction } from 'discord.js';
Bot,
editOriginalInteractionResponse,
Interaction,
sendInteractionResponse,
type CreateSlashApplicationCommand,
} from "../deps.ts";
import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils.ts"; import { subscriptions } from "../index";
import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils";
const nothingToSkipResponse = formatCallbackData(`The queue is empty.`); const nothingToSkipResponse = formatCallbackData(`The queue is empty.`);
const skippedResponse = formatCallbackData(`The song has been skipped.`); const skippedResponse = formatCallbackData(`The song has been skipped.`);
export const skipCommand = <CreateSlashApplicationCommand>{ export const skipCommand = {
name: "skip", name: "skip",
description: "Skips the current song", description: "Skips the current song"
dmPermission: false,
}; };
export async function skip(bot: Bot, interaction: Interaction) { export async function skip( interaction: Interaction) {
if (!interaction.guildId) return; if (!interaction.guildId) return;
await ensureVoiceConnection(bot, interaction.guildId); await ensureVoiceConnection(interaction);
const player = bot.helpers.getPlayer(interaction.guildId); const subscription = subscriptions.get(interaction.guildId);
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse); await interaction.followUp(waitingForResponse);
if(!player.nowPlaying) { if(!subscription.playing) {
await editOriginalInteractionResponse(bot, interaction.token, nothingToSkipResponse); await interaction.editReply(nothingToSkipResponse);
} else { } else {
await player.skip(); await subscription.audioPlayer.stop();
await editOriginalInteractionResponse(bot, interaction.token, skippedResponse); await interaction.editReply(skippedResponse);
} }
} }

View file

@ -1,45 +1,39 @@
import { import { Interaction } from 'discord.js';
Bot,
editOriginalInteractionResponse,
Interaction,
sendInteractionResponse,
type CreateSlashApplicationCommand
} from "../deps.ts";
import { subscriptions } from "../index.js";
import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils.ts"; import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils.ts";
function notLoopingResponse(bot: Bot, interaction: Interaction) { function notLoopingResponse(interaction: Interaction) {
const player = bot.helpers.getPlayer(interaction.guildId); const subscription = subscriptions.get(interaction.guildId);
return formatCallbackData(`Looping is already disabled. return formatCallbackData(`Looping is already disabled.
Currently playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`); Currently playing: **${subscription.queue[0].title}**, added by ${subscriptions.queue[0].added_by}`);
} }
const nothingToLoopResponse = formatCallbackData(`The queue is empty.`); const nothingToLoopResponse = formatCallbackData(`The queue is empty.`);
function loopDisabledResponse(bot: Bot, interaction: Interaction) { function loopDisabledResponse(interaction: Interaction) {
const player = bot.helpers.getPlayer(interaction.guildId); const subscription = subscriptions.get(interaction.guildId);
return formatCallbackData(`Looping the current song has been disabled. return formatCallbackData(`Looping the current song has been disabled.
Currently playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`); Currently playing: **${subscription.queue[0].title}**, added by ${subscription.queue[0].addedBy}`);
} }
export const unloopCommand = <CreateSlashApplicationCommand>{ export const unloopCommand = {
name: "unloop", name: "unloop",
description: "Disables looping the current song. At the current song's end, the queue will proceed as normal.", description: "Disables looping the current song. At the current song's end, the queue will proceed as normal."
dmPermission: false,
}; };
export async function unloop(bot: Bot, interaction: Interaction) { export async function unloop(interaction: Interaction) {
if (!interaction.guildId) return; if (!interaction.guildId) return;
await ensureVoiceConnection(bot, interaction.guildId); await ensureVoiceConnection(interaction);
const player = bot.helpers.getPlayer(interaction.guildId); const subscription = subscriptions.get(interaction.guildId);
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse); await interaction.reply(waitingForResponse);
if(!player.nowPlaying) { if(!subscription.playing) {
await editOriginalInteractionResponse(bot, interaction.token, nothingToLoopResponse); await interaction.editReply(interaction.token, nothingToLoopResponse);
} else if(player.looping){ } else if(subscription.looping){
await player.loop(false); subscription.audioPlayer.loop = false;
await editOriginalInteractionResponse(bot, interaction.token, loopDisabledResponse(bot, interaction)); await interaction.editReply(loopDisabledResponse(interaction));
} else { } else {
await editOriginalInteractionResponse(bot, interaction.token, notLoopingResponse(bot, interaction)); await interaction.editReply(notLoopingResponse(interaction));
} }
} }

23
deps.ts
View file

@ -1,23 +0,0 @@
export { ApplicationCommandOptionTypes, createBot, Intents, startBot } from "https://deno.land/x/discordeno@18.0.1/mod.ts";
export {
createGlobalApplicationCommand,
editGlobalApplicationCommand,
getChannel,
getChannels,
getGlobalApplicationCommands,
getGuild,
sendFollowupMessage,
sendMessage,
upsertGlobalApplicationCommands
} from "https://deno.land/x/discordeno@18.0.1/helpers/mod.ts";
export { type BigString, type CreateApplicationCommand, type CreateSlashApplicationCommand, type InteractionCallbackData } from "https://deno.land/x/discordeno@18.0.1/types/mod.ts";
export { type CreateMessage } from "https://deno.land/x/discordeno@18.0.1/helpers/messages/mod.ts";
export { type InteractionResponse } from "https://deno.land/x/discordeno@18.0.1/types/discordeno.ts";
export { editOriginalInteractionResponse, sendInteractionResponse } from "https://deno.land/x/discordeno@18.0.1/helpers/interactions/mod.ts";
export { sendPrivateInteractionResponse } from "https://deno.land/x/discordeno@18.0.1/plugins/mod.ts";
export { type Channel } from "https://deno.land/x/discordeno@18.0.1/transformers/channel.ts";
export { type Bot } from "https://deno.land/x/discordeno@18.0.1/bot.ts";
export { type Interaction } from "https://deno.land/x/discordeno@18.0.1/transformers/interaction.ts";
export { type ApplicationCommandOption, type ApplicationCommandOptionChoice, type Embed } from "https://deno.land/x/discordeno@18.0.1/transformers/mod.ts";
export { leaveVoiceChannel } from "https://deno.land/x/discordeno@18.0.1/helpers/guilds/mod.ts";
export { getConnectionData } from "./discordeno-audio-plugin/src/connection-data.ts";

View file

@ -1,2 +0,0 @@
# VS code settings
.vscode

View file

@ -1,96 +0,0 @@
# Discordeno Audio Plugin
This plugin enables your bot to send and receive audio.
Play either local files or stream straight from YouTube using [ytdl-core](https://github.com/DjDeveloperr/ytdl_core).
Or create your own audio sources!
No external plugins like FFMPEG required!
IMPORTANT: The `--unstable` flag is required as the unstable [datagram](https://doc.deno.land/deno/unstable/~/Deno.listenDatagram) implementation is used.
## Enable Audio usage
Enabling the plugin is similar to the cache plugin.
After that just connect to a channel and play your songs!
```js
import { enableAudioPlugin } from "https://deno.land/x/discordeno_audio_plugin/mod.ts";
const baseBot = createBot({}); // Create your bot
const bot = enableAudioPlugin(baseBot); // Enable the plugin
await startBot(bot);
// Connect to a channel like normal
bot.helpers.connectToVoiceChannel(
"your-guildid",
"channel-id"
);
// Play music :)
const player = bot.helpers.getPlayer("your-guildid");
player.pushQuery("Obi-Wan - Hello there.");
```
## Playing sound
Sound can be enqueued using the helper functions that have been added by `enableAudioPlugin`.
Sounds are managed using a `QueuePlayer`, allowing for multiple to be queued, shuffled, etc.
```js
const player = bot.helpers.getPlayer("your-guildid");
// Pushes a song to the end of the queue
// In this case it will stream directly from youtube
player.pushQuery("Obi-Wan - Hello there.");
// Local files have to be raw pcm files with data in the following format:
// 2 channel, 16bit Little Endian @48kHz
player.pushQuery("./hello/world.pcm");
// Interrupts the current sound, resumes when done
player.interruptQuery("rEq1Z0bjdwc");
```
## Playing your own source
Given that you have an audio source in the correct format you can play from any source that you want.
IMPORTANT:
The data needs to be Opus encoded 2 channel 16bit Little Endian @48kHz.
You can use `encodePCMAudio` to encode PCM as Opus.
```js
// Create and play your own source!
const source = createAudioSource("Title", () => AsyncIterableIterator<Uint8Array>)
player.push(source);
```
Or pass in your own `loadSource` function when enabling the plugin:
```js
const loadSource = (query: string) => {
return createAudioSource("Title", () => AsyncIterableIterator<Uint8Array>);
}
const bot = enableAudioPlugin(baseBot, loadSource);
player.pushQuery("Query to pass to loadSource");
```
## Listening
While receiving audio is not officially supported by Discord, it does work for now.
```js
// Audio received is not a single stream!
// Each user is received separately
// Decoded audio is again 2 channel 16bit LE 48kHz pcm data
for await (const { user, decoded } from bot.helpers.onAudio("guildId")) {
...
}
// You can also filter out one or more users
for await (const { decoded } from bot.helpers.onAudio("guildId", "userid")) {
...
}
```

View file

@ -1,6 +0,0 @@
export * from "https://deno.land/x/discordeno@18.0.1/mod.ts";
export * from "https://deno.land/x/discordeno@18.0.1/plugins/cache/mod.ts";
export * as opus from "https://unpkg.com/@evan/wasm@0.0.95/target/opus/deno.js";
export * from "https://unpkg.com/@evan/wasm@0.0.95/target/nacl/deno.js";
export { getVideoInfo, ytDownload } from "https://deno.land/x/yt_download@1.7/mod.ts";
export { default as YouTube } from "https://deno.land/x/youtube_sr@v4.1.17/mod.ts";

View file

@ -1 +0,0 @@
export * from "./src/mod.ts";

View file

@ -1,39 +0,0 @@
let lastId = -1n;
export type AudioSource = {
id: bigint;
title: string;
data: () =>
| Promise<AsyncIterableIterator<Uint8Array>>
| AsyncIterableIterator<Uint8Array>;
guildId: bigint;
added_by?: string;
};
export async function* empty() {}
export function createAudioSource(
title: string,
data: () =>
| Promise<AsyncIterableIterator<Uint8Array>>
| AsyncIterableIterator<Uint8Array>,
guildId: bigint,
added_by?: string
): AudioSource {
lastId++;
return {
id: lastId,
title,
data: () => {
try {
return data();
} catch (error) {
console.error(error);
console.log(`Failed to play ${title}\n Returning empty stream`);
return empty();
}
},
guildId,
added_by
};
}

View file

@ -1,14 +0,0 @@
import { createAudioSource } from "./audio-source.ts";
import { encodePCMAudio } from "../encoding.ts";
import { streamAsyncIterator } from "../../utils/mod.ts";
export function getLocalSources(...queries: string[]) {
return queries.map((query) => getLocalSource(query));
}
export function getLocalSource(query: string) {
//return createAudioSource(query, async () => {
//const file = await Deno.open(query, { read: true });
//return encodePCMAudio(streamAsyncIterator(file.readable));
//});
}

View file

@ -1,4 +0,0 @@
export * from "./audio-source.ts";
export * from "./universal.ts";
export * from "./youtube.ts";
export * from "./local.ts";

View file

@ -1,23 +0,0 @@
import { YouTube } from "../../deps.ts";
import { getYoutubeSources } from "./youtube.ts";
import { isPlaylist } from "../../../utils.ts";
export type LoadSource = typeof loadLocalOrYoutube;
export async function loadLocalOrYoutube(query: string, guildId: bigint, added_by?: string) {
const queries = [];
if(isPlaylist(query))
{
const playlist = await YouTube.getPlaylist(query);
for(const video of playlist.videos) {
const videoId = video.id ? video.id : "";
queries.push(videoId);
}
} else {
queries.push(query);
}
return getYoutubeSources(guildId, String(added_by), queries);
}

View file

@ -1,83 +0,0 @@
import { getVideoInfo, YouTube, ytDownload } from "../../deps.ts";
import { bufferIter, retry } from "../../utils/mod.ts";
import { demux } from "../demux/mod.ts";
import { createAudioSource, empty } from "./audio-source.ts";
import { errorMessageCallback, isPlaylist } from "../../../utils.ts";
export async function getYoutubeSources(guildId: bigint, added_by?: string, queries: string[]) {
const sources = queries.map((query) => getYoutubeSource(query, guildId, added_by));
const awaitedSources = await Promise.all(sources);
return awaitedSources
.filter((source) => source !== undefined)
.map((source) => source!);
}
/*export async function getYoutubeSource(query: string, guildId: bigint, added_by?: string) {
if(isPlaylist(query)) {
const playlist = await YouTube.getPlaylist(query);
const count = playlist.videoCount;
const sources = [];
for(const video of playlist.videos) {
const videoId = video.id ? video.id : "";
sources.push(getVideo(videoId, guildId, added_by));
}
return sources;
}
return await getVideo(query, guildId, added_by);
}*/
async function getYoutubeSource(query: string, guildId: bigint, added_by?: string) {
try {
const result = await getVideoInfo(query);
if(result.videoDetails.videoId) {
const id = result.videoDetails.videoId;
const title = result.videoDetails.title;
return createAudioSource(title, async () => {
const stream = await retry(
async () =>
await ytDownload(id, {
mimeType: `audio/webm; codecs="opus"`,
})
);
if (stream === undefined) {
errorMessageCallback(guildId, `There was an error trying to play **${title}**:\n
The stream couldn't be found`);
console.log(`Failed to play ${title}\n Returning empty stream`);
return empty();
}
return bufferIter(demux(stream));
}, guildId, added_by);
}
//const result = await ytDownload(query, { mimeType: `audio/webm; codecs="opus"`, });
//console.log(result);
/*const results = await YouTube.search(query, { limit: 1, type: "video" });
if (results.length > 0) {
const { id, title } = results[0];
return createAudioSource(title!, async () => {
const stream = await retry(
async () =>
await ytDownload(id!, {
mimeType: `audio/webm; codecs="opus"`,
})
);
if (stream === undefined) {
errorMessageCallback(guildId, `There was an error trying to play **${title}**:\n
The stream couldn't be found`);
console.log(`Failed to play ${title}\n Returning empty stream`);
return empty();
}
return bufferIter(demux(stream));
}, guildId, added_by);
}*/
} catch(err) {
console.error(err);
return undefined;
}
}

View file

@ -1,165 +0,0 @@
import { Bot } from "../deps.ts";
import { EventSource } from "../utils/mod.ts";
import { AudioSource, LoadSource, UdpArgs } from "./mod.ts";
import { QueuePlayer } from "./player/mod.ts";
import { sendAudioPacket } from "./udp/packet.ts";
export type BotData = {
bot: Bot;
guildData: Map<bigint, ConnectionData>;
udpSource: EventSource<UdpArgs>;
bufferSize: number;
loadSource: (query: string, guild_id: bigint, added_by?: string) => AudioSource[] | Promise<AudioSource[]>;
};
export type ConnectionData = {
player: QueuePlayer;
audio: EventSource<Uint8Array>;
guildId: bigint;
udpSocket: Deno.DatagramConn;
udpRaw: EventSource<Uint8Array>;
ssrcToUser: Map<number, bigint>;
usersToSsrc: Map<bigint, number>;
context: {
ssrc: number;
ready: boolean;
speaking: boolean;
sequence: number;
timestamp: number;
missedHeart: number;
lastHeart?: number;
reconnect: number;
};
connectInfo: {
endpoint?: string;
sessionId?: string;
token?: string;
};
stopHeart: () => void;
remote?: { port: number; hostname: string };
ws?: WebSocket;
secret?: Uint8Array;
mode?: string;
resume?: boolean;
};
const connectionData = new Map<bigint, BotData>();
/**
* Returns a random number that is in the range of n bits.
*
* @param n - The number of bits
*/
function randomNBit(n: number) {
return Math.floor(Math.random() * 2 ** n);
}
export function createBotData(
bot: Bot,
udpSource: EventSource<UdpArgs>,
loadSource: LoadSource,
bufferSize = 10
) {
const botData: BotData = {
bot,
guildData: new Map(),
udpSource,
bufferSize,
loadSource,
};
connectionData.set(bot.id, botData);
return botData;
}
export function getConnectionData(botId: bigint, guildId: bigint) {
const botData = connectionData.get(botId);
if (botData === undefined) {
throw "Bot first needs to be connected!";
}
let data = botData.guildData.get(guildId);
if (data === undefined) {
let currentPort = 5000;
let listening = false;
let udpSocket: Deno.DatagramConn | undefined;
while (!listening) {
try {
udpSocket = Deno.listenDatagram({
hostname: "0.0.0.0",
port: currentPort,
transport: "udp",
});
listening = true;
} catch (_err) {
if (_err instanceof TypeError) {
throw new Error("Please enable unstable by adding --unstable.");
}
currentPort++;
}
}
udpSocket = udpSocket as Deno.DatagramConn;
const udpRaw = new EventSource<Uint8Array>();
data = {
player: undefined as unknown as QueuePlayer,
guildId,
udpSocket,
udpRaw,
context: {
ssrc: 1,
ready: false,
speaking: false,
sequence: randomNBit(16),
timestamp: randomNBit(32),
missedHeart: 0,
reconnect: 0,
},
connectInfo: {},
audio: new EventSource<Uint8Array>(),
ssrcToUser: new Map<number, bigint>(),
usersToSsrc: new Map<bigint, number>(),
stopHeart: () => {},
};
data.player = new QueuePlayer(data, botData.loadSource);
botData.guildData.set(guildId, data);
connectSocketToSource(
botData.bot,
guildId,
udpSocket,
botData.udpSource,
udpRaw
);
connectAudioIterable(data);
}
return data;
}
async function connectAudioIterable(conn: ConnectionData) {
for await (const chunk of conn.audio.iter()) {
await sendAudioPacket(conn, chunk);
}
}
export function getUserSSRC(conn: ConnectionData, user: bigint) {
return conn.usersToSsrc.get(user);
}
export function getUserBySSRC(conn: ConnectionData, ssrc: number) {
return conn.ssrcToUser.get(ssrc);
}
export function setUserSSRC(conn: ConnectionData, user: bigint, ssrc: number) {
conn.usersToSsrc.set(user, ssrc);
conn.ssrcToUser.set(ssrc, user);
}
async function connectSocketToSource(
bot: Bot,
guildId: bigint,
socket: Deno.DatagramConn,
source: EventSource<UdpArgs>,
localSource: EventSource<Uint8Array>
) {
for await (const [data, _address] of socket) {
source.trigger({ bot, guildId, data });
localSource.trigger(data);
}
}

View file

@ -1,20 +0,0 @@
import { WebmBaseDemuxer } from "./prism.js";
const demuxer = new WebmBaseDemuxer();
// @ts-ignore: Worker stuff
self.onmessage = (event) => {
const data = event.data;
demux(data).then((demuxed) => {
// @ts-ignore: Worker stuff
self.postMessage(demuxed);
});
};
async function demux(value: Uint8Array) {
const result: Uint8Array[] = [];
for await (const demuxed of demuxer.demux(value)) {
result.push(demuxed);
}
return result;
}

View file

@ -1,23 +0,0 @@
const workerUrl = new URL("./demux-worker.ts", import.meta.url).href;
export async function* demux(source: AsyncIterable<Uint8Array>) {
const worker = new Worker(workerUrl, {
type: "module",
});
try {
for await (const value of source) {
const nextValue = new Promise<Uint8Array[]>((resolve) => {
worker.onmessage = (e) => {
resolve(e.data);
};
});
worker.postMessage(value);
for (const demuxed of await nextValue) {
yield demuxed;
}
}
} catch (error) {
console.error(error);
}
worker.terminate();
}

View file

@ -1,51 +0,0 @@
import { opus } from "../deps.ts";
import { CHANNELS, FRAME_SIZE, SAMPLE_RATE } from "./sample-consts.ts";
const encoder = new opus.Encoder({
channels: CHANNELS,
sample_rate: SAMPLE_RATE,
application: "audio",
});
export function createAudioDecoder() {
const decoder = new opus.Decoder({
channels: CHANNELS,
sample_rate: SAMPLE_RATE,
});
let lastTimestamp = 0;
let last: Uint8Array;
return (audio: Uint8Array, timestamp: number) => {
if (lastTimestamp !== timestamp) {
lastTimestamp = timestamp;
last = decoder.decode(audio);
}
return last;
};
}
const decoders = new Map<
number,
(opus: Uint8Array, timestamp: number) => Uint8Array
>();
function getDecoder(ssrc: number) {
const decoder = decoders.get(ssrc) || createAudioDecoder();
decoders.set(ssrc, decoder);
return decoder;
}
export function decodeAudio(
audio: Uint8Array,
ssrc: number,
timestamp: number
) {
const decoder = getDecoder(ssrc);
return decoder(audio, timestamp);
}
export async function* encodePCMAudio(audio: AsyncIterator<Uint8Array>) {
const gen = encoder.encode_pcm_stream(FRAME_SIZE, audio);
for await (const frame of gen) {
yield frame as Uint8Array;
}
}

View file

@ -1,76 +0,0 @@
import { Bot } from "../deps.ts";
import { asArray } from "../utils/array.ts";
import { EventSource } from "../utils/event-source.ts";
import { IterExpanded } from "../utils/iterator/util/iter-utils.ts";
import { getConnectionData } from "./connection-data.ts";
import { decodeAudio } from "./encoding.ts";
import { stripRTP } from "./mod.ts";
type ReceivedAudio = {
bot: Bot;
user: bigint;
guildId: bigint;
raw: Uint8Array;
packet: {
rtp: {
version: number;
type: number;
sequence: number;
timestamp: number;
ssrc: number;
};
nonce: Uint8Array;
data: Uint8Array;
};
decoded: Uint8Array;
};
export function createOnAudio(
source: EventSource<{
bot: Bot;
guildId: bigint;
data: Uint8Array;
}>
) {
return (
guild: bigint | bigint[],
user?: bigint | bigint[]
): IterExpanded<ReceivedAudio> => {
const guilds = asArray(guild);
const users = asArray(user);
return source
.iter()
.filter(({ data }) => {
return data[1] === 120;
})
.filter(({ guildId }) => guilds.includes(guildId))
.map((payload) => {
const conn = getConnectionData(payload.bot.id, payload.guildId);
try {
const packet = stripRTP(conn, payload.data);
const user = conn.ssrcToUser.get(packet.rtp.ssrc);
if (users.length > 0 && user && !users.includes(user)) {
return undefined;
}
const decoded = decodeAudio(
packet.data,
packet.rtp.ssrc,
packet.rtp.timestamp
);
return {
bot: payload.bot,
user,
guildId: payload.guildId,
raw: payload.data,
packet,
decoded,
} as ReceivedAudio;
} catch {
console.log("Something is wrong...");
return undefined;
}
})
.filter((data) => data !== undefined)
.map((data) => data!);
};
}

View file

@ -1,93 +0,0 @@
import { asArray, EventSource } from "../utils/mod.ts";
import { createBotData, getConnectionData } from "./connection-data.ts";
import { QueuePlayer } from "./player/mod.ts";
import { connectWebSocket } from "./websocket/mod.ts";
import { Bot } from "../deps.ts";
import { createOnAudio } from "./listen.ts";
import { loadLocalOrYoutube, LoadSource } from "./audio-source/universal.ts";
export * from "./connection-data.ts";
export * from "./websocket/mod.ts";
export * from "./udp/mod.ts";
export * from "./encoding.ts";
export * from "./demux/mod.ts";
export * from "./player/mod.ts";
export * from "./audio-source/mod.ts";
export type UdpArgs = {
bot: Bot;
guildId: bigint;
data: Uint8Array;
};
export type AudioBot<T extends Bot> = T & {
helpers: ReturnType<typeof createAudioHelpers>;
};
export function enableAudioPlugin<T extends Bot>(
bot: T,
loadSource = loadLocalOrYoutube
): AudioBot<T> {
Object.assign(bot.helpers, createAudioHelpers(bot, loadSource));
return bot as AudioBot<T>;
}
function createAudioHelpers(bot: Bot, loadSource: LoadSource) {
const udpSource = new EventSource<UdpArgs>();
createBotData(bot, udpSource, loadSource);
const resetPlayer = (guildId: bigint) => {
const conn = getConnectionData(bot.id, guildId);
const oldPlayer = conn.player;
const oldQueue = asArray(oldPlayer.current());
oldQueue.push(...oldPlayer.upcoming());
conn.player = new QueuePlayer(conn, loadSource);
conn.player.push(...oldQueue);
oldPlayer.stopInterrupt();
oldPlayer.stop();
conn.player.loop(oldPlayer.looping);
return conn.player;
};
const oldStateListener = bot.events.voiceStateUpdate;
bot.events.voiceStateUpdate = (bot, event) => {
const { guildId, userId, sessionId } = event;
if (bot.id === userId && guildId) {
const conn = getConnectionData(bot.id, guildId);
if(!conn.player.nowPlaying) {
conn.connectInfo.sessionId = sessionId;
connectWebSocket(conn, bot.id, guildId);
}
}
oldStateListener(bot, event);
};
const oldServerListener = bot.events.voiceServerUpdate;
bot.events.voiceServerUpdate = (bot, event) => {
const { guildId, endpoint, token } = event;
const conn = getConnectionData(bot.id, guildId);
if (
conn.connectInfo.endpoint === endpoint &&
conn.connectInfo.token === token
) {
return;
}
conn.connectInfo.endpoint = endpoint;
conn.connectInfo.token = token;
connectWebSocket(conn, bot.id, guildId);
oldServerListener(bot, event);
};
return {
getPlayer: (guildId: bigint) => {
const conn = getConnectionData(bot.id, guildId);
return conn.player;
},
resetPlayer,
fixAudio: (guildId: bigint) => {
resetPlayer(guildId);
const conn = getConnectionData(bot.id, guildId);
conn.ws?.close();
},
/**
* Creates an async iterable with decoded audio packets
*/
onAudio: createOnAudio(udpSource),
};
}

View file

@ -1,35 +0,0 @@
import { BasicSource } from "../../utils/event-source.ts";
export type AllEventTypes = RawEventTypes | QueueEventTypes;
export type RawEventTypes = "next" | "done";
export type QueueEventTypes = "loop";
export type PlayerListener<T, J extends AllEventTypes> = (
data: EventData<T>[J]
) => void;
type PlayerEvent<T, J extends AllEventTypes> = {
type: J;
data: EventData<T>[J];
};
type EventData<T> = {
next: T;
done: T;
loop: T;
};
export class PlayerEventSource<T, K extends AllEventTypes> {
#source = new BasicSource<PlayerEvent<T, K>>();
on<J extends K>(event: J, listener: PlayerListener<T, J>) {
return this.#source.listen((value) => {
if (value.type === event) {
listener((value as PlayerEvent<T, J>).data);
}
});
}
trigger<J extends K>(event: J, data: EventData<T>[J]) {
this.#source.trigger({ type: event, data });
}
}

View file

@ -1,3 +0,0 @@
export * from "./types.ts";
export * from "./queue-player.ts";
export * from "./raw-player.ts";

View file

@ -1,133 +0,0 @@
import { Queue } from "../../utils/mod.ts";
import { AudioSource, LoadSource } from "../audio-source/mod.ts";
import { ConnectionData } from "../connection-data.ts";
import { PlayerEventSource, AllEventTypes, PlayerListener } from "./events.ts";
import { RawPlayer } from "./raw-player.ts";
import { Player } from "./types.ts";
import { nowPlayingCallback } from "../../../commands/np.ts";
export class QueuePlayer
extends Queue<AudioSource>
implements Player<AudioSource>
{
playing = true;
looping = false;
playingSince?: number;
nowPlaying?: AudioSource;
#rawPlayer: RawPlayer;
#loadSource: LoadSource;
#events = new PlayerEventSource<AudioSource, AllEventTypes>();
constructor(conn: ConnectionData, loadSource: LoadSource) {
super();
this.#loadSource = loadSource;
this.#rawPlayer = new RawPlayer(conn);
this.playNext();
this.#rawPlayer.on("done", async () => {
const current = this.current();
if (current) {
this.#events.trigger("done", current);
}
await this.playNext();
if (!this.playing) {
this.pause();
}
});
}
async playNext() {
let song;
const current = this.current();
if (this.looping && current !== undefined) {
song = current;
this.#events.trigger("loop", song);
} else {
song = await super.next();
this.#events.trigger("next", song);
}
this.playingSince = Date.now();
this.nowPlaying = song;
this.#rawPlayer.setAudio(await song.data());
await nowPlayingCallback(this.#rawPlayer.conn);
}
clear() {
return super.clear();
}
play() {
this.playing = true;
this.#rawPlayer.play();
return Promise.resolve();
}
pause() {
this.playing = false;
this.#rawPlayer.pause();
}
stop() {
this.skip();
this.pause();
}
skip() {
this.looping = false;
this.#rawPlayer.clear();
}
loop(value: boolean) {
this.looping = value;
}
stopInterrupt() {
this.#rawPlayer.interrupt(undefined);
}
/**
* Listen to events:
*
* `next`: New sound started playing
*
* `done`: Last sound is done playing
*
* `loop`: New loop iteration was started
* @param event Event to listen to
* @param listener Triggered on event
* @returns Function that disconnects the listener
*/
on<J extends AllEventTypes>(
event: J,
listener: PlayerListener<AudioSource, J>
) {
return this.#events.on(event, listener);
}
/**
* Interrupts the current song, resumes when finished
* @param query Loads a universal song (local file or youtube search)
*/
async interruptQuery(guildId: bigint, query: string) {
const sources = await this.#loadSource(query as string, guildId);
this.#rawPlayer.interrupt(await sources[0].data());
}
async pushQuery(guildId: bigint, added_by?: string, ...queries: string[]) {
const sources = [];
for (const query of queries) {
sources.push(...(await this.#loadSource(query as string, guildId, String(added_by))));
this.push(...sources);
}
return sources;
}
async unshiftQuery(guildId: bigint, ...queries: string[]) {
const sources = [];
for (const query of queries) {
sources.push(...(await this.#loadSource(query as string, guildId)));
this.unshift(...sources);
}
return sources;
}
}

View file

@ -1,110 +0,0 @@
import { ConnectionData } from "../connection-data.ts";
import { FRAME_DURATION } from "../sample-consts.ts";
import { Player } from "./types.ts";
import { setDriftlessInterval, clearDriftless } from "npm:driftless";
import { PlayerEventSource, RawEventTypes, PlayerListener } from "./events.ts";
import { errorMessageCallback } from "../../../utils.ts";
export class RawPlayer implements Player<AsyncIterableIterator<Uint8Array>> {
#audio?: AsyncIterableIterator<Uint8Array>;
#interrupt?: AsyncIterableIterator<Uint8Array>;
playing = false;
conn: ConnectionData;
#events = new PlayerEventSource<
AsyncIterableIterator<Uint8Array>,
RawEventTypes
>();
constructor(conn: ConnectionData) {
this.conn = conn;
}
play() {
try {
if (this.playing) {
return;
}
this.playing = true;
const inter = setDriftlessInterval(async () => {
if (this.playing === false) {
clearDriftless(inter);
return;
}
const frame = await this.#getFrame();
if (frame === undefined) {
return;
}
this.conn.audio.trigger(frame);
}, FRAME_DURATION);
} catch(err) {
errorMessageCallback(this.conn.guildId, `The player broke for some reason: ${err}`);
console.log("error while playing!!");
console.error(err);
}
}
pause() {
this.playing = false;
}
stop() {
this.clear();
this.pause();
}
clear() {
if (this.#audio) {
this.#events.trigger("done", this.#audio);
}
this.#interrupt = undefined;
}
setAudio(audio: AsyncIterableIterator<Uint8Array>) {
this.#audio = audio;
this.#events.trigger("next", audio);
this.play();
}
interrupt(audio?: AsyncIterableIterator<Uint8Array>) {
this.#interrupt = audio;
if (!this.playing) {
this.play();
}
}
on<J extends RawEventTypes>(
event: J,
listener: PlayerListener<AsyncIterableIterator<Uint8Array>, J>
) {
return this.#events.on(event, listener);
}
async #getFrame() {
const interrupt = await this.#getNextFrame(this.#interrupt);
if (interrupt !== undefined) {
return interrupt;
}
const audio = await this.#getNextFrame(this.#audio);
if (audio === undefined) {
this.#handleAudioStopped();
}
return audio;
}
async #getNextFrame(source: AsyncIterableIterator<Uint8Array> | undefined) {
const nextResult = await source?.next();
return nextResult !== undefined && !nextResult?.done
? nextResult.value
: undefined;
}
#handleAudioStopped() {
if (this.#audio !== undefined) {
this.#events.trigger("done", this.#audio);
}
this.#audio = undefined;
this.playing = false;
}
}

View file

@ -1,13 +0,0 @@
import { PlayerListener, RawEventTypes } from "./events.ts";
export type Player<T> = {
playing: boolean;
play(): void;
pause(): void;
stop(): void;
clear(): void;
on<J extends RawEventTypes>(
event: J,
listener: PlayerListener<T, J>
): () => void;
};

View file

@ -1,4 +0,0 @@
export const SAMPLE_RATE = 48000;
export const FRAME_DURATION = 20;
export const CHANNELS = 2;
export const FRAME_SIZE = (SAMPLE_RATE * FRAME_DURATION) / 1000;

View file

@ -1,23 +0,0 @@
import { secretbox } from "../../deps.ts";
export function encryptPacket(
secret: Uint8Array,
packet: Uint8Array,
nonce: Uint8Array
) {
if (!secret) {
throw "Secret is not known!";
}
return secretbox.seal(packet, secret, nonce);
}
export function decryptPacket(
secret: Uint8Array,
packet: Uint8Array,
nonce: Uint8Array
) {
if (!secret) {
throw "Secret is not known!";
}
return secretbox.open(packet, secret, nonce);
}

View file

@ -1,32 +0,0 @@
import { ConnectionData } from "../connection-data.ts";
export async function discoverIP(
conn: ConnectionData,
ssrc: string,
hostname: string,
port: number
) {
const discover = createDiscoverPacket(ssrc);
await conn.udpSocket.send(discover, {
hostname,
port,
transport: "udp",
});
const value = await conn.udpRaw.next();
const decoder = new TextDecoder();
const localIp = decoder.decode(value.slice(8, value.indexOf(0, 8)));
const localPort = new DataView(value.buffer).getUint16(72, false);
return { ip: localIp, port: localPort };
}
function createDiscoverPacket(ssrc: string): Uint8Array {
const buffer = new ArrayBuffer(74);
const header_data = new DataView(buffer);
let offset = 0;
header_data.setInt16(offset, 1, false);
offset += 2;
header_data.setInt16(offset, 70, false);
offset += 2;
header_data.setInt32(offset, Number.parseInt(ssrc), false);
return new Uint8Array(buffer);
}

View file

@ -1,3 +0,0 @@
export * from "./discover.ts";
export * from "./speaking.ts";
export * from "./packet.ts";

View file

@ -1,84 +0,0 @@
import { ConnectionData } from "../connection-data.ts";
import { FRAME_SIZE } from "../sample-consts.ts";
import { decryptPacket, encryptPacket } from "./code-and-crypt.ts";
export function stripRTP(conn: ConnectionData, packet: Uint8Array) {
const dataView = new DataView(packet.buffer);
const rtp = {
version: dataView.getUint8(0),
type: dataView.getUint8(1),
sequence: dataView.getUint16(2, false),
timestamp: dataView.getUint32(4, false),
ssrc: dataView.getUint32(8, false),
};
if (!conn.secret) {
throw "Secret is not known!";
}
const nonce = new Uint8Array([...packet.slice(0, 12), ...new Uint8Array(12)]);
const encrypted = packet.slice(12, packet.length);
let data = decryptPacket(conn.secret, encrypted, nonce);
const view = new DataView(data.buffer);
if (data[0] === 0xbe && data[1] === 0xde && data.length > 4) {
const length = view.getUint16(2, false);
// let offset = 4;
// for (let i = 0; i < length; i++) {
// const byte = view.getUint8(offset);
// if (byte === 0) continue;
// offset += 1 + (byte & 15);
// }
if (length === 1) {
data = data.slice(8); // IDK when actually calculating it should be 6 offset but it is 8...
}
}
return { rtp, nonce, data };
}
export function addRTP(conn: ConnectionData, packet: Uint8Array) {
const {
secret,
context: { sequence, timestamp, ssrc },
} = conn;
if (!ssrc) {
throw "SSRC is not known!";
}
if (!secret) {
throw "Secret is not known!";
}
const rtp = new Uint8Array(12);
const rtpView = new DataView(rtp.buffer);
rtpView.setUint8(0, 0x80);
rtpView.setUint8(1, 0x78);
rtpView.setUint16(2, sequence, false);
rtpView.setUint32(4, timestamp, false);
rtpView.setUint32(8, ssrc, false);
const nonce = new Uint8Array([...rtp, ...new Uint8Array(12)]);
const encrypted = encryptPacket(secret, packet, nonce);
return new Uint8Array([...rtp, ...encrypted]);
}
export function incrementAudioMetaData(context: ConnectionData["context"]) {
context.sequence++;
context.timestamp += FRAME_SIZE;
context.sequence %= 2 ** 16;
context.timestamp %= 2 ** 32;
}
export async function sendAudioPacket(conn: ConnectionData, audio: Uint8Array) {
if (!conn || !conn.udpSocket || !conn.remote || !conn.context.ready) {
return;
}
incrementAudioMetaData(conn.context);
try {
const packet = addRTP(conn, audio);
await conn.udpSocket.send(packet, {
...conn.remote,
transport: "udp",
});
} catch (error) {
console.log(`Packet not send, ${error}`);
}
}

View file

@ -1,22 +0,0 @@
import { VoiceOpcodes } from "../../deps.ts";
import { ConnectionData } from "../connection-data.ts";
export function setSpeaking(
{ ws, context }: ConnectionData,
speaking: boolean
) {
if (context.speaking === speaking || ws?.readyState !== WebSocket.OPEN) {
return;
}
context.speaking = speaking;
ws?.send(
JSON.stringify({
op: VoiceOpcodes.Speaking,
d: {
speaking: speaking ? 1 : 0,
delay: 0,
ssrc: context.ssrc,
},
})
);
}

View file

@ -1,77 +0,0 @@
import { VoiceOpcodes } from "../../deps.ts";
import { ConnectionData, setUserSSRC } from "../connection-data.ts";
import { discoverIP } from "../udp/discover.ts";
import { setSpeaking } from "../udp/speaking.ts";
import { sendHeart } from "./heartbeat.ts";
import { ReceiveVoiceOpcodes } from "./opcodes.ts";
export const socketHandlers: Record<
ReceiveVoiceOpcodes,
(connectionData: ConnectionData, d: any) => void
> = {
[ReceiveVoiceOpcodes.Ready]: ready,
[ReceiveVoiceOpcodes.SessionDescription]: sessionDescription,
[ReceiveVoiceOpcodes.Speaking]: speaking,
[ReceiveVoiceOpcodes.HeartbeatACK]: heartbeatACK,
[ReceiveVoiceOpcodes.Hello]: hello,
[ReceiveVoiceOpcodes.Resumed]: resumed,
[ReceiveVoiceOpcodes.ClientDisconnect]: clientDisconnect,
};
function hello(conn: ConnectionData, d: { heartbeat_interval: number }) {
conn.stopHeart = sendHeart(conn, d.heartbeat_interval);
}
function ready(conn: ConnectionData, d: any) {
const { ssrc, port, ip } = d;
conn.context.ssrc = ssrc;
conn.remote = { port, hostname: ip };
discoverIP(conn, ssrc, ip, port).then((info) => {
if (conn.ws?.readyState === WebSocket.OPEN) {
conn.ws.send(
JSON.stringify({
op: VoiceOpcodes.SelectProtocol,
d: {
protocol: "udp",
data: {
address: info.ip,
port: info.port,
mode: "xsalsa20_poly1305",
},
},
})
);
}
});
}
function resumed(conn: ConnectionData) {
conn.context.ready = true;
conn.context.reconnect = 0;
setSpeaking(conn, true);
}
function sessionDescription(conn: ConnectionData, d: any) {
const secret = d.secret_key;
const mode = d.mode;
conn.secret = new Uint8Array(secret);
conn.mode = mode;
conn.context.ready = true;
conn.context.reconnect = 0;
setSpeaking(conn, true);
}
function speaking(conn: ConnectionData, d: any) {
const user_id = BigInt(d.user_id);
const ssrc = Number(d.ssrc);
setUserSSRC(conn, user_id, ssrc);
}
function heartbeatACK(conn: ConnectionData, d: number) {
if (conn.context.lastHeart === d) {
conn.context.missedHeart = 0;
conn.context.lastHeart = undefined;
}
}
function clientDisconnect() {}

View file

@ -1,41 +0,0 @@
import { VoiceOpcodes } from "../../deps.ts";
import { setDriftlessTimeout } from "npm:driftless";
import { ConnectionData } from "../mod.ts";
function sendHeartBeat(conn: ConnectionData) {
if (conn.context.lastHeart !== undefined) {
conn.context.missedHeart++;
}
conn.context.lastHeart = Date.now();
conn.ws?.send(
JSON.stringify({
op: VoiceOpcodes.Heartbeat,
d: conn.context.lastHeart,
})
);
}
export function sendHeart(conn: ConnectionData, interval: number) {
let last = Date.now();
if (conn.ws?.readyState === WebSocket.OPEN) {
sendHeartBeat(conn);
}
let done = false;
const repeatBeat = () => {
if (done || conn.ws?.readyState !== WebSocket.OPEN) {
return;
}
if (conn.context.missedHeart >= 3) {
console.log("Missed too many heartbeats, attempting reconnect");
conn.ws?.close();
return;
}
last = Date.now();
sendHeartBeat(conn);
setDriftlessTimeout(repeatBeat, interval + (last - Date.now()));
};
setDriftlessTimeout(repeatBeat, interval + (last - Date.now()));
return () => {
done = true;
};
}

View file

@ -1,109 +0,0 @@
import { VoiceCloseEventCodes } from "../../deps.ts";
import { ConnectionData } from "../connection-data.ts";
import { socketHandlers } from "./handlers.ts";
import { SendVoiceOpcodes } from "./opcodes.ts";
export function connectWebSocket(
conn: ConnectionData,
userId: bigint,
guildId: bigint
) {
conn.context.ready = false;
const { token, sessionId, endpoint } = conn.connectInfo;
if (
token === undefined ||
sessionId === undefined ||
endpoint === undefined
) {
return;
}
const ws = new WebSocket(`wss://${endpoint}?v=4`);
conn.ws = ws;
const identifyRequest = JSON.stringify({
op: SendVoiceOpcodes.Identify,
d: {
server_id: guildId.toString(),
user_id: userId.toString(),
session_id: sessionId,
token,
},
});
const resumeRequest = JSON.stringify({
op: SendVoiceOpcodes.Resume,
d: {
server_id: guildId.toString(),
session_id: sessionId,
token,
},
});
const open = () => {
handleOpen(conn, identifyRequest, resumeRequest);
};
const message = (event: MessageEvent) => {
handleMessage(conn, event);
};
const error = () => handleError(conn);
const close = (event: CloseEvent) => {
ws.removeEventListener("open", open);
ws.removeEventListener("message", message);
ws.removeEventListener("close", close);
ws.removeEventListener("error", error);
handleClose(conn, event, userId, guildId);
};
ws.addEventListener("open", open);
ws.addEventListener("message", message);
ws.addEventListener("close", close);
ws.addEventListener("error", error);
}
function handleOpen(
conn: ConnectionData,
identifyRequest: string,
resumeRequest: string
) {
if (conn.ws?.readyState !== WebSocket.OPEN) {
return;
}
conn.ws.send(conn.resume ? resumeRequest : identifyRequest);
conn.resume = false;
}
function handleMessage(conn: ConnectionData, ev: MessageEvent<any>) {
const data = JSON.parse(ev.data);
socketHandlers[data.op]?.(conn, data.d);
}
function handleClose(
conn: ConnectionData,
event: CloseEvent,
userId: bigint,
guildId: bigint
) {
conn.stopHeart();
conn.context.ready = false;
conn.context.speaking = false;
conn.context.lastHeart = undefined;
conn.context.missedHeart = 0;
if (VoiceCloseEventCodes.Disconnect === event.code) {
console.log("Couldn't reconnect :(");
return;
}
if (conn.context.reconnect >= 3) {
return;
}
conn.context.reconnect++;
conn.resume = true;
connectWebSocket(conn, userId, guildId);
}
/**
* Errors are scary just drop connection.
* Hope it reconnects😬
* @param event
* @param conn
* @param userId
* @param guildId
*/
function handleError(conn: ConnectionData) {
conn.ws?.close();
}

View file

@ -1,31 +0,0 @@
import { VoiceOpcodes } from "../../deps.ts";
export enum ReceiveVoiceOpcodes {
/** Complete the websocket handshake. */
Ready = VoiceOpcodes.Ready,
/** Describe the session. */
SessionDescription = VoiceOpcodes.SessionDescription,
/** Indicate which users are speaking. */
Speaking = VoiceOpcodes.Speaking,
/** Sent to acknowledge a received client heartbeat. */
HeartbeatACK = VoiceOpcodes.HeartbeatACK,
/** Time to wait between sending heartbeats in milliseconds. */
Hello = VoiceOpcodes.Hello,
/** Acknowledge a successful session resume. */
Resumed = VoiceOpcodes.Resumed,
/** A client has disconnected from the voice channel */
ClientDisconnect = VoiceOpcodes.ClientDisconnect,
}
export enum SendVoiceOpcodes {
/** Begin a voice websocket connection. */
Identify = VoiceOpcodes.Identify,
/** Select the voice protocol. */
SelectProtocol = VoiceOpcodes.SelectProtocol,
/** Keep the websocket connection alive. */
Heartbeat = VoiceOpcodes.Heartbeat,
/** Indicate which users are speaking. */
Speaking = VoiceOpcodes.Speaking,
/** Resume a connection. */
Resume = VoiceOpcodes.Resume,
}

View file

@ -1,97 +0,0 @@
import { clamp } from "./number.ts";
export function arrayEquals<T extends any[], J extends any[]>(a: T, b: J) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
export function asArray<T>(value?: T | T[]): T[] {
if (value === undefined) {
return [];
}
return Array.isArray(value) ? [...value] : [value];
}
export function arrayMove<T>(
array: T[] | undefined,
from: number,
to: number
): T[] {
const result = spread(array);
const min = 0;
const max = result.length;
clamp(from, min, max);
clamp(from, min, max);
result.splice(to, 0, result.splice(from, 1)[0]);
return result;
}
export function arrayShuffle<T>(array: T[]) {
let currentIndex = array.length,
randomIndex;
while (currentIndex != 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
}
return array;
}
export function spread<T>(array: T[] | undefined, ...additional: T[]): T[] {
return [...(array || []), ...additional];
}
export function combine<T>(array: T[] | undefined, add: T[] | undefined): T[] {
return [...(array || []), ...(add || [])];
}
export function repeat<T>(amount: number, ...value: T[]): T[] {
let result: T[] = [];
while (amount > 0) {
amount--;
result = [...result, ...value];
}
return result;
}
export function remove<T>(array: T[] | undefined, ...toRemove: T[]): T[] {
const result = spread(array);
for (const entry of toRemove) {
const index = result.indexOf(entry);
result.splice(index, 1);
}
return result;
}
export function findRemove<T>(
array: T[] | undefined,
equals: (first: T, second: T) => boolean,
...toRemove: T[]
): T[] {
const result = spread(array);
for (const entry of toRemove) {
const index = result.findIndex((other) => equals(entry, other));
result.splice(index, 1);
}
return result;
}
export function diff<T>(first: T[], second: T[]): T[] {
const result = [];
for (const item of second) {
if (!first.includes(item)) {
result.push(item);
}
}
return result;
}

View file

@ -1,34 +0,0 @@
export async function* bufferIter<T>(
iterator: AsyncIterableIterator<T>,
size = 60
) {
const buffer: T[] = [];
for (let i = 0; i < size; i++) {
const result = await iterator.next();
if (result.done) {
break;
} else {
buffer.push(result.value);
}
}
let done = false;
while (!done || buffer.length > 0) {
if (buffer.length > 0) {
yield buffer.shift()!;
iterator.next().then((result) => {
if (result.done) {
done = true;
} else {
buffer.push(result.value);
}
});
} else {
const result = await iterator.next();
if (result.done) {
return;
} else {
yield result.value;
}
}
}
}

View file

@ -1,52 +0,0 @@
import { IterSource, fromCallback } from "./iterator/mod.ts";
type Listener<T> = (arg: T) => void;
export class BasicSource<T> {
listeners: Listener<T>[] = [];
trigger(value: T) {
for (const listener of this.listeners) {
listener(value);
}
}
listen(listener: Listener<T>) {
this.listeners.push(listener);
return () => {
this.removeListener(listener);
};
}
removeListener(listener: Listener<T>) {
const index = this.listeners.indexOf(listener);
this.listeners.splice(index, 1);
}
listenOnce(listener: Listener<T>) {
const disconnect = this.listen((value) => {
disconnect();
listener(value);
});
}
next() {
return new Promise<T>((resolve) => {
this.listenOnce(resolve);
});
}
}
export class EventSource<T> extends BasicSource<T> {
iter: IterSource<T>["iterator"];
disconnect: IterSource<T>["disconnect"];
constructor() {
super();
const { iterator, disconnect } = fromCallback<T>((listener) =>
this.listen(listener)
);
this.iter = iterator;
this.disconnect = disconnect;
}
}

View file

@ -1,14 +0,0 @@
export async function* streamAsyncIterator(stream: ReadableStream) {
// Get a lock on the stream
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
reader.releaseLock();
}
}

View file

@ -1,55 +0,0 @@
import { pushIter, addIterUtils } from "./util/mod.ts";
type Listener<T> = { push: (arg: T) => void; done: () => void };
export type IterSource<T> = ReturnType<typeof fromCallback<T>>;
export function fromCallback<T>(
source: (listener: (value: T) => void) => void,
disconnect?: () => void
) {
let isDone = false;
let listeners: Listener<T>[] = [];
function trigger(value: T) {
if (isDone) {
return;
}
for (const listener of listeners) {
listener.push(value);
}
}
source(trigger);
function done() {
disconnect?.();
if (isDone) {
return;
}
for (const listener of listeners) {
listener.done();
}
listeners = [];
isDone = true;
}
function addListener(listener: Listener<T>) {
listeners.push(listener);
return () => {
removeListener(listener);
};
}
function removeListener(listener: Listener<T>) {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
}
return {
iterator: () => {
const { push, done, getIterator } = pushIter<T>();
addListener({ push, done });
return addIterUtils<T>(getIterator());
},
disconnect: done,
};
}

View file

@ -1,20 +0,0 @@
// import { EventSource } from "../event-source.ts";
// type CombineReturn<J> = J extends AsyncIterableIterator<infer I>[]
// ? I
// : unknown;
// export function combineIter<J extends AsyncIterableIterator<any>[]>(
// ...iterators: J
// ) {
// const source = new EventSource<CombineReturn<J>>();
// const handler = (value: CombineReturn<J>) => {
// source.trigger(value);
// };
// iterators.forEach(async (iterator) => {
// for await (const value of iterator) {
// handler(value);
// }
// });
// return source.stream();
// }

View file

@ -1,42 +0,0 @@
export type IterExpanded<T> = ReturnType<typeof addIterUtils<T>>;
export function addIterUtils<T>(values: AsyncIterableIterator<T>) {
return Object.assign(values, { map, filter, nextValue });
}
async function nextValue<T>(this: AsyncIterableIterator<T>): Promise<T> {
const result = await this.next();
return result.value;
}
function map<T, J>(this: AsyncIterableIterator<T>, map: (from: T) => J) {
const mapGen = async function* (this: AsyncIterableIterator<T>) {
for await (const value of this) {
yield map(value);
}
};
return addIterUtils<J>(mapGen.bind(this)());
}
function filter<T>(
this: AsyncIterableIterator<T>,
filter: (value: T) => boolean
) {
const filterGen = async function* (this: AsyncIterableIterator<T>) {
for await (const value of this) {
if (filter(value)) {
yield value;
}
}
};
const boundFilter = filterGen.bind(this);
return addIterUtils<T>(boundFilter());
}
// function combine<T, J extends AsyncIterableIterator<any>[]>(
// this: AsyncIterableIterator<T>,
// ...others: J
// ) {
// return combineIter(this, ...others);
// }

View file

@ -1,2 +0,0 @@
export * from "./iter-utils.ts";
export * from "./push-iter.ts";

View file

@ -1,59 +0,0 @@
export type GetNext<T> = () => Promise<IteratorResult<Awaited<T>, void>>;
export function pushIter<T>() {
type Value = IteratorResult<T, undefined>;
const values: Value[] = [];
let resolvers: ((value: Value) => void)[] = [];
const pushValue = (value: T) => {
if (resolvers.length <= 0) {
values.push({ done: false, value });
}
for (const resolve of resolvers) {
resolve({ done: false, value });
}
resolvers = [];
};
const pushDone = () => {
if (resolvers.length <= 0) {
values.push({ done: true, value: undefined });
}
for (const resolve of resolvers) {
resolve({ done: true, value: undefined });
}
resolvers = [];
};
const pullValue = () => {
return new Promise<Value>((resolve) => {
if (values.length > 0) {
const value = values.shift();
resolve(value!);
} else {
resolvers.push(resolve);
}
});
};
return {
push: pushValue,
done: pushDone,
getIterator: () => iterFromPull(pullValue),
getIterNext: (): GetNext<T> => {
const iter = iterFromPull(pullValue);
return () => {
return iter.next();
};
},
};
}
async function* iterFromPull<T>(
pullValue: () => Promise<IteratorResult<T, undefined>>
) {
let done;
while (!done) {
const result = await pullValue();
done = result.done;
if (!result.done) {
yield result.value;
}
}
}

View file

@ -1,10 +0,0 @@
export * from "./iterator/mod.ts";
export * from "./array.ts";
export * from "./event-source.ts";
export * from "./queue.ts";
export * from "./number.ts";
export * from "./wait.ts";
export * from "./file.ts";
export * from "./buffer.ts";
export * from "./retry.ts";
// export * from "./buffered-iterator.ts";

View file

@ -1,3 +0,0 @@
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}

View file

@ -1,87 +0,0 @@
import { assertEquals } from "https://deno.land/std@0.104.0/testing/asserts.ts";
import { arrayMove, arrayShuffle } from "./array.ts";
export class Queue<T> {
#current: T | undefined;
#queue: T[] = [];
#waiting: ((value: T) => void)[] = [];
clear() {
const cleared = this.#queue;
this.#queue = [];
return cleared;
}
current() {
return this.#current;
}
upcoming() {
return [...this.#queue];
}
push(...values: T[]) {
this.#queue.push(...values);
for (const waiting of this.#waiting) {
const value = this.#queue.shift();
this.#current = value;
if (value == undefined) {
break;
}
waiting(value);
}
this.#waiting = [];
}
unshift(...values: T[]) {
this.#queue.unshift(...values);
}
shuffle() {
this.#queue = arrayShuffle([...this.#queue]);
}
remove(equals: (first: T) => boolean) {
const original = this.#queue.length;
this.#queue = [...this.#queue.filter((track) => !equals(track))];
return original !== this.#queue.length;
}
move(position: number, equals: (first: T) => boolean) {
const queue = this.#queue;
const from = queue.findIndex((entry) => equals(entry));
if (from !== -1) {
this.#queue = arrayMove(queue, from, position);
return true;
}
return false;
}
async next() {
let value = this.#queue.shift();
this.#current = value;
if (value === undefined) {
value = await new Promise<T>((resolve) => {
this.#waiting.push(resolve);
});
}
return value;
}
}
Deno.test({
name: "Test",
fn: async () => {
const queue = new Queue<string>();
const promise0 = queue.next();
queue.push("Hello");
queue.push("World!");
assertEquals("Hello", await promise0);
assertEquals("World!", await queue.next());
const promise1 = queue.next();
const promise2 = queue.next();
queue.push("Multiple", "Words!");
assertEquals("Multiple", await promise1);
assertEquals("Words!", await promise2);
},
});

View file

@ -1,15 +0,0 @@
export async function retry<T>(
func: () => Promise<T>,
max_tries = 3
): Promise<T | undefined> {
let tries = 0;
while (tries < max_tries) {
try {
return await func();
} catch (error) {
console.log(error);
tries++;
}
}
return undefined;
}

View file

@ -1,5 +0,0 @@
export function wait(time: number) {
return new Promise<void>((resolve) => {
setTimeout(resolve, time);
});
}

40
index.js Normal file
View file

@ -0,0 +1,40 @@
import chalk from 'chalk';
import Discord, { GatewayIntentBits } from 'discord.js';
import { Track } from "./track";
import { MusicSubscription } from './subscription';
import { configs } from "./configs.ts";
import { parseCommand } from "./commands.ts";
import { helpCommand } from "./commands/help.ts";
import { leaveCommand } from "./commands/leave.ts";
import { loopCommand } from "./commands/loop.ts";
import { npCommand } from "./commands/np.ts";
import { pauseCommand, stopCommand } from "./commands/pause.ts";
import { playCommand } from "./commands/play.ts";
import { skipCommand } from "./commands/skip.ts";
import { unloopCommand } from "./commands/unloop.ts";
export const client = new Discord.Client({ intents: [GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds] });
export const subscriptions = new Map();
client.on('ready', () => {
//await registerCommands(bot);
console.log(`${chalk.cyan("permanent waves")} is ready to go`);
});
client.on('interactionCreate', async (interaction) => {
if (!interaction.isCommand() || !interaction.guildId) return;
let subscription = subscriptions.get(interaction.guildId);
parseCommand(interaction, subscription);
});
client.on('error', (e) => {
console.warn;
console.log(e);
});
void client.login(configs.discord_token);
async function registerCommands() {
//console.log(await upsertGlobalApplicationCommands(bot, [helpCommand, leaveCommand, loopCommand, npCommand, pauseCommand, playCommand, skipCommand, stopCommand, unloopCommand]));
}

57
main.ts
View file

@ -1,57 +0,0 @@
import { configs } from "./configs.ts";
import {
Bot,
createBot,
Intents,
startBot,
upsertGlobalApplicationCommands
} from "./deps.ts";
import { parseCommand } from "./commands.ts";
import { cyan } from "https://deno.land/std@0.161.0/fmt/colors.ts";
import { helpCommand } from "./commands/help.ts";
import { leaveCommand } from "./commands/leave.ts";
import { loopCommand } from "./commands/loop.ts";
import { npCommand } from "./commands/np.ts";
import { pauseCommand, stopCommand } from "./commands/pause.ts";
import { playCommand } from "./commands/play.ts";
import { skipCommand } from "./commands/skip.ts";
import { unloopCommand } from "./commands/unloop.ts";
import { enableAudioPlugin } from "./discordeno-audio-plugin/mod.ts";
const baseBot = createBot({
token: configs.discord_token,
intents: Intents.Guilds | Intents.GuildMessages | Intents.GuildVoiceStates,
});
export const bot = enableAudioPlugin(baseBot);
bot.events.ready = async function () {
//await registerCommands(bot);
console.log(`${cyan("permanent waves")} is ready to go`);
}
// Another way to do events
bot.events.interactionCreate = async function (bot, interaction) {
await parseCommand(bot, interaction);
};
await startBot(bot);
async function registerCommands(bot: Bot) {
console.log(await upsertGlobalApplicationCommands(bot, [helpCommand, leaveCommand, loopCommand, npCommand, pauseCommand, playCommand, skipCommand, stopCommand, unloopCommand]));
}
/*
import ytdl from "https://deno.land/x/ytdl_core/mod.ts";
const stream = await ytdl("DhobsmmyGFs", { filter: "audio" });
const chunks: Uint8Array[] = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
const blob = new Blob(chunks);
await Deno.writeFile("video.mp3", new Uint8Array(await blob.arrayBuffer()));*/

1
node_modules/.bin/color-support generated vendored Symbolic link
View file

@ -0,0 +1 @@
../color-support/bin.js

1
node_modules/.bin/esbuild generated vendored Symbolic link
View file

@ -0,0 +1 @@
../esbuild/bin/esbuild

1
node_modules/.bin/mkdirp generated vendored Symbolic link
View file

@ -0,0 +1 @@
../mkdirp/bin/cmd.js

1
node_modules/.bin/node-pre-gyp generated vendored Symbolic link
View file

@ -0,0 +1 @@
../@discordjs/node-pre-gyp/bin/node-pre-gyp

1
node_modules/.bin/nopt generated vendored Symbolic link
View file

@ -0,0 +1 @@
../nopt/bin/nopt.js

1
node_modules/.bin/rimraf generated vendored Symbolic link
View file

@ -0,0 +1 @@
../rimraf/bin.js

1
node_modules/.bin/tsx generated vendored Symbolic link
View file

@ -0,0 +1 @@
../tsx/dist/cli.mjs

1272
node_modules/.package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load diff

19
node_modules/@derhuerst/http-basic/LICENSE generated vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2022 Forbes Lindesay & Jannis R
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

101
node_modules/@derhuerst/http-basic/README.md generated vendored Normal file
View file

@ -0,0 +1,101 @@
# http-basic
**This is a temporary fork of [`ForbesLindesay/http-basic`](https://github.com/ForbesLindesay/http-basic).**
---
Simple wrapper arround http.request/https.request
[![Build Status](https://img.shields.io/travis/ForbesLindesay/http-basic/master.svg)](https://travis-ci.org/ForbesLindesay/http-basic)
[![Dependency Status](https://img.shields.io/david/ForbesLindesay/http-basic.svg)](https://david-dm.org/ForbesLindesay/http-basic)
[![NPM version](https://img.shields.io/npm/v/http-basic.svg)](https://www.npmjs.org/package/http-basic)
## Installation
npm install http-basic
## Usage
```js
var request = require('http-basic');
var options = {followRedirects: true, gzip: true, cache: 'memory'};
var req = request('GET', 'http://example.com', options, function (err, res) {
if (err) throw err;
console.dir(res.statusCode);
res.body.resume();
});
req.end();
```
**method:**
The http method (e.g. `GET`, `POST`, `PUT`, `DELETE` etc.)
**url:**
The url as a string (e.g. `http://example.com`). It must be fully qualified and either http or https.
**options:**
- `headers` - (default `{}`) http headers
- `agent` - (default: `false`) controlls keep-alive (see http://nodejs.org/api/http.html#http_http_request_options_callback)
- `duplex` - (default: `true` except for `GET`, `OPTIONS` and `HEAD` requests) allows you to explicitly set a body on a request that uses a method that normally would not have a body
- `followRedirects` - (default: `false`) - if true, redirects are followed (note that this only affects the result in the callback)
- `maxRedirects` - (default: `Infinity`) - limit the number of redirects allowed.
- `allowRedirectHeaders` (default: `null`) - an array of headers allowed for redirects (none if `null`).
- `gzip` (default: `false`) - automatically accept gzip and deflate encodings. This is kept completely transparent to the user.
- `cache` - (default: `null`) - `'memory'` or `'file'` to use the default built in caches or you can pass your own cache implementation.
- `timeout` (default: `false`) - times out if no response is returned within the given number of milliseconds.
- `socketTimeout` (default: `false`) - calls `req.setTimeout` internally which causes the request to timeout if no new data is seen for the given number of milliseconds.
- `retry` (default: `false`) - retry GET requests. Set this to `true` to retry when the request errors or returns a status code greater than or equal to 400 (can also be a function that takes `(err, req, attemptNo) => shouldRetry`)
- `retryDelay` (default: `200`) - the delay between retries (can also be set to a function that takes `(err, res, attemptNo) => delay`)
- `maxRetries` (default: `5`) - the number of times to retry before giving up.
- `ignoreFailedInvalidation` (default: `false`) - whether the cache should swallow errors if there is a problem removing a cached response. Note that enabling this setting may result in incorrect, cached data being returned to the user.
- `isMatch` - `(requestHeaders: Headers, cachedResponse: CachedResponse, defaultValue: boolean) => boolean` - override the default behaviour for testing whether a cached response matches a request.
- `isExpired` - `(cachedResponse: CachedResponse, defaultValue: boolean) => boolean` - override the default behaviour for testing whether a cached response has expired
- `canCache` - `(res: Response<NodeJS.ReadableStream>, defaultValue: boolean) => boolean` - override the default behaviour for testing whether a response can be cached
**callback:**
The callback is called with `err` as the first argument and `res` as the second argument. `res` is an [http-response-object](https://github.com/ForbesLindesay/http-response-object). It has the following properties:
- `statusCode` - a number representing the HTTP Status Code
- `headers` - an object representing the HTTP headers
- `body` - a readable stream respresenting the request body.
- `url` - the URL that was requested (in the case of redirects, this is the final url that was requested)
**returns:**
If the method is `GET`, `DELETE` or `HEAD`, it returns `undefined`.
Otherwise, it returns a writable stream for the body of the request.
## Implementing a Cache
A `Cache` is an object with three methods:
- `getResponse(url, callback)` - retrieve a cached response object
- `setResponse(url, response)` - cache a response object
- `invalidateResponse(url, callback)` - remove a response which is no longer valid
A cached response object is an object with the following properties:
- `statusCode` - Number
- `headers` - Object (key value pairs of strings)
- `body` - Stream (a stream of binary data)
- `requestHeaders` - Object (key value pairs of strings)
- `requestTimestamp` - Number
`getResponse` should call the callback with an optional error and either `null` or a cached response object, depending on whether the url can be found in the cache. Only `GET`s are cached.
`setResponse` should just swallow any errors it has (or resport them using `console.warn`).
`invalidateResponse` should call the callback with an optional error if it is unable to invalidate a response.
A cache may also define any of the methods from `lib/cache-utils.js` to override behaviour for what gets cached. It is currently still only possible to cache "get" requests, although this could be changed.
## License
MIT

View file

@ -0,0 +1,10 @@
/// <reference types="node" />
import { Headers } from './Headers';
interface CachedResponse {
statusCode: number;
headers: Headers;
body: NodeJS.ReadableStream;
requestHeaders: Headers;
requestTimestamp: number;
}
export { CachedResponse };

View file

@ -0,0 +1,2 @@
"use strict";
exports.__esModule = true;

View file

@ -0,0 +1,14 @@
// @flow
// Generated using flowgen2
import type {Headers} from './Headers';
interface CachedResponse {
statusCode: number;
headers: Headers;
body: stream$Readable;
requestHeaders: Headers;
requestTimestamp: number;
}
export type {CachedResponse};

4
node_modules/@derhuerst/http-basic/lib/Callback.d.ts generated vendored Normal file
View file

@ -0,0 +1,4 @@
/// <reference types="node" />
import Response = require('http-response-object');
declare type Callback = (err: NodeJS.ErrnoException | null, response?: Response<NodeJS.ReadableStream>) => void;
export { Callback };

2
node_modules/@derhuerst/http-basic/lib/Callback.js generated vendored Normal file
View file

@ -0,0 +1,2 @@
"use strict";
exports.__esModule = true;

View file

@ -0,0 +1,11 @@
// @flow
// Generated using flowgen2
const Response = require('http-response-object');
type Callback = (
err: ErrnoError | null,
response?: Response<stream$Readable>,
) => void;
export type {Callback};

12
node_modules/@derhuerst/http-basic/lib/FileCache.d.ts generated vendored Normal file
View file

@ -0,0 +1,12 @@
/// <reference types="node" />
import { ICache } from './ICache';
import { CachedResponse } from './CachedResponse';
export default class FileCache implements ICache {
private readonly _location;
constructor(location: string);
getResponse(url: string, callback: (err: null | Error, response: null | CachedResponse) => void): void;
setResponse(url: string, response: CachedResponse): void;
updateResponseHeaders(url: string, response: Pick<CachedResponse, 'headers' | 'requestTimestamp'>): void;
invalidateResponse(url: string, callback: (err: NodeJS.ErrnoException | null) => void): void;
getCacheKey(url: string): string;
}

112
node_modules/@derhuerst/http-basic/lib/FileCache.js generated vendored Normal file
View file

@ -0,0 +1,112 @@
'use strict';
exports.__esModule = true;
var fs = require("fs");
var path_1 = require("path");
var crypto_1 = require("crypto");
function jsonParse(data, cb) {
var result = null;
try {
result = JSON.parse(data);
}
catch (ex) {
if (ex instanceof Error) {
return cb(ex);
}
return cb(new Error(ex + ''));
}
cb(null, result);
}
var FileCache = /** @class */ (function () {
function FileCache(location) {
this._location = location;
}
FileCache.prototype.getResponse = function (url, callback) {
var key = (0, path_1.resolve)(this._location, this.getCacheKey(url));
fs.readFile(key + '.json', 'utf8', function (err, data) {
if (err && err.code === 'ENOENT')
return callback(null, null);
else if (err)
return callback(err, null);
jsonParse(data, function (err, response) {
if (err) {
return callback(err, null);
}
var body = fs.createReadStream(key + '.body');
response.body = body;
callback(null, response);
});
});
};
FileCache.prototype.setResponse = function (url, response) {
var key = (0, path_1.resolve)(this._location, this.getCacheKey(url));
var errored = false;
fs.mkdir(this._location, { recursive: true }, function (err) {
if (err && err.code !== 'EEXIST') {
console.warn('Error creating cache: ' + err.message);
return;
}
response.body.pipe(fs.createWriteStream(key + '.body')).on('error', function (err) {
errored = true;
console.warn('Error writing to cache: ' + err.message);
}).on('close', function () {
if (!errored) {
fs.writeFile(key + '.json', JSON.stringify({
statusCode: response.statusCode,
headers: response.headers,
requestHeaders: response.requestHeaders,
requestTimestamp: response.requestTimestamp
}, null, ' '), function (err) {
if (err) {
console.warn('Error writing to cache: ' + err.message);
}
});
}
});
});
};
FileCache.prototype.updateResponseHeaders = function (url, response) {
var key = (0, path_1.resolve)(this._location, this.getCacheKey(url));
fs.readFile(key + '.json', 'utf8', function (err, data) {
if (err) {
console.warn('Error writing to cache: ' + err.message);
return;
}
var parsed = null;
try {
parsed = JSON.parse(data);
}
catch (ex) {
if (ex instanceof Error) {
console.warn('Error writing to cache: ' + ex.message);
}
return;
}
fs.writeFile(key + '.json', JSON.stringify({
statusCode: parsed.statusCode,
headers: response.headers,
requestHeaders: parsed.requestHeaders,
requestTimestamp: response.requestTimestamp
}, null, ' '), function (err) {
if (err) {
console.warn('Error writing to cache: ' + err.message);
}
});
});
};
FileCache.prototype.invalidateResponse = function (url, callback) {
var key = (0, path_1.resolve)(this._location, this.getCacheKey(url));
fs.unlink(key + '.json', function (err) {
if (err && err.code === 'ENOENT')
return callback(null);
else
callback(err || null);
});
};
FileCache.prototype.getCacheKey = function (url) {
var hash = (0, crypto_1.createHash)('sha512');
hash.update(url);
return hash.digest('hex');
};
return FileCache;
}());
exports["default"] = FileCache;

View file

@ -0,0 +1,24 @@
// @flow
// Generated using flowgen2
import type {ICache} from './ICache';
import type {CachedResponse} from './CachedResponse';
declare class FileCache {
constructor(location: string): void;
getResponse(
url: string,
callback: (err: null | Error, response: null | CachedResponse) => void,
): void;
setResponse(url: string, response: CachedResponse): void;
updateResponseHeaders(
url: string,
response: {[key: 'headers' | 'requestTimestamp']: any},
): void;
invalidateResponse(
url: string,
callback: (err: ErrnoError | null) => void,
): void;
getCacheKey(url: string): string;
}
export default FileCache;

3
node_modules/@derhuerst/http-basic/lib/Headers.d.ts generated vendored Normal file
View file

@ -0,0 +1,3 @@
/// <reference types="node" />
import { IncomingHttpHeaders } from 'http';
export declare type Headers = IncomingHttpHeaders;

2
node_modules/@derhuerst/http-basic/lib/Headers.js generated vendored Normal file
View file

@ -0,0 +1,2 @@
"use strict";
exports.__esModule = true;

View file

@ -0,0 +1,7 @@
// @flow
// Generated using flowgen2
type IncomingHttpHeaders = Object;
type Headers = IncomingHttpHeaders;
export type {Headers};

2
node_modules/@derhuerst/http-basic/lib/HttpVerb.d.ts generated vendored Normal file
View file

@ -0,0 +1,2 @@
declare type HttpVerb = ('GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH');
export { HttpVerb };

2
node_modules/@derhuerst/http-basic/lib/HttpVerb.js generated vendored Normal file
View file

@ -0,0 +1,2 @@
"use strict";
exports.__esModule = true;

View file

@ -0,0 +1,15 @@
// @flow
// Generated using flowgen2
type HttpVerb =
| 'GET'
| 'HEAD'
| 'POST'
| 'PUT'
| 'DELETE'
| 'CONNECT'
| 'OPTIONS'
| 'TRACE'
| 'PATCH';
export type {HttpVerb};

8
node_modules/@derhuerst/http-basic/lib/ICache.d.ts generated vendored Normal file
View file

@ -0,0 +1,8 @@
import { CachedResponse } from './CachedResponse';
interface ICache {
getResponse(url: string, cb: (err: Error | null, response: CachedResponse | null) => void): void;
setResponse(url: string, response: CachedResponse | null): void;
updateResponseHeaders?: (url: string, response: Pick<CachedResponse, 'headers' | 'requestTimestamp'>) => void;
invalidateResponse(url: string, cb: (err: Error | null) => void): void;
}
export { ICache };

2
node_modules/@derhuerst/http-basic/lib/ICache.js generated vendored Normal file
View file

@ -0,0 +1,2 @@
"use strict";
exports.__esModule = true;

19
node_modules/@derhuerst/http-basic/lib/ICache.js.flow generated vendored Normal file
View file

@ -0,0 +1,19 @@
// @flow
// Generated using flowgen2
import type {CachedResponse} from './CachedResponse';
interface ICache {
getResponse(
url: string,
cb: (err: Error | null, response: CachedResponse | null) => void,
): void;
setResponse(url: string, response: CachedResponse | null): void;
updateResponseHeaders?: (
url: string,
response: {[key: 'headers' | 'requestTimestamp']: any},
) => void;
invalidateResponse(url: string, cb: (err: Error | null) => void): void;
}
export type {ICache};

View file

@ -0,0 +1,9 @@
/// <reference types="node" />
import { CachedResponse } from './CachedResponse';
export default class MemoryCache {
private readonly _cache;
getResponse(url: string, callback: (err: null | Error, response: null | CachedResponse) => void): void;
updateResponseHeaders(url: string, response: Pick<CachedResponse, 'headers' | 'requestTimestamp'>): void;
setResponse(url: string, response: CachedResponse): void;
invalidateResponse(url: string, callback: (err: NodeJS.ErrnoException | null) => void): void;
}

59
node_modules/@derhuerst/http-basic/lib/MemoryCache.js generated vendored Normal file
View file

@ -0,0 +1,59 @@
'use strict';
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
exports.__esModule = true;
var stream_1 = require("stream");
var concat = require("concat-stream");
var MemoryCache = /** @class */ (function () {
function MemoryCache() {
this._cache = {};
}
MemoryCache.prototype.getResponse = function (url, callback) {
var cache = this._cache;
if (cache[url]) {
var body = new stream_1.PassThrough();
body.end(cache[url].body);
callback(null, {
statusCode: cache[url].statusCode,
headers: cache[url].headers,
body: body,
requestHeaders: cache[url].requestHeaders,
requestTimestamp: cache[url].requestTimestamp
});
}
else {
callback(null, null);
}
};
MemoryCache.prototype.updateResponseHeaders = function (url, response) {
this._cache[url] = __assign(__assign({}, this._cache[url]), { headers: response.headers, requestTimestamp: response.requestTimestamp });
};
MemoryCache.prototype.setResponse = function (url, response) {
var cache = this._cache;
response.body.pipe(concat(function (body) {
cache[url] = {
statusCode: response.statusCode,
headers: response.headers,
body: body,
requestHeaders: response.requestHeaders,
requestTimestamp: response.requestTimestamp
};
}));
};
MemoryCache.prototype.invalidateResponse = function (url, callback) {
var cache = this._cache;
delete cache[url];
callback(null);
};
return MemoryCache;
}());
exports["default"] = MemoryCache;

View file

@ -0,0 +1,21 @@
// @flow
// Generated using flowgen2
import type {CachedResponse} from './CachedResponse';
declare class MemoryCache {
getResponse(
url: string,
callback: (err: null | Error, response: null | CachedResponse) => void,
): void;
updateResponseHeaders(
url: string,
response: {[key: 'headers' | 'requestTimestamp']: any},
): void;
setResponse(url: string, response: CachedResponse): void;
invalidateResponse(
url: string,
callback: (err: ErrnoError | null) => void,
): void;
}
export default MemoryCache;

27
node_modules/@derhuerst/http-basic/lib/Options.d.ts generated vendored Normal file
View file

@ -0,0 +1,27 @@
/// <reference types="node" />
/// <reference types="node" />
import { Agent } from 'http';
import { Headers } from './Headers';
import { ICache } from './ICache';
import Response = require('http-response-object');
import { CachedResponse } from './CachedResponse';
interface Options {
agent?: Agent | boolean;
allowRedirectHeaders?: string[];
cache?: 'file' | 'memory' | ICache;
duplex?: boolean;
followRedirects?: boolean;
gzip?: boolean;
headers?: Headers;
ignoreFailedInvalidation?: boolean;
maxRedirects?: number;
maxRetries?: number;
retry?: boolean | ((err: NodeJS.ErrnoException | null, res: Response<NodeJS.ReadableStream> | void, attemptNumber: number) => boolean);
retryDelay?: number | ((err: NodeJS.ErrnoException | null, res: Response<NodeJS.ReadableStream> | void, attemptNumber: number) => number);
socketTimeout?: number;
timeout?: number;
isMatch?: (requestHeaders: Headers, cachedResponse: CachedResponse, defaultValue: boolean) => boolean;
isExpired?: (cachedResponse: CachedResponse, defaultValue: boolean) => boolean;
canCache?: (res: Response<NodeJS.ReadableStream>, defaultValue: boolean) => boolean;
}
export { Options };

2
node_modules/@derhuerst/http-basic/lib/Options.js generated vendored Normal file
View file

@ -0,0 +1,2 @@
"use strict";
exports.__esModule = true;

49
node_modules/@derhuerst/http-basic/lib/Options.js.flow generated vendored Normal file
View file

@ -0,0 +1,49 @@
// @flow
// Generated using flowgen2
import {Agent} from 'http';
import type {Headers} from './Headers';
import type {ICache} from './ICache';
const Response = require('http-response-object');
import type {CachedResponse} from './CachedResponse';
interface Options {
agent?: Agent | boolean;
allowRedirectHeaders?: Array<string>;
cache?: 'file' | 'memory' | ICache;
duplex?: boolean;
followRedirects?: boolean;
gzip?: boolean;
headers?: Headers;
ignoreFailedInvalidation?: boolean;
maxRedirects?: number;
maxRetries?: number;
retry?:
| boolean
| ((
err: ErrnoError | null,
res: Response<stream$Readable> | void,
attemptNumber: number,
) => boolean);
retryDelay?:
| number
| ((
err: ErrnoError | null,
res: Response<stream$Readable> | void,
attemptNumber: number,
) => number);
socketTimeout?: number;
timeout?: number;
isMatch?: (
requestHeaders: Headers,
cachedResponse: CachedResponse,
defaultValue: boolean,
) => boolean;
isExpired?: (
cachedResponse: CachedResponse,
defaultValue: boolean,
) => boolean;
canCache?: (res: Response<stream$Readable>, defaultValue: boolean) => boolean;
}
export type {Options};

View file

@ -0,0 +1,14 @@
import { CachedResponse } from './CachedResponse';
import Response = require('http-response-object');
export declare type Policy = {
maxage: number | null;
};
/**
* returns true if this response is cacheable (according to cache-control headers)
*/
export declare function isCacheable<T>(res: Response<T> | CachedResponse): boolean;
/**
* if the response is cacheable, returns an object detailing the maxage of the cache
* otherwise returns null
*/
export declare function cachePolicy<T>(res: Response<T> | CachedResponse): Policy | null;

View file

@ -0,0 +1,54 @@
"use strict";
exports.__esModule = true;
exports.cachePolicy = exports.isCacheable = void 0;
var parseCacheControl = require('parse-cache-control');
function parseCacheControlHeader(res) {
var cacheControl = res.headers['cache-control'];
var normalisedCacheControl = typeof cacheControl === 'string' ? cacheControl.trim() : ''; // must be normalised for parsing (e.g. parseCacheControl)
if (!cacheControl) {
return null;
}
return parseCacheControl(cacheControl);
}
// for the purposes of this library, we err on the side of caution and do not cache anything except public (or implicit public)
var nonCaching = ['private', 'no-cache', 'no-store', 'no-transform', 'must-revalidate', 'proxy-revalidate'];
function isCacheControlCacheable(parsedCacheControl) {
if (!parsedCacheControl) {
return false;
}
if (parsedCacheControl.public) {
return true;
}
// note that the library does not currently support s-maxage
if (parsedCacheControl["max-age"]) {
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
// The max-age directive on a response implies that the response is cacheable (i.e., "public") unless some other, more restrictive cache directive is also present.
for (var i = 0; i < nonCaching.length; i++) {
if (parsedCacheControl[nonCaching[i]]) {
return false;
}
}
return true;
}
return false;
}
/**
* returns true if this response is cacheable (according to cache-control headers)
*/
function isCacheable(res) {
return isCacheControlCacheable(parseCacheControlHeader(res));
}
exports.isCacheable = isCacheable;
function buildPolicy(parsedCacheControl) {
// note that the library does not currently support s-maxage
return { maxage: parsedCacheControl['max-age'] || null };
}
/**
* if the response is cacheable, returns an object detailing the maxage of the cache
* otherwise returns null
*/
function cachePolicy(res) {
var parsed = parseCacheControlHeader(res);
return parsed && isCacheControlCacheable(parsed) ? buildPolicy(parsed) : null;
}
exports.cachePolicy = cachePolicy;

View file

@ -0,0 +1,16 @@
// @flow
// Generated using flowgen2
import type {CachedResponse} from './CachedResponse';
const Response = require('http-response-object');
type Policy = {maxage: number | null};
export type {Policy};
declare function isCacheable<T>(res: Response<T> | CachedResponse): boolean;
export {isCacheable};
declare function cachePolicy<T>(
res: Response<T> | CachedResponse,
): Policy | null;
export {cachePolicy};

View file

@ -0,0 +1,6 @@
import Response = require('http-response-object');
import { Headers } from './Headers';
import { CachedResponse } from './CachedResponse';
export declare function isMatch(requestHeaders: Headers, cachedResponse: CachedResponse): boolean;
export declare function isExpired(cachedResponse: CachedResponse): boolean;
export declare function canCache<T>(res: Response<T>): boolean;

45
node_modules/@derhuerst/http-basic/lib/cache-utils.js generated vendored Normal file
View file

@ -0,0 +1,45 @@
"use strict";
exports.__esModule = true;
exports.canCache = exports.isExpired = exports.isMatch = void 0;
var cache_control_utils_1 = require("./cache-control-utils");
function isMatch(requestHeaders, cachedResponse) {
var vary = cachedResponse.headers['vary'];
if (vary && cachedResponse.requestHeaders) {
vary = '' + vary;
return vary.split(',').map(function (header) { return header.trim().toLowerCase(); }).every(function (header) {
return requestHeaders[header] === cachedResponse.requestHeaders[header];
});
}
else {
return true;
}
}
exports.isMatch = isMatch;
;
function isExpired(cachedResponse) {
var policy = (0, cache_control_utils_1.cachePolicy)(cachedResponse);
if (policy) {
var time = (Date.now() - cachedResponse.requestTimestamp) / 1000;
if (policy.maxage !== null && policy.maxage > time) {
return false;
}
}
if (cachedResponse.statusCode === 301 || cachedResponse.statusCode === 308)
return false;
return true;
}
exports.isExpired = isExpired;
;
function canCache(res) {
if (res.headers['etag'])
return true;
if (res.headers['last-modified'])
return true;
if ((0, cache_control_utils_1.isCacheable)(res))
return true;
if (res.statusCode === 301 || res.statusCode === 308)
return true;
return false;
}
exports.canCache = canCache;
;

View file

@ -0,0 +1,18 @@
// @flow
// Generated using flowgen2
const Response = require('http-response-object');
import type {Headers} from './Headers';
import type {CachedResponse} from './CachedResponse';
declare function isMatch(
requestHeaders: Headers,
cachedResponse: CachedResponse,
): boolean;
export {isMatch};
declare function isExpired(cachedResponse: CachedResponse): boolean;
export {isExpired};
declare function canCache<T>(res: Response<T>): boolean;
export {canCache};

22
node_modules/@derhuerst/http-basic/lib/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,22 @@
/// <reference types="node" />
/// <reference types="node" />
import FileCache from './FileCache';
import MemoryCache from './MemoryCache';
import { Callback } from './Callback';
import { CachedResponse } from './CachedResponse';
import { HttpVerb } from './HttpVerb';
import { ICache } from './ICache';
import { Options } from './Options';
import Response = require('http-response-object');
import { URL } from 'url';
declare function request(method: HttpVerb, url: string | URL, options: Options | null | void, callback: Callback): void | NodeJS.WritableStream;
declare function request(method: HttpVerb, url: string | URL, callback: Callback): void | NodeJS.WritableStream;
export default request;
export { HttpVerb };
export { Options };
export { FileCache };
export { MemoryCache };
export { Callback };
export { Response };
export { CachedResponse };
export { ICache };

Some files were not shown because too many files have changed in this diff Show more