Compare commits
No commits in common. "3234e14a15cf066aa608fa61d05d39b9411eaa0c" and "4680bfecc92c6c9c2fd28c9d51792ac1de598500" have entirely different histories.
3234e14a15
...
4680bfecc9
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
/errors.txt
|
/errors.txt
|
||||||
/unused_extras.ts
|
/unused_extras.ts
|
||||||
/test
|
/test
|
||||||
|
/node_modules
|
|
@ -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/).
|
35
commands.ts
35
commands.ts
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.`
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
116
commands/play.ts
116
commands/play.ts
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
23
deps.ts
|
@ -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";
|
|
2
discordeno-audio-plugin/.gitignore
vendored
2
discordeno-audio-plugin/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
# VS code settings
|
|
||||||
.vscode
|
|
|
@ -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")) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -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";
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./src/mod.ts";
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
//});
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export * from "./audio-source.ts";
|
|
||||||
export * from "./universal.ts";
|
|
||||||
export * from "./youtube.ts";
|
|
||||||
export * from "./local.ts";
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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!);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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),
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./types.ts";
|
|
||||||
export * from "./queue-player.ts";
|
|
||||||
export * from "./raw-player.ts";
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./discover.ts";
|
|
||||||
export * from "./speaking.ts";
|
|
||||||
export * from "./packet.ts";
|
|
|
@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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() {}
|
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
// }
|
|
|
@ -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);
|
|
||||||
// }
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from "./iter-utils.ts";
|
|
||||||
export * from "./push-iter.ts";
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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";
|
|
|
@ -1,3 +0,0 @@
|
||||||
export function clamp(value: number, min: number, max: number): number {
|
|
||||||
return Math.min(Math.max(value, min), max);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export function wait(time: number) {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
setTimeout(resolve, time);
|
|
||||||
});
|
|
||||||
}
|
|
40
index.js
Normal file
40
index.js
Normal 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
57
main.ts
|
@ -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
1
node_modules/.bin/color-support
generated
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../color-support/bin.js
|
1
node_modules/.bin/esbuild
generated
vendored
Symbolic link
1
node_modules/.bin/esbuild
generated
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../esbuild/bin/esbuild
|
1
node_modules/.bin/mkdirp
generated
vendored
Symbolic link
1
node_modules/.bin/mkdirp
generated
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../mkdirp/bin/cmd.js
|
1
node_modules/.bin/node-pre-gyp
generated
vendored
Symbolic link
1
node_modules/.bin/node-pre-gyp
generated
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../@discordjs/node-pre-gyp/bin/node-pre-gyp
|
1
node_modules/.bin/nopt
generated
vendored
Symbolic link
1
node_modules/.bin/nopt
generated
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../nopt/bin/nopt.js
|
1
node_modules/.bin/rimraf
generated
vendored
Symbolic link
1
node_modules/.bin/rimraf
generated
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../rimraf/bin.js
|
1
node_modules/.bin/tsx
generated
vendored
Symbolic link
1
node_modules/.bin/tsx
generated
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../tsx/dist/cli.mjs
|
1272
node_modules/.package-lock.json
generated
vendored
Normal file
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
19
node_modules/@derhuerst/http-basic/LICENSE
generated
vendored
Normal 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
101
node_modules/@derhuerst/http-basic/README.md
generated
vendored
Normal 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
|
10
node_modules/@derhuerst/http-basic/lib/CachedResponse.d.ts
generated
vendored
Normal file
10
node_modules/@derhuerst/http-basic/lib/CachedResponse.d.ts
generated
vendored
Normal 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 };
|
2
node_modules/@derhuerst/http-basic/lib/CachedResponse.js
generated
vendored
Normal file
2
node_modules/@derhuerst/http-basic/lib/CachedResponse.js
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";
|
||||||
|
exports.__esModule = true;
|
14
node_modules/@derhuerst/http-basic/lib/CachedResponse.js.flow
generated
vendored
Normal file
14
node_modules/@derhuerst/http-basic/lib/CachedResponse.js.flow
generated
vendored
Normal 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
4
node_modules/@derhuerst/http-basic/lib/Callback.d.ts
generated
vendored
Normal 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
2
node_modules/@derhuerst/http-basic/lib/Callback.js
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";
|
||||||
|
exports.__esModule = true;
|
11
node_modules/@derhuerst/http-basic/lib/Callback.js.flow
generated
vendored
Normal file
11
node_modules/@derhuerst/http-basic/lib/Callback.js.flow
generated
vendored
Normal 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
12
node_modules/@derhuerst/http-basic/lib/FileCache.d.ts
generated
vendored
Normal 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
112
node_modules/@derhuerst/http-basic/lib/FileCache.js
generated
vendored
Normal 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;
|
24
node_modules/@derhuerst/http-basic/lib/FileCache.js.flow
generated
vendored
Normal file
24
node_modules/@derhuerst/http-basic/lib/FileCache.js.flow
generated
vendored
Normal 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
3
node_modules/@derhuerst/http-basic/lib/Headers.d.ts
generated
vendored
Normal 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
2
node_modules/@derhuerst/http-basic/lib/Headers.js
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";
|
||||||
|
exports.__esModule = true;
|
7
node_modules/@derhuerst/http-basic/lib/Headers.js.flow
generated
vendored
Normal file
7
node_modules/@derhuerst/http-basic/lib/Headers.js.flow
generated
vendored
Normal 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
2
node_modules/@derhuerst/http-basic/lib/HttpVerb.d.ts
generated
vendored
Normal 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
2
node_modules/@derhuerst/http-basic/lib/HttpVerb.js
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";
|
||||||
|
exports.__esModule = true;
|
15
node_modules/@derhuerst/http-basic/lib/HttpVerb.js.flow
generated
vendored
Normal file
15
node_modules/@derhuerst/http-basic/lib/HttpVerb.js.flow
generated
vendored
Normal 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
8
node_modules/@derhuerst/http-basic/lib/ICache.d.ts
generated
vendored
Normal 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
2
node_modules/@derhuerst/http-basic/lib/ICache.js
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";
|
||||||
|
exports.__esModule = true;
|
19
node_modules/@derhuerst/http-basic/lib/ICache.js.flow
generated
vendored
Normal file
19
node_modules/@derhuerst/http-basic/lib/ICache.js.flow
generated
vendored
Normal 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};
|
9
node_modules/@derhuerst/http-basic/lib/MemoryCache.d.ts
generated
vendored
Normal file
9
node_modules/@derhuerst/http-basic/lib/MemoryCache.d.ts
generated
vendored
Normal 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
59
node_modules/@derhuerst/http-basic/lib/MemoryCache.js
generated
vendored
Normal 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;
|
21
node_modules/@derhuerst/http-basic/lib/MemoryCache.js.flow
generated
vendored
Normal file
21
node_modules/@derhuerst/http-basic/lib/MemoryCache.js.flow
generated
vendored
Normal 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
27
node_modules/@derhuerst/http-basic/lib/Options.d.ts
generated
vendored
Normal 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
2
node_modules/@derhuerst/http-basic/lib/Options.js
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";
|
||||||
|
exports.__esModule = true;
|
49
node_modules/@derhuerst/http-basic/lib/Options.js.flow
generated
vendored
Normal file
49
node_modules/@derhuerst/http-basic/lib/Options.js.flow
generated
vendored
Normal 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};
|
14
node_modules/@derhuerst/http-basic/lib/cache-control-utils.d.ts
generated
vendored
Normal file
14
node_modules/@derhuerst/http-basic/lib/cache-control-utils.d.ts
generated
vendored
Normal 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;
|
54
node_modules/@derhuerst/http-basic/lib/cache-control-utils.js
generated
vendored
Normal file
54
node_modules/@derhuerst/http-basic/lib/cache-control-utils.js
generated
vendored
Normal 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;
|
16
node_modules/@derhuerst/http-basic/lib/cache-control-utils.js.flow
generated
vendored
Normal file
16
node_modules/@derhuerst/http-basic/lib/cache-control-utils.js.flow
generated
vendored
Normal 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};
|
6
node_modules/@derhuerst/http-basic/lib/cache-utils.d.ts
generated
vendored
Normal file
6
node_modules/@derhuerst/http-basic/lib/cache-utils.d.ts
generated
vendored
Normal 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
45
node_modules/@derhuerst/http-basic/lib/cache-utils.js
generated
vendored
Normal 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;
|
||||||
|
;
|
18
node_modules/@derhuerst/http-basic/lib/cache-utils.js.flow
generated
vendored
Normal file
18
node_modules/@derhuerst/http-basic/lib/cache-utils.js.flow
generated
vendored
Normal 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
22
node_modules/@derhuerst/http-basic/lib/index.d.ts
generated
vendored
Normal 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
Loading…
Reference in a new issue