discord4j.voice.VoiceGatewayClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of discord4j-voice Show documentation
Show all versions of discord4j-voice Show documentation
A JVM-based REST/WS wrapper for the official Discord Bot API
/*
* This file is part of Discord4J.
*
* Discord4J is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Discord4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Discord4J. If not, see .
*/
package discord4j.voice;
import com.discord4j.fsm.FiniteStateMachine;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.iwebpp.crypto.TweetNaclFast;
import discord4j.voice.VoiceGatewayEvent.Start;
import discord4j.voice.VoiceGatewayEvent.Stop;
import discord4j.voice.VoiceGatewayState.*;
import discord4j.voice.json.*;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import reactor.core.Disposable;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.netty.NettyPipeline;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.websocket.WebsocketInbound;
import reactor.netty.http.websocket.WebsocketOutbound;
import reactor.util.Logger;
import reactor.util.Loggers;
import java.io.InputStream;
import java.time.Duration;
import static io.netty.handler.codec.http.HttpHeaderNames.USER_AGENT;
public class VoiceGatewayClient {
private final Logger log = Loggers.getLogger("discord4j.voice.gateway.client");
private final FiniteStateMachine gatewayFSM;
private final EmitterProcessor> sender = EmitterProcessor.create(false);
private final ObjectMapper mapper;
final VoiceSocket voiceSocket;
public VoiceGatewayClient(long serverId, long userId, String sessionId, String token, ObjectMapper mapper,
Scheduler scheduler, AudioProvider provider, AudioReceiver receiver) {
this.mapper = mapper;
this.voiceSocket = new VoiceSocket();
this.gatewayFSM = new FiniteStateMachine() {{
startWith(Stopped.INSTANCE);
when(Stopped.class)
.on(Start.class, (curState, start) -> {
Disposable websocketTask = HttpClient.create()
.wiretap(true)
.followRedirect(true)
.headers(headers -> headers.add(USER_AGENT, "DiscordBot(https://discord4j.com, 3)")) // TODO make configurable
.websocket(Integer.MAX_VALUE)
.uri(start.gatewayUrl + "?v=3")
.handle(VoiceGatewayClient.this::handle)
.subscribe();
log.debug("VoiceGateway State Change: Stopped -> WaitingForHello");
return new WaitingForHello(websocketTask, start.connectedCallback);
});
when(WaitingForHello.class)
.on(Hello.class, (curState, hello) -> {
long heartbeatInterval = (long) (hello.getData().heartbeatInterval * .75); // it's wrong
Disposable heartbeatTask = Flux.interval(Duration.ofMillis(heartbeatInterval), Schedulers.elastic())
.map(Heartbeat::new)
.subscribe(VoiceGatewayClient.this::send);
send(new Identify(Long.toUnsignedString(serverId), Long.toUnsignedString(userId), sessionId, token));
log.debug("VoiceGateway State Change: WaitingForHello -> WaitingForReady");
return new WaitingForReady(curState.websocketTask, curState.connectedCallback, heartbeatTask);
});
when(WaitingForReady.class)
.on(Ready.class, (curState, ready) -> {
int ssrc = ready.getData().ssrc;
Disposable udpTask = voiceSocket.setup(ready.getData().ip, ready.getData().port)
.then(voiceSocket.performIpDiscovery(ssrc))
.subscribe(ipaddr -> {
String address = ipaddr.getHostName();
int port = ipaddr.getPort();
send(new SelectProtocol(VoiceSocket.PROTOCOL, address, port, VoiceSocket.ENCRYPTION_MODE));
});
log.debug("VoiceGateway State Change: WaitingForReady -> WaitingForSessionDescription");
return new WaitingForSessionDescription(curState.websocketTask, curState.connectedCallback, curState.heartbeatTask, ssrc, udpTask);
});
when(WaitingForSessionDescription.class)
.on(SessionDescription.class, (curState, sessionDesc) -> {
byte[] secretKey = sessionDesc.getData().secretKey;
TweetNaclFast.SecretBox boxer = new TweetNaclFast.SecretBox(secretKey);
PacketTransformer transformer = new PacketTransformer(curState.ssrc, boxer);
VoiceSendTask sendingTask = new VoiceSendTask(scheduler, VoiceGatewayClient.this, provider, transformer, curState.ssrc);
VoiceReceiveTask receivingTask = new VoiceReceiveTask(voiceSocket.getInbound(), transformer, receiver);
// we're completely connected
curState.connectedCallback.run();
log.debug("VoiceGateway State Change: WaitingForSessionDescription -> ReceivingEvents");
return new ReceivingEvents(curState.websocketTask, curState.connectedCallback, curState.heartbeatTask, curState.ssrc, curState.udpTask, secretKey, sendingTask, receivingTask);
});
when(ReceivingEvents.class)
.on(Stop.class, (curState, stop) -> {
// clean up running tasks
curState.heartbeatTask.dispose();
curState.sendingTask.dispose();
curState.receivingTask.dispose();
curState.udpTask.dispose();
log.debug("VoiceGateway State Change: ReceivingEvents -> Stopped");
return Stopped.INSTANCE;
});
whenAny()
.on(HeartbeatAck.class, (curState, ack) -> {
// TODO
return curState;
}).on(Speaking.class, (curState, speaking) -> {
// TODO
return curState;
})
.on(VoiceDisconnect.class, (curState, voiceDisconnect) -> {
// TODO
return curState;
});
}};
}
void start(String gatewayUrl, Runnable connectedCallback) { // TODO can this return the Mono?
gatewayFSM.onEvent(new Start(gatewayUrl, connectedCallback));
}
void stop() {
gatewayFSM.onEvent(new Stop());
}
private Mono handle(WebsocketInbound in, WebsocketOutbound out) {
Mono inboundThen = in.aggregateFrames()
.receiveFrames()
.map(WebSocketFrame::content)
.flatMap(buf -> Mono.fromCallable(() ->
mapper.readValue((InputStream) new ByteBufInputStream(buf), VoiceGatewayPayload.class)))
.doOnNext(gatewayFSM::onEvent)
.then();
Mono outboundThen = out.options(NettyPipeline.SendOptions::flushOnEach)
.sendObject(sender.flatMap(payload -> Mono.fromCallable(() ->
new TextWebSocketFrame(Unpooled.wrappedBuffer(mapper.writeValueAsBytes(payload))))))
.then();
return Mono.zip(inboundThen, outboundThen).then();
}
void send(VoiceGatewayPayload payload) {
sender.onNext(payload);
}
}