A more complete re-engineering of the voice stuff
This commit is contained in:
@@ -2,14 +2,15 @@ import dotenv from "dotenv"
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
export const config = {
|
||||
DISCORD_TOKEN,
|
||||
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 { DBUser } from "../storage/entities/DBUser";
|
||||
|
||||
export async function doesUserExist(db: Repository<DBUser>, user: User): Promise<boolean> {
|
||||
return (await db.findOne({"where": {user_snowflake: user.id}})) != null
|
||||
export async function doesUserExist(db: Repository<DBUser>, user: User | GuildMember | string): Promise<boolean> {
|
||||
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> {
|
||||
const alreadyExists: boolean = await doesUserExist(db, user)
|
||||
export async function insertUser(db: Repository<DBUser>, user: User | GuildMember | DBUser): Promise<DBUser | null> {
|
||||
const alreadyExists: boolean = await doesUserExist(db, user instanceof DBUser ? user.user_snowflake : user)
|
||||
|
||||
if(alreadyExists) {
|
||||
return null
|
||||
return await db.findOne({"where": {user_snowflake: user instanceof DBUser ? user.user_snowflake : user.id }})
|
||||
}
|
||||
|
||||
try {
|
||||
const newUser: DBUser = db.create({
|
||||
const newUser: DBUser = user instanceof DBUser ? user : db.create({
|
||||
user_snowflake: user.id,
|
||||
user_name: user.username,
|
||||
user_name: user instanceof User ? user.username : user.displayName,
|
||||
user_displayname: user.displayName
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { VoiceBasedChannel } from "discord.js";
|
||||
import { IsNull, Repository } from "typeorm";
|
||||
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> {
|
||||
return (await db.findOne({
|
||||
@@ -11,7 +13,7 @@ export async function breadbotInCall(db: Repository<DBCall>, channel: VoiceBased
|
||||
})) != 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({
|
||||
where: {
|
||||
channel: {channel_snowflake: channel.id},
|
||||
@@ -19,3 +21,72 @@ export async function getExistingCallID(db: Repository<DBCall>, channel: VoiceBa
|
||||
}
|
||||
}))?.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 { 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>) {
|
||||
client.on(Events.VoiceStateUpdate, (oldState: VoiceState, newState: VoiceState) => {
|
||||
export function setupVoice(client: Client, callDB: Repository<DBCall>, channelDB: Repository<DBChannel>, userDB: Repository<DBUser>, callUserDB: Repository<DBCallUsers>) {
|
||||
client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceState) => {
|
||||
if(oldState.channel == null && newState.channel != null) {
|
||||
// TODO Null Type Safety Risk?
|
||||
if (newState.member?.id == client.user?.id) {
|
||||
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`)
|
||||
})
|
||||
}
|
||||
|
||||
async function processCallJoin(db: Repository<DBCall>, voiceState: VoiceState) {
|
||||
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@ export class DBCall {
|
||||
transcriptions: DBCallTranscriptions[] | null
|
||||
|
||||
@OneToMany(() => DBCallUsers, (callUser: DBCallUsers) => callUser.call, {nullable: true})
|
||||
participants: DBCallUsers | null
|
||||
participants: DBCallUsers[] | null
|
||||
}
|
||||
Reference in New Issue
Block a user