Skip to content

Commit 9030478

Browse files
committed
voice recv things
1 parent b414b5f commit 9030478

File tree

8 files changed

+555
-197
lines changed

8 files changed

+555
-197
lines changed

discord/sinks/core.py

Lines changed: 302 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,32 @@
2525

2626
from __future__ import annotations
2727

28+
import asyncio
29+
from collections.abc import Callable, Coroutine
30+
from functools import partial
2831
import io
2932
import os
3033
import struct
3134
import sys
3235
import threading
3336
import time
34-
from typing import TYPE_CHECKING
37+
from typing import TYPE_CHECKING, Any, TypeVar, overload
3538

36-
from ..types import snowflake
39+
from discord.utils import MISSING
40+
41+
from .enums import FilteringMode
3742
from .errors import SinkException
3843

3944
if TYPE_CHECKING:
45+
from typing_extensions import ParamSpec
46+
47+
from discord import abc
48+
from discord.types import snowflake
4049
from ..voice.client import VoiceClient
4150

51+
R = TypeVar('R')
52+
P = ParamSpec('P')
53+
4254
__all__ = (
4355
"Filters",
4456
"Sink",
@@ -60,6 +72,90 @@
6072
}
6173

6274

75+
class Filter:
76+
"""Represents a filter for a :class:`~.Sink`.
77+
78+
This has to be inherited in order to provide a filter to a sink.
79+
80+
.. versionadded:: 2.7
81+
"""
82+
83+
@overload
84+
async def filter(self, sink: Sink, user: abc.Snowflake, ssrc: int, packet: RawData) -> bool: ...
85+
86+
@overload
87+
def filter(self, sink: Sink, user: abc.Snowflake, ssrc: int, packet: RawData) -> bool: ...
88+
89+
def filter(self, sink: Sink, user: abc.Snowflake, ssrc: int, packet: RawData) -> bool | Coroutine[Any, Any, bool]:
90+
"""|maybecoro|
91+
92+
Represents the filter callback.
93+
94+
This is called automatically everytime a voice packet is received to check whether it should be stored in
95+
``sink``.
96+
97+
Parameters
98+
----------
99+
sink: :class:`~.Sink`
100+
The sink the packet was received from, if the filter check goes through.
101+
user: :class:`~discord.abc.Snowflake`
102+
The user that the packet was received from.
103+
ssrc: :class:`int`
104+
The user's ssrc.
105+
packet: :class:`~.RawData`
106+
The raw data packet.
107+
108+
Returns
109+
-------
110+
:class:`bool`
111+
Whether the filter was successful.
112+
"""
113+
raise NotImplementedError('subclasses must implement this')
114+
115+
def cleanup(self) -> None:
116+
"""A function called when the filter is ready for cleanup."""
117+
pass
118+
119+
120+
class Handler:
121+
"""Represents a handler for a :class:`~.Sink`.
122+
123+
This has to be inherited in order to provide a handler to a sink.
124+
125+
.. versionadded:: 2.7
126+
"""
127+
128+
@overload
129+
async def handle(self, sink: Sink, user: abc.Snowflake, ssrc: int, packet: RawData) -> Any: ...
130+
131+
@overload
132+
def handle(self, sink: Sink, user: abc.Snowflake, ssrc: int, packet: RawData) -> Any: ...
133+
134+
def handle(self, sink: Sink, user: abc.Snowflake, ssrc: int, packet: RawData) -> Any | Coroutine[Any, Any, Any]:
135+
"""|maybecoro|
136+
137+
Represents the handler callback.
138+
139+
This is called automatically everytime a voice packet which has successfully passed the filters is received.
140+
141+
Parameters
142+
----------
143+
sink: :class:`~.Sink`
144+
The sink the packet was received from, if the filter check goes through.
145+
user: :class:`~discord.abc.Snowflake`
146+
The user that the packet is from.
147+
ssrc: :class:`int`
148+
The user's ssrc.
149+
packet: :class:`~.RawData`
150+
The raw data packet.
151+
"""
152+
raise NotImplementedError('subclasses must implement this')
153+
154+
def cleanup(self) -> None:
155+
"""A function called when the handler is ready for cleanup."""
156+
pass
157+
158+
63159
class Filters:
64160
"""Filters for :class:`~.Sink`
65161
@@ -249,3 +345,207 @@ def get_all_audio(self):
249345
def get_user_audio(self, user: snowflake.Snowflake):
250346
"""Gets the audio file(s) of one specific user."""
251347
return os.path.realpath(self.audio_data.pop(user))
348+
349+
350+
class Sink:
351+
"""Represents a sink for voice recording.
352+
353+
This is used as a way of "storing" the recordings.
354+
355+
This class is abstracted, and must be subclassed in order to apply functionalities to
356+
it.
357+
358+
Parameters
359+
----------
360+
filters: List[:class:`~.Filter`]
361+
The filters to apply to this sink recorder.
362+
filtering_mode: :class:`~.FilteringMode`
363+
How the filters should work. If set to :attr:`~.FilteringMode.all`, all filters must go through
364+
in order for an audio packet to be stored in this sink, else if it is set to :attr:`~.FilteringMode.any`,
365+
only one filter is required to return ``True`` in order for an audio packet to be stored in this sink.
366+
handlers: List[:class:`~.Handler`]
367+
The sink handlers. Handlers are objects that are called after filtering, and that can be used to, for example
368+
store a certain packet data in a file, or local mapping.
369+
"""
370+
371+
__listeners__: dict[str, list[Callable[..., Any]]] = {}
372+
373+
def __init_subclass__(cls) -> None:
374+
listeners: dict[str, list[Callable[..., Any]]] = {}
375+
376+
for base in reversed(cls.__mro__):
377+
for elem, value in base.__dict__.items():
378+
if elem in listeners:
379+
del listeners[elem]
380+
381+
if isinstance(value, staticmethod):
382+
value = value.__func__
383+
elif isinstance(value, classmethod):
384+
value = partial(value.__func__, cls)
385+
386+
if not hasattr(value, '__listener__'):
387+
continue
388+
389+
event_name = getattr(value, '__listener_name__', elem).removeprefix('on_')
390+
391+
try:
392+
listeners[event_name].append(value)
393+
except KeyError:
394+
listeners[event_name] = [value]
395+
396+
cls.__listeners__ = listeners
397+
398+
def __init__(
399+
self,
400+
*,
401+
filters: list[Filter] = MISSING,
402+
filtering_mode: FilteringMode = FilteringMode.all,
403+
handlers: list[Handler] = MISSING,
404+
) -> None:
405+
self.filtering_mode: FilteringMode = filtering_mode
406+
self._filters: list[Filter] = filters or []
407+
self._handlers: list[Handler] = handlers or []
408+
self.__dispatch_set: set[asyncio.Task[Any]] = set()
409+
410+
def dispatch(self, event: str, *args: Any, **kwargs: Any) -> Any:
411+
event = event.removeprefix('on_')
412+
413+
listeners = self.__listeners__.get(event, [])
414+
415+
for listener in listeners:
416+
task = asyncio.create_task(
417+
listener(*args, **kwargs),
418+
name=f'dispatch-{event}:{id(listener):#x}',
419+
)
420+
self.__dispatch_set.add(task)
421+
task.add_done_callback(self.__dispatch_set.remove)
422+
423+
def cleanup(self) -> None:
424+
"""Cleans all the data in this sink.
425+
426+
This should be called when you won't be performing any more operations in this sink.
427+
"""
428+
429+
for task in list(self.__dispatch_set):
430+
if task.done():
431+
continue
432+
task.set_result(None)
433+
434+
for filter in self._filters:
435+
filter.cleanup()
436+
437+
for handler in self._handlers:
438+
handler.cleanup()
439+
440+
def __del__(self) -> None:
441+
self.cleanup()
442+
443+
def add_filter(self, filter: Filter, /) -> None:
444+
"""Adds a filter to this sink.
445+
446+
Parameters
447+
----------
448+
filter: :class:`~.Filter`
449+
The filter to add.
450+
451+
Raises
452+
------
453+
TypeError
454+
You did not provide a Filter object.
455+
"""
456+
457+
if not isinstance(filter, Filter):
458+
raise TypeError(f'expected a Filter object, not {filter.__class__.__name__}')
459+
self._filters.append(filter)
460+
461+
def remove_filter(self, filter: Filter, /) -> None:
462+
"""Removes a filter from this sink.
463+
464+
Parameters
465+
----------
466+
filter: :class:`~.Filter`
467+
The filter to remove.
468+
"""
469+
470+
try:
471+
self._filters.remove(filter)
472+
except ValueError:
473+
pass
474+
475+
def add_handler(self, handler: Handler, /) -> None:
476+
"""Adds a handler to this sink.
477+
478+
Parameters
479+
----------
480+
handler: :class:`~.Handler`
481+
The handler to add.
482+
483+
Raises
484+
------
485+
TypeError
486+
You did not provide a Handler object.
487+
"""
488+
489+
if not isinstance(handler, Handler):
490+
raise TypeError(f'expected a Handler object, not {handler.__class__.__name__}')
491+
self._handlers.append(handler)
492+
493+
def remove_handler(self, handler: Handler, /) -> None:
494+
"""Removes a handler from this sink.
495+
496+
Parameters
497+
----------
498+
handler: :class:`~.Handler`
499+
The handler to remove.
500+
"""
501+
502+
try:
503+
self._handlers.remove(handler)
504+
except ValueError:
505+
pass
506+
507+
@staticmethod
508+
def listener(event: str = MISSING) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]]:
509+
"""Registers a function to be an event listener for this sink.
510+
511+
The events must be a :ref:`coroutine <coroutine>`, if not, :exc:`TypeError` is raised; and
512+
also must be inside a sink class.
513+
514+
Example
515+
-------
516+
517+
.. code-block:: python3
518+
519+
class MySink(Sink):
520+
@Sink.listener()
521+
async def on_member_speaking_state_update(member, ssrc, state):
522+
pass
523+
524+
Parameters
525+
----------
526+
event: :class:`str`
527+
The event name to listen to. If not provided, defaults to the function name.
528+
529+
Raises
530+
------
531+
TypeError
532+
The coroutine passed is not actually a coroutine, or the listener is not in a sink class.
533+
"""
534+
535+
def decorator(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]:
536+
parts = func.__qualname__.split('.')
537+
538+
if not parts or not len(parts) > 1:
539+
raise TypeError('event listeners must be declared in a Sink class')
540+
541+
if parts[-1] != func.__name__:
542+
raise NameError('qualified name and function name mismatch, this should not happen')
543+
544+
if not asyncio.iscoroutinefunction(func):
545+
raise TypeError('event listeners must be coroutine functions')
546+
547+
func.__listener__ = True
548+
if event is not MISSING:
549+
func.__listener_name__ = event
550+
return func
551+
return decorator

discord/sinks/enums.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-2021 Rapptz
5+
Copyright (c) 2021-present Pycord Development
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a
8+
copy of this software and associated documentation files (the "Software"),
9+
to deal in the Software without restriction, including without limitation
10+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
11+
and/or sell copies of the Software, and to permit persons to whom the
12+
Software is furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23+
DEALINGS IN THE SOFTWARE.
24+
"""
25+
from __future__ import annotations
26+
27+
from discord.enums import Enum
28+
29+
30+
class FilteringMode(Enum):
31+
all = 0
32+
any = 1

0 commit comments

Comments
 (0)