v6d3music/v6d3music/run-bot.py
2022-06-19 20:41:11 +03:00

384 lines
11 KiB
Python

import asyncio
import os
import shlex
import subprocess
import time
import discord
from v6d1tokens.client import request_token
from v6d2ctx.context import Benchmark, Context, Explicit, at, monitor
from v6d2ctx.handle_content import handle_content
from v6d2ctx.lock_for import lock_for
from v6d2ctx.serve import serve
from v6d3music.app import MusicAppFactory, session_db
from v6d3music.cache_url import cache_db
from v6d3music.config import prefix
from v6d3music.mainaudio import MainAudio, volume_db
from v6d3music.queueaudio import QueueAudio, queue_db
from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.catch import catch
from v6d3music.utils.effects_for_preset import effects_for_preset
from v6d3music.utils.options_for_effects import options_for_effects
from v6d3music.utils.presets import allowed_presets
from v6d3music.yt_audios import yt_audios
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
token = loop.run_until_complete(request_token('music', 'token'))
client = discord.Client(
intents=discord.Intents(
members=True,
guilds=True,
bans=True,
emojis=True,
invites=True,
voice_states=True,
guild_messages=True,
reactions=True
),
)
vcs_restored = False
async def restore_vcs():
global vcs_restored
vcs: list[tuple[int, int, bool]] = queue_db.get('vcs', [])
try:
for vcgid, vccid, vc_is_paused in vcs:
try:
guild: discord.Guild = await client.fetch_guild(vcgid)
async with lock_for(guild, 'not in a guild'):
channels = await guild.fetch_channels()
channel: discord.VoiceChannel
channel, = [ch for ch in channels if ch.id == vccid]
vp: discord.VoiceProtocol = await channel.connect()
assert isinstance(vp, discord.VoiceClient)
vc = vp
await main_for_raw_vc(vc, create=True)
if vc_is_paused:
vc.pause()
except Exception as e:
print(f'vc {vcgid} {vccid} {vc_is_paused} failed', e)
else:
print(f'vc restored {vcgid} {vccid}')
finally:
vcs_restored = True
@client.event
async def on_ready():
print('ready')
await client.change_presence(
activity=discord.Game(
name='феноменально',
)
)
async with lock_for('vcs_restored', '...'):
if not vcs_restored:
await restore_vcs()
@at('commands', 'help')
async def help_(ctx: Context, args: list[str]) -> None:
match args:
case []:
await ctx.reply('music bot')
case [name]:
await ctx.reply(f'help for {name}: `{name} help`')
mainasrcs: dict[discord.Guild, MainAudio] = {}
@at('commands', '/')
@at('commands', 'play')
async def play(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args,
f'''
`play ...args`
`play url [- effects] ...args`
`play url [+ preset] ...args`
presets: {shlex.join(allowed_presets)}
''',
(), 'help'
)
async with lock_for(ctx.guild, 'not in a guild'):
queue = await queue_for(ctx, create=True)
async for audio in yt_audios(ctx, args):
queue.append(audio)
await ctx.reply('done')
async def raw_vc_for(ctx: Context) -> discord.VoiceClient:
if ctx.guild is None:
raise Explicit('not in a guild')
vc: discord.VoiceProtocol = ctx.guild.voice_client
if vc is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
vs: discord.VoiceState = ctx.member.voice
if vs is None:
raise Explicit('not connected')
vch: discord.VoiceChannel = vs.channel
if vch is None:
raise Explicit('not connected')
try:
vc: discord.VoiceProtocol = await vch.connect()
except discord.ClientException:
await ctx.guild.fetch_channels()
raise Explicit('try again later')
assert isinstance(vc, discord.VoiceClient)
return vc
async def main_for_raw_vc(vc: discord.VoiceClient, *, create: bool) -> MainAudio:
if vc.guild in mainasrcs:
source = mainasrcs[vc.guild]
else:
if create:
source = mainasrcs.setdefault(
vc.guild,
await MainAudio.create(vc.guild)
)
else:
raise Explicit('not playing')
if vc.source != source or create and not vc.is_playing():
vc.play(source)
return source
async def vc_main_for(ctx: Context, *, create: bool) -> tuple[discord.VoiceClient, MainAudio]:
vc = await raw_vc_for(ctx)
return vc, await main_for_raw_vc(vc, create=create)
async def vc_for(ctx: Context, *, create: bool) -> discord.VoiceClient:
vc, source = await vc_main_for(ctx, create=create)
return vc
async def main_for(ctx: Context, *, create: bool) -> MainAudio:
vc, source = await vc_main_for(ctx, create=create)
return source
async def queue_for(ctx: Context, *, create: bool) -> QueueAudio:
return (await main_for(ctx, create=create)).queue
@at('commands', 'skip')
async def skip(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`skip [first] [last]`
''', 'help'
)
match args:
case []:
queue = await queue_for(ctx, create=False)
queue.skip_at(0, ctx.member)
case [pos] if pos.isdecimal():
pos = int(pos)
queue = await queue_for(ctx, create=False)
queue.skip_at(pos, ctx.member)
case [pos0, pos1] if pos0.isdecimal() and pos1.isdecimal():
pos0, pos1 = int(pos0), int(pos1)
queue = await queue_for(ctx, create=False)
for i in range(pos0, pos1 + 1):
if not queue.skip_at(pos0, ctx.member):
pos0 += 1
case _:
raise Explicit('misformatted')
await ctx.reply('done')
@at('commands', 'to')
async def skip_to(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`to [[h]] [m] s`
''', 'help'
)
match args:
case [h, m, s] if h.isdecimal() and m.isdecimal() and s.isdecimal():
seconds = 3600 * int(h) + 60 * int(m) + int(s)
case [m, s] if m.isdecimal() and s.isdecimal():
seconds = 60 * int(m) + int(s)
case [s] if s.isdecimal():
seconds = int(s)
case _:
raise Explicit('misformatted')
queue = await queue_for(ctx, create=False)
queue.queue[0].set_seconds(seconds)
@at('commands', 'effects')
async def effects_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`effects - effects`
`effects + preset`
''', 'help'
)
match args:
case ['-', effects]:
pass
case ['+', preset]:
effects = effects_for_preset(preset)
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await queue_for(ctx, create=False)
yta = queue.queue[0]
seconds = yta.source_seconds()
yta.options = options_for_effects(effects)
yta.set_seconds(seconds)
@at('commands', 'queue')
async def queue_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`queue`
`queue clear`
`queue resume`
`queue pause`
''', 'help'
)
match args:
case []:
await ctx.long((await (await queue_for(ctx, create=False)).format()).strip() or 'no queue')
case ['clear']:
(await queue_for(ctx, create=False)).clear(ctx.member)
await ctx.reply('done')
case ['resume']:
async with lock_for(ctx.guild, 'not in a guild'):
await queue_for(ctx, create=True)
await ctx.reply('done')
case ['pause']:
async with lock_for(ctx.guild, 'not in a guild'):
vc = await vc_for(ctx, create=True)
vc.pause()
await ctx.reply('done')
case _:
raise Explicit('misformatted')
@at('commands', 'swap')
async def swap(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`swap a b`
''', 'help'
)
match args:
case [a, b] if a.isdecimal() and b.isdecimal():
a, b = int(a), int(b)
(await queue_for(ctx, create=False)).swap(ctx.member, a, b)
case _:
raise Explicit('misformatted')
@at('commands', 'move')
async def move(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`move a b`
''', 'help'
)
match args:
case [a, b] if a.isdecimal() and b.isdecimal():
a, b = int(a), int(b)
(await queue_for(ctx, create=False)).move(ctx.member, a, b)
case _:
raise Explicit('misformatted')
@at('commands', 'volume')
async def volume_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`volume volume`
''', 'help'
)
match args:
case [volume]:
volume = float(volume)
await (await main_for(ctx, create=False)).set(volume, ctx.member)
case _:
raise Explicit('misformatted')
@at('commands', 'pause')
async def pause(ctx: Context, _args: list[str]) -> None:
vc = await vc_for(ctx, create=False)
vc.pause()
@at('commands', 'resume')
async def resume(ctx: Context, _args: list[str]) -> None:
vc = await vc_for(ctx, create=False)
vc.resume()
@client.event
async def on_message(message: discord.Message) -> None:
await handle_content(message, message.content, prefix)
async def save_queues():
for mainasrc in list(mainasrcs.values()):
await asyncio.sleep(0.01)
await mainasrc.queue.save()
async def save_vcs():
if vcs_restored:
vcs = []
vc: discord.VoiceClient
for vc in list(client.voice_clients):
await asyncio.sleep(0.01)
if vc.is_playing():
if vc.guild is not None and vc.channel is not None:
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused()))
queue_db.set_nowait('vcs', vcs)
async def save_commit():
await queue_db.set('commit', time.time())
async def save_job():
while True:
await asyncio.sleep(1)
with Benchmark('SVQ'):
await save_queues()
with Benchmark('SVV'):
await save_vcs()
with Benchmark('SVC'):
await save_commit()
async def start_app():
await MusicAppFactory.start(client)
async def setup_tasks():
loop.create_task(save_job())
loop.create_task(start_app())
async def main():
async with volume_db, queue_db, cache_db, session_db:
await client.login(token)
loop.create_task(setup_tasks())
if os.getenv('v6monitor'):
loop.create_task(monitor())
subprocess.Popen('tor')
await client.connect()
if __name__ == '__main__':
serve(main(), client, loop)