Full Stack: Join VC + Leave (Backend)
This commit is contained in:
parent
fbf2f4e09f
commit
d1a1318275
@ -10,10 +10,10 @@ export default class Channels extends DBWrapper<string, ChannelDocument> {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public async getText(id: string) {
|
||||
public async getText(id: string | undefined) {
|
||||
return await this.get(id) as TextChannelDocument;
|
||||
}
|
||||
public async getVoice(id: string) {
|
||||
public async getVoice(id: string | undefined) {
|
||||
return await this.get(id) as VoiceChannelDocument;
|
||||
}
|
||||
|
||||
@ -33,13 +33,11 @@ export default class Channels extends DBWrapper<string, ChannelDocument> {
|
||||
return this.create({ guildId, type: 'VOICE' }) as Promise<VoiceChannelDocument>;
|
||||
}
|
||||
|
||||
public async joinVC(channelId: string, userId: string) {
|
||||
const channel = await this.getVoice(channelId);
|
||||
public async joinVC(channel: VoiceChannelDocument, userId: string) {
|
||||
channel.userIds.push(userId);
|
||||
return await channel.save();
|
||||
}
|
||||
public async leaveVC(channelId: string, userId: string) {
|
||||
const channel = await this.getVoice(channelId);
|
||||
public async leaveVC(channel: VoiceChannelDocument, userId: string) {
|
||||
const index = channel.userIds.indexOf(userId);
|
||||
channel.userIds.splice(index, 1);
|
||||
return await channel.save();
|
||||
|
@ -88,6 +88,11 @@ export const User = model<UserDocument>('user', new Schema({
|
||||
validate: [patterns.username, `Invalid username`],
|
||||
},
|
||||
verified: Boolean,
|
||||
voice: {
|
||||
type: Object,
|
||||
required: [true, 'Voice state is required'],
|
||||
default: {} as UserTypes.VoiceState,
|
||||
},
|
||||
}, { toJSON: { getters: true } })
|
||||
.plugin(uniqueValidator)
|
||||
.plugin(passportLocalMongoose, { usernameField: 'email' })
|
||||
|
@ -3,10 +3,6 @@ export class VoiceService {
|
||||
|
||||
public add(channelId: string, data: VoiceData) {
|
||||
const channelConnections = this.getOrCreate(channelId);
|
||||
const doesExist = channelConnections.some(c => c.userId === data.userId);
|
||||
if (doesExist)
|
||||
throw new TypeError('User already connected to voice');
|
||||
|
||||
channelConnections.push(data);
|
||||
this.connections.set(channelId, channelConnections);
|
||||
}
|
||||
|
@ -6,14 +6,16 @@ import { WSEvent } from './ws-event';
|
||||
import { WebSocket } from '../websocket';
|
||||
import { Socket } from 'socket.io';
|
||||
import { VoiceService } from '../../voice/voice-service';
|
||||
import Users from '../../data/users';
|
||||
import { validateUser } from '../../rest/modules/middleware';
|
||||
|
||||
export default class implements WSEvent<'CHANNEL_JOIN'> {
|
||||
on = 'CHANNEL_JOIN' as const;
|
||||
|
||||
constructor(
|
||||
private channels = Deps.get<Channels>(Channels),
|
||||
private guard = Deps.get<WSGuard>(WSGuard),
|
||||
private voice = Deps.get<VoiceService>(VoiceService),
|
||||
private users = Deps.get<Users>(Users),
|
||||
) {}
|
||||
|
||||
public async invoke(ws: WebSocket, client: Socket, { channelId }: WS.Params.ChannelJoin) {
|
||||
@ -23,8 +25,35 @@ export default class implements WSEvent<'CHANNEL_JOIN'> {
|
||||
|
||||
// TODO: validate can join
|
||||
const userId = ws.sessions.get(client.id);
|
||||
await this.channels.joinVC(channelId, userId);
|
||||
// join voice server
|
||||
const doesExist = channel.userIds.includes(userId);
|
||||
if (doesExist)
|
||||
throw new TypeError('User already connected to voice');
|
||||
|
||||
this.voice.add(channelId, { stream: null, userId });
|
||||
|
||||
await client.join(channelId);
|
||||
await this.channels.joinVC(channel, userId);
|
||||
const user = await this.updateVoiceState(userId, channelId);
|
||||
|
||||
ws.io
|
||||
.to(channel.guildId)
|
||||
.emit('CHANNEL_UPDATE', {
|
||||
channelId: channel.id,
|
||||
partialChannel: { userIds: channel.userIds },
|
||||
} as WS.Args.ChannelUpdate);
|
||||
|
||||
ws.io
|
||||
.to(channel.id)
|
||||
.emit('VOICE_STATE_UPDATE', {
|
||||
userId: user.id,
|
||||
voice: user.voice,
|
||||
} as WS.Args.VoiceStateUpdate);
|
||||
}
|
||||
|
||||
private async updateVoiceState(userId: string, channelId: string) {
|
||||
const user = await this.users.getSelf(userId);
|
||||
user.voice = { channelId };
|
||||
return await user.save();
|
||||
}
|
||||
}
|
60
backend/src/ws/ws-events/channel-leave.ts
Normal file
60
backend/src/ws/ws-events/channel-leave.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import Channels from '../../data/channels';
|
||||
import { WS } from '../../types/ws';
|
||||
import Deps from '../../utils/deps';
|
||||
import { WSGuard } from '../modules/ws-guard';
|
||||
import { WSEvent } from './ws-event';
|
||||
import { WebSocket } from '../websocket';
|
||||
import { Socket } from 'socket.io';
|
||||
import { VoiceService } from '../../voice/voice-service';
|
||||
import Users from '../../data/users';
|
||||
import { SelfUserDocument } from '../../data/models/user';
|
||||
|
||||
export default class implements WSEvent<'CHANNEL_LEAVE'> {
|
||||
on = 'CHANNEL_LEAVE' as const;
|
||||
|
||||
constructor(
|
||||
private channels = Deps.get<Channels>(Channels),
|
||||
private guard = Deps.get<WSGuard>(WSGuard),
|
||||
private voice = Deps.get<VoiceService>(VoiceService),
|
||||
private users = Deps.get<Users>(Users),
|
||||
) {}
|
||||
|
||||
public async invoke(ws: WebSocket, client: Socket) {
|
||||
const userId = ws.sessions.get(client.id);
|
||||
const user = await this.users.getSelf(userId);
|
||||
|
||||
const channel = await this.channels.getVoice(user.voice.channelId);
|
||||
if (channel.type !== 'VOICE')
|
||||
throw new TypeError('You cannot leave a non-voice channel');
|
||||
|
||||
// TODO: validate can join
|
||||
await this.channels.leaveVC(channel, userId);
|
||||
const doesExist = channel.userIds.includes(userId);
|
||||
if (!doesExist)
|
||||
throw new TypeError('User not connected to voice');
|
||||
|
||||
// join voice server
|
||||
this.voice.remove(channel.id, userId);
|
||||
await client.leave(channel.id);
|
||||
await this.updateVoiceState(user);
|
||||
|
||||
ws.io
|
||||
.to(channel.guildId)
|
||||
.emit('CHANNEL_UPDATE', {
|
||||
channelId: channel.id,
|
||||
partialChannel: { userIds: channel.userIds },
|
||||
} as WS.Args.ChannelUpdate);
|
||||
|
||||
ws.io
|
||||
.to(channel.id)
|
||||
.emit('VOICE_STATE_UPDATE', {
|
||||
userId: user.id,
|
||||
voice: user.voice,
|
||||
} as WS.Args.VoiceStateUpdate);
|
||||
}
|
||||
|
||||
private async updateVoiceState(user: SelfUserDocument) {
|
||||
user.voice = { channelId: undefined };
|
||||
await user.save();
|
||||
}
|
||||
}
|
@ -16,13 +16,13 @@ export default class implements WSEvent<'CHANNEL_UPDATE'> {
|
||||
) {}
|
||||
|
||||
public async invoke(ws: WebSocket, client: Socket, { name, summary, overrides, channelId }: WS.Params.ChannelUpdate) {
|
||||
const channel = await this.channels.getText(channelId);
|
||||
const channel = await this.channels.get(channelId);
|
||||
await this.guard.validateCan(client, channel.guildId, 'MANAGE_CHANNELS');
|
||||
|
||||
const partialChannel: Partial<Entity.Channel> = {};
|
||||
if (name) partialChannel.name = name;
|
||||
if (overrides) partialChannel.overrides = overrides;
|
||||
partialChannel.summary = summary;
|
||||
if (summary) partialChannel.summary = summary;
|
||||
await channel.updateOne(partialChannel as any, { runValidators: true });
|
||||
|
||||
ws.io
|
||||
|
@ -5,10 +5,13 @@ import { ContextMenuTrigger } from 'react-contextmenu';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import usePerms from '../../../hooks/use-perms';
|
||||
import { joinVoiceChannel } from '../../../store/channels';
|
||||
import { getChannel, getChannelUsers, joinVoiceChannel } from '../../../store/channels';
|
||||
import { getGuildChannels } from '../../../store/guilds';
|
||||
import { actions as ui } from '../../../store/ui';
|
||||
import ChannelMenu from '../../ctx-menus/channel-menu';
|
||||
import Username from '../../user/username';
|
||||
|
||||
import './channel-tabs.scoped.css';
|
||||
|
||||
const ChannelTabs: React.FunctionComponent = () => {
|
||||
const dispatch = useDispatch();
|
||||
@ -30,6 +33,14 @@ const ChannelTabs: React.FunctionComponent = () => {
|
||||
dispatch(joinVoiceChannel(channel.id));
|
||||
};
|
||||
|
||||
const VCMembers = () => {
|
||||
const users = useSelector(getChannelUsers(channel.id));
|
||||
|
||||
if (channel.type !== 'VOICE') return null;
|
||||
|
||||
return <div className="p-2 pl-3">{users.map(u => <Username user={u} />)}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuTrigger key={channel.id} id={channel.id}>
|
||||
<Link
|
||||
@ -54,11 +65,12 @@ const ChannelTabs: React.FunctionComponent = () => {
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
<VCMembers />
|
||||
<ChannelMenu channel={channel} />
|
||||
</ContextMenuTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{guildChannels.map(c => <ChannelTab channel={c} />)}</>
|
||||
return <>{guildChannels.map(c => <ChannelTab key={c.id} channel={c} />)}</>
|
||||
};
|
||||
export default ChannelTabs;
|
@ -4,8 +4,6 @@ import { actions as ui } from '../../../store/ui';
|
||||
import GuildDropdown from '../../dropdowns/guild-dropdown';
|
||||
import ChannelTabs from './channel-tabs';
|
||||
|
||||
import './sidebar-content.scoped.css';
|
||||
|
||||
const SidebarContent: React.FunctionComponent = () => {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
|
@ -8,7 +8,7 @@ import Image from '../utils/image';
|
||||
export interface UsernameProps {
|
||||
user: Entity.User;
|
||||
guild?: Entity.Guild;
|
||||
size?: 'md' | 'lg';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const Username: React.FunctionComponent<UsernameProps> = ({ guild, user, size = 'md' }) => {
|
||||
|
@ -142,6 +142,11 @@ const WSListener: React.FunctionComponent = () => {
|
||||
dispatch(auth.updatedUser(args));
|
||||
dispatch(users.updated(args));
|
||||
});
|
||||
ws.on('VOICE_STATE_UPDATE', (args) => {
|
||||
const data = { userId: args.userId, partialUser: { voice: args.voice } };
|
||||
dispatch(auth.updatedUser(data));
|
||||
dispatch(users.updated(data ));
|
||||
});
|
||||
|
||||
dispatch(meta.listenedToWS());
|
||||
}, [hasListened]);
|
||||
|
@ -62,4 +62,13 @@ export const getChannel = (id: string) =>
|
||||
createSelector<Store.AppState, Entity.Channel[], Entity.Channel | undefined>(
|
||||
state => state.entities.channels,
|
||||
channels => channels.find(c => c.id === id),
|
||||
);
|
||||
|
||||
export const getChannelUsers = (channelId: string) =>
|
||||
createSelector<Store.AppState, { channels, users }, Entity.User[]>(
|
||||
state => ({ channels: state.entities.channels, users: state.entities.users }),
|
||||
({ channels, users }) => {
|
||||
const vc = channels.find(c => c.id === channelId) as ChannelTypes.Voice;
|
||||
return vc.userIds.map(id => users.find(u => u.id === id))
|
||||
},
|
||||
);
|
4
types/entity.d.ts
vendored
4
types/entity.d.ts
vendored
@ -71,6 +71,7 @@ declare namespace Entity {
|
||||
guildIds: string[];
|
||||
status: UserTypes.StatusType;
|
||||
username: string;
|
||||
voice: UserTypes.VoiceState;
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,4 +141,7 @@ declare namespace UserTypes {
|
||||
userIds: string[];
|
||||
};
|
||||
}
|
||||
export interface VoiceState {
|
||||
channelId?: string;
|
||||
}
|
||||
}
|
7
types/ws.d.ts
vendored
7
types/ws.d.ts
vendored
@ -104,7 +104,8 @@ declare namespace WS {
|
||||
'USER_DELETE': {};
|
||||
/** Called the client user settings are updated. */
|
||||
'USER_UPDATE': WS.Args.UserUpdate;
|
||||
/** Called when a websocket message is sent. */
|
||||
/** Called when a user voice state is updated in the client's voice channel. */
|
||||
'VOICE_STATE_UPDATE': WS.Args.VoiceStateUpdate;
|
||||
'error': object;
|
||||
}
|
||||
|
||||
@ -355,6 +356,10 @@ declare namespace WS {
|
||||
userId: string;
|
||||
partialUser: Partial<UserTypes.Self>;
|
||||
}
|
||||
export interface VoiceStateUpdate {
|
||||
userId: string;
|
||||
voice: UserTypes.VoiceState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user