sessionservice

This commit is contained in:
AF 2023-02-28 12:32:19 +00:00
parent 4aa5a679c1
commit 07bae21312
6 changed files with 160 additions and 364 deletions

View File

@ -6,7 +6,7 @@ setup(
packages=['v6d3music'], packages=['v6d3music'],
url='', url='',
license='', license='',
author='PARRRATE T&V', author='PARRRATE TNV',
author_email='', author_email='',
description='' description=''
) )

View File

@ -3,41 +3,41 @@ import time
import discord import discord
from typing_extensions import Self from typing_extensions import Self
from v6d2ctx.integration.responsetype import *
from v6d2ctx.integration.targets import *
from rainbowadn.instrument import Instrumentation from rainbowadn.instrument import Instrumentation
from v6d2ctx.context import * from v6d2ctx.context import *
from v6d2ctx.integration.responsetype import *
from v6d2ctx.integration.targets import *
from v6d3music.core.mainaudio import * from v6d3music.core.mainaudio import *
from v6d3music.core.mainservice import * from v6d3music.core.mainservice import *
__all__ = ('Api',) __all__ = ("Api",)
class Api: class Api:
class MisusedApi(Exception): class MisusedApi(Exception):
def json(self) -> dict: def json(self) -> dict:
return {'error': list(map(str, self.args)), 'errormessage': str(self)} return {"error": list(map(str, self.args)), "errormessage": str(self)}
class UnknownApi(MisusedApi): class UnknownApi(MisusedApi):
def json(self) -> dict: def json(self) -> dict:
return super().json() | {'unknownapi': None} return super().json() | {"unknownapi": None}
class ExplicitFailure(MisusedApi): class ExplicitFailure(MisusedApi):
def __init__(self, explicit: Explicit) -> None: def __init__(self, explicit: Explicit) -> None:
super().__init__(*explicit.args) super().__init__(*explicit.args)
def json(self) -> dict: def json(self) -> dict:
return super().json() | {'explicit': None} return super().json() | {"explicit": None}
def __init__(self, mainservice: MainService, roles: dict[str, str]) -> None: def __init__(self, mainservice: MainService, roles: dict[str, str]) -> None:
self.mainservice = mainservice self.mainservice = mainservice
self.client = mainservice.client self.client = mainservice.client
self.roles = roles self.roles = roles
self.targets = mainservice.targets self.targets = mainservice.targets
self.targets.register_instance(self, 'api', Async) self.targets.register_instance(self, "api", Async)
self.targets.register_instrumentation('Count', lambda t, n: Count(t, n)) self.targets.register_instrumentation("Count", lambda t, n: Count(t, n))
self.targets.register_instrumentation('Concurrency', lambda t, n: Concurrency(t, n), Async) self.targets.register_instrumentation("Concurrency", lambda t, n: Concurrency(t, n), Async)
def user_id(self) -> int | None: def user_id(self) -> int | None:
if self.client.user is None: if self.client.user is None:
@ -46,15 +46,15 @@ class Api:
return self.client.user.id return self.client.user.id
def is_operator(self, user_id: int) -> bool: def is_operator(self, user_id: int) -> bool:
return '(operator)' in self.roles.get(f'roles{user_id}', '') return "(operator)" in self.roles.get(f"roles{user_id}", "")
async def api(self, request: dict, user_id: int) -> ResponseType: async def api(self, request: dict, user_id: int) -> ResponseType:
response = await UserApi(ApiSession(self), request, user_id).api() response = await UserApi(ApiSession(self), request, user_id).api()
match response, request: match response, request:
case {'time': _}, _: case {"time": _}, _:
pass pass
case dict() as d, {'time': _}: case dict() as d, {"time": _}:
response = d | {'time': time.time()} response = d | {"time": time.time()}
return response return response
@ -65,7 +65,7 @@ class ApiSession:
def api(self): def api(self):
if self.__complexity <= 0: if self.__complexity <= 0:
raise Api.MisusedApi('hit complexity limit') raise Api.MisusedApi("hit complexity limit")
self.__complexity -= 1 self.__complexity -= 1
return self.__api return self.__api
@ -73,7 +73,7 @@ class ApiSession:
class UserApi: class UserApi:
class UnknownMember(Api.MisusedApi): class UnknownMember(Api.MisusedApi):
def json(self) -> dict: def json(self) -> dict:
return super().json() | {'unknownmember': None} return super().json() | {"unknownmember": None}
def __init__(self, session: ApiSession, request: dict, user_id: int) -> None: def __init__(self, session: ApiSession, request: dict, user_id: int) -> None:
self.session = session self.session = session
@ -86,21 +86,19 @@ class UserApi:
async def subs(self, requests: list[dict] | dict[str, dict]) -> ResponseType: async def subs(self, requests: list[dict] | dict[str, dict]) -> ResponseType:
match self.request: match self.request:
case {'idkey': str() as idkey}: case {"idkey": str() as idkey}:
pass pass
case _: case _:
idkey = 'type' idkey = "type"
match self.request: match self.request:
case {'idbase': dict() as base}: case {"idbase": dict() as base}:
pass pass
case _: case _:
base = {} base = {}
match requests: match requests:
case list(): case list():
return list( return list(
await asyncio.gather( await asyncio.gather(*(self.sub(request, key).api() for (key, request) in enumerate(requests)))
*(self.sub(request, key).api() for (key, request) in enumerate(requests))
)
) )
case dict(): case dict():
items = list(requests.items()) items = list(requests.items())
@ -109,7 +107,7 @@ class UserApi:
) )
return dict((key, response) for (key, _), response in zip(items, responses)) return dict((key, response) for (key, _), response in zip(items, responses))
case _: case _:
raise Api.MisusedApi('that should not happen') raise Api.MisusedApi("that should not happen")
def _sub(self, request: dict) -> Self: def _sub(self, request: dict) -> Self:
return UserApi(self.session, request, self.user_id) return UserApi(self.session, request, self.user_id)
@ -120,43 +118,43 @@ class UserApi:
sub._key = key sub._key = key
return sub return sub
async def to_guild_api(self, guild_id: int) -> 'GuildApi': async def to_guild_api(self, guild_id: int) -> "GuildApi":
guild = self.client.get_guild(guild_id) or await self.client.fetch_guild(guild_id) guild = self.client.get_guild(guild_id) or await self.client.fetch_guild(guild_id)
if guild is None: if guild is None:
raise UserApi.UnknownMember('unknown guild') raise UserApi.UnknownMember("unknown guild")
member = guild.get_member(self.user_id) or await guild.fetch_member(self.user_id) member = guild.get_member(self.user_id) or await guild.fetch_member(self.user_id)
if member is None: if member is None:
raise UserApi.UnknownMember('unknown member of a guild') raise UserApi.UnknownMember("unknown member of a guild")
return GuildApi(self, member) return GuildApi(self, member)
async def to_operator_api(self) -> 'OperatorApi': async def to_operator_api(self) -> "OperatorApi":
if not self.pi.is_operator(self.user_id): if not self.pi.is_operator(self.user_id):
raise UserApi.UnknownMember('not an operator') raise UserApi.UnknownMember("not an operator")
return OperatorApi(self) return OperatorApi(self)
def _api_text(self) -> str: def _api_text(self) -> str:
return 'user api' return "user api"
async def _fall_through_api(self) -> ResponseType: async def _fall_through_api(self) -> ResponseType:
match self.request: match self.request:
case {'type': '?'}: case {"type": "?"}:
return f'this is {self._api_text()}' return f"this is {self._api_text()}"
case {'type': '*', 'requests': list() | dict() as requests}: case {"type": "*", "requests": list() | dict() as requests}:
return await self.subs(requests) return await self.subs(requests)
case _: case _:
raise Api.UnknownApi(f'unknown {self._api_text()}') raise Api.UnknownApi(f"unknown {self._api_text()}")
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'guild': str() as guild_id_str} if guild_id_str.isdecimal() and len(guild_id_str) < 100: case {"guild": str() as guild_id_str} if guild_id_str.isdecimal() and len(guild_id_str) < 100:
self.request.pop('guild') self.request.pop("guild")
return await (await self.to_guild_api(int(guild_id_str))).api() return await (await self.to_guild_api(int(guild_id_str))).api()
case {'operator': _}: case {"operator": _}:
self.request.pop('operator') self.request.pop("operator")
return await (await self.to_operator_api()).api() return await (await self.to_operator_api()).api()
case {'type': 'ping', 't': (float() | int()) as t}: case {"type": "ping", "t": (float() | int()) as t}:
return time.time() - t return time.time() - t
case {'type': 'guilds'}: case {"type": "guilds"}:
guilds = [] guilds = []
for guild in self.client.guilds: for guild in self.client.guilds:
if guild.get_member(self.user_id) is not None: if guild.get_member(self.user_id) is not None:
@ -172,10 +170,10 @@ class UserApi:
except Explicit as e: except Explicit as e:
raise Api.ExplicitFailure(e) from e raise Api.ExplicitFailure(e) from e
except Api.MisusedApi as e: except Api.MisusedApi as e:
catches = self.request.get('catches', {}) catches = self.request.get("catches", {})
if len(e.args) and (key := e.args[0]) in catches: if len(e.args) and (key := e.args[0]) in catches:
return catches[key] return catches[key]
if '*' in catches: if "*" in catches:
return e.json() return e.json()
raise raise
@ -183,50 +181,48 @@ class UserApi:
class GuildApi(UserApi): class GuildApi(UserApi):
class VoiceNotConnected(Api.MisusedApi): class VoiceNotConnected(Api.MisusedApi):
def json(self) -> dict: def json(self) -> dict:
return super().json() | {'notconnected': None} return super().json() | {"notconnected": None}
def __init__(self, api: UserApi, member: discord.Member) -> None: def __init__(self, api: UserApi, member: discord.Member) -> None:
super().__init__(api.session, api.request, member.id) super().__init__(api.session, api.request, member.id)
self.member = member self.member = member
self.guild = member.guild self.guild = member.guild
async def to_voice_api(self) -> 'VoiceApi': async def to_voice_api(self) -> "VoiceApi":
voice = self.member.voice voice = self.member.voice
if voice is None: if voice is None:
raise GuildApi.VoiceNotConnected('you are not connected to voice') raise GuildApi.VoiceNotConnected("you are not connected to voice")
channel = voice.channel channel = voice.channel
if channel is None: if channel is None:
raise GuildApi.VoiceNotConnected('you are not connected to a voice channel') raise GuildApi.VoiceNotConnected("you are not connected to a voice channel")
if self.client.user is None: if self.client.user is None:
raise GuildApi.VoiceNotConnected('bot client user not initialised') raise GuildApi.VoiceNotConnected("bot client user not initialised")
if self.client.user.id not in channel.voice_states: if self.client.user.id not in channel.voice_states:
raise GuildApi.VoiceNotConnected('bot not connected') raise GuildApi.VoiceNotConnected("bot not connected")
return VoiceApi(self, channel) return VoiceApi(self, channel)
def _sub(self, request: dict) -> Self: def _sub(self, request: dict) -> Self:
return GuildApi(super()._sub(request), self.member) return GuildApi(super()._sub(request), self.member)
def _api_text(self) -> str: def _api_text(self) -> str:
return 'guild api' return "guild api"
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'voice': _}: case {"voice": _}:
self.request.pop('voice') self.request.pop("voice")
return await (await self.to_voice_api()).api() return await (await self.to_voice_api()).api()
case _: case _:
return await self._fall_through_api() return await self._fall_through_api()
class VoiceApi(GuildApi): class VoiceApi(GuildApi):
def __init__( def __init__(self, api: GuildApi, channel: discord.VoiceChannel | discord.StageChannel) -> None:
self, api: GuildApi, channel: discord.VoiceChannel | discord.StageChannel
) -> None:
super().__init__(api, api.member) super().__init__(api, api.member)
self.channel = channel self.channel = channel
self.mainservice = self.pi.mainservice self.mainservice = self.pi.mainservice
async def to_main_api(self) -> 'MainApi': async def to_main_api(self) -> "MainApi":
vc = await self.mainservice.raw_vc_for_member(self.member) vc = await self.mainservice.raw_vc_for_member(self.member)
main = await self.mainservice.mode(create=False, force_play=False).main_for_raw_vc(vc) main = await self.mainservice.mode(create=False, force_play=False).main_for_raw_vc(vc)
return MainApi(self, vc, main) return MainApi(self, vc, main)
@ -235,21 +231,19 @@ class VoiceApi(GuildApi):
return VoiceApi(super()._sub(request), self.channel) return VoiceApi(super()._sub(request), self.channel)
def _api_text(self) -> str: def _api_text(self) -> str:
return 'voice api' return "voice api"
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'main': _}: case {"main": _}:
self.request.pop('main') self.request.pop("main")
return await (await self.to_main_api()).api() return await (await self.to_main_api()).api()
case _: case _:
return await self._fall_through_api() return await self._fall_through_api()
class MainApi(VoiceApi): class MainApi(VoiceApi):
def __init__( def __init__(self, api: VoiceApi, vc: discord.VoiceClient, main: MainAudio) -> None:
self, api: VoiceApi, vc: discord.VoiceClient, main: MainAudio
) -> None:
super().__init__(api, api.channel) super().__init__(api, api.channel)
self.vc = vc self.vc = vc
self.main = main self.main = main
@ -258,20 +252,20 @@ class MainApi(VoiceApi):
return MainApi(super()._sub(request), self.vc, self.main) return MainApi(super()._sub(request), self.vc, self.main)
def _api_text(self) -> str: def _api_text(self) -> str:
return 'main api' return "main api"
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'type': 'volume'}: case {"type": "volume"}:
return self.main.volume return self.main.volume
case {'type': 'playing'}: case {"type": "playing"}:
return self.vc.is_playing() return self.vc.is_playing()
case {'type': 'paused'}: case {"type": "paused"}:
return self.vc.is_paused() return self.vc.is_paused()
case {'type': 'queueformat'}: case {"type": "queueformat"}:
return await self.main.queue.format() return await self.main.queue.format()
case {'type': 'queuejson'}: case {"type": "queuejson"}:
return await self.main.queue.pubjson(self.member, self.request.get('limit', 1000)) return await self.main.queue.pubjson(self.member, self.request.get("limit", 1000))
case _: case _:
return await self._fall_through_api() return await self._fall_through_api()
@ -287,30 +281,30 @@ class OperatorApi(UserApi):
return OperatorApi(super()._sub(request)) return OperatorApi(super()._sub(request))
def _api_text(self) -> str: def _api_text(self) -> str:
return 'operator api' return "operator api"
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'target': str() as targetname}: case {"target": str() as targetname}:
return await InstrumentationApi(self, targetname).api() return await InstrumentationApi(self, targetname).api()
case {'type': 'resetmonitoring'}: case {"type": "resetmonitoring"}:
return self.pi.mainservice.pmonitoring.reset() return self.pi.mainservice.pmonitoring.reset()
case {'type': 'guilds'}: case {"type": "guilds"}:
guilds = [] guilds = []
for guild in self.client.guilds: for guild in self.client.guilds:
if self._guild_visible(guild): if self._guild_visible(guild):
guilds.append( guilds.append(
{ {
'id': str(guild.id), "id": str(guild.id),
'member_count': guild.member_count, "member_count": guild.member_count,
'name': guild.name, "name": guild.name,
} }
) )
return guilds return guilds
case {'type': 'sleep', 'duration': (float() | int()) as duration, 'echo': _ as echo}: case {"type": "sleep", "duration": (float() | int()) as duration, "echo": _ as echo}:
await asyncio.sleep(duration) await asyncio.sleep(duration)
return echo return echo
case {'type': 'pool'}: case {"type": "pool"}:
return self.pi.mainservice.pool_json() return self.pi.mainservice.pool_json()
case _: case _:
return await self._fall_through_api() return await self._fall_through_api()
@ -319,7 +313,7 @@ class OperatorApi(UserApi):
class InstrumentationApi(OperatorApi): class InstrumentationApi(OperatorApi):
class UnknownTarget(Api.UnknownApi): class UnknownTarget(Api.UnknownApi):
def json(self) -> dict: def json(self) -> dict:
return super().json() | {'unknowntarget': None} return super().json() | {"unknowntarget": None}
def __init__(self, api: OperatorApi, targetname: str) -> None: def __init__(self, api: OperatorApi, targetname: str) -> None:
super().__init__(api) super().__init__(api)
@ -327,20 +321,18 @@ class InstrumentationApi(OperatorApi):
self.targetname = targetname self.targetname = targetname
target_tuple = self.targets.targets.get(targetname, None) target_tuple = self.targets.targets.get(targetname, None)
if target_tuple is None: if target_tuple is None:
raise InstrumentationApi.UnknownTarget('unknown target', targetname) raise InstrumentationApi.UnknownTarget("unknown target", targetname)
self.target, self.methodname = target_tuple.value self.target, self.methodname = target_tuple.value
def _sub(self, request: dict) -> Self: def _sub(self, request: dict) -> Self:
return InstrumentationApi(super()._sub(request), self.targetname) return InstrumentationApi(super()._sub(request), self.targetname)
def _api_text(self) -> str: def _api_text(self) -> str:
return 'instrumentation api' return "instrumentation api"
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case { case {"type": str() as instrumentationname} if (
'type': str() as instrumentationname
} if (
instrumentation_factory := self.targets.instrumentations.get(instrumentationname) instrumentation_factory := self.targets.instrumentations.get(instrumentationname)
) is not None: ) is not None:
try: try:
@ -355,7 +347,7 @@ class InstrumentationApi(OperatorApi):
) )
except KeyError as e: except KeyError as e:
raise InstrumentationApi.UnknownTarget( raise InstrumentationApi.UnknownTarget(
'binding failed', self.targetname, instrumentationname, str(e) "binding failed", self.targetname, instrumentationname, str(e)
) from e ) from e
if not isinstance(instrumentation, JsonLike): if not isinstance(instrumentation, JsonLike):
raise TypeError raise TypeError

View File

@ -1,244 +1,34 @@
import asyncio import asyncio
import functools
import urllib.parse
from contextlib import AsyncExitStack
from pathlib import Path
from typing import Any, Callable, Coroutine, Generic, Hashable, TypeVar
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
from ptvp35 import * from v6d0auth.appfactory import AppFactory
from v6d0auth.appfactory import * from v6d0auth.run_app import start_app
from v6d0auth.run_app import * from v6d1tokens.client import request_token
from v6d1tokens.client import * from v6d3music.api import Api
from v6d3music.api import * from v6d3music.core.set_config import set_config
from v6d3music.config import auth_redirect, myroot
from v6d3music.utils.bytes_hash import *
__all__ = ('AppContext',) __all__ = ("AppContext",)
T = TypeVar('T')
TKey = TypeVar('TKey', bound=Hashable)
class CachedEntry(Generic[T]):
def __init__(self, value: T, getter: Callable[[], Coroutine[Any, Any, T]]) -> None:
self.__value: T = value
self.__getter = getter
self.__task: asyncio.Future[T] = asyncio.Future()
self.__task.set_result(value)
async def _set(self) -> T:
self.__value = await self.__getter()
return self.__value
def get_nowait(self) -> T:
if self.__task.done():
self.__task = asyncio.create_task(self._set())
return self.__value
async def get(self) -> T:
if self.__task.done():
self.__task = asyncio.create_task(self._set())
return await self.__task
class CachedDictionary(Generic[TKey, T]):
def __init__(self, factory: Callable[[TKey], Coroutine[Any, Any, T]]) -> None:
self.__factory = factory
self.__entries: dict[TKey, CachedEntry[T]] = {}
def entry(self, key: TKey, default: T) -> CachedEntry[T]:
if key not in self.__entries:
self.__entries[key] = CachedEntry(default, functools.partial(self.__factory, key))
return self.__entries[key]
class MusicAppFactory(AppFactory): class MusicAppFactory(AppFactory):
def __init__( def __init__(self, secret: str, api: Api):
self,
secret: str,
api: Api,
db: DbConnection
):
self.secret = secret self.secret = secret
self.redirect = auth_redirect
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
self._api = api self._api = api
self.db = db
self._token_clients: CachedDictionary[str, dict | None] = CachedDictionary(
self._token_client
)
def auth_link(self) -> str:
client_id = self._api.user_id()
if client_id is None:
return ''
else:
return f'https://discord.com/api/oauth2/authorize?client_id={client_id}' \
f'&redirect_uri={urllib.parse.quote(self.redirect)}&response_type=code&scope=identify'
async def code_token(self, code: str) -> dict:
client_id = self._api.user_id()
assert client_id is not None
data = {
'client_id': str(client_id),
'client_secret': self.secret,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': self.redirect
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
async with aiohttp.ClientSession() as session:
async with session.post('https://discord.com/api/oauth2/token', data=data, headers=headers) as response:
return await response.json()
async def _token_client(self, access_token: str) -> dict | None:
headers = {
'Authorization': f'Bearer {access_token}'
}
async with aiohttp.ClientSession() as session:
async with session.get('https://discord.com/api/oauth2/@me', headers=headers) as response:
return await response.json()
async def token_client(self, access_token: str) -> dict | None:
return self._token_clients.entry(access_token, None).get_nowait()
async def session_client(self, data: dict) -> dict | None:
match data:
case {'token': {'access_token': str() as access_token}}:
pass
case _:
return None
return await self.token_client(access_token)
@classmethod
def client_status(cls, sclient: dict) -> dict:
user = cls.client_user(sclient)
return {
'expires': sclient.get('expires'),
'user': (None if user is None else cls.user_status(user)),
}
@classmethod
def user_status(cls, user: dict) -> dict:
return {
'avatar': cls.user_avatar_url(user),
'id': cls.user_id(user),
'username': cls.user_username_full(user)
}
@classmethod
def user_username_full(cls, user: dict) -> str | None:
match user:
case {'username': str() as username, 'discriminator': str() as discriminator}:
return f'{username}#{discriminator}'
case _:
return None
@classmethod
def client_user(cls, sclient: dict) -> dict | None:
return sclient.get('user')
@classmethod
def user_id(cls, user: dict) -> str | int | None:
return user.get('id')
@classmethod
def user_avatar(cls, user: dict) -> str | None:
return user.get('avatar')
@classmethod
def user_avatar_url(cls, user: dict) -> str | None:
cid = cls.user_id(user)
if cid is None:
return None
avatar = cls.user_avatar(user)
if avatar is None:
return None
return f'https://cdn.discordapp.com/avatars/{cid}/{avatar}.png'
async def session_status(self, session: str) -> dict:
data = self.session_data(session)
sclient = await self.session_client(data)
return {
'code_set': data.get('code') is not None,
'token_set': data.get('token') is not None,
'client': (None if sclient is None else self.client_status(sclient))
}
async def session_queue(self, session: str):
data = self.session_data(session)
sclient = await self.session_client(data)
if sclient is None:
return None
user = self.client_user(sclient)
if user is None:
return None
cid = self.user_id(user)
return cid
def session_data(self, session: str | None) -> dict:
if session is None:
return {}
data = self.db.get(session, {})
if not isinstance(data, dict):
return {}
return data
def define_routes(self, routes: web.RouteTableDef) -> None: def define_routes(self, routes: web.RouteTableDef) -> None:
@routes.get('/authlink/') @routes.post("/api/")
async def authlink(_request: web.Request) -> web.StreamResponse:
return web.Response(text=self.auth_link())
@routes.get('/auth/')
async def auth(request: web.Request) -> web.StreamResponse:
session = request.query.get('session')
state = request.query.get('state')
code = request.query.get('code')
match session, state, code:
case str() as session, str() as state, str() as code:
if bytes_hash(session.encode()) != state:
raise web.HTTPBadRequest
data = self.session_data(session)
data['code'] = code
data['token'] = await self.code_token(code)
await self.db.set(session, data)
return web.HTTPFound('/')
case _:
raise web.HTTPBadRequest
@routes.get('/state/')
async def get_state(request: web.Request) -> web.Response:
session = str(request.query.get('session'))
return web.json_response(
data=f'{bytes_hash(session.encode())}'
)
@routes.get('/status/')
async def status(request: web.Request) -> web.Response:
session = str(request.query.get('session'))
return web.json_response(
data=await self.session_status(session)
)
@routes.post('/api/')
async def api(request: web.Request) -> web.Response: async def api(request: web.Request) -> web.Response:
session = request.query.get('session') async with aiohttp.ClientSession() as s, s.get(
data = self.session_data(session) "http://sessionservice/userid/", params={"session": request.query.get("session")}
sclient = await self.session_client(data) ) as response:
if sclient is None: user_id: int | None = await response.json()
raise web.HTTPUnauthorized
user = self.client_user(sclient)
if user is None:
raise web.HTTPUnauthorized
user_id = self.user_id(user)
if user_id is None: if user_id is None:
raise web.HTTPUnauthorized raise web.HTTPUnauthorized
user_id = int(user_id) if not isinstance(user_id, int):
raise TypeError
jsr = await request.json() jsr = await request.json()
assert isinstance(jsr, dict) assert isinstance(jsr, dict)
try: try:
@ -246,12 +36,6 @@ class MusicAppFactory(AppFactory):
except Api.MisusedApi as e: except Api.MisusedApi as e:
return web.json_response(e.json(), status=404) return web.json_response(e.json(), status=404)
@routes.get('/whaturl/')
async def whaturl(request: web.Request) -> web.StreamResponse:
if request.headers.get('X-Forwarded-Proto') == 'https':
request = request.clone(scheme='https')
return web.json_response(str(request.url))
class AppContext: class AppContext:
def __init__(self, api: Api) -> None: def __init__(self, api: Api) -> None:
@ -259,36 +43,26 @@ class AppContext:
async def start(self) -> tuple[web.Application, asyncio.Task[None]] | None: async def start(self) -> tuple[web.Application, asyncio.Task[None]] | None:
try: try:
factory = MusicAppFactory( factory = MusicAppFactory(await request_token("music-client", "token"), self.api)
await request_token('music-client', 'token'), await set_config("secret", factory.secret)
self.api,
self.__db
)
except aiohttp.ClientConnectorError: except aiohttp.ClientConnectorError:
print('no web app (likely due to no token)') print("no web app (likely due to no token)")
else: else:
app = factory.app() app = factory.app()
task = asyncio.create_task(start_app(app)) task = asyncio.create_task(start_app(app))
return app, task return app, task
async def __aenter__(self) -> 'AppContext': async def __aenter__(self) -> "AppContext":
async with AsyncExitStack() as es: self.__task: asyncio.Task[tuple[web.Application, asyncio.Task[None]] | None] = asyncio.create_task(self.start())
self.__db = await es.enter_async_context(DbFactory(myroot / 'session.db', kvfactory=KVJson())) return self
self.__task: asyncio.Task[
tuple[web.Application, asyncio.Task[None]] | None
] = asyncio.create_task(self.start())
self.__es = es.pop_all()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
async with self.__es: if self.__task.done():
if self.__task.done(): result = await self.__task
result = await self.__task if result is not None:
if result is not None: app, task = result
app, task = result await task
await task await app.shutdown()
await app.shutdown() await app.cleanup()
await app.cleanup() else:
else: self.__task.cancel()
self.__task.cancel()
del self.__es

View File

@ -2,7 +2,6 @@ import os
from v6d0auth.config import root from v6d0auth.config import root
prefix = os.getenv('v6prefix', '?/') prefix = os.getenv("v6prefix", "?/")
auth_redirect = os.getenv('v6redirect', 'https://music.parrrate.ru/auth/') myroot = root / "v6d3music"
myroot = root / 'v6d3music'
myroot.mkdir(exist_ok=True) myroot.mkdir(exist_ok=True)

View File

@ -0,0 +1,24 @@
import asyncio
from typing import Any, Callable, Coroutine, TypeVar
import aiohttp
T = TypeVar("T")
async def repeat(repeated: Callable[[], Coroutine[Any, Any, T]]) -> T:
for _ in range(60):
try:
return await repeated()
except aiohttp.ClientConnectorError:
await asyncio.sleep(1)
raise RuntimeError("cannot reach sessionservice")
async def set_config(key: str, value: Any) -> None:
json = {"key": key, "value": value}
async def call() -> None:
async with aiohttp.ClientSession() as s, s.post("http://sessionservice/config/", json=json) as response:
if response.status != 200:
raise RuntimeError("config request failed")
await repeat(call)

View File

@ -6,13 +6,13 @@ import time
from traceback import print_exc from traceback import print_exc
import discord import discord
from v6d2ctx.integration.event import *
from v6d2ctx.integration.targets import *
from ptvp35 import * from ptvp35 import *
from rainbowadn.instrument import Instrumentation from rainbowadn.instrument import Instrumentation
from v6d1tokens.client import * from v6d1tokens.client import *
from v6d2ctx.handle_content import * from v6d2ctx.handle_content import *
from v6d2ctx.integration.event import *
from v6d2ctx.integration.targets import *
from v6d2ctx.pain import * from v6d2ctx.pain import *
from v6d2ctx.serve import * from v6d2ctx.serve import *
from v6d3music.api import * from v6d3music.api import *
@ -22,6 +22,7 @@ from v6d3music.config import prefix
from v6d3music.core.caching import * from v6d3music.core.caching import *
from v6d3music.core.default_effects import * from v6d3music.core.default_effects import *
from v6d3music.core.mainservice import * from v6d3music.core.mainservice import *
from v6d3music.core.set_config import set_config
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
@ -47,7 +48,7 @@ _client = MusicClient(
) )
banned_guilds = set(map(int, filter(bool, map(str.strip, os.getenv('banned_guilds', '').split(':'))))) banned_guilds = set(map(int, filter(bool, map(str.strip, os.getenv("banned_guilds", "").split(":")))))
def guild_allowed(guild: discord.Guild | None) -> bool: def guild_allowed(guild: discord.Guild | None) -> bool:
@ -71,31 +72,36 @@ def register_handlers(client: discord.Client, mainservice: MainService):
@client.event @client.event
async def on_ready(): async def on_ready():
print('ready') print("ready")
if client.user is None:
raise RuntimeError
await set_config("user-id", client.user.id)
await set_config("ready", True)
await client.change_presence( await client.change_presence(
activity=discord.Game( activity=discord.Game(
name='феноменально', name="феноменально",
) )
) )
await mainservice.restore() await mainservice.restore()
print("ready (startup finished)")
class UpgradeABMInit(Instrumentation): class UpgradeABMInit(Instrumentation):
def __init__(self): def __init__(self):
super().__init__(ABlockMonitor, '__init__') super().__init__(ABlockMonitor, "__init__")
def instrument(self, method, abm, *, threshold=0.0, delta=10.0, interval=0.0): def instrument(self, method, abm, *, threshold=0.0, delta=10.0, interval=0.0):
print('created upgraded') print("created upgraded")
method(abm, threshold=threshold, delta=delta, interval=interval) method(abm, threshold=threshold, delta=delta, interval=interval)
abm.threshold = threshold abm.threshold = threshold
class UpgradeABMTask(Instrumentation): class UpgradeABMTask(Instrumentation):
def __init__(self): def __init__(self):
super().__init__(ABlockMonitor, '_monitor') super().__init__(ABlockMonitor, "_monitor")
async def instrument(self, _, abm): async def instrument(self, _, abm):
print('started upgraded') print("started upgraded")
while True: while True:
delta = abm.delta delta = abm.delta
t = time.time() t = time.time()
@ -104,8 +110,7 @@ class UpgradeABMTask(Instrumentation):
delay = spent - delta delay = spent - delta
if delay > abm.threshold: if delay > abm.threshold:
abm.threshold = delay abm.threshold = delay
print( print(f"upgraded block monitor reached new peak delay {delay:.4f}")
f'upgraded block monitor reached new peak delay {delay:.4f}')
interval = abm.interval interval = abm.interval
if interval > 0: if interval > 0:
await asyncio.sleep(interval) await asyncio.sleep(interval)
@ -130,45 +135,47 @@ class PathPrint(Instrumentation):
print(self.pref, db._DbConnection__path) # type: ignore print(self.pref, db._DbConnection__path) # type: ignore
except Exception: except Exception:
from traceback import print_exc from traceback import print_exc
print_exc() print_exc()
return result return result
def _db_ee() -> contextlib.ExitStack: def _db_ee() -> contextlib.ExitStack:
with contextlib.ExitStack() as es: with contextlib.ExitStack() as es:
es.enter_context(PathPrint('_initialize', 'open :')) es.enter_context(PathPrint("_initialize", "open :"))
es.enter_context(PathPrint('aclose', 'close:')) es.enter_context(PathPrint("aclose", "close:"))
return es.pop_all() return es.pop_all()
raise RuntimeError raise RuntimeError
async def amain(client: discord.Client): async def amain(client: discord.Client):
roles = {key: value for key, value in os.environ.items() if key.startswith('roles')} roles = {key: value for key, value in os.environ.items() if key.startswith("roles")}
async with ( async with (
client, client,
DefaultEffects() as defaulteffects, DefaultEffects() as defaulteffects,
MainService(Targets(), defaulteffects, client, Events()) as mainservice, MainService(Targets(), defaulteffects, client, Events()) as mainservice,
AppContext(Api(mainservice, roles)), AppContext(Api(mainservice, roles)),
ABlockMonitor(delta=0.5) ABlockMonitor(delta=0.5),
): ):
register_handlers(client, mainservice) register_handlers(client, mainservice)
if 'guerilla' in sys.argv: if "guerilla" in sys.argv:
from pathlib import Path from pathlib import Path
tokenpath = Path('.token.txt')
tokenpath = Path(".token.txt")
if tokenpath.exists(): if tokenpath.exists():
token = tokenpath.read_text() token = tokenpath.read_text()
else: else:
token = input('token:') token = input("token:")
tokenpath.write_text(token) tokenpath.write_text(token)
elif (token_ := os.getenv('trial_token')): elif token_ := os.getenv("trial_token"):
token = token_ token = token_
else: else:
token = await request_token('music', 'token') token = await request_token("music", "token")
await client.login(token) await client.login(token)
if os.getenv('v6tor', None) is None: if os.getenv("v6tor", None) is None:
print('no tor') print("no tor")
await client.connect() await client.connect()
print('exited') print("exited")
def main() -> None: def main() -> None: