You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
495 lines
16 KiB
495 lines
16 KiB
"""
|
|
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.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union
|
|
|
|
from . import utils
|
|
from .errors import ClientException
|
|
from .utils import cached_slot_property
|
|
from .voice_client import VoiceClient
|
|
|
|
if TYPE_CHECKING:
|
|
from . import abc
|
|
from .abc import T as ConnectReturn
|
|
from .channel import DMChannel, GroupChannel
|
|
from .client import Client
|
|
from .member import VoiceState
|
|
from .message import Message
|
|
from .state import ConnectionState
|
|
from .types.gateway import CallCreateEvent, CallUpdateEvent
|
|
from .user import BaseUser, User
|
|
|
|
_PrivateChannel = Union[abc.DMChannel, abc.GroupChannel]
|
|
|
|
__all__ = (
|
|
'CallMessage',
|
|
'PrivateCall',
|
|
'GroupCall',
|
|
)
|
|
|
|
|
|
def _running_only(func: Callable):
|
|
def decorator(self: Call, *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`]
|
|
An aware 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.
|
|
"""
|
|
|
|
__slots__ = ('message', 'ended_timestamp', 'participants')
|
|
|
|
def __init__(self, message: Message, *, participants: List[User], ended_timestamp: Optional[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:`.abc.User`: Returns the user that started the call."""
|
|
return self.message.author # type: ignore # Cannot be a Member in private messages
|
|
|
|
@property
|
|
def channel(self) -> _PrivateChannel:
|
|
""":class:`.abc.PrivateChannel`: The private channel associated with this message."""
|
|
return self.message.channel # type: ignore # Can only be a private channel here
|
|
|
|
@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 utils.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.
|
|
|
|
.. versionadded:: 1.9
|
|
|
|
Attributes
|
|
-----------
|
|
channel: :class:`DMChannel`
|
|
The channel the call is in.
|
|
unavailable: :class:`bool`
|
|
Denotes if this call is unavailable.
|
|
region: :class:`str`
|
|
The region the call is being hosted at.
|
|
|
|
.. versionchanged:: 2.0
|
|
The type of this attribute has changed to :class:`str`.
|
|
"""
|
|
|
|
__slots__ = ('_state', '_ended', 'channel', '_cs_message', '_ringing', '_message_id', 'region', 'unavailable')
|
|
|
|
if TYPE_CHECKING:
|
|
channel: DMChannel
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
data: Union[CallCreateEvent, CallUpdateEvent],
|
|
state: ConnectionState,
|
|
message: Optional[Message],
|
|
channel: abc.PrivateChannel,
|
|
) -> None:
|
|
self._state = state
|
|
self._cs_message = message
|
|
self.channel = channel # type: ignore # Will always be a DMChannel here
|
|
self._ended: bool = False
|
|
self._update(data)
|
|
|
|
def _delete(self) -> None:
|
|
self._ringing = tuple()
|
|
self._ended = True
|
|
|
|
def _get_recipients(self) -> Tuple[BaseUser, ...]:
|
|
channel = self.channel
|
|
return channel.me, channel.recipient
|
|
|
|
def _is_participating(self, user: BaseUser) -> bool:
|
|
state = self.voice_state_for(user)
|
|
return bool(state and state.channel and state.channel.id == self.channel.id)
|
|
|
|
def _update(self, data: Union[CallCreateEvent, CallUpdateEvent]) -> None:
|
|
self._message_id = int(data['message_id'])
|
|
self.unavailable = data.get('unavailable', False)
|
|
try:
|
|
self.region: str = data['region']
|
|
except KeyError:
|
|
pass
|
|
|
|
channel = self.channel
|
|
recipients = self._get_recipients()
|
|
lookup = {u.id: u for u in recipients}
|
|
self._ringing = tuple(filter(None, map(lookup.get, [int(x) for x in data.get('ringing', [])])))
|
|
|
|
for vs in data.get('voice_states', []):
|
|
self._state._update_voice_state(vs, channel.id)
|
|
|
|
@property
|
|
def ringing(self) -> List[BaseUser]:
|
|
"""List[:class:`.abc.User`]: A list of users that are currently being rung to join the call."""
|
|
return list(self._ringing)
|
|
|
|
@property
|
|
def initiator(self) -> Optional[User]:
|
|
"""Optional[:class:`.abc.User`]: Returns the user that started the call. Returns ``None`` if the message is not cached."""
|
|
return getattr(self.message, 'author', None)
|
|
|
|
@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 library)."""
|
|
return self._is_participating(self.channel.me)
|
|
|
|
@property
|
|
def members(self) -> List[BaseUser]:
|
|
"""List[:class:`.abc.User`]: Returns all users that are currently in this call."""
|
|
recipients = self._get_recipients()
|
|
return [u for u in recipients if self._is_participating(u)]
|
|
|
|
@property
|
|
def voice_states(self) -> Dict[int, VoiceState]:
|
|
"""Mapping[:class:`int`, :class:`VoiceState`]: Returns a mapping of user IDs who have voice states in this call."""
|
|
return {
|
|
k: v for k, v in self._state._voice_states.items() if bool(v and v.channel and v.channel.id == self.channel.id)
|
|
}
|
|
|
|
@cached_slot_property('_cs_message')
|
|
def message(self) -> Optional[Message]:
|
|
"""Optional[:class:`Message`]: The message associated with this call. Sometimes may not be cached."""
|
|
return self._state._get_message(self._message_id)
|
|
|
|
async def fetch_message(self) -> Message:
|
|
"""|coro|
|
|
|
|
Fetches and caches the message associated with this call.
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Retrieving the message failed.
|
|
|
|
Returns
|
|
-------
|
|
:class:`Message`
|
|
The message associated with this call.
|
|
"""
|
|
message = await self.channel.fetch_message(self._message_id)
|
|
state = self._state
|
|
if self.message is None:
|
|
if state._messages is not None:
|
|
state._messages.append(message)
|
|
self._cs_message = message
|
|
return message
|
|
|
|
async def change_region(self, region: str) -> None:
|
|
"""|coro|
|
|
|
|
Changes the channel's voice region.
|
|
|
|
Parameters
|
|
-----------
|
|
region: :class:`str`
|
|
A region to change the voice region to.
|
|
|
|
.. versionchanged:: 2.0
|
|
The type of this parameter has changed to :class:`str`.
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Failed to change the channel's voice region.
|
|
"""
|
|
await self._state.http.change_call_voice_region(self.channel.id, region)
|
|
|
|
@_running_only
|
|
async def ring(self) -> None:
|
|
"""|coro|
|
|
|
|
Rings the other recipient.
|
|
|
|
Raises
|
|
-------
|
|
Forbidden
|
|
Not allowed to ring the other recipient.
|
|
HTTPException
|
|
Ringing failed.
|
|
ClientException
|
|
The call has ended.
|
|
"""
|
|
channel = self.channel
|
|
await self._state.http.ring(channel.id)
|
|
|
|
@_running_only
|
|
async def stop_ringing(self) -> None:
|
|
"""|coro|
|
|
|
|
Stops ringing the other recipient.
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Stopping the ringing failed.
|
|
ClientException
|
|
The call has ended.
|
|
"""
|
|
channel = self.channel
|
|
await self._state.http.stop_ringing(channel.id, channel.recipient.id)
|
|
|
|
@_running_only
|
|
async def connect(
|
|
self,
|
|
*,
|
|
timeout: float = 60.0,
|
|
reconnect: bool = True,
|
|
cls: Callable[[Client, abc.VocalChannel], ConnectReturn] = VoiceClient,
|
|
) -> ConnectReturn:
|
|
"""|coro|
|
|
|
|
Connects to voice and creates a :class:`~discord.VoiceClient` to establish
|
|
your connection to the voice server.
|
|
|
|
There is an alias of this called :attr:`join`.
|
|
|
|
Parameters
|
|
-----------
|
|
timeout: :class:`float`
|
|
The timeout in seconds to wait for the voice endpoint.
|
|
reconnect: :class:`bool`
|
|
Whether the bot should automatically attempt
|
|
a reconnect if a part of the handshake fails
|
|
or the gateway goes down.
|
|
cls: Type[:class:`~discord.VoiceProtocol`]
|
|
A type that subclasses :class:`~discord.VoiceProtocol` to connect with.
|
|
Defaults to :class:`~discord.VoiceClient`.
|
|
|
|
Raises
|
|
-------
|
|
asyncio.TimeoutError
|
|
Could not connect to the voice channel in time.
|
|
~discord.ClientException
|
|
You are already connected to a voice channel.
|
|
~discord.opus.OpusNotLoaded
|
|
The opus library has not been loaded.
|
|
|
|
Returns
|
|
--------
|
|
:class:`~discord.VoiceProtocol`
|
|
A voice client that is fully connected to the voice server.
|
|
"""
|
|
return await self.channel.connect(timeout=timeout, reconnect=reconnect, cls=cls, ring=False)
|
|
|
|
@_running_only
|
|
async def join(
|
|
self,
|
|
*,
|
|
timeout: float = 60.0,
|
|
reconnect: bool = True,
|
|
cls: Callable[[Client, abc.VocalChannel], ConnectReturn] = VoiceClient,
|
|
) -> ConnectReturn:
|
|
"""|coro|
|
|
|
|
Connects to voice and creates a :class:`~discord.VoiceClient` to establish
|
|
your connection to the voice server.
|
|
|
|
This is an alias of :attr:`connect`.
|
|
|
|
Parameters
|
|
-----------
|
|
timeout: :class:`float`
|
|
The timeout in seconds to wait for the voice endpoint.
|
|
reconnect: :class:`bool`
|
|
Whether the bot should automatically attempt
|
|
a reconnect if a part of the handshake fails
|
|
or the gateway goes down.
|
|
cls: Type[:class:`~discord.VoiceProtocol`]
|
|
A type that subclasses :class:`~discord.VoiceProtocol` to connect with.
|
|
Defaults to :class:`~discord.VoiceClient`.
|
|
|
|
Raises
|
|
-------
|
|
asyncio.TimeoutError
|
|
Could not connect to the voice channel in time.
|
|
~discord.ClientException
|
|
You are already connected to a voice channel.
|
|
~discord.opus.OpusNotLoaded
|
|
The opus library has not been loaded.
|
|
|
|
Returns
|
|
--------
|
|
:class:`~discord.VoiceProtocol`
|
|
A voice client that is fully connected to the voice server.
|
|
"""
|
|
return await self.connect(timeout=timeout, reconnect=reconnect, cls=cls)
|
|
|
|
@_running_only
|
|
async def disconnect(self, force: bool = False) -> None:
|
|
"""|coro|
|
|
|
|
Disconnects this voice client from voice.
|
|
|
|
There is an alias of this called :attr:`leave`.
|
|
"""
|
|
state = self._state
|
|
if not (client := state._get_voice_client(self.channel.me.id)):
|
|
return
|
|
|
|
return await client.disconnect(force=force)
|
|
|
|
@_running_only
|
|
async def leave(self, force: bool = False) -> None:
|
|
"""|coro|
|
|
|
|
Disconnects this voice client from voice.
|
|
|
|
This is an alias of :attr:`disconnect`.
|
|
"""
|
|
return await self.disconnect(force=force)
|
|
|
|
def voice_state_for(self, user: abc.Snowflake) -> 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.
|
|
unavailable: :class:`bool`
|
|
Denotes if this group call is unavailable.
|
|
region: :class:`str`
|
|
The region the group call is being hosted in.
|
|
|
|
.. versionchanged:: 2.0
|
|
The type of this attribute has changed to :class:`str`.
|
|
"""
|
|
|
|
__slots__ = ()
|
|
|
|
if TYPE_CHECKING:
|
|
channel: GroupChannel
|
|
|
|
def _get_recipients(self) -> Tuple[BaseUser, ...]:
|
|
channel = self.channel
|
|
return *channel.recipients, channel.me
|
|
|
|
@_running_only
|
|
async def ring(self, *recipients: abc.Snowflake) -> None:
|
|
r"""|coro|
|
|
|
|
Rings the specified recipients.
|
|
|
|
Parameters
|
|
-----------
|
|
\*recipients: :class:`User`
|
|
The recipients to ring. The default is to ring all recipients.
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Stopping the ringing failed.
|
|
ClientException
|
|
The call has ended.
|
|
"""
|
|
await self._state.http.ring(self.channel.id, *{r.id for r in recipients})
|
|
|
|
@_running_only
|
|
async def stop_ringing(self, *recipients: abc.Snowflake) -> None:
|
|
r"""|coro|
|
|
|
|
Stops ringing the specified recipients.
|
|
|
|
Parameters
|
|
-----------
|
|
\*recipients: :class:`User`
|
|
The recipients to stop ringing.
|
|
|
|
Raises
|
|
-------
|
|
HTTPException
|
|
Ringing failed.
|
|
ClientException
|
|
The call has ended.
|
|
"""
|
|
channel = self.channel
|
|
await self._state.http.stop_ringing(channel.id, *{r.id for r in recipients or channel.recipients})
|
|
|
|
|
|
Call = Union[PrivateCall, GroupCall]
|
|
|