1 changed files with 315 additions and 0 deletions
@ -0,0 +1,315 @@ |
|||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-present Rapptz |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a |
||||
|
copy of this software and associated documentation files (the "Software"), |
||||
|
to deal in the Software without restriction, including without limitation |
||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
|
and/or sell copies of the Software, and to permit persons to whom the |
||||
|
Software is furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
|
DEALINGS IN THE SOFTWARE. |
||||
|
""" |
||||
|
|
||||
|
import datetime |
||||
|
from typing import List, Optional, TYPE_CHECKING |
||||
|
|
||||
|
from . import utils |
||||
|
from .enums import VoiceRegion, try_enum |
||||
|
from .errors import ClientException |
||||
|
from .utils import MISSING |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from .abc import PrivateChannel |
||||
|
from .channel import DMChannel, GroupChannel |
||||
|
from .member import VoiceState |
||||
|
from .message import Message |
||||
|
from .state import ConnectionState |
||||
|
from .types.snowflake import Snowflake, SnowflakeList |
||||
|
from .types.voice import GuildVoiceState |
||||
|
from .user import User |
||||
|
from .voice_client import VoiceProtocol |
||||
|
|
||||
|
|
||||
|
def _running_only(func): |
||||
|
def decorator(self, *args, **kwargs): |
||||
|
if self._ended: |
||||
|
raise ClientException('Call is over') |
||||
|
else: |
||||
|
return func(self, *args, **kwargs) |
||||
|
return decorator |
||||
|
|
||||
|
|
||||
|
class CallMessage: |
||||
|
"""Represents a group call message from Discord. |
||||
|
|
||||
|
This is only received in cases where the message type is equivalent to |
||||
|
:attr:`MessageType.call`. |
||||
|
|
||||
|
Attributes |
||||
|
----------- |
||||
|
ended_timestamp: Optional[:class:`datetime.datetime`] |
||||
|
A naive UTC datetime object that represents the time that the call has ended. |
||||
|
participants: List[:class:`User`] |
||||
|
A list of users that participated in the call. |
||||
|
message: :class:`Message` |
||||
|
The message associated with this call message. |
||||
|
""" |
||||
|
|
||||
|
def __init__( |
||||
|
self, message: Message, *, participants: List[User], ended_timestamp: str |
||||
|
) -> None: |
||||
|
self.message = message |
||||
|
self.ended_timestamp = utils.parse_time(ended_timestamp) |
||||
|
self.participants = participants |
||||
|
|
||||
|
@property |
||||
|
def call_ended(self) -> bool: |
||||
|
""":class:`bool`: Indicates if the call has ended.""" |
||||
|
return self.ended_timestamp is not None |
||||
|
|
||||
|
@property |
||||
|
def initiator(self) -> User: |
||||
|
""":class:`User`: Returns the user that started the call.""" |
||||
|
return self.message.author |
||||
|
|
||||
|
@property |
||||
|
def channel(self) -> PrivateChannel: |
||||
|
r""":class:`PrivateChannel`\: The private channel associated with this message.""" |
||||
|
return self.message.channel |
||||
|
|
||||
|
@property |
||||
|
def duration(self) -> datetime.timedelta: |
||||
|
"""Queries the duration of the call. |
||||
|
|
||||
|
If the call has not ended then the current duration will |
||||
|
be returned. |
||||
|
|
||||
|
Returns |
||||
|
--------- |
||||
|
:class:`datetime.timedelta` |
||||
|
The timedelta object representing the duration. |
||||
|
""" |
||||
|
if self.ended_timestamp is None: |
||||
|
return datetime.datetime.utcnow() - self.message.created_at |
||||
|
else: |
||||
|
return self.ended_timestamp - self.message.created_at |
||||
|
|
||||
|
|
||||
|
class PrivateCall: |
||||
|
"""Represents the actual group call from Discord. |
||||
|
|
||||
|
This is accompanied with a :class:`CallMessage` denoting the information. |
||||
|
|
||||
|
Attributes |
||||
|
----------- |
||||
|
channel: :class:`DMChannel` |
||||
|
The channel the call is in. |
||||
|
message: Optional[:class:`Message`] |
||||
|
The message associated with this call (if available). |
||||
|
unavailable: :class:`bool` |
||||
|
Denotes if this call is unavailable. |
||||
|
ringing: List[:class:`User`] |
||||
|
A list of users that are currently being rung to join the call. |
||||
|
region: :class:`VoiceRegion` |
||||
|
The region the call is being hosted at. |
||||
|
""" |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
channel: DMChannel |
||||
|
ringing: List[User] |
||||
|
region: VoiceRegion |
||||
|
|
||||
|
def __init__( |
||||
|
self, |
||||
|
state: ConnectionState, |
||||
|
*, |
||||
|
message_id: Snowflake, |
||||
|
channel_id: Snowflake, |
||||
|
message: Message = None, |
||||
|
channel: PrivateChannel, |
||||
|
unavailable: bool, |
||||
|
voice_states: List[GuildVoiceState] = [], |
||||
|
**kwargs, |
||||
|
) -> None: |
||||
|
self._state = state |
||||
|
self._message_id: int = int(message_id) |
||||
|
self._channel_id: int = int(channel_id) |
||||
|
self.message: Optional[Message] = message |
||||
|
self.channel = channel # type: ignore |
||||
|
self.unavailable: bool = unavailable |
||||
|
self._ended: bool = False |
||||
|
|
||||
|
for vs in voice_states: |
||||
|
state._update_voice_state(vs) |
||||
|
|
||||
|
self._update(**kwargs) |
||||
|
|
||||
|
def _deleteup(self) -> None: |
||||
|
self.ringing = [] |
||||
|
self._ended = True |
||||
|
|
||||
|
def _update( |
||||
|
self, *, ringing: SnowflakeList = {}, region: VoiceRegion = MISSING |
||||
|
) -> None: |
||||
|
if region is not MISSING: |
||||
|
self.region = try_enum(VoiceRegion, region) |
||||
|
channel = self.channel |
||||
|
recipients = {channel.me, channel.recipient} |
||||
|
lookup = {u.id: u for u in recipients} |
||||
|
self.ringing = list(filter(None, map(lookup.get, ringing))) |
||||
|
|
||||
|
@property |
||||
|
def initiator(self) -> Optional[User]: |
||||
|
"""Optional[:class:`User`]: Returns the user that started the call. The call message must be available to obtain this information.""" |
||||
|
if self.message: |
||||
|
return self.message.author |
||||
|
|
||||
|
@property |
||||
|
def connected(self) -> bool: |
||||
|
""":class:`bool`: Returns whether you're in the call (this does not mean you're in the call through the lib).""" |
||||
|
return self.voice_state_for(self.channel.me).channel.id == self._channel_id |
||||
|
|
||||
|
@property |
||||
|
def members(self) -> List[User]: |
||||
|
"""List[:class:`User`]: Returns all users that are currently in this call.""" |
||||
|
channel = self.channel |
||||
|
recipients = {channel.me, channel.recipient} |
||||
|
ret = [u for u in recipients if self.voice_state_for(u).channel.id == self._channel_id] |
||||
|
|
||||
|
return ret |
||||
|
|
||||
|
@property |
||||
|
def voice_states(self) -> List[VoiceState]: |
||||
|
"""Mapping[:class:`int`, :class:`VoiceState`]: Returns a mapping of user IDs who have voice states in this call.""" |
||||
|
return set(self._voice_states) |
||||
|
|
||||
|
async def fetch_message(self) -> Optional[Message]: |
||||
|
message = await self.channel.fetch_message(self._message_id) |
||||
|
if message is not None and self.message is None: |
||||
|
self.message = message |
||||
|
return message |
||||
|
|
||||
|
@_running_only |
||||
|
async def change_region(self, region) -> None: |
||||
|
"""|coro| |
||||
|
|
||||
|
Changes the channel's voice region. |
||||
|
|
||||
|
Parameters |
||||
|
----------- |
||||
|
region: :class:`VoiceRegion` |
||||
|
A :class:`VoiceRegion` to change the voice region to. |
||||
|
|
||||
|
Raises |
||||
|
------- |
||||
|
HTTPException |
||||
|
Failed to change the channel's voice region. |
||||
|
""" |
||||
|
await self._state.http.change_call_voice_region(self.channel.id, str(region)) |
||||
|
|
||||
|
@_running_only |
||||
|
async def ring(self) -> None: |
||||
|
channel = self.channel |
||||
|
await self._state.http.ring(channel.id, channel.recipient.id) |
||||
|
|
||||
|
@_running_only |
||||
|
async def stop_ringing(self) -> None: |
||||
|
channel = self.channel |
||||
|
await self._state.http.stop_ringing(channel.id, channel.recipient.id) |
||||
|
|
||||
|
@_running_only |
||||
|
async def join(self, **kwargs) -> VoiceProtocol: |
||||
|
return await self.channel._connect(**kwargs) |
||||
|
|
||||
|
connect = join |
||||
|
|
||||
|
@_running_only |
||||
|
async def leave(self, **kwargs) -> None: |
||||
|
state = self._state |
||||
|
if not (client := state._get_voice_client(self.channel.me.id)): |
||||
|
return |
||||
|
|
||||
|
return await client.disconnect(**kwargs) |
||||
|
|
||||
|
disconnect = leave |
||||
|
|
||||
|
def voice_state_for(self, user) -> Optional[VoiceState]: |
||||
|
"""Retrieves the :class:`VoiceState` for a specified :class:`User`. |
||||
|
|
||||
|
If the :class:`User` has no voice state then this function returns |
||||
|
``None``. |
||||
|
|
||||
|
Parameters |
||||
|
------------ |
||||
|
user: :class:`User` |
||||
|
The user to retrieve the voice state for. |
||||
|
|
||||
|
Returns |
||||
|
-------- |
||||
|
Optional[:class:`VoiceState`] |
||||
|
The voice state associated with this user. |
||||
|
""" |
||||
|
return self._state._voice_state_for(user.id) |
||||
|
|
||||
|
|
||||
|
class GroupCall(PrivateCall): |
||||
|
"""Represents a Discord group call. |
||||
|
|
||||
|
This is accompanied with a :class:`CallMessage` denoting the information. |
||||
|
|
||||
|
Attributes |
||||
|
----------- |
||||
|
channel: :class:`GroupChannel` |
||||
|
The channel the group call is in. |
||||
|
message: Optional[:class:`Message`] |
||||
|
The message associated with this group call (if available). |
||||
|
unavailable: :class:`bool` |
||||
|
Denotes if this group call is unavailable. |
||||
|
ringing: List[:class:`User`] |
||||
|
A list of users that are currently being rung to join the call. |
||||
|
region: :class:`VoiceRegion` |
||||
|
The region the group call is being hosted in. |
||||
|
""" |
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
channel: GroupChannel |
||||
|
|
||||
|
def _update( |
||||
|
self, *, ringing: SnowflakeList = [], region: VoiceRegion = MISSING |
||||
|
) -> None: |
||||
|
if region is not MISSING: |
||||
|
self.region = try_enum(VoiceRegion, region) |
||||
|
lookup = {u.id: u for u in self.channel.recipients} |
||||
|
me = self.channel.me |
||||
|
lookup[me.id] = me |
||||
|
self.ringing = list(filter(None, map(lookup.get, ringing))) |
||||
|
|
||||
|
@property |
||||
|
def members(self) -> List[User]: |
||||
|
"""List[:class:`User`]: Returns all users that are currently in this call.""" |
||||
|
ret = [u for u in self.channel.recipients if self.voice_state_for(u).channel.id == self._channel_id] |
||||
|
me = self.channel.me |
||||
|
if self.voice_state_for(me).channel.id == self._channel_id: |
||||
|
ret.append(me) |
||||
|
|
||||
|
return ret |
||||
|
|
||||
|
@_running_only |
||||
|
async def ring(self, *recipients) -> None: |
||||
|
await self._state.http.ring(self._channel_id, *{r.id for r in recipients}) |
||||
|
|
||||
|
@_running_only |
||||
|
async def stop_ringing(self, *recipients) -> None: |
||||
|
await self._state.http.stop_ringing(self._channel_id, *{r.id for r in recipients}) |
Loading…
Reference in new issue