All Downloads are FREE. Search and download functionalities are using the official Maven repository.

discord4j.voice.VoiceGatewayClient Maven / Gradle / Ivy

There is a newer version: 3.3.0-RC1
Show newest version
/*
 * 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);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy