discord4j.voice.VoiceGatewayClient.java Source code

Java tutorial

Introduction

Here is the source code for discord4j.voice.VoiceGatewayClient.java

Source

/*
 * 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 <http://www.gnu.org/licenses/>.
 */
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<VoiceGatewayState, VoiceGatewayEvent> gatewayFSM;

    private final EmitterProcessor<VoiceGatewayPayload<?>> 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<VoiceGatewayState, VoiceGatewayEvent>() {
            {
                startWith(Stopped.INSTANCE);

                when(Stopped.class).on(Start.class, (curState, start) -> {
                    Disposable websocketTask = HttpClient.create().wiretap(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<VoiceConnection>?
        gatewayFSM.onEvent(new Start(gatewayUrl, connectedCallback));
    }

    void stop() {
        gatewayFSM.onEvent(new Stop());
    }

    private Mono<Void> handle(WebsocketInbound in, WebsocketOutbound out) {
        Mono<Void> inboundThen = in.aggregateFrames().receiveFrames().map(WebSocketFrame::content)
                .flatMap(buf -> Mono.fromCallable(() -> mapper.readValue((InputStream) new ByteBufInputStream(buf),
                        VoiceGatewayPayload.class)))
                .doOnNext(gatewayFSM::onEvent).then();

        Mono<Void> 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();
    }

    <T> void send(VoiceGatewayPayload<T> payload) {
        sender.onNext(payload);
    }
}