Compare commits
No commits in common. "3234e14a15cf066aa608fa61d05d39b9411eaa0c" and "4680bfecc92c6c9c2fd28c9d51792ac1de598500" have entirely different histories.
3234e14a15
...
4680bfecc9
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
/configs.ts
|
||||
/errors.txt
|
||||
/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 { invalidCommand } from "./commands/invalid_command.ts";
|
||||
import { leave } from "./commands/leave.ts";
|
||||
|
@ -9,53 +12,51 @@ import { play } from "./commands/play.ts";
|
|||
import { skip } from "./commands/skip.ts";
|
||||
import { unloop } from "./commands/unloop.ts";
|
||||
|
||||
import { red } from "https://deno.land/std@0.161.0/fmt/colors.ts";
|
||||
|
||||
export async function parseCommand(bot: Bot, interaction: Interaction) {
|
||||
if(!interaction.data) {
|
||||
console.log(red("invalid interaction data was passed through somehow:"));
|
||||
export async function parseCommand(interaction: Interaction, subscription: MusicSubscription) {
|
||||
if(!interaction) {
|
||||
console.log(chalk.red("invalid interaction data was passed through somehow:"));
|
||||
console.log(interaction);
|
||||
return;
|
||||
}
|
||||
switch(interaction.data.name) {
|
||||
switch(interaction.commandName) {
|
||||
case "help": {
|
||||
await help(bot, interaction);
|
||||
await help(interaction);
|
||||
break;
|
||||
}
|
||||
case "leave": {
|
||||
await leave(bot, interaction);
|
||||
await leave(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
case "loop": {
|
||||
await loop(bot, interaction);
|
||||
await loop(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
case "np": {
|
||||
await np(bot, interaction);
|
||||
await np(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
case "pause": {
|
||||
await pause(bot, interaction);
|
||||
await pause(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
case "play": {
|
||||
await play(bot, interaction);
|
||||
await play(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
case "skip": {
|
||||
await skip(bot, interaction);
|
||||
await skip(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
case "stop": {
|
||||
await pause(bot, interaction);
|
||||
await pause(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
case "unloop": {
|
||||
await unloop(bot, interaction);
|
||||
await unloop(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
await invalidCommand(bot, interaction);
|
||||
await invalidCommand(interaction, subscription);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +1,50 @@
|
|||
import {
|
||||
Bot,
|
||||
Interaction,
|
||||
sendPrivateInteractionResponse,
|
||||
type ApplicationCommandOption,
|
||||
type ApplicationCommandOptionChoice,
|
||||
type CreateSlashApplicationCommand,
|
||||
type InteractionResponse
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
|
||||
const helpChoices = [
|
||||
<ApplicationCommandOptionChoice>{
|
||||
{
|
||||
name: "play",
|
||||
value: "play"
|
||||
}
|
||||
];
|
||||
|
||||
const helpResponse = <InteractionResponse>{
|
||||
type: 4, // ChannelMessageWithSource
|
||||
data: {
|
||||
content: `/help: displays this message\n/play: plays a song`
|
||||
}
|
||||
const helpResponse = {
|
||||
content: `/help: displays this message\n/play: plays a song`,
|
||||
ephemeral: true
|
||||
}
|
||||
|
||||
const playResponse = <InteractionResponse>{
|
||||
type: 4, // ChannelMessageWithSource
|
||||
data: {
|
||||
content: `/play: Add a song or playlist to the queue and starts the music if it's not already playing
|
||||
const playResponse = {
|
||||
content: `/play: Add a song or playlist to the queue and starts the music if it's not already playing
|
||||
**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",
|
||||
description: "Lists the bot's commands and describes how to use them",
|
||||
dmPermission: false,
|
||||
options: [
|
||||
<ApplicationCommandOption>{
|
||||
type: 3, // string
|
||||
name: "command",
|
||||
description: "Displays additional info about a particular command",
|
||||
choices: helpChoices,
|
||||
required: false
|
||||
},
|
||||
{
|
||||
type: 'STRING' as const,
|
||||
name: "command",
|
||||
description: "Displays additional info about a particular command",
|
||||
choices: helpChoices,
|
||||
required: false
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
export async function help(bot: Bot, interaction: Interaction) {
|
||||
export async function help(interaction: Interaction) {
|
||||
if(!interaction.guildId) return;
|
||||
if(!interaction.data) return;
|
||||
|
||||
if(interaction.data.options) {
|
||||
switch(interaction.data.options[0].value) {
|
||||
case "play": {
|
||||
await sendPrivateInteractionResponse(bot, interaction.id, interaction.token, playResponse);
|
||||
await interaction.reply(playResponse);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await sendPrivateInteractionResponse(bot, interaction.id, interaction.token, helpResponse);
|
||||
await interaction.reply(helpResponse);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
import {
|
||||
Bot,
|
||||
Interaction,
|
||||
sendPrivateInteractionResponse,
|
||||
type InteractionResponse
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
|
||||
export async function invalidCommand(bot: Bot, interaction: Interaction) {
|
||||
export async function invalidCommand(interaction: Interaction) {
|
||||
if (!interaction.guildId) return;
|
||||
await sendPrivateInteractionResponse(bot, interaction.id, interaction.token, invalidCommandResponse);
|
||||
await interaction.reply(invalidCommandResponse);
|
||||
}
|
||||
|
||||
const invalidCommandResponse = <InteractionResponse>{
|
||||
type: 4, // ChannelMessageWithSource
|
||||
data: {
|
||||
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.`
|
||||
}
|
||||
const invalidCommandResponse = {
|
||||
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.`,
|
||||
ephemeral: true
|
||||
}
|
|
@ -1,34 +1,27 @@
|
|||
import {
|
||||
Bot,
|
||||
editOriginalInteractionResponse,
|
||||
getConnectionData,
|
||||
Interaction,
|
||||
leaveVoiceChannel,
|
||||
sendInteractionResponse,
|
||||
type CreateSlashApplicationCommand
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
|
||||
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 leftResponse = formatCallbackData(`Left channel.`);
|
||||
|
||||
export const leaveCommand = <CreateSlashApplicationCommand>{
|
||||
export const leaveCommand = {
|
||||
name: "leave",
|
||||
description: "Makes the bot leave the current voice channel",
|
||||
dmPermission: false,
|
||||
description: "Makes the bot leave the current voice channel"
|
||||
};
|
||||
|
||||
export async function leave(bot: Bot, interaction: Interaction) {
|
||||
export async function leave(interaction: Interaction, subscription: MusicSubscription) {
|
||||
if (!interaction.guildId) return;
|
||||
const conn = getConnectionData(bot.id, interaction.guildId);
|
||||
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse);
|
||||
await interaction.reply(waitingForResponse);
|
||||
|
||||
if(!conn.connectInfo.endpoint) {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, notInVoiceResponse);
|
||||
if(!subscription) {
|
||||
await interaction.editReply(notInVoiceResponse);
|
||||
} else {
|
||||
await leaveVoiceChannel(bot, interaction.guildId);
|
||||
await editOriginalInteractionResponse(bot, interaction.token, leftResponse);
|
||||
subscription.voiceConnection.destroy();
|
||||
subscriptions.delete(interaction.guildId);
|
||||
await interaction.editReply(leftResponse);
|
||||
}
|
||||
}
|
|
@ -1,45 +1,39 @@
|
|||
import {
|
||||
Bot,
|
||||
editOriginalInteractionResponse,
|
||||
Interaction,
|
||||
sendInteractionResponse,
|
||||
type CreateSlashApplicationCommand
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
|
||||
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) {
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
function alreadyLoopingResponse(interaction: Interaction) {
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
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.`);
|
||||
|
||||
function loopEnabledResponse(bot: Bot, interaction: Interaction) {
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
function loopEnabledResponse(interaction: Interaction) {
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
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",
|
||||
description: "Loops the currently playijng song. All other songs remain in the queue",
|
||||
dmPermission: false,
|
||||
description: "Loops the currently playijng song. All other songs remain in the queue"
|
||||
};
|
||||
|
||||
export async function loop(bot: Bot, interaction: Interaction) {
|
||||
export async function loop(interaction: Interaction, subscription: MusicSubscription) {
|
||||
if (!interaction.guildId) return;
|
||||
await ensureVoiceConnection(bot, interaction.guildId);
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse);
|
||||
await ensureVoiceConnection(interaction);
|
||||
await interaction.reply(waitingForResponse);
|
||||
|
||||
if(!player.nowPlaying) {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, nothingToLoopResponse);
|
||||
} else if(!player.looping){
|
||||
await player.loop(true);
|
||||
await editOriginalInteractionResponse(bot, interaction.token, loopEnabledResponse(bot, interaction));
|
||||
if(subscription.queue.length === 0) {
|
||||
await interaction.editReply(nothingToLoopResponse);
|
||||
} else if(!subscription.looping){
|
||||
subscription.looping = true;
|
||||
await interaction.editReply(interaction);
|
||||
} else {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, alreadyLoopingResponse(bot, interaction));
|
||||
await interaction.editReply(interaction);
|
||||
}
|
||||
}
|
|
@ -1,76 +1,48 @@
|
|||
import {
|
||||
Bot,
|
||||
Interaction,
|
||||
InteractionResponse,
|
||||
sendInteractionResponse,
|
||||
sendMessage,
|
||||
type CreateMessage,
|
||||
type CreateSlashApplicationCommand,
|
||||
type Embed,
|
||||
type InteractionCallbackData
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
|
||||
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) {
|
||||
await sendInteractionResponse(bot, interaction.id, interaction.token, nowPlayingResponse(bot, interaction));
|
||||
export async function np(interaction: Interaction) {
|
||||
await interaction.reply(nowPlayingResponse(interaction));
|
||||
}
|
||||
|
||||
function formatQueue(bot: Bot, interaction: Interaction) {
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
function formatQueue(interaction: Interaction) {
|
||||
let formattedText = "";
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
|
||||
if(!player.nowPlaying) {
|
||||
if(subscription.queue.length === 0) {
|
||||
return "Nothing is currently in the queue.";
|
||||
} 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:`);
|
||||
|
||||
for(let audioSource of player.upcoming()) {
|
||||
formattedText = formattedText.concat(`\n- **${audioSource.title}**, added by ${audioSource.added_by}`)
|
||||
}
|
||||
subscription.queue.forEach((track) => {
|
||||
formattedText = formattedText.concat(`\n- **${track.title}**, added by ${track.addedBy}`)
|
||||
});
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
|
||||
function nowPlayingResponse(bot: Bot, interaction: Interaction) {
|
||||
return <InteractionResponse>{
|
||||
type: 4,
|
||||
data: <InteractionCallbackData>
|
||||
{
|
||||
content: "",
|
||||
embeds: [<Embed>{
|
||||
title: "In the queue",
|
||||
color: configs.embed_color,
|
||||
description: formatQueue(bot, interaction)
|
||||
}]
|
||||
}
|
||||
}
|
||||
function nowPlayingResponse(interaction: Interaction) {
|
||||
return formatCallbackData(formatQueue(interaction), "In the queue");
|
||||
}
|
||||
|
||||
function nowPlayingMessage(bot: Bot, guildId: BigInt) {
|
||||
const player = bot.helpers.getPlayer(guildId);
|
||||
return <CreateMessage>{
|
||||
embeds: [<Embed>{
|
||||
color: configs.embed_color,
|
||||
description: `Now playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`
|
||||
}]
|
||||
}
|
||||
function nowPlayingMessage(track: Track) {
|
||||
return formatCallbackData(`Now playing: [**${track.title}**](${track.url}), added by ${track.addedBy}`);
|
||||
}
|
||||
|
||||
export async function nowPlayingCallback(connectionData: ConnectionData) {
|
||||
const channel = await getAllowedTextChannel(bot, connectionData.guildId);
|
||||
await sendMessage(bot, channel.id, nowPlayingMessage(bot, connectionData.guildId));
|
||||
export async function nowPlayingCallback(subscription: MusicSubscription) {
|
||||
const channel = await getAllowedTextChannel(subscription);
|
||||
await channel.send(nowPlayingMessage(subscription.nowPlaying));
|
||||
}
|
||||
|
||||
export const npCommand = <CreateSlashApplicationCommand>{
|
||||
export const npCommand = {
|
||||
name: "np",
|
||||
description: "Shows the currently-playing song along with the next five songs in the queue",
|
||||
dmPermission: false
|
||||
description: "Shows the currently-playing song along with the next five songs in the queue"
|
||||
};
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import {
|
||||
Bot,
|
||||
editOriginalInteractionResponse,
|
||||
Interaction,
|
||||
sendInteractionResponse,
|
||||
type CreateSlashApplicationCommand
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
|
||||
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.`);
|
||||
|
||||
|
@ -14,33 +10,32 @@ const emptyQueueResponse = formatCallbackData(`There's nothing in the queue righ
|
|||
|
||||
const nowPausedResponse = formatCallbackData(`The player has been paused.`);
|
||||
|
||||
export const pauseCommand = <CreateSlashApplicationCommand>{
|
||||
export const pauseCommand = {
|
||||
name: "pause",
|
||||
description: "Pauses the player",
|
||||
dmPermission: false,
|
||||
description: "Pauses the player"
|
||||
};
|
||||
|
||||
export const stopCommand = <CreateSlashApplicationCommand>{
|
||||
export const stopCommand = {
|
||||
name: "stop",
|
||||
description: "Pauses the player, alias for /pause",
|
||||
dmPermission: false,
|
||||
description: "Pauses the player, alias for /pause"
|
||||
};
|
||||
|
||||
export async function pause(bot: Bot, interaction: Interaction) {
|
||||
export async function pause(interaction: Interaction) {
|
||||
if (!interaction.guildId) return;
|
||||
await ensureVoiceConnection(bot, interaction.guildId);
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse);
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
await ensureVoiceConnection(interaction);
|
||||
await interaction.reply(waitingForResponse);
|
||||
|
||||
if(player.playing && !player.waiting) {
|
||||
if(subscription.playing) {
|
||||
if(player.nowPlaying) {
|
||||
await player.pause();
|
||||
await editOriginalInteractionResponse(bot, interaction.token, nowPausedResponse);
|
||||
await subscription.audioPlayer.pause();
|
||||
subscription.playing = false;
|
||||
await interaction.editReply(nowPausedResponse);
|
||||
} else {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, emptyQueueResponse);
|
||||
await interaction.editReply(emptyQueueResponse);
|
||||
}
|
||||
} else {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, alreadyPausedResponse);
|
||||
await interaction.editReply(alreadyPausedResponse);
|
||||
}
|
||||
|
||||
}
|
116
commands/play.ts
116
commands/play.ts
|
@ -1,30 +1,24 @@
|
|||
import {
|
||||
Bot,
|
||||
editOriginalInteractionResponse,
|
||||
Interaction,
|
||||
sendInteractionResponse,
|
||||
type ApplicationCommandOption,
|
||||
type CreateSlashApplicationCommand
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
import { YouTube } from "youtube-sr";
|
||||
|
||||
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";
|
||||
|
||||
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 addedPlaylistResponse(interaction: Interaction, url: string, length: number, title: string) {
|
||||
return formatCallbackData(`${interaction.user.username} added ${length} videos from [**${title}**](${url}) to the queue.`);
|
||||
}
|
||||
|
||||
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) {
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
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.`);
|
||||
|
@ -37,13 +31,13 @@ function nowPlayingResponse(bot: Bot, interaction: Interaction) {
|
|||
Currently playing: **${player.nowPlaying.title}**, added by ${player.nowPlaying.added_by}`);
|
||||
}
|
||||
|
||||
export const playCommand = <CreateSlashApplicationCommand>{
|
||||
export const playCommand = {
|
||||
name: "play",
|
||||
description: "Adds a song or playlist to the queue and starts the music if it's not already playing",
|
||||
dmPermission: false,
|
||||
options: [
|
||||
<ApplicationCommandOption>{
|
||||
type: 3, // string
|
||||
{
|
||||
type: 'STRING' as const,
|
||||
name: "url",
|
||||
description: "The URL or video ID of the song or playlist to play",
|
||||
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;
|
||||
await ensureVoiceConnection(bot, interaction.guildId);
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse);
|
||||
await interaction.reply(waitingForResponse);
|
||||
await ensureVoiceConnection(interaction);
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
|
||||
let parsed_url;
|
||||
if(!interaction) return;
|
||||
if(!interaction.data) return;
|
||||
if(interaction.options.get('url') !== undefined) {
|
||||
const url = interaction.options.get('url')!.value! as string;
|
||||
|
||||
if(interaction.data.options) {
|
||||
if(!interaction.data.options[0].value) return;
|
||||
|
||||
try {
|
||||
parsed_url = new URL(interaction.data.options[0].value.toString());
|
||||
} catch {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, badUrlResponse);
|
||||
}
|
||||
|
||||
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("?"))
|
||||
if(isPlaylist(url)) {
|
||||
const videos = await YouTube.getPlaylist(url, {fetchAll: true});
|
||||
videos.videos.forEach(async (video) => {
|
||||
const track = await createTrack(interaction, subscription, `https://youtube.com/watch?v=${video.id}`);
|
||||
await subscription.enqueue(track);
|
||||
});
|
||||
await interaction.editReply(addedPlaylistResponse(interaction, url, videos.videos.length, videos.title));
|
||||
} else {
|
||||
href = parsed_url.href;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
const track = await createTrack(interaction, subscription, url);
|
||||
await subscription.enqueue(track);
|
||||
await interaction.editReply(addedSongResponse(interaction, track.title))
|
||||
}
|
||||
} else {
|
||||
// restart the player if there's no url
|
||||
if(player.waiting || !player.playing) {
|
||||
if(player.nowPlaying) {
|
||||
await player.play();
|
||||
await editOriginalInteractionResponse(bot, interaction.token, nowPlayingResponse(bot, interaction));
|
||||
if(!subscription.playing) {
|
||||
if(subscription.queue.length === 0) {
|
||||
await interaction.editReply(emptyQueueResponse);
|
||||
} else {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, emptyQueueResponse);
|
||||
subscription.audioPlayer.unpause();
|
||||
await interaction.editReply(nowPlayingResponse(interaction));
|
||||
}
|
||||
} 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 {
|
||||
Bot,
|
||||
editOriginalInteractionResponse,
|
||||
Interaction,
|
||||
sendInteractionResponse,
|
||||
type CreateSlashApplicationCommand,
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
|
||||
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 skippedResponse = formatCallbackData(`The song has been skipped.`);
|
||||
|
||||
export const skipCommand = <CreateSlashApplicationCommand>{
|
||||
export const skipCommand = {
|
||||
name: "skip",
|
||||
description: "Skips the current song",
|
||||
dmPermission: false,
|
||||
description: "Skips the current song"
|
||||
};
|
||||
|
||||
export async function skip(bot: Bot, interaction: Interaction) {
|
||||
export async function skip( interaction: Interaction) {
|
||||
if (!interaction.guildId) return;
|
||||
await ensureVoiceConnection(bot, interaction.guildId);
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse);
|
||||
await ensureVoiceConnection(interaction);
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
await interaction.followUp(waitingForResponse);
|
||||
|
||||
if(!player.nowPlaying) {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, nothingToSkipResponse);
|
||||
if(!subscription.playing) {
|
||||
await interaction.editReply(nothingToSkipResponse);
|
||||
} else {
|
||||
await player.skip();
|
||||
await editOriginalInteractionResponse(bot, interaction.token, skippedResponse);
|
||||
await subscription.audioPlayer.stop();
|
||||
await interaction.editReply(skippedResponse);
|
||||
}
|
||||
}
|
|
@ -1,45 +1,39 @@
|
|||
import {
|
||||
Bot,
|
||||
editOriginalInteractionResponse,
|
||||
Interaction,
|
||||
sendInteractionResponse,
|
||||
type CreateSlashApplicationCommand
|
||||
} from "../deps.ts";
|
||||
import { Interaction } from 'discord.js';
|
||||
|
||||
import { subscriptions } from "../index.js";
|
||||
import { ensureVoiceConnection, formatCallbackData, waitingForResponse } from "../utils.ts";
|
||||
|
||||
function notLoopingResponse(bot: Bot, interaction: Interaction) {
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
function notLoopingResponse(interaction: Interaction) {
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
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.`);
|
||||
|
||||
function loopDisabledResponse(bot: Bot, interaction: Interaction) {
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
function loopDisabledResponse(interaction: Interaction) {
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
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",
|
||||
description: "Disables looping the current song. At the current song's end, the queue will proceed as normal.",
|
||||
dmPermission: false,
|
||||
description: "Disables looping the current song. At the current song's end, the queue will proceed as normal."
|
||||
};
|
||||
|
||||
export async function unloop(bot: Bot, interaction: Interaction) {
|
||||
export async function unloop(interaction: Interaction) {
|
||||
if (!interaction.guildId) return;
|
||||
await ensureVoiceConnection(bot, interaction.guildId);
|
||||
const player = bot.helpers.getPlayer(interaction.guildId);
|
||||
await sendInteractionResponse(bot, interaction.id, interaction.token, waitingForResponse);
|
||||
await ensureVoiceConnection(interaction);
|
||||
const subscription = subscriptions.get(interaction.guildId);
|
||||
await interaction.reply(waitingForResponse);
|
||||
|
||||
if(!player.nowPlaying) {
|
||||
await editOriginalInteractionResponse(bot, interaction.token, nothingToLoopResponse);
|
||||
} else if(player.looping){
|
||||
await player.loop(false);
|
||||
await editOriginalInteractionResponse(bot, interaction.token, loopDisabledResponse(bot, interaction));
|
||||
if(!subscription.playing) {
|
||||
await interaction.editReply(interaction.token, nothingToLoopResponse);
|
||||
} else if(subscription.looping){
|
||||
subscription.audioPlayer.loop = false;
|
||||
await interaction.editReply(loopDisabledResponse(interaction));
|
||||
} 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