chore: Redirect voice examples to the new repository (#8737)

This commit is contained in:
Jiralite
2022-10-12 11:27:02 +01:00
committed by GitHub
parent cb3826ce6d
commit ef29b5e51f
24 changed files with 5 additions and 850 deletions

View File

@@ -1,2 +1 @@
*.d.ts
examples

View File

@@ -64,6 +64,10 @@ try installing another.
- [`FFmpeg`](https://ffmpeg.org/) (installed and added to environment)
- `ffmpeg-static`: ^4.2.7 (npm install)
## Examples
The [voice-examples][voice-examples] repository contains examples on how to use this package. Feel free to check them out if you need a nudge in the right direction.
## Links
- [Website][website] ([source][website-source])
@@ -99,3 +103,4 @@ nudge in the right direction, please don't hesitate to join our official [discor
[npm]: https://www.npmjs.com/package/@discordjs/voice
[related-libs]: https://discord.com/developers/docs/topics/community-resources#libraries
[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md
[voice-examples]: https://github.com/discordjs/voice-examples

View File

@@ -1,7 +0,0 @@
# Examples
| Example | Description |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Basic](./basic) | A simple "Hello World" TypeScript example that plays an mp3 file. Notably, it works with discord.js v12 and so it also contains an example of creating an adapter |
| [Radio Bot](./radio-bot) | A fun JavaScript example of what you can create using @discordjs/voice. A radio bot that plays output from your speakers in a Discord voice channel |
| [Recorder](./recorder) | An example of using voice receive to create a bot that can record audio from users |

View File

@@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <http://unlicense.org/>

View File

@@ -1,91 +0,0 @@
# Basic Example
This example will demonstrate how to join a voice channel and play resources, with some best practice
assistance on making sure you aren't waiting indefinitely for things to happen.
To achieve this, the example sets some fairly arbitrary time constraints for things such as joining
voice channels and audio becoming available.
## Code snippet
This code snippet doesn't include any comments for brevity. If you want to see the full source code,
check the other files in this folder!
```ts
import { Client, VoiceChannel, Intents } from 'discord.js';
import {
joinVoiceChannel,
createAudioPlayer,
createAudioResource,
entersState,
StreamType,
AudioPlayerStatus,
VoiceConnectionStatus,
} from '@discordjs/voice';
import { createDiscordJSAdapter } from './adapter';
const player = createAudioPlayer();
function playSong() {
const resource = createAudioResource('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', {
inputType: StreamType.Arbitrary,
});
player.play(resource);
return entersState(player, AudioPlayerStatus.Playing, 5e3);
}
async function connectToChannel(channel: VoiceChannel) {
const connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: createDiscordJSAdapter(channel),
});
try {
await entersState(connection, VoiceConnectionStatus.Ready, 30e3);
return connection;
} catch (error) {
connection.destroy();
throw error;
}
}
const client = new Client({
ws: { intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_VOICE_STATES] },
});
client.login('token here');
client.on('ready', async () => {
console.log('Discord.js client is ready!');
try {
await playSong();
console.log('Song is ready to play!');
} catch (error) {
console.error(error);
}
});
client.on('message', async (message) => {
if (!message.guild) return;
if (message.content === '-join') {
const channel = message.member?.voice.channel;
if (channel) {
try {
const connection = await connectToChannel(channel);
connection.subscribe(player);
message.reply('Playing now!');
} catch (error) {
console.error(error);
}
} else {
message.reply('Join a voice channel then try again!');
}
}
});
```

View File

@@ -1,73 +0,0 @@
import { DiscordGatewayAdapterCreator, DiscordGatewayAdapterLibraryMethods } from '../../';
import { Snowflake, Client, Guild, VoiceBasedChannel, Status, Events } from 'discord.js';
import {
GatewayDispatchEvents,
GatewayVoiceServerUpdateDispatchData,
GatewayVoiceStateUpdateDispatchData,
} from 'discord-api-types/v9';
const adapters = new Map<Snowflake, DiscordGatewayAdapterLibraryMethods>();
const trackedClients = new Set<Client>();
/**
* Tracks a Discord.js client, listening to VOICE_SERVER_UPDATE and VOICE_STATE_UPDATE events
*
* @param client - The Discord.js Client to track
*/
function trackClient(client: Client) {
if (trackedClients.has(client)) return;
trackedClients.add(client);
client.ws.on(GatewayDispatchEvents.VoiceServerUpdate, (payload: GatewayVoiceServerUpdateDispatchData) => {
adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
});
client.ws.on(GatewayDispatchEvents.VoiceStateUpdate, (payload: GatewayVoiceStateUpdateDispatchData) => {
if (payload.guild_id && payload.session_id && payload.user_id === client.user?.id) {
adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
}
});
client.on(Events.ShardDisconnect, (_, shardId) => {
const guilds = trackedShards.get(shardId);
if (guilds) {
for (const guildID of guilds.values()) {
adapters.get(guildID)?.destroy();
}
}
trackedShards.delete(shardId);
});
}
const trackedShards = new Map<number, Set<Snowflake>>();
function trackGuild(guild: Guild) {
let guilds = trackedShards.get(guild.shardId);
if (!guilds) {
guilds = new Set();
trackedShards.set(guild.shardId, guilds);
}
guilds.add(guild.id);
}
/**
* Creates an adapter for a Voice Channel.
*
* @param channel - The channel to create the adapter for
*/
export function createDiscordJSAdapter(channel: VoiceBasedChannel): DiscordGatewayAdapterCreator {
return (methods) => {
adapters.set(channel.guild.id, methods);
trackClient(channel.client);
trackGuild(channel.guild);
return {
sendPayload(data) {
if (channel.guild.shard.status === Status.Ready) {
channel.guild.shard.send(data);
return true;
}
return false;
},
destroy() {
return adapters.delete(channel.guild.id);
},
};
};
}

View File

@@ -1,153 +0,0 @@
import { Client, VoiceBasedChannel, VoiceChannel } from 'discord.js';
import {
joinVoiceChannel,
createAudioPlayer,
createAudioResource,
entersState,
StreamType,
AudioPlayerStatus,
VoiceConnectionStatus,
} from '@discordjs/voice';
import { createDiscordJSAdapter } from './adapter';
import { GatewayIntentBits } from 'discord-api-types/v9';
/**
* In this example, we are creating a single audio player that plays to a number of voice channels.
* The audio player will play a single track.
*/
/**
* Create the audio player. We will use this for all of our connections.
*/
const player = createAudioPlayer();
function playSong() {
/**
* Here we are creating an audio resource using a sample song freely available online
* (see https://www.soundhelix.com/audio-examples)
*
* We specify an arbitrary inputType. This means that we aren't too sure what the format of
* the input is, and that we'd like to have this converted into a format we can use. If we
* were using an Ogg or WebM source, then we could change this value. However, for now we
* will leave this as arbitrary.
*/
const resource = createAudioResource('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', {
inputType: StreamType.Arbitrary,
});
/**
* We will now play this to the audio player. By default, the audio player will not play until
* at least one voice connection is subscribed to it, so it is fine to attach our resource to the
* audio player this early.
*/
player.play(resource);
/**
* Here we are using a helper function. It will resolve if the player enters the Playing
* state within 5 seconds, otherwise it will reject with an error.
*/
return entersState(player, AudioPlayerStatus.Playing, 5e3);
}
async function connectToChannel(channel: VoiceBasedChannel) {
/**
* Here, we try to establish a connection to a voice channel. If we're already connected
* to this voice channel, @discordjs/voice will just return the existing connection for us!
*/
const connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: createDiscordJSAdapter(channel),
});
/**
* If we're dealing with a connection that isn't yet Ready, we can set a reasonable
* time limit before giving up. In this example, we give the voice connection 30 seconds
* to enter the ready state before giving up.
*/
try {
/**
* Allow ourselves 30 seconds to join the voice channel. If we do not join within then,
* an error is thrown.
*/
await entersState(connection, VoiceConnectionStatus.Ready, 30e3);
/**
* At this point, the voice connection is ready within 30 seconds! This means we can
* start playing audio in the voice channel. We return the connection so it can be
* used by the caller.
*/
return connection;
} catch (error) {
/**
* At this point, the voice connection has not entered the Ready state. We should make
* sure to destroy it, and propagate the error by throwing it, so that the calling function
* is aware that we failed to connect to the channel.
*/
connection.destroy();
throw error;
}
}
/**
* Main code
* =========
* Here we will implement the helper functions that we have defined above.
*/
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates],
});
void client.login('token here');
client.on('ready', async () => {
console.log('Discord.js client is ready!');
/**
* Try to get our song ready to play for when the bot joins a voice channel
*/
try {
await playSong();
console.log('Song is ready to play!');
} catch (error) {
/**
* The song isn't ready to play for some reason :(
*/
console.error(error);
}
});
client.on('messageCreate', async (message) => {
if (!message.guild) return;
if (message.content === '-join') {
const channel = message.member?.voice.channel;
if (channel) {
/**
* The user is in a voice channel, try to connect.
*/
try {
const connection = await connectToChannel(channel);
/**
* We have successfully connected! Now we can subscribe our connection to
* the player. This means that the player will play audio in the user's
* voice channel.
*/
connection.subscribe(player);
await message.reply('Playing now!');
} catch (error) {
/**
* Unable to connect to the voice channel within 30 seconds :(
*/
console.error(error);
}
} else {
/**
* The user is not in a voice channel.
*/
void message.reply('Join a voice channel then try again!');
}
}
});

View File

@@ -1,2 +0,0 @@
config.json
package-lock.json

View File

@@ -1,64 +0,0 @@
# Discord Radio Bot 🎧
A proof-of-concept radio bot that uses @discordjs/voice and discord.js. Streams audio from an audio output hardware device on your computer over a Discord voice channel.
**Works on:**
- Linux (via PulseAudio `pulse`)
- Windows (via DirectShow `dshow`)
## Usage
```bash
# Clone the main @discordjs/voice repo, then install dependencies and build
$ npm install
$ npm run build
# Enter this example's directory, create a config file and start!
$ cd examples/radio-bot
$ npm install
$ nano config.json
$ npm start
# Join a voice channel in Discord, then send "-join"
```
## Configuring on Windows via `dshow`
Run `ffmpeg -list_devices true -f dshow -i dummy` and observe output containing something similar:
```
DirectShow audio devices
"Stereo Mix (Realtek(R) Audio)"
Alternative name "@device_cm_{ID1}\wave_{ID2}"
```
For example, playing the above device will mirror audio from the speaker output of your machine. Your `config.json` should then be considered like so:
```json
{
"token": "discord_bot_token",
"device": "Stereo Mix (Realtek(R) Audio)",
"type": "dshow",
"maxTransmissionGap": 5000
}
```
## Configuring on Linux via `pulse`
Run `pactl list short sources` and observe output containing something similar:
```
5 alsa_output.pci.3.analog-stereo.monitor module-alsa-card.c s16le 2ch 44100Hz IDLE
```
Then configure your `config.json` with the device you'd like to use:
```json
{
"token": "discord_bot_token",
"device": "alsa_output.pci.3.analog-stereo.monitor",
"type": "pulse",
"maxTransmissionGap": 5000
}
```

View File

@@ -1,6 +0,0 @@
{
"token": "discord_bot_token",
"device": "audio_hw_device_id",
"type": "pulse",
"maxTransmissionGap": 5000
}

View File

@@ -1,104 +0,0 @@
require('module-alias/register');
const { Client } = require('discord.js');
const prism = require('prism-media');
const config = require('./config.json');
const {
NoSubscriberBehavior,
StreamType,
createAudioPlayer,
createAudioResource,
entersState,
AudioPlayerStatus,
VoiceConnectionStatus,
joinVoiceChannel,
} = require('@discordjs/voice');
const player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
maxMissedFrames: Math.round(config.maxTransmissionGap / 20),
},
});
player.on('stateChange', (oldState, newState) => {
if (oldState.status === AudioPlayerStatus.Idle && newState.status === AudioPlayerStatus.Playing) {
console.log('Playing audio output on audio player');
} else if (newState.status === AudioPlayerStatus.Idle) {
console.log('Playback has stopped. Attempting to restart.');
attachRecorder();
}
});
function attachRecorder() {
player.play(
createAudioResource(
new prism.FFmpeg({
args: [
'-analyzeduration',
'0',
'-loglevel',
'0',
'-f',
config.type,
'-i',
config.type === 'dshow' ? `audio=${config.device}` : config.device,
'-acodec',
'libopus',
'-f',
'opus',
'-ar',
'48000',
'-ac',
'2',
],
}),
{
inputType: StreamType.OggOpus,
},
),
);
console.log('Attached recorder - ready to go!');
}
async function connectToChannel(channel) {
const connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator,
});
try {
await entersState(connection, VoiceConnectionStatus.Ready, 30_000);
return connection;
} catch (error) {
connection.destroy();
throw error;
}
}
const client = new Client({ intents: ['GUILDS', 'GUILD_MESSAGES', 'GUILD_VOICE_STATES'] });
client.on('ready', async () => {
console.log('discord.js client is ready!');
attachRecorder();
});
client.on('messageCreate', async (message) => {
if (!message.guild) return;
if (message.content === '-join') {
const channel = message.member?.voice.channel;
if (channel) {
try {
const connection = await connectToChannel(channel);
connection.subscribe(player);
await message.reply('Playing now!');
} catch (error) {
console.error(error);
}
} else {
await message.reply('Join a voice channel then try again!');
}
}
});
void client.login(config.token);

View File

@@ -1,32 +0,0 @@
{
"name": "discord-radio-bot",
"version": "1.0.0",
"description": "A proof-of-concept radio bot for @discordjs/voice",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"keywords": [
"discord",
"radio",
"bot",
"audio",
"speakers",
"hardware",
"dj"
],
"author": "Amish Shah <amish@shah.gg>",
"license": "MIT",
"dependencies": {
"@discordjs/voice": "file:../../",
"discord.js": "^13.7.0",
"libsodium-wrappers": "^0.7.9",
"module-alias": "^2.2.2",
"prism-media": "^1.3.1"
},
"_moduleAliases": {
"@root": ".",
"@discordjs/voice": "../../",
"libsodium-wrappers": "./node_modules/libsodium-wrappers"
}
}

View File

@@ -1,7 +0,0 @@
{
"root": true,
"extends": "../../.eslintrc.json",
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

View File

@@ -1,4 +0,0 @@
package-lock.json
auth.json
tsconfig.tsbuildinfo
recordings/*.ogg

View File

@@ -1,23 +0,0 @@
# 👂 Recorder Bot
This example shows how you can use the voice receive functionality in @discordjs/voice to record users in voice channels
and save the audio to local Ogg files.
## Usage
```sh-session
# Clone the main repository, and then run:
$ npm install
$ npm run build
# Open this example and install dependencies
$ cd examples/recorder
$ npm install
# Set a bot token (see auth.example.json)
$ cp auth.example.json auth.json
$ nano auth.json
# Start the bot!
$ npm start
```

View File

@@ -1,3 +0,0 @@
{
"token": "Your Discord bot token here"
}

View File

@@ -1,28 +0,0 @@
{
"name": "receiver-bot",
"version": "0.0.1",
"description": "An example receiver bot written using @discordjs/voice",
"scripts": {
"start": "npm run build && node -r tsconfig-paths/register dist/bot",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"prettier": "prettier --write .",
"build": "tsc",
"build:check": "tsc --noEmit --incremental false"
},
"author": "Amish Shah <contact@shah.gg>",
"license": "MIT",
"dependencies": {
"@discordjs/opus": "^0.8.0",
"discord-api-types": "^0.33.3",
"discord.js": "^13.8.0",
"libsodium-wrappers": "^0.7.9",
"node-crc": "^1.3.2",
"prism-media": "^2.0.0-alpha.0"
},
"devDependencies": {
"tsconfig-paths": "^3.10.1",
"typescript": "^4.7.4"
}
}

View File

@@ -1,50 +0,0 @@
import { getVoiceConnection } from '@discordjs/voice';
import { GatewayIntentBits } from 'discord-api-types/v9';
import Discord, { Interaction, Constants } from 'discord.js';
import { deploy } from './deploy';
import { interactionHandlers } from './interactions';
const { token } = require('../auth.json') as { token: string };
const client = new Discord.Client({
intents: [GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds],
});
const { Events } = Constants;
client.on(Events.CLIENT_READY, () => console.log('Ready!'));
client.on(Events.MESSAGE_CREATE, async (message) => {
if (!message.guild) return;
if (!client.application?.owner) await client.application?.fetch();
if (message.content.toLowerCase() === '!deploy' && message.author.id === client.application?.owner?.id) {
await deploy(message.guild);
await message.reply('Deployed!');
}
});
/**
* The IDs of the users that can be recorded by the bot.
*/
const recordable = new Set<string>();
client.on(Events.INTERACTION_CREATE, async (interaction: Interaction) => {
if (!interaction.isCommand() || !interaction.guildId) return;
const handler = interactionHandlers.get(interaction.commandName);
try {
if (handler) {
await handler(interaction, recordable, client, getVoiceConnection(interaction.guildId));
} else {
await interaction.reply('Unknown command');
}
} catch (error) {
console.warn(error);
}
});
client.on(Events.ERROR, console.warn);
void client.login(token);

View File

@@ -1,42 +0,0 @@
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream';
import { EndBehaviorType, VoiceReceiver } from '@discordjs/voice';
import type { User } from 'discord.js';
import * as prism from 'prism-media';
function getDisplayName(userId: string, user?: User) {
return user ? `${user.username}_${user.discriminator}` : userId;
}
export function createListeningStream(receiver: VoiceReceiver, userId: string, user?: User) {
const opusStream = receiver.subscribe(userId, {
end: {
behavior: EndBehaviorType.AfterSilence,
duration: 1000,
},
});
const oggStream = new prism.opus.OggLogicalBitstream({
opusHead: new prism.opus.OpusHead({
channelCount: 2,
sampleRate: 48000,
}),
pageSizeControl: {
maxPackets: 10,
},
});
const filename = `./recordings/${Date.now()}-${getDisplayName(userId, user)}.ogg`;
const out = createWriteStream(filename);
console.log(`👂 Started recording ${filename}`);
pipeline(opusStream, oggStream, out, (err) => {
if (err) {
console.warn(`❌ Error recording file ${filename} - ${err.message}`);
} else {
console.log(`✅ Recorded ${filename}`);
}
});
}

View File

@@ -1,27 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v9';
import type { Guild } from 'discord.js';
export const deploy = async (guild: Guild) => {
await guild.commands.set([
{
name: 'join',
description: 'Joins the voice channel that you are in',
},
{
name: 'record',
description: 'Enables recording for a user',
options: [
{
name: 'speaker',
type: ApplicationCommandOptionType.User,
description: 'The user to record',
required: true,
},
],
},
{
name: 'leave',
description: 'Leave the voice channel',
},
]);
};

View File

@@ -1,92 +0,0 @@
import { entersState, joinVoiceChannel, VoiceConnection, VoiceConnectionStatus } from '@discordjs/voice';
import { Client, CommandInteraction, GuildMember, Snowflake } from 'discord.js';
import { createListeningStream } from './createListeningStream';
async function join(
interaction: CommandInteraction,
recordable: Set<Snowflake>,
client: Client,
connection?: VoiceConnection,
) {
await interaction.deferReply();
if (!connection) {
if (interaction.member instanceof GuildMember && interaction.member.voice.channel) {
const channel = interaction.member.voice.channel;
connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
selfDeaf: false,
selfMute: true,
adapterCreator: channel.guild.voiceAdapterCreator,
});
} else {
await interaction.followUp('Join a voice channel and then try that again!');
return;
}
}
try {
await entersState(connection, VoiceConnectionStatus.Ready, 20e3);
const receiver = connection.receiver;
receiver.speaking.on('start', (userId) => {
if (recordable.has(userId)) {
createListeningStream(receiver, userId, client.users.cache.get(userId));
}
});
} catch (error) {
console.warn(error);
await interaction.followUp('Failed to join voice channel within 20 seconds, please try again later!');
}
await interaction.followUp('Ready!');
}
async function record(
interaction: CommandInteraction,
recordable: Set<Snowflake>,
client: Client,
connection?: VoiceConnection,
) {
if (connection) {
const userId = interaction.options.get('speaker')!.value! as Snowflake;
recordable.add(userId);
const receiver = connection.receiver;
if (connection.receiver.speaking.users.has(userId)) {
createListeningStream(receiver, userId, client.users.cache.get(userId));
}
await interaction.reply({ ephemeral: true, content: 'Listening!' });
} else {
await interaction.reply({ ephemeral: true, content: 'Join a voice channel and then try that again!' });
}
}
async function leave(
interaction: CommandInteraction,
recordable: Set<Snowflake>,
_client: Client,
connection?: VoiceConnection,
) {
if (connection) {
connection.destroy();
recordable.clear();
await interaction.reply({ ephemeral: true, content: 'Left the channel!' });
} else {
await interaction.reply({ ephemeral: true, content: 'Not playing in this server!' });
}
}
export const interactionHandlers = new Map<
string,
(
interaction: CommandInteraction,
recordable: Set<Snowflake>,
client: Client,
connection?: VoiceConnection,
) => Promise<void>
>();
interactionHandlers.set('join', join);
interactionHandlers.set('record', record);
interactionHandlers.set('leave', leave);

View File

@@ -1,3 +0,0 @@
{
"extends": "./tsconfig.json"
}

View File

@@ -1,14 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist",
"skipLibCheck": true,
"paths": {
"@discordjs/voice": ["../../"],
"libsodium-wrappers": ["./node_modules/libsodium-wrappers"]
}
},
"include": ["src/*.ts"],
"exclude": [""]
}