Use IoC, impliment queue

This commit is contained in:
Max Isom 2020-03-12 22:41:26 -05:00
parent 8eb4c8a6c0
commit 17ba78f7b7
17 changed files with 1081 additions and 131 deletions

86
src/bot.ts Normal file
View file

@ -0,0 +1,86 @@
import makeDir from 'make-dir';
import {Client, Message, Collection} from 'discord.js';
import {inject, injectable} from 'inversify';
import {TYPES} from './types';
import {Settings} from './models';
import {sequelize} from './utils/db';
import handleGuildCreate from './events/guild-create';
import container from './inversify.config';
import Command from './commands';
@injectable()
export default class {
private readonly client: Client;
private readonly token: string;
private readonly clientId: string;
private readonly dataDir: string;
private readonly cacheDir: string;
private readonly commands!: Collection<string, Command>;
constructor(@inject(TYPES.Client) client: Client, @inject(TYPES.Config.DISCORD_TOKEN) token: string, @inject(TYPES.Config.DISCORD_CLIENT_ID) clientId: string, @inject(TYPES.Config.DATA_DIR) dataDir: string, @inject(TYPES.Config.CACHE_DIR) cacheDir: string) {
this.client = client;
this.token = token;
this.clientId = clientId;
this.dataDir = dataDir;
this.cacheDir = cacheDir;
this.commands = new Collection();
}
public async listen(): Promise<string> {
// Load in commands
container.getAll<Command>(TYPES.Command).forEach(command => {
this.commands.set(command.name, command);
});
this.client.on('message', async (msg: Message) => {
// Get guild settings
if (!msg.guild) {
return;
}
const settings = await Settings.findByPk(msg.guild.id);
if (!settings) {
// Got into a bad state, send owner welcome message
return this.client.emit('guildCreate', msg.guild);
}
const {prefix, channel} = settings;
if (!msg.content.startsWith(prefix) || msg.author.bot || msg.channel.id !== channel) {
return;
}
const args = msg.content.slice(prefix.length).split(/ +/);
const command = args.shift()!.toLowerCase();
if (!this.commands.has(command)) {
return;
}
try {
const handler = this.commands.get(command);
handler!.execute(msg, args);
} catch (error) {
console.error(error);
msg.reply('there was an error trying to execute that command!');
}
});
this.client.on('ready', async () => {
// Create directory if necessary
await makeDir(this.dataDir);
await makeDir(this.cacheDir);
await sequelize.sync({});
console.log(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.clientId}&scope=bot`);
});
// Register event handlers
this.client.on('guildCreate', handleGuildCreate);
return this.client.login(this.token);
}
}

View file

@ -1,11 +1,14 @@
import {TextChannel} from 'discord.js';
import {CommandHandler} from '../interfaces';
import {TextChannel, Message} from 'discord.js';
import {injectable} from 'inversify';
import {Settings} from '../models';
import Command from '.';
const config: CommandHandler = {
name: 'config',
description: 'Change various bot settings.',
execute: async (msg, args) => {
@injectable()
export default class implements Command {
public name = 'config';
public description = 'changes various bot settings';
public async execute(msg: Message, args: string []): Promise<void> {
if (args.length === 0) {
// Show current settings
const settings = await Settings.findByPk(msg.guild!.id);
@ -58,6 +61,4 @@ const config: CommandHandler = {
await msg.channel.send('🚫 I\'ve never met this setting in my life');
}
}
};
export default config;
}

7
src/commands/index.ts Normal file
View file

@ -0,0 +1,7 @@
import {Message} from 'discord.js';
export default interface Command {
name: string;
description: string;
execute: (msg: Message, args: string[]) => Promise<void>;
}

View file

@ -1,21 +1,205 @@
import {CommandHandler} from '../interfaces';
import {TextChannel, Message} from 'discord.js';
import YouTube from 'youtube.ts';
import Spotify from 'spotify-web-api-node';
import {URL} from 'url';
import ytsr from 'ytsr';
import pLimit from 'p-limit';
import spotifyURI from 'spotify-uri';
import got from 'got';
import {parse, toSeconds} from 'iso8601-duration';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
import Queue, {QueuedSong, QueuedPlaylist} from '../services/queue';
import Player from '../services/player';
import {getMostPopularVoiceChannel} from '../utils/channels';
import getYouTubeStream from '../utils/get-youtube-stream';
import LoadingMessage from '../utils/loading-message';
import Command from '.';
const play: CommandHandler = {
name: 'play',
description: 'plays a song',
execute: async (msg, args) => {
const url = args[0];
@injectable()
export default class implements Command {
public name = 'play';
public description = 'plays a song';
private readonly queue: Queue;
private readonly player: Player;
private readonly youtube: YouTube;
private readonly youtubeKey: string;
private readonly spotify: Spotify;
constructor(@inject(TYPES.Services.Queue) queue: Queue, @inject(TYPES.Services.Player) player: Player, @inject(TYPES.Lib.YouTube) youtube: YouTube, @inject(TYPES.Config.YOUTUBE_API_KEY) youtubeKey: string, @inject(TYPES.Lib.Spotify) spotify: Spotify) {
this.queue = queue;
this.player = player;
this.youtube = youtube;
this.youtubeKey = youtubeKey;
this.spotify = spotify;
}
public async execute(msg: Message, args: string []): Promise<void> {
const newSongs: QueuedSong[] = [];
const res = new LoadingMessage(msg.channel as TextChannel, 'hold on a sec');
await res.start();
const addSingleSong = async (source: string): Promise<void> => {
const videoDetails = await this.youtube.videos.get(source);
newSongs.push({title: videoDetails.snippet.title, length: toSeconds(parse(videoDetails.contentDetails.duration)), url: videoDetails.id, playlist: null});
};
// Test if it's a complete URL
try {
const url = new URL(args[0]);
const YOUTUBE_HOSTS = ['www.youtube.com', 'youtu.be', 'youtube.com'];
if (YOUTUBE_HOSTS.includes(url.host)) {
// YouTube source
if (url.searchParams.get('list')) {
// YouTube playlist
const playlist = await this.youtube.playlists.get(url.searchParams.get('list') as string);
const {items} = await this.youtube.playlists.items(url.searchParams.get('list') as string, {maxResults: '50'});
// Unfortunately, package doesn't provide a method for this
const res: any = await got('https://www.googleapis.com/youtube/v3/videos', {searchParams: {
part: 'contentDetails',
id: items.map(item => item.contentDetails.videoId).join(','),
key: this.youtubeKey
}}).json();
const queuedPlaylist = {title: playlist.snippet.title, source: playlist.id};
items.forEach(video => {
const length = toSeconds(parse(res.items.find((i: any) => i.id === video.contentDetails.videoId).contentDetails.duration));
newSongs.push({title: video.snippet.title, length, url: video.contentDetails.videoId, playlist: queuedPlaylist});
});
} else {
// Single video
try {
await addSingleSong(url.href);
} catch (error) {
await res.stop('that doesn\'t exist');
return;
}
}
} else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') {
// Spotify source
const parsed = spotifyURI.parse(args[0]);
const tracks: SpotifyApi.TrackObjectSimplified[] = [];
let playlist: QueuedPlaylist | null = null;
switch (parsed.type) {
case 'album': {
const uri = parsed as spotifyURI.Album;
const [{body: album}, {body: {items}}] = await Promise.all([this.spotify.getAlbum(uri.id), this.spotify.getAlbumTracks(uri.id, {limit: 50})]);
tracks.push(...items);
playlist = {title: album.name, source: album.href};
break;
}
case 'playlist': {
const uri = parsed as spotifyURI.Playlist;
let [{body: playlistResponse}, {body: tracksResponse}] = await Promise.all([this.spotify.getPlaylist(uri.id), this.spotify.getPlaylistTracks(uri.id, {limit: 1})]);
playlist = {title: playlistResponse.name, source: playlistResponse.href};
tracks.push(...tracksResponse.items.map(playlistItem => playlistItem.track));
while (tracksResponse.next) {
// eslint-disable-next-line no-await-in-loop
({body: tracksResponse} = await this.spotify.getPlaylistTracks(uri.id, {
limit: parseInt(new URL(tracksResponse.next).searchParams.get('limit') ?? '1', 10),
offset: parseInt(new URL(tracksResponse.next).searchParams.get('offset') ?? '0', 10)
}));
tracks.push(...tracksResponse.items.map(playlistItem => playlistItem.track));
}
break;
}
case 'track': {
const uri = parsed as spotifyURI.Track;
const {body} = await this.spotify.getTrack(uri.id);
tracks.push(body);
break;
}
case 'artist': {
await res.stop('ope, can\'t add a whole artist');
return;
}
default: {
await res.stop('huh?');
return;
}
}
// Search YouTube for each track
const searchForTrack = async (track: any): Promise<QueuedSong|null> => {
try {
const {items: [video]} = await ytsr(`${track.name as string} ${track.artists[0].name as string} offical`, {limit: 1});
return {title: video.title, length: track.duration_ms / 1000, url: video.link, playlist};
} catch (_) {
// TODO: handle error
return null;
}
};
// Limit concurrency so hopefully we don't get banned
const limit = pLimit(3);
let songs = await Promise.all(tracks.map(async track => limit(async () => searchForTrack(track))));
// Get rid of null values
songs = songs.reduce((accum: QueuedSong[], song) => {
if (song) {
accum.push(song);
}
return accum;
}, []);
newSongs.push(...(songs as QueuedSong[]));
}
} catch (_) {
// Not a URL, must search YouTube
const query = args.join(' ');
try {
const {items: [video]} = await this.youtube.videos.search({q: query, maxResults: 1, type: 'video'});
await addSingleSong(video.id.videoId);
} catch (_) {
await res.stop('that doesn\'t exist');
return;
}
}
if (newSongs.length === 0) {
// TODO: better response
await res.stop('huh?');
return;
}
newSongs.forEach(song => this.queue.add(msg.guild!.id, song));
// TODO: better response
await res.stop('song(s) queued');
const channel = getMostPopularVoiceChannel(msg.guild!);
const conn = await channel.join();
// TODO: don't connect if already connected.
await this.player.connect(msg.guild!.id, channel);
const stream = await getYouTubeStream(url);
conn.play(stream, {type: 'webm/opus'});
await this.player.play(msg.guild!.id);
}
};
export default play;
}

22
src/commands/queue.ts Normal file
View file

@ -0,0 +1,22 @@
import {Message} from 'discord.js';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
import Queue from '../services/queue';
import Command from '.';
@injectable()
export default class implements Command {
public name = 'queue';
public description = 'shows current queue';
private readonly queue: Queue;
constructor(@inject(TYPES.Services.Queue) queue: Queue) {
this.queue = queue;
}
public async execute(msg: Message, _: string []): Promise<void> {
const queue = this.queue.get(msg.guild!.id);
await msg.channel.send('`' + JSON.stringify(queue.slice(0, 10)) + '`');
}
}

View file

@ -1,68 +1,15 @@
import fs from 'fs';
import path from 'path';
import makeDir from 'make-dir';
import Discord from 'discord.js';
import {DISCORD_TOKEN, DISCORD_CLIENT_ID, DATA_DIR, CACHE_DIR} from './utils/config';
import {Settings} from './models';
import {sequelize} from './utils/db';
import {CommandHandler} from './interfaces';
import handleGuildCreate from './events/guild-create';
import container from './inversify.config';
import Spotify from 'spotify-web-api-node';
import {TYPES} from './types';
import Bot from './bot';
const client = new Discord.Client();
const commands = new Discord.Collection();
let bot = container.get<Bot>(TYPES.Bot);
const spotify = container.get<Spotify>(TYPES.Lib.Spotify);
// Load in commands
const commandFiles = fs.readdirSync(path.join(__dirname, 'commands')).filter(file => file.endsWith('.js'));
(async () => {
const auth = await spotify.clientCredentialsGrant();
for (const file of commandFiles) {
const command = require(`./commands/${file}`).default;
spotify.setAccessToken(auth.body.access_token);
commands.set(command.name, command);
}
// Generic message handler
client.on('message', async (msg: Discord.Message) => {
// Get guild settings
const settings = await Settings.findByPk(msg.guild!.id);
if (!settings) {
// Got into a bad state, send owner welcome message
return client.emit('guildCreate', msg.guild);
}
const {prefix, channel} = settings;
if (!msg.content.startsWith(prefix) || msg.author.bot || msg.channel.id !== channel) {
return;
}
const args = msg.content.slice(prefix.length).split(/ +/);
const command = args.shift()!.toLowerCase();
if (!commands.has(command)) {
return;
}
try {
const handler = commands.get(command) as CommandHandler;
handler.execute(msg, args);
} catch (error) {
console.error(error);
msg.reply('there was an error trying to execute that command!');
}
});
client.on('ready', async () => {
// Create directory if necessary
await makeDir(DATA_DIR);
await makeDir(CACHE_DIR);
await sequelize.sync({});
console.log(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&scope=bot`);
});
client.on('guildCreate', handleGuildCreate);
client.login(DISCORD_TOKEN);
bot.listen();
})();

View file

@ -1,7 +0,0 @@
import {Message} from 'discord.js';
export interface CommandHandler {
name: string;
description: string;
execute: (msg: Message, args: string[]) => void;
}

54
src/inversify.config.ts Normal file
View file

@ -0,0 +1,54 @@
import 'reflect-metadata';
import {Container} from 'inversify';
import {TYPES} from './types';
import Bot from './bot';
import {Client} from 'discord.js';
import YouTube from 'youtube.ts';
import Spotify from 'spotify-web-api-node';
import {
DISCORD_TOKEN,
DISCORD_CLIENT_ID,
YOUTUBE_API_KEY,
SPOTIFY_CLIENT_ID,
SPOTIFY_CLIENT_SECRET,
DATA_DIR,
CACHE_DIR
} from './utils/config';
// Services
import Queue from './services/queue';
import Player from './services/player';
// Comands
import Command from './commands';
import Config from './commands/config';
import Play from './commands/play';
import QueueCommad from './commands/queue';
let container = new Container();
// Bot
container.bind<Bot>(TYPES.Bot).to(Bot).inSingletonScope();
container.bind<Client>(TYPES.Client).toConstantValue(new Client());
// Services
container.bind<Player>(TYPES.Services.Player).to(Player).inSingletonScope();
container.bind<Queue>(TYPES.Services.Queue).to(Queue).inSingletonScope();
// Commands
container.bind<Command>(TYPES.Command).to(Config).inSingletonScope();
container.bind<Command>(TYPES.Command).to(Play).inSingletonScope();
container.bind<Command>(TYPES.Command).to(QueueCommad).inSingletonScope();
// Config values
container.bind<string>(TYPES.Config.DISCORD_TOKEN).toConstantValue(DISCORD_TOKEN);
container.bind<string>(TYPES.Config.DISCORD_CLIENT_ID).toConstantValue(DISCORD_CLIENT_ID);
container.bind<string>(TYPES.Config.YOUTUBE_API_KEY).toConstantValue(YOUTUBE_API_KEY);
container.bind<string>(TYPES.Config.DATA_DIR).toConstantValue(DATA_DIR);
container.bind<string>(TYPES.Config.CACHE_DIR).toConstantValue(CACHE_DIR);
// Static libraries
container.bind<YouTube>(TYPES.Lib.YouTube).toConstantValue(new YouTube(YOUTUBE_API_KEY));
container.bind<Spotify>(TYPES.Lib.Spotify).toConstantValue(new Spotify({clientId: SPOTIFY_CLIENT_ID, clientSecret: SPOTIFY_CLIENT_SECRET}));
export default container;

1
src/packages.d.ts vendored
View file

@ -1 +1,2 @@
declare module 'node-emoji';
declare module 'ytsr';

88
src/services/player.ts Normal file
View file

@ -0,0 +1,88 @@
import {inject, injectable} from 'inversify';
import {VoiceConnection, VoiceChannel} from 'discord.js';
import {TYPES} from '../types';
import Queue from './queue';
import getYouTubeStream from '../utils/get-youtube-stream';
export enum Status {
Playing,
Paused,
Disconnected
}
export interface GuildPlayer {
status: Status;
voiceConnection: VoiceConnection | null;
}
@injectable()
export default class {
private readonly guildPlayers = new Map<string, GuildPlayer>();
private readonly queue: Queue;
constructor(@inject(TYPES.Services.Queue) queue: Queue) {
this.queue = queue;
}
async connect(guildId: string, channel: VoiceChannel): Promise<void> {
this.initGuild(guildId);
const guildPlayer = this.guildPlayers.get(guildId);
const conn = await channel.join();
guildPlayer!.voiceConnection = conn;
this.guildPlayers.set(guildId, guildPlayer!);
}
disconnect(guildId: string): void {
this.initGuild(guildId);
const guildPlayer = this.guildPlayers.get(guildId);
if (guildPlayer?.voiceConnection) {
guildPlayer.voiceConnection.disconnect();
}
}
async play(guildId: string): Promise<void> {
const guildPlayer = this.get(guildId);
if (guildPlayer.voiceConnection === null) {
throw new Error('Not connected to a voice channel.');
}
if (guildPlayer.status === Status.Playing) {
// Already playing, return
return;
}
const songs = this.queue.get(guildId);
if (songs.length === 0) {
throw new Error('Queue empty.');
}
const song = songs[0];
const stream = await getYouTubeStream(song.url);
this.get(guildId).voiceConnection!.play(stream, {type: 'webm/opus'});
guildPlayer.status = Status.Playing;
this.guildPlayers.set(guildId, guildPlayer);
}
get(guildId: string): GuildPlayer {
this.initGuild(guildId);
return this.guildPlayers.get(guildId) as GuildPlayer;
}
private initGuild(guildId: string): void {
if (!this.guildPlayers.get(guildId)) {
this.guildPlayers.set(guildId, {status: Status.Disconnected, voiceConnection: null});
}
}
}

89
src/services/queue.ts Normal file
View file

@ -0,0 +1,89 @@
import {injectable} from 'inversify';
export interface QueuedPlaylist {
title: string;
source: string;
}
export interface QueuedSong {
title: string;
url: string;
length: number;
playlist: QueuedPlaylist | null;
}
@injectable()
export default class {
private readonly guildQueues = new Map<string, QueuedSong[]>();
private readonly queuePositions = new Map<string, number>();
forward(guildId: string): void {
const currentPosition = this.queuePositions.get(guildId);
if (currentPosition && currentPosition + 1 <= this.size(guildId)) {
this.queuePositions.set(guildId, currentPosition + 1);
} else {
throw new Error('No songs in queue to forward to.');
}
}
back(guildId: string): void {
const currentPosition = this.queuePositions.get(guildId);
if (currentPosition && currentPosition - 1 >= 0) {
this.queuePositions.set(guildId, currentPosition - 1);
} else {
throw new Error('No songs in queue to go back to.');
}
}
get(guildId: string): QueuedSong[] {
const currentPosition = this.queuePositions.get(guildId);
if (currentPosition === undefined) {
return [];
}
const guildQueue = this.guildQueues.get(guildId);
if (!guildQueue) {
throw new Error('Bad state. Queue for guild exists but position does not.');
}
return guildQueue.slice(currentPosition);
}
add(guildId: string, song: QueuedSong): void {
if (!this.guildQueues.get(guildId)) {
this.guildQueues.set(guildId, []);
this.queuePositions.set(guildId, 0);
}
if (song.playlist) {
// Add to end of queue
this.guildQueues.set(guildId, [...this.guildQueues.get(guildId)!, song]);
} else if (this.guildQueues.get(guildId)!.length === 0) {
// Queue is currently empty
this.guildQueues.set(guildId, [song]);
} else {
// Not from playlist, add immediately
let insertAt = 0;
// Loop until playlist song
this.guildQueues.get(guildId)!.some(song => {
if (song.playlist) {
return true;
}
insertAt++;
return false;
});
this.guildQueues.set(guildId, [...this.guildQueues.get(guildId)!.slice(0, insertAt), song, ...this.guildQueues.get(guildId)!.slice(insertAt)]);
}
}
size(guildId: string): number {
return this.get(guildId).length;
}
}

20
src/types.ts Normal file
View file

@ -0,0 +1,20 @@
export const TYPES = {
Bot: Symbol('Bot'),
Client: Symbol('Client'),
Config: {
DISCORD_TOKEN: Symbol('DISCORD_TOKEN'),
DISCORD_CLIENT_ID: Symbol('DISCORD_CLIENT_ID'),
YOUTUBE_API_KEY: Symbol('YOUTUBE_API_KEY'),
DATA_DIR: Symbol('DATA_DIR'),
CACHE_DIR: Symbol('CACHE_DIR')
},
Command: Symbol('Command'),
Services: {
Player: Symbol('Player'),
Queue: Symbol('Queue')
},
Lib: {
YouTube: Symbol('YouTube'),
Spotify: Symbol('Spotify')
}
};

View file

@ -4,5 +4,8 @@ dotenv.config();
export const DISCORD_TOKEN: string = process.env.DISCORD_TOKEN ? process.env.DISCORD_TOKEN : '';
export const DISCORD_CLIENT_ID: string = process.env.DISCORD_CLIENT_ID ? process.env.DISCORD_CLIENT_ID : '';
export const YOUTUBE_API_KEY: string = process.env.YOUTUBE_API_KEY ? process.env.YOUTUBE_API_KEY : '';
export const SPOTIFY_CLIENT_ID: string = process.env.SPOTIFY_CLIENT_ID ? process.env.SPOTIFY_CLIENT_ID : '';
export const SPOTIFY_CLIENT_SECRET: string = process.env.SPOTIFY_CLIENT_SECRET ? process.env.SPOTIFY_CLIENT_SECRET : '';
export const DATA_DIR = path.resolve(process.env.DATA_DIR ? process.env.DATA_DIR : './data');
export const CACHE_DIR = path.join(DATA_DIR, 'cache');

View file

@ -3,6 +3,7 @@ import {Readable, PassThrough} from 'stream';
import path from 'path';
import hasha from 'hasha';
import ytdl from 'ytdl-core';
import prism from 'prism-media';
import {CACHE_DIR} from './config';
const nextBestFormat = (formats: ytdl.videoFormat[]): ytdl.videoFormat => {
@ -24,9 +25,11 @@ export default async (url: string): Promise<Readable> => {
const filter = (format: ytdl.videoFormat): boolean => format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate !== undefined && parseInt(format.audioSampleRate, 10) === 48000;
let format = formats.find(filter);
let canDirectPlay = true;
if (!format) {
format = nextBestFormat(info.formats);
canDirectPlay = false;
}
try {
@ -46,6 +49,30 @@ export default async (url: string): Promise<Readable> => {
await fs.rename(cacheTempPath, cachedPath);
});
return ytdl.downloadFromInfo(info, {format}).pipe(pass);
if (canDirectPlay) {
return ytdl.downloadFromInfo(info, {format}).pipe(pass);
}
const transcoder = new prism.FFmpeg({
args: [
'-reconnect',
'1',
'-reconnect_streamed',
'1',
'-reconnect_delay_max',
'5',
'-i',
format.url,
'-loglevel',
'verbose',
'-vn',
'-acodec',
'libopus',
'-f',
'webm'
]
});
return transcoder.pipe(pass);
}
};

View file

@ -0,0 +1,67 @@
import {TextChannel, Message} from 'discord.js';
import delay from 'delay';
export default class {
private readonly channel: TextChannel;
private readonly text: string;
private msg!: Message;
private isStopped: boolean = false;
constructor(channel: TextChannel, text: string) {
this.channel = channel;
this.text = text;
}
async start(): Promise<void> {
this.msg = await this.channel.send(this.text);
const period = 500;
const icons = ['⚪', '🔵', '⚫'];
const reactions = [];
let i = 0;
let isRemoving = false;
(async () => {
while (!this.isStopped) {
if (reactions.length === icons.length) {
isRemoving = true;
}
// eslint-disable-next-line no-await-in-loop
await delay(period);
if (isRemoving) {
const reactionToRemove = reactions.shift();
if (reactionToRemove) {
// eslint-disable-next-line no-await-in-loop
await reactionToRemove.remove();
} else {
isRemoving = false;
}
} else {
if (!this.isStopped) {
// eslint-disable-next-line no-await-in-loop
reactions.push(await this.msg.react(icons[i % icons.length]));
}
i++;
}
}
})();
}
async stop(str?: string): Promise<Message> {
this.isStopped = true;
if (str) {
await Promise.all([this.msg.reactions.removeAll(), this.msg.edit(str)]);
} else {
await this.msg.reactions.removeAll();
}
return this.msg;
}
}