summary refs log tree commit diff
path: root/src/gateway/opcodes/StreamCreate.ts
blob: 69bcb32c552d25381041c4e3b9bc1e5ccf834036 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import {
	genVoiceToken,
	Payload,
	WebSocket,
	generateStreamKey,
} from "@spacebar/gateway";
import {
	Channel,
	Config,
	emitEvent,
	Member,
	Region,
	Snowflake,
	Stream,
	StreamCreateEvent,
	StreamCreateSchema,
	StreamServerUpdateEvent,
	StreamSession,
	VoiceState,
	VoiceStateUpdateEvent,
} from "@spacebar/util";
import { check } from "./instanceOf";

export async function onStreamCreate(this: WebSocket, data: Payload) {
	const startTime = Date.now();
	check.call(this, StreamCreateSchema, data.d);
	const body = data.d as StreamCreateSchema;

	if (body.channel_id.trim().length === 0) return;

	// first check if we are in a voice channel already. cannot create a stream if there's no existing voice connection
	const voiceState = await VoiceState.findOne({
		where: { user_id: this.user_id },
	});

	if (!voiceState || !voiceState.channel_id) return;

	if (body.guild_id) {
		voiceState.member = await Member.findOneOrFail({
			where: { id: voiceState.user_id, guild_id: voiceState.guild_id },
			relations: ["user", "roles"],
		});
	}

	// TODO: permissions check - if it's a guild, check if user is allowed to create stream in this guild

	const channel = await Channel.findOne({
		where: { id: body.channel_id },
	});

	if (
		!channel ||
		(body.type === "guild" && channel.guild_id != body.guild_id)
	)
		return this.close(4000, "invalid channel");

	// TODO: actually apply preferred_region from the event payload
	const regions = Config.get().regions;
	const guildRegion = regions.available.filter(
		(r) => r.id === regions.default,
	)[0];

	// first make sure theres no other streams for this user that somehow didnt get cleared
	await Stream.delete({
		owner_id: this.user_id,
	});

	// create a new entry in db containing the token for authenticating user in stream gateway IDENTIFY
	const stream = Stream.create({
		id: Snowflake.generate(),
		owner_id: this.user_id,
		channel_id: body.channel_id,
		endpoint: guildRegion.endpoint,
	});

	await stream.save();

	const token = genVoiceToken();

	const streamSession = StreamSession.create({
		stream_id: stream.id,
		user_id: this.user_id,
		session_id: this.session_id,
		token,
	});

	await streamSession.save();

	const streamKey = generateStreamKey(
		body.type,
		body.guild_id,
		body.channel_id,
		this.user_id,
	);

	await emitEvent({
		event: "STREAM_CREATE",
		data: {
			stream_key: streamKey,
			rtc_server_id: stream.id, // for voice connections in guilds it is guild_id, for dm voice calls it seems to be DM channel id, for GoLive streams a generated number
			viewer_ids: [],
			region: guildRegion.name,
			paused: false,
		},
		user_id: this.user_id,
	} as StreamCreateEvent);

	await emitEvent({
		event: "STREAM_SERVER_UPDATE",
		data: {
			token: streamSession.token,
			stream_key: streamKey,
			guild_id: null, // not sure why its always null
			endpoint: stream.endpoint,
		},
		user_id: this.user_id,
	} as StreamServerUpdateEvent);

	voiceState.self_stream = true;
	await voiceState.save();

	await emitEvent({
		event: "VOICE_STATE_UPDATE",
		data: voiceState.toPublicVoiceState(),
		guild_id: voiceState.guild_id,
		channel_id: voiceState.channel_id,
	} as VoiceStateUpdateEvent);

	console.log(
		`[Gateway] STREAM_CREATE for user ${this.user_id} in channel ${body.channel_id} with stream key ${streamKey} in ${Date.now() - startTime}ms`,
	);
}

//stream key:
// guild:${guild_id}:${channel_id}:${user_id}
// call:${channel_id}:${user_id}