mirror of
https://github.com/stoatchat/python-client-sdk.git
synced 2026-07-24 16:05:29 -04:00
feat(livekit): inital livekit support
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -24,3 +24,6 @@ class AutumnDisabled(FeatureDisabled):
|
||||
|
||||
class Forbidden(HTTPError):
|
||||
"Missing permissions"
|
||||
|
||||
class NotAVoiceChannel(RevoltError):
|
||||
"Channel is not a voice channel"
|
||||
+8
-2
@@ -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")
|
||||
@@ -12,3 +12,4 @@ from .permissions import *
|
||||
from .role import *
|
||||
from .server import *
|
||||
from .user import *
|
||||
from .voice import *
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user