2024-01-27 03:54:37 +00:00
|
|
|
|
import { exec } from 'node:child_process';
|
2024-01-28 02:20:29 +00:00
|
|
|
|
import {
|
|
|
|
|
readFile,
|
|
|
|
|
writeFile
|
|
|
|
|
} from 'node:fs/promises';
|
2024-01-27 03:54:37 +00:00
|
|
|
|
|
2022-07-11 09:34:15 +00:00
|
|
|
|
import { overrideConsole } from 'nodejs-better-console';
|
|
|
|
|
import Eris from 'eris';
|
2024-01-27 19:38:33 +00:00
|
|
|
|
import { Command, Argument } from 'commander';
|
2024-01-27 19:18:53 +00:00
|
|
|
|
import KeyEvent from 'keyevent.json' assert { type: 'json' };
|
2024-01-28 02:20:29 +00:00
|
|
|
|
import { M3uParser } from 'm3u-parser-generator';
|
|
|
|
|
import { parse as parseXspf } from 'xspf-js';
|
|
|
|
|
import outdent from 'outdent';
|
2024-01-27 21:25:29 +00:00
|
|
|
|
import AsciiTable from 'ascii-table';
|
2024-01-27 03:54:37 +00:00
|
|
|
|
import { parse } from 'shell-quote';
|
2022-07-11 09:34:15 +00:00
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
botToken,
|
2024-01-27 03:54:37 +00:00
|
|
|
|
botWhitelist,
|
|
|
|
|
adbIp,
|
|
|
|
|
adbPort
|
2022-07-11 09:34:15 +00:00
|
|
|
|
} from './config.js';
|
|
|
|
|
|
|
|
|
|
overrideConsole();
|
|
|
|
|
|
|
|
|
|
const
|
|
|
|
|
eris = new Eris(
|
|
|
|
|
botToken,
|
|
|
|
|
{ intents: ['directMessages'] }
|
2024-01-27 03:54:37 +00:00
|
|
|
|
),
|
|
|
|
|
adb = command => new Promise((resolve, reject) => exec(
|
|
|
|
|
`adb -s ${adbIp}:${adbPort} shell ${command}`,
|
|
|
|
|
(
|
|
|
|
|
error,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr
|
|
|
|
|
) => {
|
|
|
|
|
if(error || stderr)
|
|
|
|
|
reject(error || stderr);
|
|
|
|
|
else
|
2024-01-27 21:20:45 +00:00
|
|
|
|
resolve(stdout.replace(/\r/g, '').trim());
|
2024-01-27 03:54:37 +00:00
|
|
|
|
}
|
|
|
|
|
)),
|
2024-01-28 02:20:29 +00:00
|
|
|
|
getConfig = async id => {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(await readFile(`./data/${id}.json`, 'utf8'));
|
|
|
|
|
}
|
|
|
|
|
catch {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
setConfig = async (id, config) => await writeFile(`./data/${id}.json`, JSON.stringify(config)),
|
|
|
|
|
handleCommand = async ({
|
|
|
|
|
content,
|
|
|
|
|
authorID,
|
|
|
|
|
attachments
|
|
|
|
|
}) => {
|
2024-01-27 03:54:37 +00:00
|
|
|
|
const program = new Command('');
|
|
|
|
|
let response;
|
|
|
|
|
program.exitOverride();
|
|
|
|
|
program.configureOutput({
|
|
|
|
|
writeOut: output => response = { output, isRaw: true },
|
|
|
|
|
writeErr: output => response = { output, isRaw: true }
|
|
|
|
|
});
|
|
|
|
|
program
|
|
|
|
|
.command('shell')
|
|
|
|
|
.description('Execute arbitrary shell command')
|
|
|
|
|
.argument('<command>', 'Arbitrary shell command')
|
|
|
|
|
.action(async command => {
|
|
|
|
|
response = {
|
|
|
|
|
output: await adb(command),
|
|
|
|
|
isRaw: true
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-01-27 19:38:33 +00:00
|
|
|
|
program
|
|
|
|
|
.command('home')
|
|
|
|
|
.alias('h')
|
|
|
|
|
.description('Go to home')
|
|
|
|
|
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_HOME}`));
|
|
|
|
|
program
|
|
|
|
|
.command('back')
|
|
|
|
|
.alias('b')
|
|
|
|
|
.description('Go back')
|
|
|
|
|
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_BACK}`));
|
2024-01-27 03:54:37 +00:00
|
|
|
|
program
|
|
|
|
|
.command('p')
|
|
|
|
|
.description('Play/pause media')
|
2024-01-27 19:18:53 +00:00
|
|
|
|
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE}`));
|
2024-01-27 03:54:37 +00:00
|
|
|
|
program
|
|
|
|
|
.command('play')
|
|
|
|
|
.description('Play media')
|
2024-01-27 19:18:53 +00:00
|
|
|
|
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_PLAY}`));
|
2024-01-27 03:54:37 +00:00
|
|
|
|
program
|
|
|
|
|
.command('pause')
|
|
|
|
|
.description('Pause media')
|
2024-01-27 19:18:53 +00:00
|
|
|
|
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_PAUSE}`));
|
2024-01-27 19:38:33 +00:00
|
|
|
|
program
|
|
|
|
|
.command('prev')
|
|
|
|
|
.description('Previous media')
|
|
|
|
|
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_PREVIOUS}`));
|
|
|
|
|
program
|
|
|
|
|
.command('next')
|
|
|
|
|
.description('Next media')
|
|
|
|
|
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_NEXT}`));
|
|
|
|
|
program
|
|
|
|
|
.command('type')
|
|
|
|
|
.description('Type text')
|
|
|
|
|
.argument('<text>', 'Text to type')
|
|
|
|
|
.action(text => adb(`input text ${text}`));
|
|
|
|
|
program
|
|
|
|
|
.command('press')
|
|
|
|
|
.description('Press key')
|
|
|
|
|
.addArgument(
|
|
|
|
|
new Argument('<key>', 'Key to press')
|
2024-01-27 23:05:45 +00:00
|
|
|
|
.choices(['tab', 'enter'])
|
2024-01-27 19:38:33 +00:00
|
|
|
|
)
|
|
|
|
|
.action(key => adb(`input keyevent ${{
|
|
|
|
|
'tab': KeyEvent.KEYCODE_TAB,
|
|
|
|
|
'enter': KeyEvent.KEYCODE_ENTER
|
|
|
|
|
}[key]}`));
|
2024-01-27 03:54:37 +00:00
|
|
|
|
program
|
|
|
|
|
.command('spotify')
|
|
|
|
|
.description('Launch Spotify')
|
|
|
|
|
.action(() => adb('monkey -p com.spotify.music 1'));
|
|
|
|
|
program
|
|
|
|
|
.command('mpv')
|
2024-01-28 02:18:32 +00:00
|
|
|
|
.description('Play media on MPV')
|
2024-01-27 03:54:37 +00:00
|
|
|
|
.argument('<url>', 'Media URL')
|
|
|
|
|
.action(url => adb(`am start -a android.intent.action.VIEW -d ${url} -t video/any is.xyz.mpv`));
|
2024-01-28 02:20:29 +00:00
|
|
|
|
program
|
|
|
|
|
.command('playlist')
|
|
|
|
|
.alias('pl')
|
|
|
|
|
.description('Manage playlists & play on MPV')
|
|
|
|
|
.argument('[name]', 'Playlist name')
|
|
|
|
|
.argument('[item]', 'Playlist item index/name/partial name')
|
|
|
|
|
.action(async (name, item) => {
|
|
|
|
|
const
|
|
|
|
|
config = await getConfig(authorID) || {},
|
|
|
|
|
newPlaylistUrl = attachments[0]?.url;
|
|
|
|
|
let { playlists } = config;
|
|
|
|
|
if(!playlists)
|
|
|
|
|
config.playlists = playlists = [];
|
|
|
|
|
if(!name){
|
|
|
|
|
if(!playlists.length)
|
|
|
|
|
return response = { embed: { title: `❌ No existing playlist` } };
|
|
|
|
|
return response = {
|
|
|
|
|
embed: {
|
|
|
|
|
title: `ℹ️ Your playlists :`,
|
|
|
|
|
description: playlists.map(playlist => `- [${playlist.name}](${playlist.url})`).join('\n')
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
let playlist = playlists.find(playlist => playlist.name === name);
|
|
|
|
|
if(newPlaylistUrl){
|
|
|
|
|
if(!/\.(m3u8?|xspf)$/i.test(new URL(newPlaylistUrl).pathname))
|
|
|
|
|
return response = { embed: { title: `❌ Invalid playlist` } };
|
|
|
|
|
response = { embed: { title: `✅ Playlist updated` } };
|
|
|
|
|
if(!playlist){
|
|
|
|
|
playlists.push(playlist = { name });
|
|
|
|
|
response = { embed: { title: `✅ Playlist created` } };
|
|
|
|
|
}
|
|
|
|
|
playlist.url = newPlaylistUrl;
|
|
|
|
|
return setConfig(authorID, config);
|
|
|
|
|
}
|
|
|
|
|
if(!playlist)
|
|
|
|
|
return response = { embed: { title: `❌ Playlist not found` } };
|
|
|
|
|
const { pathname: playlistPathname } = new URL(playlist.url);
|
|
|
|
|
let
|
|
|
|
|
rawPlaylistData = await (await fetch(playlist.url)).text(),
|
|
|
|
|
playlistItems;
|
|
|
|
|
if(/\.m3u8?$/i.test(playlistPathname)){
|
|
|
|
|
if(!rawPlaylistData.startsWith('#EXTM3U'))
|
|
|
|
|
rawPlaylistData = `#EXTM3U\n${rawPlaylistData}`;
|
|
|
|
|
playlistItems = M3uParser.parse(rawPlaylistData).medias.map(item => ({
|
|
|
|
|
url: item['location'],
|
|
|
|
|
title: item['name'] || new URL(item['location']).pathname.split('/').slice(-1)[0]
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
if(/\.xspf?$/i.test(playlistPathname)){
|
|
|
|
|
playlistItems = parseXspf(rawPlaylistData)['playlist']['tracks'].map(item => ({
|
|
|
|
|
url: item['location'],
|
|
|
|
|
title: item['title'] || new URL(item['location']).pathname.split('/').slice(-1)[0]
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
if(!item)
|
|
|
|
|
return response = {
|
|
|
|
|
output: outdent `
|
|
|
|
|
📀 ${playlist.name}
|
|
|
|
|
${playlistItems.map((item, index) => `${index + 1}. \`${item.title}\``).join('\n')}
|
|
|
|
|
`
|
|
|
|
|
};
|
|
|
|
|
const playlistItem = /^\d+$/.test(item)
|
|
|
|
|
? playlistItems[parseInt(item) - 1]
|
|
|
|
|
: playlistItems.find(({ title }) => title.toLowerCase().includes(item.toLowerCase()));
|
|
|
|
|
if(!playlistItem)
|
|
|
|
|
return response = { embed: { title: `❌ Item not found` } };
|
|
|
|
|
await adb(`am start -a android.intent.action.VIEW -d ${playlistItem.url} -t video/any is.xyz.mpv`);
|
|
|
|
|
return response = { embed: { title: `▶️ \`${playlistItem.title}\`` } };
|
|
|
|
|
});
|
|
|
|
|
program
|
|
|
|
|
.command('playlist.rm')
|
|
|
|
|
.alias('pl.rm')
|
|
|
|
|
.description('Remove playlist')
|
|
|
|
|
.argument('<name>', 'Playlist name')
|
|
|
|
|
.action(async name => {
|
|
|
|
|
const
|
|
|
|
|
config = await getConfig(authorID) || {},
|
|
|
|
|
{ playlists } = config,
|
|
|
|
|
playlistIndex = playlists?.findIndex(playlist => playlist.name === name);
|
|
|
|
|
if([undefined, -1].includes(playlistIndex))
|
|
|
|
|
return response = { embed: { title: `❌ No existing playlist by that name` } };
|
|
|
|
|
playlists.splice(playlistIndex, 1);
|
|
|
|
|
await setConfig(authorID, config);
|
|
|
|
|
response = { embed: { title: `✅ Playlist removed` } };
|
|
|
|
|
});
|
2024-01-27 19:38:33 +00:00
|
|
|
|
program
|
|
|
|
|
.command('kiwi')
|
|
|
|
|
.description('Launch Kiwi browser')
|
|
|
|
|
.argument('<url>', 'Website URL')
|
|
|
|
|
.action(url => adb(`am start -a android.intent.action.VIEW -d ${url} -t text/plain com.kiwibrowser.browser`));
|
2024-01-27 21:25:29 +00:00
|
|
|
|
program
|
|
|
|
|
.command('notifications')
|
|
|
|
|
.description('Show notification history')
|
|
|
|
|
.action(async () => {
|
|
|
|
|
const items = [];
|
|
|
|
|
let item;
|
|
|
|
|
for(let line of (await adb('dumpsys notification --noredact')).split('\n')){
|
|
|
|
|
line = line.trim();
|
|
|
|
|
if(line.startsWith('NotificationRecord('))
|
|
|
|
|
items.push(item = { app: line.slice(35).split(' ')[0] });
|
|
|
|
|
if(line.startsWith('android.title='))
|
|
|
|
|
item.title = line.slice(line.indexOf('(') + 1, -1);
|
|
|
|
|
if(line.startsWith('android.text='))
|
|
|
|
|
item.text = line.slice(line.indexOf('(') + 1, -1);
|
|
|
|
|
if(line.startsWith('mCreationTimeMs='))
|
|
|
|
|
item.date = parseInt(line.slice(16));
|
|
|
|
|
}
|
|
|
|
|
items.sort((a, b) => b.date - a.date);
|
|
|
|
|
response = {
|
|
|
|
|
output: new AsciiTable()
|
|
|
|
|
.setHeading('App', 'Time', 'Content')
|
|
|
|
|
.addRowMatrix(items
|
|
|
|
|
.sort((a, b) => b.date - a.date)
|
|
|
|
|
.map(item => [
|
|
|
|
|
item.app,
|
|
|
|
|
new Date(item.date).toLocaleTimeString(),
|
|
|
|
|
`${item.title}${item.title.length + item.text.length < 50 ? ` · ${item.text}` : ''}`
|
|
|
|
|
])
|
|
|
|
|
).toString(),
|
|
|
|
|
isRaw: true
|
|
|
|
|
};
|
|
|
|
|
});
|
2024-01-27 03:54:37 +00:00
|
|
|
|
try {
|
|
|
|
|
await program.parseAsync(
|
2024-01-28 02:20:29 +00:00
|
|
|
|
parse(content),
|
2024-01-27 03:54:37 +00:00
|
|
|
|
{ from: 'user' }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
catch(error){
|
2024-01-28 02:08:10 +00:00
|
|
|
|
if(!error.code?.startsWith?.('commander.'))
|
2024-01-27 03:54:37 +00:00
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
return response;
|
|
|
|
|
};
|
2022-07-12 15:01:25 +00:00
|
|
|
|
|
|
|
|
|
eris.on(
|
|
|
|
|
'error',
|
|
|
|
|
error => console.error(error)
|
|
|
|
|
);
|
|
|
|
|
|
2022-07-11 09:34:15 +00:00
|
|
|
|
eris.on(
|
|
|
|
|
'messageCreate',
|
2022-07-13 17:36:44 +00:00
|
|
|
|
async message => {
|
2024-01-27 03:54:37 +00:00
|
|
|
|
const
|
|
|
|
|
{
|
|
|
|
|
guildID,
|
|
|
|
|
author: { id: authorID },
|
|
|
|
|
channel: { id: channelID },
|
|
|
|
|
id: messageID,
|
2024-01-28 02:20:29 +00:00
|
|
|
|
content,
|
|
|
|
|
attachments
|
2024-01-27 03:54:37 +00:00
|
|
|
|
} = message,
|
|
|
|
|
reply = async (content, isCodeBlock) => {
|
|
|
|
|
const lines = content.split('\n');
|
|
|
|
|
let chunk = isCodeBlock ? '```\n' : '';
|
|
|
|
|
for(let lineIndex = 0; lineIndex < lines.length; lineIndex++){
|
|
|
|
|
const
|
|
|
|
|
line = lines[lineIndex] + '\n',
|
|
|
|
|
canAddLine = chunk.length + line.length + (isCodeBlock ? 3 : 0) <= 2000;
|
|
|
|
|
if(canAddLine)
|
|
|
|
|
chunk += line;
|
|
|
|
|
if(!canAddLine || lineIndex === lines.length - 1){
|
|
|
|
|
if(isCodeBlock)
|
|
|
|
|
chunk += '```';
|
|
|
|
|
await eris.createMessage(
|
|
|
|
|
channelID,
|
|
|
|
|
{
|
|
|
|
|
content: chunk,
|
|
|
|
|
messageReference: { messageID }
|
|
|
|
|
}
|
|
|
|
|
);
|
2024-01-28 02:08:10 +00:00
|
|
|
|
chunk = isCodeBlock ? `\`\`\`\n${line}` : line;
|
2024-01-27 03:54:37 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if(message.author.bot) return;
|
2022-07-11 09:34:15 +00:00
|
|
|
|
if(
|
|
|
|
|
guildID
|
|
|
|
|
||
|
|
|
|
|
!botWhitelist.includes(authorID)
|
2024-01-27 03:54:37 +00:00
|
|
|
|
) return message.addReaction('❌');
|
|
|
|
|
await message.addReaction('⏳');
|
|
|
|
|
try {
|
|
|
|
|
const {
|
|
|
|
|
output,
|
2024-01-28 02:20:29 +00:00
|
|
|
|
isRaw,
|
|
|
|
|
embed
|
|
|
|
|
} = await handleCommand({
|
|
|
|
|
content,
|
|
|
|
|
authorID,
|
|
|
|
|
attachments
|
|
|
|
|
}) || {};
|
2024-01-27 03:54:37 +00:00
|
|
|
|
if(output)
|
|
|
|
|
await reply(output, isRaw);
|
2024-01-28 02:20:29 +00:00
|
|
|
|
if(embed)
|
|
|
|
|
await eris.createMessage(
|
|
|
|
|
channelID,
|
|
|
|
|
{
|
|
|
|
|
embed,
|
|
|
|
|
messageReference: { messageID }
|
|
|
|
|
}
|
|
|
|
|
);
|
2024-01-27 03:54:37 +00:00
|
|
|
|
await message.addReaction('✅');
|
|
|
|
|
}
|
|
|
|
|
catch(error){
|
|
|
|
|
console.error(error);
|
|
|
|
|
await message.addReaction('❌');
|
2022-07-11 09:34:15 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
eris.connect(),
|
2024-01-27 22:51:29 +00:00
|
|
|
|
new Promise(resolve => eris.once('ready', resolve)),
|
|
|
|
|
new Promise(resolve => exec(`adb connect ${adbIp}:${adbPort}`, resolve))
|
2022-07-11 09:34:15 +00:00
|
|
|
|
]);
|
|
|
|
|
console.log('Ready');
|
|
|
|
|
})().catch(error => console.error(error));
|