Guía para el desarrollo de un bot conversacional usando Discord4j, Spring Boot y programación reactiva

Discord es actualmente una de las plataformas más famosas para gaming y en estos tiempos pandémicos en los que nos ha tocado vivir, su uso se ha extendido hasta utilizar sus servidores para la organización desde congresos hasta jornadas virtuales de rol en vivo. Por ello, aunque el catálogo de bots disponibles en la comunidad es muy amplio, es probable que ninguno de ellos se ajuste a tus necesidades y prefieras programar tu propio bot o poner un granito de arena y colaborar con su comunidad de desarrolladores.

Si hacemos una búsqueda rápida en Internet, vemos que estos bots que por definición deben ser reactivos y limitar al mínimo el bloqueo de sus hilos durante la ejecución, habitualmente se programan con NodeJs. Sin embargo, gracias a la librería Discord4j y Spring Boot Webflux que incorpora como dependencia la librería Reactor, es posible también desarrollar un bot conversacional utilizando Java (1.8 o superior) y convertirlo en un microservicio que pueda ser desplegado en cualquier servidor Cloud.

En este artículo vamos a introducir con unas pequeñas pinceladas la programación reactiva, un nuevo paradigma de programación que nos permitirá programar código que cumpla con el manifiesto reactivo. Para ello vamos a explicar cómo programar un bot totalmente reactivo que imprima en un canal de texto chistes de programación cuando un usuario se lo solicite mediante la ejecución de un comando determinado. Los chistes no nos los inventaremos, sino que los obtendremos invocando a una API Rest pública disponible en Internet. Para seguir este manual deberás tener cuenta de usuario en Discord y al menos un Discord Server con privilegios de administrador.

Puedes descargar el código completo en github.

Desarrollando un bot conversacional paso a paso

El primer paso es acceder al Discord Developer Portal. Una vez dentro hacer click en el botón New Application.

Debemos completar el formulario asignándole un nombre y un icono a nuestra aplicación. Este icono será el aspecto visual de nuestro bot.

Por último, hacemos click en el menú lateral a Bot y solicitamos a Discord que convierta nuestra aplicación en un bot.

Para Discord nuestro bot ya está dado de alta y todo está listo para vincularlo a nuestro código y empezar a usarlo.

Sin embargo, aún no hemos desarrollado ni una línea de código. Para ello vamos a abrir nuestro IDE favorito y crear un microservicio basado en Spring Boot. Si no recuerdas cómo se crea un microservicio tienes disponible el artículo enlazado.

Abrimos el fichero pom.xml y añadimos las siguientes dependencias:

<!-- Spring starter for reactive programming  -->
<dependency>
<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<!--Discord library -->
<dependency>
	<groupId>com.discord4j</groupId>
	<artifactId>discord4j-core</artifactId>
	<version>3.1.6</version>
</dependency>

Spring-boot-starter-webflux es un starter que incorpora al proyecto las librerías necesarias para ejecutar Spring de forma reactiva. Discord4j-core añade las librerías necesarias para la comunicación con Discord.

Vamos a crear el servicio que nos permitirá comunicarnos con la API Rest que nos proporcionará los chistes sobre programación que publicará nuestro bot. Como queremos que sea reactiva y no bloquee la ejecución de nuestro bot en vez de usar el cliente habitual de Spring RestTemplate usaremos la versión reactiva de este: Webclient.

public class JokeClient {

	
   public String getJoke() {	
		
	WebClient webClient = WebClient.builder()
		.baseUrl("https://v2.jokeapi.dev")
		.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
		.build();
		
	Mono<JokeModel> respuesta = webClient.get()
	        .uri("/joke/Programming?type=single")
	        .retrieve()
	        .bodyToMono(JokeModel.class);


	return respuesta.block().getJoke();
   }

}

Como puede verse el código de este cliente es muy sencillo. Utilizamos el builder que Spring nos proporciona de WebClient para crear la instancia del cliente que utilizaremos e inicializarlo con la url base del servicio con el que nos vamos a conectar. Además configuramos las cabeceras por defecto de la petición http que va a crear cuando invoque una operación.

Una vez creado y configurado el cliente, invocamos la operación get que nos interesa pasándole los parámetros que necesitamos directamente en la url. El resultado es un objeto de tipo Mono<JokeModel>, que es la versión reactiva del objeto JokeModel, con el contenido del chiste. Si es la primera vez que te enfrentas a Spring WebFlux y a la programación reactiva quizás quieras echarle un ojo a la documentación oficial publicada por la comunidad Spring.

Discord se comunica con sus bots a través de eventos. Tiene disponible eventos que se ejecutan cada vez que se crea (MessageCreateEvent), se edita (MessageUpdateEvent) o se elimina un mensaje (MessageDeleteEvent) entre tantos otros. En nuestro caso, nuestro bot solo atenderá eventos de tipo MessageCreateEvent.

Creamos una clase que haga de listener para que lea y procese este tipo de eventos. Todos nuestros eventos implementarán la interfaz de creación propia EventListener. Declaramos nuestro listener como un bean de tipo servicio gracias a la etiqueta @Service e implementamos en el método processCommand la lógica para procesar el comando.

@Service
public class MessageCreateListener  implements EventListener<MessageCreateEvent> {
	
    @Autowired
    private JokeClient jokeClient;

    @Override
    public Class<MessageCreateEvent> getEventType() {
        return MessageCreateEvent.class;
    }

    @Override
    public Mono<Void> execute(MessageCreateEvent event) {
        return processCommand(event.getMessage());
    }
   
    private Mono<Void> processCommand(Message eventMessage) {
        return Mono.just(eventMessage)
           .filter(message -> (message.getContent().startsWith("!joke")))        		
           .filter(message -> message.getAuthor().map(user -> !user.isBot()).orElse(false))
           .flatMap(Message::getChannel)
           .flatMap(channel -> channel.createMessage(jokeClient.getJoke()))
           .then();
    }
}

Es muy importante que este listener sea lo más reactivo posible ya que hay que tener en cuenta que absolutamente todos los eventos de tipo MessageCreateEvent pasarán por él. Nuestro bot sabrá que un mensaje va dirigido a él si el texto de dicho mensaje empieza con la cadena “!joke”, por lo tanto, debemos de asegurarnos de descartar todos los mensajes que no empiecen por “!joke” lo más rápido posible para no afectar al rendimiento del bot y en última instancia el rendimiento del propio Discord Server desde el que se esté ejecutando. Puesto que nuestro bot solo atenderá mensajes creados por humanos también descartaremos aquellos mensajes creados por bots.

Cuando nuestro bot detecta que un mensaje va dirigido a él invoca al cliente web JokeClient que tiene configurado como dependencia gracias a la etiqueta @Autoriwed y devuelve como mensaje la respuesta de este cliente.

Si volvemos al Discord Developer Portal, en OAuth2 podremos copiar el token de autenticación que Discord ha establecido para comunicarse con nuestro bot:

Copiamos ese token y lo ponemos en nuestro fichero application.properties

discord.token: copy_here_your_bot_discord_auth_token

En este punto ya tenemos creado el servicio que obtiene los chistes que va a contar nuestro bot y también tenemos creado el listener que nos va a permitir recoger cuando un usuario desde un canal de texto de un Discord Server solicita un chiste mediante el comando “!joke” por último nos queda crear los bean necesarios para que todo esto se ejecute correctamente.

Creamos el fichero de configuración de nuestro microservicio donde declararemos los beans necesarios para la correcta ejecución de nuestro bot:

@Configuration
public class BotConfiguration {

    private static final Logger log = LoggerFactory.getLogger( BotConfiguration.class );

    @Value("${discord.token}")
    private String token;

    @Bean
    public <T extends Event> GatewayDiscordClient gatewayDiscordClient(List<EventListener<T>> eventListeners) {
        GatewayDiscordClient client = null;

        try {
            client = DiscordClientBuilder.create(token)
              .build()
              .login()
              .block();

            for(EventListener<T> listener : eventListeners) {
                client.getEventDispatcher().on(listener.getEventType())
                  .flatMap(listener::execute)
                  .onErrorResume(listener::handleError)
  .subscribe(event -> {System.out.println("Event registered: " +  listener.getEventType());});
            } 
            
            client.onDisconnect().block();
            
        }
        catch ( Exception exception ) {
            log.error( "Be sure to use a valid bot token!", exception );
        }

        return client;
    }
    @Bean
    public JokeClient jokeClient() {
    	return new JokeClient();
    }
}

El bean gatewayDiscordClient es el que configura la comunicación con Discord. Para establecer correctamente esta conexión necesita el token de autenticación que Discord ha creado para nuestro bot. Este token está parametrizado en el fichero application.properties con el identificador discord.token. Este bean además itera sobre todos bean que implementen la interfaz EventListener y se subscribe a ellos, de esta manera cuando desde nuestro canal de mensajes se cree un evento de creación de mensaje nuestro bot será informado de ello y podrá iniciar su funcionamiento.

Finalmente tenemos nuestro bot completamente implementado y solo nos falta instalarlo en nuestro Discord Server. Nuevamente desde Discord Developer Portal volvemos a OAuth2. Seleccionamos en el apartado Scopes la opción bot y configuramos los Bot Permissions. Dada la funcionalidad de nuestro bot en este caso solo le daremos permisos del tipo Send Messages.

Copiamos la url que se ha generado y la abrimos en un navegador web. El formulario nos pedirá que indiquemos en qué Discord Server queremos instalar el bot y nos mostrará qué permisos vamos a darle a ese bot en nuestro servidor. Una vez finalizado el proceso si todo ha ido bien tendremos un mensaje como este:

Nuestro bot ya está instalado pero aparece como desconectado porque no tenemos el código levantado y corriendo. Para hacer pruebas en local basta con arrancar en nuestro IDE la aplicación Spring Boot de la manera habitual. Al arrancar obtendremos un log parecido al que se muestra a continuación:

Sin embargo dependiendo de nuestra conexión de red, la conexión con discord y las pruebas en local pueden ser algo lentas por lo que se recomienda, una vez finalizado el proceso de desarrollo, crear el correspondiente fichero Dockerfile de la aplicación y desplegar el bot en un servidor cloud para así tenerlo disponible aunque el entorno local esté parado. Puedes consultar como “dockerizar” un microservicio en este artículo de nuestro blog.

Una vez arrancado nuestro bot nos aparecerá en Discord como “conectado” y estaremos listos para realizar una prueba.

Nuestro bot ya está finalizado, corriendo y listo para ser el alma de la fiesta ☺.