Discord4J est une bibliothèque rapide, puissante, sans opinion et réactive pour permettre le développement rapide et facile de robots Discord pour Java, Kotlin et d'autres langages JVM à l'aide de l'API officielle Discord Bot.
Dans cet exemple pour la version 3.2, chaque fois qu'un utilisateur envoie un message !ping
, le bot répondra immédiatement avec Pong!
.
Assurez-vous que l'intention Contenu du message est activée sur votre bot dans votre portail de développeur.
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 ();
}
}
Pour un exemple de projet complet, consultez notre référentiel de projets d'exemples ici.
Réactif - Discord4J suit le protocole de flux réactifs pour garantir que les robots Discord fonctionnent correctement et efficacement, quelle que soit leur taille.
Officiel - Limitation automatique du débit, stratégies de reconnexion automatique et conventions de dénomination cohérentes font partie des nombreuses fonctionnalités proposées par Discord4J pour garantir que vos robots Discord fonctionnent conformément aux spécifications de Discord et pour fournir le moins de surprises lors de l'interaction avec notre bibliothèque.
Modulaire - Discord4J se divise en modules pour permettre aux utilisateurs avancés d'interagir avec notre API à des niveaux inférieurs pour créer des environnements d'exécution minimaux et rapides ou même ajouter leurs propres abstractions.
⚔️ Puissant – Discord4J peut être utilisé pour développer n’importe quel bot, grand ou petit. Nous proposons de nombreux outils pour développer des robots à grande échelle à partir de frameworks de distribution personnalisés, de mise en cache hors tas, et son interaction avec Reactor permet une intégration complète avec des frameworks tels que Spring et Micronaut.
? Communauté - Nous sommes fiers de notre communauté inclusive et sommes prêts à vous aider chaque fois que des défis surviennent ; ou si vous voulez juste discuter ! Nous proposons une aide allant des problèmes spécifiques à Discord4J à une aide générale en matière de programmation et de développement Web, et même des questions spécifiques à Reactor. N'oubliez pas de nous rendre visite sur notre serveur 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 comprend des API plus simples et plus puissantes pour créer des requêtes, un nouveau cache d'entités et des améliorations de performances grâce aux mises à niveau des dépendances. Consultez notre guide de migration pour plus de détails.
Discord4J | Soutien | Passerelle/API | Intentions | Interactions |
---|---|---|---|---|
v3.3.x | En développement | v9 | Par défaut obligatoire et non privilégié | Entièrement pris en charge |
v3.2.x | Actuel | v8 | Par défaut obligatoire et non privilégié | Entièrement pris en charge |
v3.1.x | Entretien uniquement | v6 | Facultatif, aucune intention par défaut | Entretien uniquement |
Consultez nos documents pour plus de détails sur la compatibilité.
Nous tenons à remercier tout particulièrement tous nos sponsors pour nous avoir fourni le financement nécessaire pour continuer à développer et à héberger des ressources de référentiel ainsi qu'à faire avancer les initiatives pour les programmes communautaires. Nous souhaitons tout particulièrement remercier ces merveilleuses personnes :
Voici quelques exemples concrets de gros robots utilisant Discord4J :
Possédez-vous un gros bot utilisant Discord4J ? Demandez à un administrateur dans notre Discord ou soumettez une pull request pour ajouter votre bot à la liste !
Discord4J utilise Project Reactor comme base de notre framework asynchrone. Reactor fournit une API simple mais extrêmement puissante qui permet aux utilisateurs de réduire les ressources et d'augmenter les performances.
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 fournit également plusieurs méthodes pour faciliter de meilleures compositions de chaînes réactives, telles que GatewayDiscordClient#withGateway
et EventDispatcher#on
avec une surcharge de gestion des erreurs.
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 ();
}
}
En utilisant Reactor, Discord4J dispose d'une intégration native avec les coroutines Kotlin lorsqu'il est associé à la bibliothèque 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()
À compter du 1er septembre 2022, Discord exige que les robots activent l'intention « MESSAGE_CONTENT » pour accéder au contenu des messages. Pour activer l'intention, accédez au portail des développeurs Discord et sélectionnez votre bot. Ensuite, allez dans l'onglet « Bot » et activez l'intention « Contenu du message ». Ensuite, ajoutez l'intention à votre bot lors de la création du 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 ();
Les utilisateurs préfèrent généralement travailler avec des noms plutôt qu’avec des identifiants. Cet exemple montrera comment rechercher tous les membres ayant un rôle avec un nom spécifique.
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 ;
Alternativement, en utilisant Reactor :
Guild guild = ...
return guild . getMembers ()
. filterWhen ( member -> member . getRoles ()
. map ( Role :: getName )
. any ( "Developers" :: equalsIgnoreCase ));
Discord4J offre une prise en charge complète des connexions vocales et la possibilité d'envoyer de l'audio à d'autres utilisateurs connectés au même canal. Discord4J peut accepter n'importe quelle source audio Opus, LavaPlayer étant la solution préférée pour télécharger et encoder de l'audio à partir de YouTube, SoundCloud et d'autres fournisseurs.
Avertissement
Le LavaPlayer d'origine n'est plus maintenu. Une nouvelle version maintenue peut être trouvée ici. Si vous avez besoin du support de Java 8, vous pouvez utiliser le fork LavaPlayer de Walkyst, mais il n'est également plus maintenu !
Pour commencer, vous devrez d’abord instancier et configurer un AudioPlayerManager
, classiquement 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 );
}
Ensuite, nous devons autoriser Discord4J à lire depuis un AudioPlayer
vers 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 ;
}
}
En règle générale, les lecteurs audio disposent de files d'attente ou de listes de lecture internes permettant aux utilisateurs de parcourir automatiquement les chansons lorsqu'elles sont terminées ou lorsqu'il est demandé de les ignorer. Nous pouvons gérer cette file d'attente en externe et la transmettre à d'autres zones de notre code pour permettre aux pistes d'être visualisées, mises en file d'attente ou ignorées en créant 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 ();
}
}
}
Actuellement, Discord n'autorise qu'une seule connexion vocale par serveur. En travaillant dans cette limitation, il est logique de penser que les 3 composants avec lesquels nous avons travaillé jusqu'à présent ( AudioPlayer
, LavaPlayerAudioProvider
et AudioTrackScheduler
) doivent être corrélés à une Guild
spécifique, naturellement unique par certains Snowflake
. Logiquement, il est logique de combiner ces objets en un seul, afin qu'ils puissent être placés dans une Map
pour une récupération plus facile lors de la connexion à un canal vocal ou lorsque vous travaillez avec des commandes.
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
}
Enfin, nous devons nous connecter au canal vocal. Après la connexion, vous recevez un objet VoiceConnection
que vous pourrez utiliser ultérieurement pour vous déconnecter du canal vocal en appelant 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 */ })
En règle générale, une fois que tout le monde a quitté un canal vocal, le bot doit se déconnecter automatiquement, car les utilisateurs oublient généralement de déconnecter le bot manuellement. Ce problème peut être résolu de manière assez élégante en utilisant une approche réactive plutôt qu'impérative, comme le démontre l'exemple ci-dessous.
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 ());
});