Discord4J es una biblioteca reactiva, rápida, potente y sin opiniones que permite el desarrollo rápido y sencillo de bots de Discord para Java, Kotlin y otros lenguajes JVM utilizando la API oficial de Discord Bot.
En este ejemplo de v3.2, cada vez que un usuario envía un mensaje !ping
, el bot responderá inmediatamente con Pong!
.
Asegúrese de que su bot tenga habilitada la intención Contenido del mensaje en su portal de desarrollador.
public class ExampleBot {
public static void main ( String [] args ) {
String token = args [ 0 ];
DiscordClient client = DiscordClient . create ( token );
GatewayDiscordClient gateway = client . login (). block ();
gateway . on ( MessageCreateEvent . class ). subscribe ( event -> {
Message message = event . getMessage ();
if ( "!ping" . equals ( message . getContent ())) {
MessageChannel channel = message . getChannel (). block ();
channel . createMessage ( "Pong!" ). block ();
}
});
gateway . onDisconnect (). block ();
}
}
Para ver un ejemplo de proyecto completo, consulte nuestro repositorio de proyectos de ejemplo aquí.
Reactivo : Discord4J sigue el protocolo de flujos reactivos para garantizar que los bots de Discord funcionen sin problemas y de manera eficiente, independientemente del tamaño.
Oficial : la limitación automática de velocidad, las estrategias de reconexión automática y las convenciones de nomenclatura consistentes se encuentran entre las muchas características que ofrece Discord4J para garantizar que sus bots de Discord funcionen según las especificaciones de Discord y para brindar la menor cantidad de sorpresas al interactuar con nuestra biblioteca.
Modular : Discord4J se divide en módulos para permitir a los usuarios avanzados interactuar con nuestra API en niveles inferiores para crear tiempos de ejecución mínimos y rápidos o incluso agregar sus propias abstracciones.
⚔️ Potente : Discord4J se puede utilizar para desarrollar cualquier bot, grande o pequeño. Ofrecemos muchas herramientas para desarrollar bots a gran escala a partir de marcos de distribución personalizados, almacenamiento en caché fuera del montón y su interacción con Reactor permite una integración completa con marcos como Spring y Micronaut.
? Comunidad : nos enorgullecemos de nuestra comunidad inclusiva y estamos dispuestos a ayudar cuando surjan desafíos; o si simplemente quieres charlar! Ofrecemos ayuda que va desde problemas específicos de Discord4J hasta ayuda general de programación y desarrollo web, e incluso preguntas específicas de Reactor. ¡Asegúrate de visitarnos en nuestro servidor de Discord!
repositories {
mavenCentral()
}
dependencies {
implementation ' com.discord4j:discord4j-core:3.2.7 '
}
repositories {
mavenCentral()
}
dependencies {
implementation( " com.discord4j:discord4j-core:3.2.7 " )
}
< dependencies >
< dependency >
< groupId >com.discord4j</ groupId >
< artifactId >discord4j-core</ artifactId >
< version >3.2.7</ version >
</ dependency >
</ dependencies >
libraryDependencies ++= Seq (
" com.discord4j " % " discord4j-core " % " 3.2.7 "
)
Discord4J 3.2.x incluye API más simples y potentes para crear solicitudes, un nuevo caché de entidades y mejoras de rendimiento a partir de actualizaciones de dependencias. Consulte nuestra Guía de migración para obtener más detalles.
discordia4J | Apoyo | Puerta de enlace/API | Intenciones | Interacciones |
---|---|---|---|---|
v3.3.x | En desarrollo | v9 | Valor predeterminado obligatorio y sin privilegios | Totalmente compatible |
v3.2.x | Actual | v8 | Valor predeterminado obligatorio y sin privilegios | Totalmente compatible |
v3.1.x | Sólo mantenimiento | v6 | Opcional, sin intención predeterminada | Sólo mantenimiento |
Consulte nuestros documentos para obtener más detalles sobre la compatibilidad.
Nos gustaría agradecer especialmente a todos nuestros patrocinadores por brindarnos los fondos para continuar desarrollando y albergando recursos de repositorio, así como por impulsar iniciativas para programas comunitarios. En particular, nos gustaría agradecer especialmente a estas maravillosas personas:
A continuación se muestran algunos ejemplos del mundo real de robots grandes que utilizan Discord4J:
¿Tienes un bot grande que usa Discord4J? ¡Pregúntale a un administrador en nuestro Discord o envía una solicitud de extracción para agregar tu bot a la lista!
Discord4J utiliza Project Reactor como base para nuestro marco asincrónico. Reactor proporciona una API simple pero extremadamente poderosa que permite a los usuarios reducir recursos y aumentar el rendimiento.
public class ExampleBot {
public static void main ( String [] args ) {
String token = args [ 0 ];
DiscordClient client = DiscordClient . create ( token );
client . login (). flatMapMany ( gateway -> gateway . on ( MessageCreateEvent . class ))
. map ( MessageCreateEvent :: getMessage )
. filter ( message -> "!ping" . equals ( message . getContent ()))
. flatMap ( Message :: getChannel )
. flatMap ( channel -> channel . createMessage ( "Pong!" ))
. blockLast ();
}
}
Discord4J también proporciona varios métodos para ayudar a mejorar las composiciones de la cadena reactiva, como GatewayDiscordClient#withGateway
y EventDispatcher#on
con una sobrecarga de manejo de errores.
public class ExampleBot {
public static void main ( String [] args ) {
String token = args [ 0 ];
DiscordClient client = DiscordClient . create ( token );
client . withGateway ( gateway -> {
Publisher <?> pingPong = gateway . on ( MessageCreateEvent . class , event ->
Mono . just ( event . getMessage ())
. filter ( message -> "!ping" . equals ( message . getContent ()))
. flatMap ( Message :: getChannel )
. flatMap ( channel -> channel . createMessage ( "Pong!" )));
Publisher <?> onDisconnect = gateway . onDisconnect ()
. doOnTerminate (() -> System . out . println ( "Disconnected!" ));
return Mono . when ( pingPong , onDisconnect );
}). block ();
}
}
Al utilizar Reactor, Discord4J tiene integración nativa con las corrutinas de Kotlin cuando se combina con la biblioteca kotlinx-coroutines-reactor.
val token = args[ 0 ]
val client = DiscordClient .create(token)
client.withGateway {
mono {
it.on( MessageCreateEvent :: class .java)
.asFlow()
.collect {
val message = it.message
if (message.content == " !ping " ) {
val channel = message.channel.awaitSingle()
channel.createMessage( " Pong! " ).awaitSingle()
}
}
}
}
.block()
A partir del 1 de septiembre de 2022, Discord requiere que los bots habiliten la intención "MESSAGE_CONTENT" para acceder al contenido de los mensajes. Para habilitar la intención, vaya al Portal para desarrolladores de Discord y seleccione su bot. Luego, vaya a la pestaña "Bot" y habilite la intención "Contenido del mensaje". Luego, agregue la intención a su bot al crear DiscordClient:
GatewayDiscordClient client = DiscordClient . create ( token )
. gateway ()
. setEnabledIntents ( IntentSet . nonPrivileged (). or ( IntentSet . of ( Intent . MESSAGE_CONTENT )))
. login ()
. block ();
// IMAGE_URL = https://cdn.betterttv.net/emote/55028cd2135896936880fdd7/3x
// ANY_URL = https://www.youtube.com/watch?v=5zwY50-necw
MessageChannel channel = ...
EmbedCreateSpec . Builder builder = EmbedCreateSpec . builder ();
builder . author ( "setAuthor" , ANY_URL , IMAGE_URL );
builder . image ( IMAGE_URL );
builder . title ( "setTitle/setUrl" );
builder . url ( ANY_URL );
builder . description ( "setDescription n " +
"big D: is setImage n " +
"small D: is setThumbnail n " +
"<-- setColor" );
builder . addField ( "addField" , "inline = true" , true );
builder . addField ( "addFIeld" , "inline = true" , true );
builder . addField ( "addFile" , "inline = false" , false );
builder . thumbnail ( IMAGE_URL );
builder . footer ( "setFooter --> setTimestamp" , IMAGE_URL );
builder . timestamp ( Instant . now ());
channel . createMessage ( builder . build ()). block ();
Los usuarios suelen preferir trabajar con nombres en lugar de identificaciones. Este ejemplo demostrará cómo buscar todos los miembros que tienen un rol con un nombre específico.
Guild guild = ...
Set < Member > roleMembers = new HashSet <>();
for ( Member member : guild . getMembers (). toIterable ()) {
for ( Role role : member . getRoles (). toIterable ()) {
if ( "Developers" . equalsIgnoreCase ( role . getName ())) {
roleMembers . add ( member );
break ;
}
}
}
return roleMembers ;
Alternativamente, usando Reactor:
Guild guild = ...
return guild . getMembers ()
. filterWhen ( member -> member . getRoles ()
. map ( Role :: getName )
. any ( "Developers" :: equalsIgnoreCase ));
Discord4J brinda soporte completo para conexiones de voz y la capacidad de enviar audio a otros usuarios conectados al mismo canal. Discord4J puede aceptar cualquier fuente de audio Opus y LavaPlayer es la solución preferida para descargar y codificar audio de YouTube, SoundCloud y otros proveedores.
Advertencia
El LavaPlayer original ya no se mantiene. Puede encontrar una nueva versión mantenida aquí. Si necesita compatibilidad con Java 8, puede utilizar la bifurcación LavaPlayer de Walkyst, ¡pero ya no se mantiene!
Para comenzar, primero deberá crear una instancia y configurar un AudioPlayerManager
, convencionalmente global.
public static final AudioPlayerManager PLAYER_MANAGER ;
static {
PLAYER_MANAGER = new DefaultAudioPlayerManager ();
// This is an optimization strategy that Discord4J can utilize to minimize allocations
PLAYER_MANAGER . getConfiguration (). setFrameBufferFactory ( NonAllocatingAudioFrameBuffer :: new );
AudioSourceManagers . registerRemoteSources ( PLAYER_MANAGER );
AudioSourceManagers . registerLocalSource ( PLAYER_MANAGER );
}
A continuación, debemos permitir que Discord4J lea desde un AudioPlayer
a un AudioProvider
.
public class LavaPlayerAudioProvider extends AudioProvider {
private final AudioPlayer player ;
private final MutableAudioFrame frame ;
public LavaPlayerAudioProvider ( AudioPlayer player ) {
// Allocate a ByteBuffer for Discord4J's AudioProvider to hold audio data for Discord
super ( ByteBuffer . allocate ( StandardAudioDataFormats . DISCORD_OPUS . maximumChunkSize ()));
// Set LavaPlayer's AudioFrame to use the same buffer as Discord4J's
frame = new MutableAudioFrame ();
frame . setBuffer ( getBuffer ());
this . player = player ;
}
@ Override
public boolean provide () {
// AudioPlayer writes audio data to the AudioFrame
boolean didProvide = player . provide ( frame );
if ( didProvide ) {
getBuffer (). flip ();
}
return didProvide ;
}
}
Por lo general, los reproductores de audio tendrán colas o listas de reproducción internas para que los usuarios puedan recorrer automáticamente las canciones a medida que terminan o solicitan omitirlas. Podemos administrar esta cola externamente y pasarla a otras áreas de nuestro código para permitir que las pistas se vean, pongan en cola u omitan creando un AudioTrackScheduler
.
public class AudioTrackScheduler extends AudioEventAdapter {
private final List < AudioTrack > queue ;
private final AudioPlayer player ;
public AudioTrackScheduler ( AudioPlayer player ) {
// The queue may be modifed by different threads so guarantee memory safety
// This does not, however, remove several race conditions currently present
queue = Collections . synchronizedList ( new LinkedList <>());
this . player = player ;
}
public List < AudioTrack > getQueue () {
return queue ;
}
public boolean play ( AudioTrack track ) {
return play ( track , false );
}
public boolean play ( AudioTrack track , boolean force ) {
boolean playing = player . startTrack ( track , ! force );
if (! playing ) {
queue . add ( track );
}
return playing ;
}
public boolean skip () {
return ! queue . isEmpty () && play ( queue . remove ( 0 ), true );
}
@ Override
public void onTrackEnd ( AudioPlayer player , AudioTrack track , AudioTrackEndReason endReason ) {
// Advance the player if the track completed naturally (FINISHED) or if the track cannot play (LOAD_FAILED)
if ( endReason . mayStartNext ) {
skip ();
}
}
}
Actualmente, Discord solo permite 1 conexión de voz por servidor. Trabajando dentro de esta limitación, es lógico pensar que los 3 componentes con los que hemos trabajado hasta ahora ( AudioPlayer
, LavaPlayerAudioProvider
y AudioTrackScheduler
) estén correlacionados con un Guild
específico, naturalmente único por algún Snowflake
. Lógicamente, tiene sentido combinar estos objetos en uno, de modo que puedan colocarse en un Map
para recuperarlos más fácilmente cuando se conecta a un canal de voz o cuando se trabaja con comandos.
public class GuildAudioManager {
private static final Map < Snowflake , GuildAudioManager > MANAGERS = new ConcurrentHashMap <>();
public static GuildAudioManager of ( Snowflake id ) {
return MANAGERS . computeIfAbsent ( id , ignored -> new GuildAudioManager ());
}
private final AudioPlayer player ;
private final AudioTrackScheduler scheduler ;
private final LavaPlayerAudioProvider provider ;
private GuildAudioManager () {
player = PLAYER_MANAGER . createPlayer ();
scheduler = new AudioTrackScheduler ( player );
provider = new LavaPlayerAudioProvider ( player );
player . addListener ( scheduler );
}
// getters
}
Finalmente, necesitamos conectarnos al canal de voz. Después de conectarse, se le proporciona un objeto VoiceConnection
donde puede utilizarlo más tarde para desconectarse del canal de voz llamando VoiceConnection#disconnect
.
VoiceChannel channel = ...
AudioProvider provider = GuildAudioManager . of ( channel . getGuildId ()). getProvider ();
VoiceConnection connection = channel . join ( spec -> spec . setProvider ( provider )). block ();
// In the AudioLoadResultHandler, add AudioTrack instances to the AudioTrackScheduler (and send notifications to users)
PLAYER_MANAGER . loadItem ( "https://www.youtube.com/watch?v=dQw4w9WgXcQ" , new AudioLoadResultHandler () { /* overrides */ })
Normalmente, después de que todos hayan abandonado un canal de voz, el bot debería desconectarse automáticamente, ya que los usuarios suelen olvidarse de desconectarlo manualmente. Este problema se puede resolver de manera bastante elegante utilizando un enfoque reactivo en lugar de uno imperativo, como lo demuestra el siguiente ejemplo.
VoiceChannel channel = ...
Mono < Void > onDisconnect = channel . join ( spec -> { /* TODO Initialize */ })
. flatMap ( connection -> {
// The bot itself has a VoiceState; 1 VoiceState signals bot is alone
Publisher < Boolean > voiceStateCounter = channel . getVoiceStates ()
. count ()
. map ( count -> 1L == count );
// After 10 seconds, check if the bot is alone. This is useful if
// the bot joined alone, but no one else joined since connecting
Mono < Void > onDelay = Mono . delay ( Duration . ofSeconds ( 10L ))
. filterWhen ( ignored -> voiceStateCounter )
. switchIfEmpty ( Mono . never ())
. then ();
// As people join and leave `channel`, check if the bot is alone.
// Note the first filter is not strictly necessary, but it does prevent many unnecessary cache calls
Mono < Void > onEvent = channel . getClient (). getEventDispatcher (). on ( VoiceStateUpdateEvent . class )
. filter ( event -> event . getOld (). flatMap ( VoiceState :: getChannelId ). map ( channel . getId ():: equals ). orElse ( false ))
. filterWhen ( ignored -> voiceStateCounter )
. next ()
. then ();
// Disconnect the bot if either onDelay or onEvent are completed!
return Mono . first ( onDelay , onEvent ). then ( connection . disconnect ());
});