Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding voice #155

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config.example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
PUBLIC_HOST = "127.0.0.1:8080"

GATEWAY_HOST = "127.0.0.1:8000/gateway"
VOICE_GATEWAY_HOST = "127.0.0.1:8000/voice"
CDN_HOST = "127.0.0.1:8000/media"

STORAGE = {
Expand Down Expand Up @@ -113,3 +114,6 @@
"client_secret": None,
},
}

# Voice workers addresses
VOICE_WORKERS = []
13 changes: 12 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ yc-protobuf3-to-dict = "^0.3.0"
s3lite = "^0.1.4"
fast-depends = ">=2.4.2"
faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.5.4"}
semanticsdp = "^0.1.0b9"

[tool.poetry.group.dev.dependencies]
pytest = "^8.2.0"
Expand Down
7 changes: 4 additions & 3 deletions yepcord/asgi.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
YEPCord: Free open source selfhostable fully discord-compatible chat
Copyright (C) 2022-2024 RuslanUC
Copyright (C) 2022-2023 RuslanUC

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
Expand All @@ -21,6 +21,7 @@
from quart_schema import RequestSchemaValidationError, QuartSchema
import yepcord.rest_api.main as rest_api
import yepcord.gateway.main as gateway
import yepcord.voice_gateway.main as voice_gateway
import yepcord.cdn.main as cdn
import yepcord.remote_auth.main as remote_auth
from yepcord.rest_api.routes import auth, connections
Expand Down Expand Up @@ -78,8 +79,8 @@
app.route("/api/v9/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])(rest_api.other_api_endpoints)

app.websocket("/gateway", strict_slashes=False)(gateway.ws_gateway)
remote_auth.ws_gateway.__name__ = "ws_ra_gateway"
app.websocket("/remote-auth", strict_slashes=False)(remote_auth.ws_gateway)
app.websocket("/remote-auth", strict_slashes=False)(remote_auth.ws_gateway_remote_auth)
app.websocket("/voice", strict_slashes=False)(voice_gateway.ws_gateway_voice)

app.register_blueprint(cdn.cdn, url_prefix="/media")

Expand Down
78 changes: 72 additions & 6 deletions yepcord/gateway/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@

from base64 import b64encode
from time import time
from typing import List, TYPE_CHECKING
from typing import List, TYPE_CHECKING, Optional

from ..yepcord.config import Config
from ..yepcord.enums import GatewayOp
from ..yepcord.models import Emoji, Application, Integration, ConnectedAccount
from ..yepcord.models import Emoji, Application, Integration, ConnectedAccount, VoiceState
from ..yepcord.models.interaction import Interaction
from ..yepcord.snowflake import Snowflake

if TYPE_CHECKING: # pragma: no cover
from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings
from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings, Guild
from ..yepcord.core import Core
from .gateway import GatewayClient
from .presences import Presence
Expand Down Expand Up @@ -173,16 +173,28 @@ def __init__(self, friends_presences: list[dict], guilds_ids: list[int]):
self.guilds_ids = guilds_ids

async def json(self) -> dict:
g = [{"voice_states": [], "id": str(i), "embedded_activities": []} for i in self.guilds_ids] # TODO
g = [
{
"voice_states": [
state.ds_json() for state in await VoiceState.filter(
guild__id=i, last_heartbeat__gt=int(time()-30)
).select_related("user", "channel")
],
"id": str(i),
"embedded_activities": []
}
for i in self.guilds_ids
]

return {
"t": self.NAME,
"op": self.OP,
"d": {
"merged_presences": {
"guilds": [[]], # TODO
"guilds": [[]],
"friends": self.friends_presences
},
"merged_members": [[]], # TODO
"merged_members": [[]],
"guilds": g
}
}
Expand Down Expand Up @@ -1054,3 +1066,57 @@ async def json(self) -> dict:
"token_data": None,
}
}


class VoiceStateUpdate(DispatchEvent):
NAME = "VOICE_STATE_UPDATE"

def __init__(self, user_id: int, session_id: str, channel: Optional[Channel], guild: Optional[Guild],
member: Optional[GuildMember], **kwargs):
self.user_id = user_id
self.session_id = session_id
self.channel = channel
self.guild = guild
self.member = member
self.kwargs = kwargs

async def json(self) -> dict:
data = {
"t": self.NAME,
"op": self.OP,
"d": {
"user_id": str(self.user_id),
"channel_id": str(self.channel.id) if self.channel is not None else None,
"deaf": False,
"mute": False,
"session_id": self.session_id,
**self.kwargs
}
}
if self.guild:
data["d"]["guild_id"] = str(self.guild.id)
data["d"]["member"] = await self.member.ds_json()
return data


class VoiceServerUpdate(DispatchEvent):
NAME = "VOICE_SERVER_UPDATE"

def __init__(self, voice_state: VoiceState):
self.state = voice_state

async def json(self) -> dict:
data = {
"t": self.NAME,
"op": self.OP,
"d": {
"token": f"{self.state.id}.{self.state.token}",
"endpoint": Config.VOICE_GATEWAY_HOST
}
}
if self.state.guild:
data["d"]["guild_id"] = str(self.state.guild.id)
if self.state.channel:
data["d"]["channel_id"] = str(self.state.channel.id)

return data
77 changes: 73 additions & 4 deletions yepcord/gateway/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import warnings
from json import dumps as jdumps
from typing import Optional, Union
from typing import Union

from quart import Websocket
from redis.asyncio import Redis
Expand All @@ -31,8 +31,10 @@
from ..yepcord.classes.fakeredis import FakeRedis
from ..yepcord.core import Core
from ..yepcord.ctx import getCore
from ..yepcord.enums import GatewayOp
from ..yepcord.enums import GatewayOp, GuildPermissions
from ..yepcord.gateway_dispatcher import GatewayDispatcher
from ..yepcord.models import Session, User, UserSettings, Bot, GuildMember
from ..yepcord.models.voice_state import VoiceState
from ..yepcord.mq_broker import getBroker


Expand All @@ -53,10 +55,24 @@ def __init__(self, ws, gateway: Gateway):
def connected(self):
return self._connected

def disconnect(self) -> None:
async def disconnect(self) -> None:
self._connected = False
self.ws = None

state = await VoiceState.get_or_none(user__id=self.user_id, session_id=self.sid)\
.select_related("channel", "guild")
if state is not None:
member = await getCore().getGuildMember(state.guild, self.user_id)
voice_event = VoiceStateUpdate(
self.id, self.sid, None, state.guild, member, self_mute=True, self_deaf=True
)
await self.gateway.broker.publish(channel="yepcord_events", message={
"data": await voice_event.json(),
"event": voice_event.NAME,
**(await GatewayDispatcher.getChannelFilter(state.channel, GuildPermissions.VIEW_CHANNEL))
})
await state.delete()

async def send(self, data: dict):
self.seq += 1
data["s"] = self.seq
Expand Down Expand Up @@ -175,6 +191,59 @@ async def handle_GUILD_MEMBERS(self, data: dict) -> None:
presences = [] # TODO: add presences
await self.esend(GuildMembersChunkEvent(members, presences, guild_id))

@require_auth
async def handle_VOICE_STATE(self, data: dict) -> None:
self_mute = bool(data.get("self_mute"))
self_deaf = bool(data.get("self_deaf"))

voice_state = await VoiceState.get_or_none(user__id=self.user_id).select_related("channel", "guild")
if voice_state is not None:
if voice_state.session_id != self.sid and data["channel_id"] is None:
return
if str(voice_state.guild.id) != data["guild_id"] and voice_state.session_id == self.sid:
member = await getCore().getGuildMember(voice_state.guild, self.user_id)
voice_event = VoiceStateUpdate(
self.id, self.sid, None, voice_state.guild, member, self_mute=self_mute, self_deaf=self_deaf
)
await self.gateway.broker.publish(channel="yepcord_events", message={
"data": await voice_event.json(),
"event": voice_event.NAME,
**(await GatewayDispatcher.getChannelFilter(voice_state.channel, GuildPermissions.VIEW_CHANNEL))
})
if data["guild_id"] is None:
return await voice_state.delete()

if data["channel_id"] is None or data["guild_id"] is None:
return
if not (channel := await getCore().getChannel(data["channel_id"])): return
if not await getCore().getUserByChannel(channel, self.user_id): return
if (guild := await getCore().getGuild(data["guild_id"])) is None or channel.guild != guild or \
(member := await getCore().getGuildMember(guild, self.user_id)) is None:
return

if voice_state is not None:
await voice_state.update(guild=guild, channel=channel, session_id=self.sid)
else:
voice_state = await VoiceState.create(guild=guild, channel=channel, user=member.user, session_id=self.sid)

if member is None:
member = await getCore().getGuildMember(voice_state.guild, self.user_id)

voice_event = VoiceStateUpdate(
self.id, self.sid, voice_state.channel, voice_state.guild, member, self_mute=self_mute, self_deaf=self_deaf
)
await self.gateway.mcl_yepcordEventsCallback({
"data": await voice_event.json(),
"event": voice_event.NAME,
"user_ids": None,
"guild_id": None,
"role_ids": None,
"session_id": None,
"exclude": [],
} | await GatewayDispatcher.getChannelFilter(voice_state.channel, GuildPermissions.VIEW_CHANNEL))
await self.esend(VoiceServerUpdate(voice_state))
print("should connect now")


class GatewayEvents:
BOTS_EVENTS_BLACKLIST = {"MESSAGE_ACK"}
Expand Down Expand Up @@ -404,7 +473,7 @@ async def process(self, ws: Websocket, data: dict):
print(f" Data: {data}")

async def disconnect(self, ws: Websocket):
getattr(ws, "_yepcord_client").disconnect()
await getattr(ws, "_yepcord_client").disconnect()

async def getFriendsPresences(self, uid: int) -> list[dict]:
presences = []
Expand Down
15 changes: 9 additions & 6 deletions yepcord/gateway/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@
from ..yepcord.utils import b64decode


def require_auth(func):
async def wrapped(self, *args, **kwargs):
if self.user_id is None:
return self.ws.close(4005)
return await func(self, *args, **kwargs)
def require_auth(func_or_code):
def decorator(func):
async def wrapped(self, *args, **kwargs):
if self.user_id is None:
return await self.ws.close(func_or_code if isinstance(func_or_code, int) else 4005)
return await func(self, *args, **kwargs)

return wrapped
return wrapped

return decorator if isinstance(func_or_code, int) else decorator(func_or_code)


class TokenType(Enum):
Expand Down
2 changes: 1 addition & 1 deletion yepcord/remote_auth/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def set_cors_headers(response): # pragma: no cover


@app.websocket("/")
async def ws_gateway():
async def ws_gateway_remote_auth():
version = websocket.args.get("v", "")
version = int(version) if version.isdigit() else 1

Expand Down
Empty file.
Loading
Loading