Compare commits
7 Commits
1927728b60
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 20e162dc32 | |||
| 09e10e4113 | |||
| cbb5a9a76a | |||
| 9025831f3d | |||
| 06926e5601 | |||
| 85c35021b5 | |||
| c44f92f777 |
@@ -8,7 +8,6 @@ services:
|
|||||||
DISCORD_TOKEN: ${DISCORD_TOKEN}
|
DISCORD_TOKEN: ${DISCORD_TOKEN}
|
||||||
DISCORD_OWNER_ID: ${DISCORD_OWNER_ID}
|
DISCORD_OWNER_ID: ${DISCORD_OWNER_ID}
|
||||||
TTS_TIKTOK_SESSIONID: ${TTS_TIKTOK_SESSIONID}
|
TTS_TIKTOK_SESSIONID: ${TTS_TIKTOK_SESSIONID}
|
||||||
TTS_ELEVENLABS_KEY: ${TTS_ELEVENLABS_KEY}
|
|
||||||
TTS_ELEVENLABS_REFRESHTOKEN: ${TTS_ELEVENLABS_REFRESHTOKEN}
|
TTS_ELEVENLABS_REFRESHTOKEN: ${TTS_ELEVENLABS_REFRESHTOKEN}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+85
-74
@@ -1,89 +1,100 @@
|
|||||||
import { ChatInputCommandInteraction, MessageCreateOptions, MessageFlags, SlashCommandBuilder, TextChannel } from "discord.js";
|
import {
|
||||||
import { Command } from "../../commands";
|
ChatInputCommandInteraction,
|
||||||
|
MessageCreateOptions,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
TextChannel
|
||||||
|
} from 'discord.js';
|
||||||
|
import { Command } from '../../commands';
|
||||||
|
|
||||||
const builder = new SlashCommandBuilder()
|
const builder = new SlashCommandBuilder()
|
||||||
.setName('bot-mimic')
|
.setName('bot-mimic')
|
||||||
.setDescription('Makes the bot send a message')
|
.setDescription('Makes the bot send a message')
|
||||||
.addStringOption(opt =>
|
.addStringOption((opt) =>
|
||||||
opt
|
opt
|
||||||
.setName('content')
|
.setName('content')
|
||||||
.setDescription('The text content of the message')
|
.setDescription('The text content of the message')
|
||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
)
|
)
|
||||||
.addAttachmentOption(opt =>
|
.addAttachmentOption((opt) =>
|
||||||
opt
|
opt
|
||||||
.setName('attachment')
|
.setName('attachment')
|
||||||
.setDescription('An attachment for the message')
|
.setDescription('An attachment for the message')
|
||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
)
|
)
|
||||||
.addStringOption(opt =>
|
.addStringOption((opt) =>
|
||||||
opt
|
opt
|
||||||
.setName('reply')
|
.setName('reply')
|
||||||
.setDescription('The message ID that the bot should reply to')
|
.setDescription('The message ID that the bot should reply to')
|
||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
name: 'bot-mimic',
|
name: 'bot-mimic',
|
||||||
builder: builder,
|
builder: builder,
|
||||||
ownerOnly: true,
|
ownerOnly: true,
|
||||||
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||||
|
|
||||||
if (!interaction.channel?.isTextBased()) {
|
if (!interaction.channel?.isTextBased()) {
|
||||||
await interaction.editReply('This command can only be used in a text channel.');
|
await interaction.editReply(
|
||||||
return;
|
'This command can only be used in a text channel.'
|
||||||
}
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!interaction.channel.isSendable()) {
|
if (!interaction.channel.isSendable()) {
|
||||||
await interaction.editReply('Channel is not sendable');
|
await interaction.editReply('Channel is not sendable');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = interaction.options.getString('content');
|
const content = interaction.options.getString('content');
|
||||||
const attachment = interaction.options.getAttachment('attachment');
|
const attachment = interaction.options.getAttachment('attachment');
|
||||||
const replyId = interaction.options.getString('reply');
|
const replyId = interaction.options.getString('reply');
|
||||||
|
|
||||||
if (!content && !attachment) {
|
if (!content && !attachment) {
|
||||||
await interaction.editReply('Unable to send empty message. Specify content or attachment, or both.');
|
await interaction.editReply(
|
||||||
return;
|
'Unable to send empty message. Specify content or attachment, or both.'
|
||||||
}
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const channel = interaction.channel as TextChannel;
|
const channel = interaction.channel as TextChannel;
|
||||||
const message: MessageCreateOptions = {};
|
const message: MessageCreateOptions = {};
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
message.content = content;
|
message.content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replyId) {
|
if (replyId) {
|
||||||
try {
|
try {
|
||||||
const replyMessage = await channel.messages.fetch(replyId);
|
const replyMessage = await channel.messages.fetch(replyId);
|
||||||
message.reply = {
|
message.reply = {
|
||||||
messageReference: replyMessage.id
|
messageReference: replyMessage.id
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
await interaction.editReply('Invalid message ID for reply.');
|
await interaction.editReply('Invalid message ID for reply.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
message.files = [{
|
message.files = [
|
||||||
attachment: attachment.proxyURL,
|
{
|
||||||
name: attachment.name
|
attachment: attachment.proxyURL,
|
||||||
}];
|
name: attachment.name
|
||||||
}
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await channel.send(message);
|
await channel.send(message);
|
||||||
await interaction.editReply('Message sent successfully.');
|
await interaction.editReply('Message sent successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send message:', error);
|
console.error('Failed to send message:', error);
|
||||||
await interaction.editReply('Failed to send message.');
|
await interaction.editReply('Failed to send message.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default command;
|
export default command;
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { TTSModule, TTSResponse } from '../tts';
|
|||||||
|
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
|
|
||||||
import { WebSocket } from 'ws'
|
import { WebSocket } from 'ws';
|
||||||
import { Logger } from '../../utils/log';
|
import { Logger } from '../../utils/log';
|
||||||
|
|
||||||
const CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4";
|
const CLIENT_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
|
||||||
const AZURE_ENDPOINT = "speech.platform.bing.com";
|
const AZURE_ENDPOINT = 'speech.platform.bing.com';
|
||||||
|
|
||||||
const READALOUD_PATH = `/consumer/speech/synthesize/readaloud`
|
const READALOUD_PATH = `/consumer/speech/synthesize/readaloud`;
|
||||||
const WEBSOCKET_URL = `wss://${AZURE_ENDPOINT}${READALOUD_PATH}/edge/v1?TrustedClientToken=${CLIENT_TOKEN}`;
|
const WEBSOCKET_URL = `wss://${AZURE_ENDPOINT}${READALOUD_PATH}/edge/v1?TrustedClientToken=${CLIENT_TOKEN}`;
|
||||||
const VOICES_PATH = `${READALOUD_PATH}/voices/list?TrustedClientToken=${CLIENT_TOKEN}`;
|
const VOICES_PATH = `${READALOUD_PATH}/voices/list?TrustedClientToken=${CLIENT_TOKEN}`;
|
||||||
|
|
||||||
@@ -28,6 +28,13 @@ interface PendingRequest {
|
|||||||
audioBuff: Buffer[];
|
audioBuff: Buffer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VoiceInfo {
|
||||||
|
// Name: string;
|
||||||
|
ShortName: string,
|
||||||
|
// Gender: string,
|
||||||
|
// Locale: string,
|
||||||
|
}
|
||||||
|
|
||||||
class AzureTTS implements TTSModule {
|
class AzureTTS implements TTSModule {
|
||||||
private voices: Array<string> | undefined = undefined;
|
private voices: Array<string> | undefined = undefined;
|
||||||
|
|
||||||
@@ -52,26 +59,25 @@ class AzureTTS implements TTSModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getVoices(): Promise<Array<string> | undefined> {
|
async getVoices(): Promise<Array<string> | undefined> {
|
||||||
if (this.voices)
|
if (this.voices) return this.voices;
|
||||||
return this.voices;
|
|
||||||
|
|
||||||
const options: https.RequestOptions = {
|
const options: https.RequestOptions = {
|
||||||
hostname: AZURE_ENDPOINT,
|
hostname: AZURE_ENDPOINT,
|
||||||
path: `${VOICES_PATH}&Sec-MS-GEC=${this.genSecToken()}&Sec-MS-GEC-Version=${SEC_VERSION}`,
|
path: `${VOICES_PATH}&Sec-MS-GEC=${this.genSecToken()}&Sec-MS-GEC-Version=${SEC_VERSION}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Pragma': 'no-cache',
|
Pragma: 'no-cache',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
"Accept-Language": "en-US,en;q=0.9",
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
"Authority": "speech.platform.bing.com",
|
Authority: 'speech.platform.bing.com',
|
||||||
"Sec-CH-UA": `" Not;A Brand";v="99", "Microsoft Edge";v="${CHROME_VERSION.split('.')[0]}", "Chromium";v="${CHROME_VERSION.split('.')[0]}"`,
|
'Sec-CH-UA': `" Not;A Brand";v="99", "Microsoft Edge";v="${CHROME_VERSION.split('.')[0]}", "Chromium";v="${CHROME_VERSION.split('.')[0]}"`,
|
||||||
"Sec-CH-UA-Mobile": "?0",
|
'Sec-CH-UA-Mobile': '?0',
|
||||||
"Accept": "*/*",
|
Accept: '*/*',
|
||||||
"Sec-Fetch-Site": "none",
|
'Sec-Fetch-Site': 'none',
|
||||||
"Sec-Fetch-Mode": "cors",
|
'Sec-Fetch-Mode': 'cors',
|
||||||
"Sec-Fetch-Dest": "empty",
|
'Sec-Fetch-Dest': 'empty'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,14 +87,14 @@ class AzureTTS implements TTSModule {
|
|||||||
res.on('data', (chunk) => chunks.push(chunk));
|
res.on('data', (chunk) => chunks.push(chunk));
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
const body = Buffer.concat(chunks).toString();
|
const body = Buffer.concat(chunks).toString();
|
||||||
this.voices = JSON.parse(body).map((v: any) => v.ShortName)
|
this.voices = JSON.parse(body).map((v: VoiceInfo) => v.ShortName);
|
||||||
resolve(this.voices);
|
resolve(this.voices);
|
||||||
});
|
});
|
||||||
req.on('error', (err) => {
|
req.on('error', (err) => {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
res.on('aborted', () => {
|
res.on('aborted', () => {
|
||||||
throw new Error('Response aborted')
|
throw new Error('Response aborted');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
req.end();
|
req.end();
|
||||||
@@ -136,8 +142,8 @@ class AzureTTS implements TTSModule {
|
|||||||
host: 'speech.platform.bing.com',
|
host: 'speech.platform.bing.com',
|
||||||
origin: 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
|
origin: 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
|
||||||
headers: {
|
headers: {
|
||||||
'Pragma': 'no-cache',
|
Pragma: 'no-cache',
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,10 +172,10 @@ class AzureTTS implements TTSModule {
|
|||||||
this.handleIncomingMessage(data, isBinary);
|
this.handleIncomingMessage(data, isBinary);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.on('close', (code/*, reason*/) => {
|
this.ws.on('close', (/*code, reason*/) => {
|
||||||
this.ready = false;
|
this.ready = false;
|
||||||
// this.log.verbose(`WS Closed: ${code}`);
|
// this.log.verbose(`WS Closed: ${code}`);
|
||||||
this.rejectAllPending(new Error("Connection closed"));
|
this.rejectAllPending(new Error('Connection closed'));
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,8 +209,11 @@ class AzureTTS implements TTSModule {
|
|||||||
if (message.includes('Path:turn.end')) {
|
if (message.includes('Path:turn.end')) {
|
||||||
request.resolve({ data: Buffer.concat(request.audioBuff) });
|
request.resolve({ data: Buffer.concat(request.audioBuff) });
|
||||||
this.pendingRequests.delete(reqId);
|
this.pendingRequests.delete(reqId);
|
||||||
} else if (message.includes('Path:turn.error') || message.includes('Path:error')) {
|
} else if (
|
||||||
request.reject(new Error("Azure synthesis error"));
|
message.includes('Path:turn.error') ||
|
||||||
|
message.includes('Path:error')
|
||||||
|
) {
|
||||||
|
request.reject(new Error('Azure synthesis error'));
|
||||||
this.pendingRequests.delete(reqId);
|
this.pendingRequests.delete(reqId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,28 +227,35 @@ class AzureTTS implements TTSModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private genSecToken(): string {
|
private genSecToken(): string {
|
||||||
const ticks = BigInt(Math.floor((Date.now() / 1000) + Number(WIN_EPOCH))) * 10000000n
|
const ticks =
|
||||||
const roundedTicks = ticks - (ticks % 3000000000n)
|
BigInt(Math.floor(Date.now() / 1000 + Number(WIN_EPOCH))) * 10000000n;
|
||||||
|
const roundedTicks = ticks - (ticks % 3000000000n);
|
||||||
|
|
||||||
const strToHash = `${roundedTicks}${CLIENT_TOKEN}`
|
const strToHash = `${roundedTicks}${CLIENT_TOKEN}`;
|
||||||
|
|
||||||
const hash = createHash('sha256')
|
const hash = createHash('sha256');
|
||||||
hash.update(strToHash, 'ascii')
|
hash.update(strToHash, 'ascii');
|
||||||
|
|
||||||
return hash.digest('hex').toUpperCase()
|
return hash.digest('hex').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
private escapeXml(unsafe: string): string {
|
private escapeXml(unsafe: string): string {
|
||||||
return unsafe.replace(/[<>&"']/g, (c) => {
|
return unsafe.replace(/[<>&"']/g, (c) => {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case '<': return '<'
|
case '<':
|
||||||
case '>': return '>'
|
return '<';
|
||||||
case '&': return '&'
|
case '>':
|
||||||
case '"': return '"'
|
return '>';
|
||||||
case "'": return '''
|
case '&':
|
||||||
default: return c
|
return '&';
|
||||||
|
case '"':
|
||||||
|
return '"';
|
||||||
|
case "'":
|
||||||
|
return ''';
|
||||||
|
default:
|
||||||
|
return c;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class ElevenLabsTTS implements TTSModule {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.settings = ElevenLabsTTS.DEFAULT_SETTINGS;
|
this.settings = ElevenLabsTTS.DEFAULT_SETTINGS;
|
||||||
this.modelId = 'eleven_flash_v2_5';
|
this.modelId = 'eleven_v3';
|
||||||
|
|
||||||
if (this.canBeUsed()) this.initializationPromise = this.init();
|
if (this.canBeUsed()) this.initializationPromise = this.init();
|
||||||
|
|
||||||
@@ -148,10 +148,7 @@ export class ElevenLabsTTS implements TTSModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canBeUsed(): boolean {
|
canBeUsed(): boolean {
|
||||||
return (
|
return config.tts_elevenlabs_refreshtoken != undefined;
|
||||||
config.tts_elevenlabs_refreshtoken != undefined &&
|
|
||||||
config.tts_elevenlabs_key != undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -176,13 +173,15 @@ export class ElevenLabsTTS implements TTSModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async fetchVoices(): Promise<void> {
|
private async fetchVoices(): Promise<void> {
|
||||||
|
if (!this.session) return;
|
||||||
|
|
||||||
const opt: https.RequestOptions = {
|
const opt: https.RequestOptions = {
|
||||||
hostname: ELEVENLABS_API_ENDPOINT,
|
hostname: ELEVENLABS_API_ENDPOINT,
|
||||||
path: '/v2/voices',
|
path: '/v2/voices',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'xi-api-key': config.tts_elevenlabs_key,
|
Authorization: `Bearer ${this.session.idToken}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -211,13 +210,15 @@ export class ElevenLabsTTS implements TTSModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async fetchModels(): Promise<void> {
|
private async fetchModels(): Promise<void> {
|
||||||
|
if (!this.session) return;
|
||||||
|
|
||||||
const opt: https.RequestOptions = {
|
const opt: https.RequestOptions = {
|
||||||
hostname: ELEVENLABS_API_ENDPOINT,
|
hostname: ELEVENLABS_API_ENDPOINT,
|
||||||
path: '/v1/models',
|
path: '/v1/models',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'xi-api-key': config.tts_elevenlabs_key,
|
Authorization: `Bearer ${this.session.idToken}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export interface Config {
|
|||||||
tts_default_mode: string | undefined;
|
tts_default_mode: string | undefined;
|
||||||
tts_default_voice: string | undefined;
|
tts_default_voice: string | undefined;
|
||||||
|
|
||||||
tts_elevenlabs_key: string | undefined;
|
|
||||||
tts_elevenlabs_refreshtoken: string | undefined;
|
tts_elevenlabs_refreshtoken: string | undefined;
|
||||||
tts_tiktok_sessionid: string | undefined;
|
tts_tiktok_sessionid: string | undefined;
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ function loadConfig(): Config {
|
|||||||
owner_id: process.env.DISCORD_OWNER_ID,
|
owner_id: process.env.DISCORD_OWNER_ID,
|
||||||
tts_default_mode: process.env.DEFAULT_TTS_MODE,
|
tts_default_mode: process.env.DEFAULT_TTS_MODE,
|
||||||
tts_default_voice: process.env.DEFAULT_TTS_VOICE,
|
tts_default_voice: process.env.DEFAULT_TTS_VOICE,
|
||||||
tts_elevenlabs_key: process.env.TTS_ELEVENLABS_KEY,
|
|
||||||
tts_elevenlabs_refreshtoken: process.env.TTS_ELEVENLABS_REFRESHTOKEN,
|
tts_elevenlabs_refreshtoken: process.env.TTS_ELEVENLABS_REFRESHTOKEN,
|
||||||
steam_webapi_key: process.env.STEAM_WEBAPI_KEY,
|
steam_webapi_key: process.env.STEAM_WEBAPI_KEY,
|
||||||
aws_access_id: process.env.AWS_ACCESS_ID,
|
aws_access_id: process.env.AWS_ACCESS_ID,
|
||||||
|
|||||||
Reference in New Issue
Block a user