A more complete re-engineering of the voice stuff
This commit is contained in:
@@ -2,14 +2,15 @@ import dotenv from "dotenv"
|
|||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DB_MODE } = process.env
|
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DB_MODE, MEDIA_VOICE_FOLDER } = process.env
|
||||||
|
|
||||||
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID || !DB_MODE) {
|
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID || !DB_MODE || !MEDIA_VOICE_FOLDER) {
|
||||||
throw new Error("Missing environment variables")
|
throw new Error("Missing environment variables")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
DISCORD_TOKEN,
|
DISCORD_TOKEN,
|
||||||
DISCORD_CLIENT_ID,
|
DISCORD_CLIENT_ID,
|
||||||
DB_MODE
|
DB_MODE,
|
||||||
|
MEDIA_VOICE_FOLDER
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import { User } from "discord.js";
|
import { GuildMember, User } from "discord.js";
|
||||||
import { Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { DBUser } from "../storage/entities/DBUser";
|
import { DBUser } from "../storage/entities/DBUser";
|
||||||
|
|
||||||
export async function doesUserExist(db: Repository<DBUser>, user: User): Promise<boolean> {
|
export async function doesUserExist(db: Repository<DBUser>, user: User | GuildMember | string): Promise<boolean> {
|
||||||
return (await db.findOne({"where": {user_snowflake: user.id}})) != null
|
return (await db.findOne({"where": {user_snowflake: user instanceof User || user instanceof GuildMember ? user.id : user }})) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function insertUser(db: Repository<DBUser>, user: User): Promise<DBUser | null> {
|
export async function insertUser(db: Repository<DBUser>, user: User | GuildMember | DBUser): Promise<DBUser | null> {
|
||||||
const alreadyExists: boolean = await doesUserExist(db, user)
|
const alreadyExists: boolean = await doesUserExist(db, user instanceof DBUser ? user.user_snowflake : user)
|
||||||
|
|
||||||
if(alreadyExists) {
|
if(alreadyExists) {
|
||||||
return null
|
return await db.findOne({"where": {user_snowflake: user instanceof DBUser ? user.user_snowflake : user.id }})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newUser: DBUser = db.create({
|
const newUser: DBUser = user instanceof DBUser ? user : db.create({
|
||||||
user_snowflake: user.id,
|
user_snowflake: user.id,
|
||||||
user_name: user.username,
|
user_name: user instanceof User ? user.username : user.displayName,
|
||||||
user_displayname: user.displayName
|
user_displayname: user.displayName
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { VoiceBasedChannel } from "discord.js";
|
import { VoiceBasedChannel } from "discord.js";
|
||||||
import { IsNull, Repository } from "typeorm";
|
import { IsNull, Repository } from "typeorm";
|
||||||
import { DBCall } from "../storage/entities/DBCall";
|
import { DBCall } from "../storage/entities/DBCall";
|
||||||
|
import { DBCallUsers } from "../storage/entities/DBCallUsers";
|
||||||
|
import { DBUser } from "../storage/entities/DBUser";
|
||||||
|
|
||||||
export async function breadbotInCall(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<boolean> {
|
export async function breadbotInCall(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<boolean> {
|
||||||
return (await db.findOne({
|
return (await db.findOne({
|
||||||
@@ -11,7 +13,7 @@ export async function breadbotInCall(db: Repository<DBCall>, channel: VoiceBased
|
|||||||
})) != null
|
})) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExistingCallID(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<Number | undefined> {
|
export async function getExistingCallID(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<number | undefined> {
|
||||||
return (await db.findOne({
|
return (await db.findOne({
|
||||||
where: {
|
where: {
|
||||||
channel: {channel_snowflake: channel.id},
|
channel: {channel_snowflake: channel.id},
|
||||||
@@ -19,3 +21,72 @@ export async function getExistingCallID(db: Repository<DBCall>, channel: VoiceBa
|
|||||||
}
|
}
|
||||||
}))?.call_id
|
}))?.call_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function returnOrCreateNewCallID(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<number> {
|
||||||
|
const oldCallID = await getExistingCallID(db, channel)
|
||||||
|
|
||||||
|
if(oldCallID !== undefined) {
|
||||||
|
return oldCallID
|
||||||
|
} else {
|
||||||
|
const newCall : DBCall = await db.create({
|
||||||
|
channel: { channel_snowflake: channel.id },
|
||||||
|
call_start_time: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (await db.save(newCall)).call_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setCallEndTime(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<DBCall | null> {
|
||||||
|
const call: DBCall | null = await db.findOne({
|
||||||
|
"where": {
|
||||||
|
channel: channel,
|
||||||
|
call_end_time: IsNull()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (call == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
call.call_end_time = new Date()
|
||||||
|
|
||||||
|
return await db.save(call)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function numberOfUsersInCall(db: Repository<DBCallUsers>, call: DBCall | number) : Promise<number> {
|
||||||
|
const activeCallUsers : DBCallUsers[] = await db.find({
|
||||||
|
"where": {
|
||||||
|
call: (call instanceof DBCall) ? call : { call_id: call },
|
||||||
|
call_leave_time: IsNull()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return activeCallUsers.length
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerUserInCall(db: Repository<DBCallUsers>, call: DBCall | number, user: DBUser) : Promise<DBCallUsers> {
|
||||||
|
return await db.save({
|
||||||
|
call: (call instanceof DBCall) ? call : {call_id: call},
|
||||||
|
user: user,
|
||||||
|
call_join_time: new Date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deregisterUserInCall(db: Repository<DBCallUsers>, call: DBCall | number, user: DBUser | string) : Promise<DBCallUsers | null> {
|
||||||
|
const callUser : DBCallUsers | null = await db.findOne({
|
||||||
|
where: {
|
||||||
|
call: (call instanceof DBCall) ? call : {call_id: call},
|
||||||
|
user: (user instanceof DBUser) ? user : {user_snowflake: user},
|
||||||
|
call_leave_time: IsNull()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if(callUser == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
callUser.call_leave_time = new Date()
|
||||||
|
|
||||||
|
return await db.save(callUser)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,118 @@
|
|||||||
import { Client, Events, VoiceState } from "discord.js"
|
import { Client, Events, GuildMember, VoiceState } from "discord.js"
|
||||||
import { Repository } from "typeorm"
|
import { Repository } from "typeorm"
|
||||||
import { DBCall } from "../storage/entities/DBCall"
|
import { DBCall } from "../storage/entities/DBCall"
|
||||||
|
import { DBChannel } from "../storage/entities/DBChannel"
|
||||||
|
import { insertChannel } from "../discord/channels"
|
||||||
|
import { deregisterUserInCall, getExistingCallID, numberOfUsersInCall, registerUserInCall, returnOrCreateNewCallID, setCallEndTime } from "../discord/voice"
|
||||||
|
import { createWriteStream, mkdirSync } from 'node:fs'
|
||||||
|
import { config } from '../../config'
|
||||||
|
import path from "node:path"
|
||||||
|
import { EndBehaviorType, entersState, getVoiceConnection, joinVoiceChannel, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice"
|
||||||
|
import { OggLogicalBitstream, OpusHead } from "prism-media/dist/opus"
|
||||||
|
import { DBUser } from "../storage/entities/DBUser"
|
||||||
|
import { insertUser } from "../discord/users"
|
||||||
|
import { DBCallUsers } from "../storage/entities/DBCallUsers"
|
||||||
|
|
||||||
export function setupVoice(client: Client, db: Repository<DBCall>) {
|
export function setupVoice(client: Client, callDB: Repository<DBCall>, channelDB: Repository<DBChannel>, userDB: Repository<DBUser>, callUserDB: Repository<DBCallUsers>) {
|
||||||
client.on(Events.VoiceStateUpdate, (oldState: VoiceState, newState: VoiceState) => {
|
client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceState) => {
|
||||||
if(oldState.channel == null && newState.channel != null) {
|
if(oldState.channel == null && newState.channel != null) {
|
||||||
// TODO Null Type Safety Risk?
|
// TODO Null Type Safety Risk?
|
||||||
if (newState.member?.id == client.user?.id) {
|
if (newState.member?.id == client.user?.id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await insertChannel(channelDB, newState.channel)
|
||||||
|
|
||||||
|
let existingCallID : number | undefined = await getExistingCallID(callDB, newState.channel)
|
||||||
|
|
||||||
|
if (existingCallID === undefined) {
|
||||||
|
existingCallID = await returnOrCreateNewCallID(callDB, newState.channel)
|
||||||
|
|
||||||
|
mkdirSync(config.MEDIA_VOICE_FOLDER + path.sep + existingCallID, {recursive: true})
|
||||||
|
|
||||||
|
// TODO NULL armor here is probably just going to blow up the call to joinVoiceChannel with no error catching
|
||||||
|
const connection : VoiceConnection = joinVoiceChannel({
|
||||||
|
channelId: newState.channelId ?? "",
|
||||||
|
guildId: newState.guild.id,
|
||||||
|
selfDeaf: false,
|
||||||
|
selfMute: true,
|
||||||
|
adapterCreator: newState.guild.voiceAdapterCreator
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await entersState(connection, VoiceConnectionStatus.Ready, 20e3)
|
||||||
|
const receiver = connection.receiver
|
||||||
|
|
||||||
|
if (receiver.speaking.listenerCount("start") == 0) {
|
||||||
|
receiver.speaking.on("start", (userID: string) => {
|
||||||
|
if(!receiver.subscriptions.has(userID)) {
|
||||||
|
receiver.subscribe(userID, {
|
||||||
|
end: {
|
||||||
|
behavior: EndBehaviorType.AfterSilence,
|
||||||
|
duration: 500
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.pipe(new OggLogicalBitstream({
|
||||||
|
opusHead: new OpusHead({
|
||||||
|
channelCount: 2,
|
||||||
|
sampleRate: 48000
|
||||||
|
}),
|
||||||
|
pageSizeControl: {
|
||||||
|
maxPackets: 10
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.pipe(createWriteStream(
|
||||||
|
config.MEDIA_VOICE_FOLDER + path.sep +
|
||||||
|
existingCallID + path.sep +
|
||||||
|
`${Date.now()}-${userID}.ogg`
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
console.log(`Attempted to create new user subscriber for ${userID} even though one already exist, receiver arm if statement protected against this`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
receiver.speaking.on("end", (userID: string) => {
|
||||||
|
console.log(`User ${userID} stopped speaking`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
//TODO Winston
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const member : GuildMember | null = newState.member
|
||||||
|
|
||||||
|
// In theory, member should never be null, because the Gateway Intents necessary
|
||||||
|
// to make it work are provided
|
||||||
|
if(member != null) {
|
||||||
|
const insertedUser: DBUser | null = await insertUser(userDB, member)
|
||||||
|
|
||||||
|
if(insertedUser != null) {
|
||||||
|
await registerUserInCall(callUserDB, existingCallID, insertedUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (oldState.channel != null && newState.channel == null) {
|
||||||
|
if(oldState.member?.id == client.user?.id) {
|
||||||
|
return // If the user is breadbot, ignore and exit
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingCall : number | undefined = await getExistingCallID(callDB, oldState.channel)
|
||||||
|
|
||||||
|
if (existingCall !== undefined && oldState.member) {
|
||||||
|
await deregisterUserInCall(callUserDB, existingCall, oldState.member?.id)
|
||||||
|
|
||||||
|
const usersInCall: number = await numberOfUsersInCall(callUserDB, existingCall)
|
||||||
|
|
||||||
|
if (usersInCall == 0) {
|
||||||
|
const connection = getVoiceConnection(oldState.guild.id)
|
||||||
|
connection?.disconnect()
|
||||||
|
|
||||||
|
await setCallEndTime(callDB, oldState.channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processCallJoin(db: Repository<DBCall>, voiceState: VoiceState) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,5 +30,5 @@ export class DBCall {
|
|||||||
transcriptions: DBCallTranscriptions[] | null
|
transcriptions: DBCallTranscriptions[] | null
|
||||||
|
|
||||||
@OneToMany(() => DBCallUsers, (callUser: DBCallUsers) => callUser.call, {nullable: true})
|
@OneToMany(() => DBCallUsers, (callUser: DBCallUsers) => callUser.call, {nullable: true})
|
||||||
participants: DBCallUsers | null
|
participants: DBCallUsers[] | null
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user