Files
Ultroid-fork/src/tgcalls.ts
Akash Pattnaik 7eb5e58721 Release v0.0.6
Co-authored-by: Aakash <BLUE-DEVIL1134@users.noreply.github.com>
Co-authored-by: Aditya <me@xditya.me>
Co-authored-by: Danish <danish@ultroid.tech>
Co-authored-by: buddhhu <buddhuu@users.noreply.github.com>
Co-authored-by: sppidy <sppidy@users.noreply.github.com>
Co-authored-by: Arnab Paryali <Arnabxd@users.noreply.github.com>
Co-authored-by: divkix <divkix@users.noreply.github.com>
Co-authored-by: hellboi_atul <hellboi-atul@users.noreply.github.com>
Co-authored-by: Programming Error <error@notavailable.live>
Co-authored-by: New-dev0 <New-dev0@notavailable.live>
2021-05-08 18:37:32 +05:30

351 lines
9.8 KiB
TypeScript

/**
* Ultroid - UserBot
* Copyright (C) 2020 TeamUltroid
*
* This file is a part of < https://github.com/TeamUltroid/Ultroid/ >
* PLease read the GNU Affero General Public License in
* <https://www.github.com/TeamUltroid/Ultroid/blob/main/LICENSE/>.
**/
import { Chat } from 'typegram';
import { exec as _exec, spawn } from 'child_process';
import { JoinVoiceCallResponse } from 'tgcalls/lib/types';
import { Stream, TGCalls } from 'tgcalls';
import env from './env';
import WebSocket from 'ws';
import { Readable } from 'stream';
import { bot } from './bot';
import { Markup } from 'telegraf';
import { getDuration } from './utils';
import escapeHtml from '@youtwitface/escape-html';
interface DownloadedSong {
stream: Readable;
info: {
id: string;
title: string;
duration: number;
};
}
interface Queue {
url: string;
info: DownloadedSong['info'];
from: {
id: string | number;
f_name: string;
};
}
interface CurrentSong {
song: DownloadedSong['info'],
by: Queue['from']
}
interface CachedConnection {
connection: TGCalls<{ chat: Chat.SupergroupChat }>;
stream: Stream;
queue: Queue[];
currentSong: CurrentSong | null;
joinResolve?: (value: JoinVoiceCallResponse) => void;
source?: number;
leftVC: boolean
}
const ws = new WebSocket(env.WEBSOCKET_URL);
const cache = new Map<number, CachedConnection>();
var connection: TGCalls<{ chat: Chat.SupergroupChat; }>;
const ffmpegOptions = "-preset ultrafast -c copy -acodec pcm_s16le -f s16le -ac 1 -ar 65000 pipe:1";
ws.on('message', (response: any) => {
const { _, data } = JSON.parse(response.toString());
switch (_) {
case 'get_join': {
const connection = cache.get(data.chat_id);
if (connection) {
connection.joinResolve?.(data);
}
break;
}
case 'left_vc': {
break;
}
default:
break;
}
});
const downloadSong = async (url: string): Promise<DownloadedSong> => {
return new Promise((resolve, reject) => {
const ytdlChunks: string[] = [];
const ytdl = spawn('youtube-dl', ['-x', '--print-json', '-g', `${url}`]);
ytdl.stderr.on('data', data => console.error(data.toString()));
ytdl.stdout.on('data', data => {
ytdlChunks.push(data.toString());
});
ytdl.on('exit', code => {
if (code !== 0) {
return reject();
}
const ytdlData = ytdlChunks.join('');
const [inputUrl, _videoInfo] = ytdlData.split('\n');
const videoInfo = JSON.parse(_videoInfo);
const ffmpeg = spawn('ffmpeg', ['-y', '-nostdin', '-i', inputUrl, ...ffmpegOptions.split(' ')]);
resolve({
stream: ffmpeg.stdout,
info: {
id: videoInfo.id,
title: videoInfo.title,
duration: videoInfo.duration,
},
});
});
});
};
export const getSongInfo = async (url: string): Promise<DownloadedSong['info']> => {
return new Promise((resolve, reject) => {
const ytdlChunks: string[] = [];
const ytdl = spawn('youtube-dl', ['-x', '--print-json', '-g', `ytsearch:"${url}"`]);
ytdl.stderr.on('data', (data) => {
console.error(data.toString())
});
ytdl.stdout.on('data', (data) => {
ytdlChunks.push(data.toString());
});
ytdl.on('exit', code => {
if (code !== 0) {
return reject();
}
const ytdlData = ytdlChunks.join('');
const [inputUrl, _videoInfo] = ytdlData.split('\n');
const videoInfo = JSON.parse(_videoInfo);
resolve({
id: videoInfo.id,
title: videoInfo.title,
duration: videoInfo.duration,
});
});
});
};
export const closeConnection = async(): Promise<void> => {
connection.close();
}
const createConnection = async(chat: Chat.SupergroupChat): Promise<void> => {
if (cache.has(chat.id)) {
return;
}
connection = new TGCalls({ chat });
const stream = new Stream();
const queue: {
url: string,
info: DownloadedSong['info'],
from: {
id: string | number,
f_name: string
}
}[] = [];
const cachedConnection: CachedConnection = {
connection,
stream,
queue,
currentSong: null,
leftVC: false
};
connection.joinVoiceCall = (payload: { source: number | undefined; ufrag: any; pwd: any; hash: any; setup: any; fingerprint: any; params: { chat: any; }; }) => {
cachedConnection.source = payload.source;
return new Promise(resolve => {
cachedConnection.joinResolve = resolve;
const data = {
_: 'join',
data: {
ufrag: payload.ufrag,
pwd: payload.pwd,
hash: payload.hash,
setup: payload.setup,
fingerprint: payload.fingerprint,
source: payload.source,
chat: payload.params.chat,
},
};
ws.send(JSON.stringify(data));
});
};
cache.set(chat.id, cachedConnection);
await connection.start(stream.createTrack());
stream.on('finish', async () => {
if (queue.length > 0) {
const { url, from } = queue.shift()!;
try {
const song = await downloadSong(url);
const { title, id, duration } = song.info
stream.setReadable(song.stream);
cachedConnection.currentSong = {
song: song.info,
by: from
};
await bot.telegram.sendPhoto(chat.id, `https://img.youtube.com/vi/${id}/hqdefault.jpg`, {
caption: `<b>Playing : </b> <a href="https://www.youtube.com/watch?v=${id}">${escapeHtml(title)}</a>\n` +
`<b>Duration : </b>${getDuration(duration)}\n` +
`<b>Requested by :</b> <a href="tg://user?id=${from.id}">${from.f_name}</a>`,
parse_mode: 'HTML',
...Markup.inlineKeyboard([
[
Markup.button.callback('Pause', `pause:${id}`),
Markup.button.callback('Skip', `skip:${id}`),
],
[
Markup.button.callback('Exit', `exitVc`),
]
])
})
} catch (error) {
console.error(error);
stream.emit('finish');
}
} else {
try {
leaveVc(chat.id);
} catch (err) {
console.error(err);
}
cachedConnection.currentSong = null;
}
});
stream.on('leave', () => {
const data = {
_: 'leave',
data: {
source: cachedConnection.source,
chat: chat
},
};
ws.send(JSON.stringify(data));
cachedConnection.leftVC = true;
});
};
export const leaveVc = (chatId: number) => {
if (cache.has(chatId)) {
const { stream } = cache.get(chatId)!;
try {
stream.emit('leave');
} catch (error) {
console.log(error.toString());
stream.emit('leave');
}
process.exit(0);
} else {
return false;
}
}
export const addToQueue = async (chat: Chat.SupergroupChat, url: string, by: Queue['from']): Promise<number | null> => {
if (!cache.has(chat.id)) {
await createConnection(chat);
return addToQueue(chat, url, by);
}
const connection = cache.get(chat.id)!;
if (connection.leftVC) {
cache.delete(chat.id);
await createConnection(chat);
return addToQueue(chat, url, by);
}
const { stream, queue } = connection;
let songInfo: DownloadedSong['info'];
if (stream.finished) {
try {
const song = await downloadSong(url);
stream.setReadable(song.stream);
connection.currentSong = {
song: song.info,
by: by
};
songInfo = song.info;
cache.set(chat.id, connection);
} catch (error) {
console.error(error);
return -1;
}
return 0;
} else {
songInfo = await getSongInfo(url);
}
return queue.push({
url: url,
from: by,
info: songInfo
});
};
export const getCurrentSong = (chatId: number): CurrentSong | null => {
if (cache.has(chatId)) {
const { currentSong } = cache.get(chatId)!;
return currentSong;
}
return null;
};
export const getQueue = (chatId: number): Queue[] | null => {
if (cache.has(chatId)) {
const { queue } = cache.get(chatId)!;
return Array.from(queue);
}
return [];
};
export const removeQueue = (chatId: number, id: number): boolean => {
if (cache.has(chatId)) {
const { queue } = cache.get(chatId)!;
if (id > queue.length) return false;
if (queue.splice(id, 1)) return true;
}
return false;
}
export const pause = (chatId: number): boolean | null => {
if (cache.has(chatId)) {
const { stream } = cache.get(chatId)!;
stream.pause();
return stream.paused;
}
return null;
};
export const skip = (chatId: number): boolean => {
if (cache.has(chatId)) {
const { stream } = cache.get(chatId)!;
stream.finish();
stream.emit('finish');
return true;
}
return false;
};