feat(livekit): inital livekit support

This commit is contained in:
Zomatree
2024-10-29 15:48:53 +00:00
parent bc8d650659
commit 8e3cd77eb3
12 changed files with 122 additions and 12 deletions
+3
View File
@@ -33,6 +33,9 @@ docs = [
"sphinx-toolbox==3.2.*",
"setuptools==65.4.*"
]
voice = [
"livekit==0.11.1"
]
[project.urls]
Homepage = "https://github.com/revoltchat/revolt.py"
+1
View File
@@ -18,5 +18,6 @@ from .permissions import *
from .role import *
from .server import *
from .user import *
from .voice import *
__version__ = "0.2.0"
+27 -2
View File
@@ -2,11 +2,14 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional, Union
from .asset import Asset
from .enums import ChannelType
from .messageable import Messageable
from .permissions import Permissions, PermissionsOverwrite
from .utils import Missing, Ulid
from .voice import VoiceInformation, VoiceState
from .errors import NotAVoiceChannel
if TYPE_CHECKING:
from .message import Message
@@ -65,6 +68,24 @@ class EditableChannel:
await self.state.http.edit_channel(self.id, remove, kwargs)
class Joinable:
__slots__ = ()
state: State
id: str
async def join(self) -> VoiceState:
if isinstance(self, TextChannel):
if self.voice is None:
raise NotAVoiceChannel()
token = await self.state.http.join_voice_channel(self.id)
state = VoiceState(self.state, token["token"])
await state.connect()
return state
class Channel(Ulid):
"""Base class for all channels
@@ -337,7 +358,7 @@ class ServerChannel(Channel):
if default_permissions is not None:
self.default_permissions = PermissionsOverwrite._from_overwrite(default_permissions)
class TextChannel(ServerChannel, Messageable, EditableChannel):
class TextChannel(ServerChannel, Messageable, EditableChannel, Joinable):
"""A text channel
Subclasses :class:`ServerChannel` and :class:`Messageable`
@@ -366,6 +387,10 @@ class TextChannel(ServerChannel, Messageable, EditableChannel):
super().__init__(data, state)
self.last_message_id: str | None = data.get("last_message_id")
self.voice: VoiceInformation | None = None
if voice := data.get("voice"):
self.voice = VoiceInformation(voice)
async def _get_channel_id(self) -> str:
return self.id
@@ -384,7 +409,7 @@ class TextChannel(ServerChannel, Messageable, EditableChannel):
return self.state.get_message(self.last_message_id)
class VoiceChannel(ServerChannel, EditableChannel):
class VoiceChannel(ServerChannel, EditableChannel, Joinable):
"""A voice channel
Subclasses :class:`ServerChannel`
+5 -2
View File
@@ -107,8 +107,7 @@ class Client:
except:
raise RevoltError(f"Cant fetch api info:\n{text}")
async def start(self, *, reconnect: bool = True) -> None:
"""Starts the client"""
async def connect(self):
api_info = await self.get_api_info()
self.api_info = api_info
@@ -116,6 +115,10 @@ class Client:
self.state = State(self.http, api_info, self.max_messages)
self.websocket = WebsocketHandler(self.session, self.token, api_info["ws"], self.dispatch, self.state)
async def start(self, *, reconnect: bool = True) -> None:
"""Starts the client"""
await self.connect()
await self.websocket.start(reconnect)
async def stop(self) -> None:
+3
View File
@@ -24,3 +24,6 @@ class AutumnDisabled(FeatureDisabled):
class Forbidden(HTTPError):
"Missing permissions"
class NotAVoiceChannel(RevoltError):
"Channel is not a voice channel"
+8 -2
View File
@@ -29,7 +29,10 @@ if TYPE_CHECKING:
from .types import SendableEmbed as SendableEmbedPayload
from .types import User as UserPayload
from .types import (Server, ServerBans, TextChannel, UserProfile, VoiceChannel, Member, Invite, ApiInfo, Channel, SavedMessages,
DMChannel, EmojiParent, GetServerMembers, GroupDMChannel, MessageReplyPayload, MessageWithUserData, PartialInvite, CreateRole)
DMChannel, EmojiParent, GetServerMembers, GroupDMChannel, MessageReplyPayload, MessageWithUserData, PartialInvite,
CreateRole, JoinVoiceChannelPayload)
from aiohttp.client import _RequestOptions
__all__ = ("HttpClient",)
@@ -49,7 +52,7 @@ class HttpClient:
async def request(self, method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"], route: str, *, json: Optional[dict[str, Any]] = None, nonce: bool = True, params: Optional[dict[str, Any]] = None) -> Any:
url = f"{self.api_url}{route}"
kwargs = {}
kwargs: _RequestOptions = {}
headers = {
"User-Agent": "Revolt.py (https://github.com/revoltchat/revolt.py)",
@@ -430,3 +433,6 @@ class HttpClient:
def delete_messages(self, channel_id: str, messages: list[str]) -> Request[None]:
return self.request("DELETE", f"/channels/{channel_id}/messages/bulk", json={"ids": messages})
def join_voice_channel(self, channel_id: str) -> Request[JoinVoiceChannelPayload]:
return self.request("POST", f"/channels/{channel_id}/join_call")
+1
View File
@@ -12,3 +12,4 @@ from .permissions import *
from .role import *
from .server import *
from .user import *
from .voice import *
+4
View File
@@ -4,9 +4,11 @@ from typing import TYPE_CHECKING, Literal, TypedDict, Union
from typing_extensions import NotRequired
if TYPE_CHECKING:
from .file import File
from .permissions import Overwrite
from .voice import VoiceInformation
__all__ = (
"SavedMessages",
@@ -53,6 +55,8 @@ class TextChannel(BaseChannel):
role_permissions: NotRequired[dict[str, Overwrite]]
nsfw: NotRequired[bool]
last_message_id: NotRequired[str]
voice: NotRequired[VoiceInformation]
class VoiceChannel(BaseChannel):
server: str
+2
View File
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
from .member import Member, MemberID
from .server import Server, SystemMessagesConfig
from .user import Status, User, UserProfile, UserRelation
from .voice import ChannelVoiceState
__all__ = (
"BasePayload",
@@ -58,6 +59,7 @@ class ReadyEventPayload(BasePayload):
channels: list[Channel]
members: list[Member]
emojis: list[Emoji]
voice_states: list[ChannelVoiceState]
class MessageEventPayload(BasePayload, Message):
pass
+7 -6
View File
@@ -11,12 +11,12 @@ if TYPE_CHECKING:
__all__ = (
"VosoFeature",
"ApiInfo",
"Autumn",
"GetServerMembers",
"MessageWithUserData",
"CreateRole",
"JoinVoiceChannelPayload"
)
@@ -24,16 +24,14 @@ class ApiFeature(TypedDict):
enabled: bool
url: str
class VosoFeature(ApiFeature):
ws: str
class Features(TypedDict):
email: bool
invite_only: bool
captcha: ApiFeature
autumn: ApiFeature
january: ApiFeature
voso: VosoFeature
livekit: ApiFeature
class ApiInfo(TypedDict):
revolt: str
@@ -56,4 +54,7 @@ class MessageWithUserData(TypedDict):
class CreateRole(TypedDict):
id: str
role: Role
role: Role
class JoinVoiceChannelPayload(TypedDict):
token: str
+24
View File
@@ -0,0 +1,24 @@
from __future__ import annotations
from typing import TypedDict
from typing_extensions import NotRequired
__all__ = (
"VoiceInformation",
"ChannelVoiceState"
)
class VoiceInformation(TypedDict):
max_users: NotRequired[int]
class ChannelVoiceState(TypedDict):
id: str
participants: list[UserVoiceState]
class UserVoiceState(TypedDict):
id: str
can_receive: bool
can_publish: bool
screensharing: bool
camera: bool
+37
View File
@@ -0,0 +1,37 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import livekit
import livekit.rtc
if TYPE_CHECKING:
from .types import VoiceInformation as VoiceInformationPayload
from .state import State
__all__ = (
"VoiceInformation",
"VoiceState"
)
class VoiceInformation:
"""Holds information on the voice configuration of the text channel
Attributes
-----------
max_users: Optional[:class:`int`]
How many users can be in the voice at once
"""
def __init__(self, data: VoiceInformationPayload) -> None:
self.max_users: int | None = data.get("max_users", None)
class VoiceState:
def __init__(self, state: State, token: str) -> None:
self.state = state
self.token = token
self.room = livekit.rtc.Room()
async def connect(self):
await self.room.connect(self.state.api_info["features"]["livekit"]["url"], self.token)