Discord4J ist eine schnelle, leistungsstarke, eigensinnige und reaktive Bibliothek, die die schnelle und einfache Entwicklung von Discord-Bots für Java, Kotlin und andere JVM-Sprachen mithilfe der offiziellen Discord Bot-API ermöglicht.
In diesem Beispiel für Version 3.2 antwortet der Bot sofort mit Pong!
“, wenn ein Benutzer eine !ping
-Nachricht sendet. .
Stellen Sie sicher, dass für Ihren Bot die Absicht „Nachrichteninhalt“ in Ihrem Entwicklerportal aktiviert ist.
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 ();
}
}
Ein vollständiges Projektbeispiel finden Sie hier in unserem Beispielprojekt-Repository.
Reaktiv – Discord4J folgt dem Reactive-Streams-Protokoll, um sicherzustellen, dass Discord-Bots unabhängig von ihrer Größe reibungslos und effizient laufen.
Offiziell – Automatische Ratenbegrenzung, automatische Wiederverbindungsstrategien und konsistente Namenskonventionen gehören zu den vielen Funktionen, die Discord4J bietet, um sicherzustellen, dass Ihre Discord-Bots den Spezifikationen von Discord entsprechen und bei der Interaktion mit unserer Bibliothek für möglichst wenige Überraschungen sorgen.
Modular – Discord4J ist in Module unterteilt, um fortgeschrittenen Benutzern die Interaktion mit unserer API auf niedrigeren Ebenen zu ermöglichen, um minimale und schnelle Laufzeiten zu erstellen oder sogar ihre eigenen Abstraktionen hinzuzufügen.
⚔️ Leistungsstark – Discord4J kann zur Entwicklung jedes Bots, ob groß oder klein, verwendet werden. Wir bieten viele Tools für die Entwicklung großer Bots aus benutzerdefinierten Distributions-Frameworks, Off-Heap-Caching und die Interaktion mit Reactor ermöglicht die vollständige Integration mit Frameworks wie Spring und Micronaut.
? Gemeinschaft – Wir sind stolz auf unsere integrative Gemeinschaft und sind bereit zu helfen, wann immer sich Herausforderungen ergeben; oder wenn Sie einfach nur chatten möchten! Wir bieten Hilfe an, die von Discord4J-spezifischen Problemen über allgemeine Programmier- und Webentwicklungshilfe bis hin zu Reactor-spezifischen Fragen reicht. Besuchen Sie uns unbedingt auf unserem Discord-Server!
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 enthält einfachere und leistungsfähigere APIs für Build-Anfragen, einen neuen Entity-Cache und Leistungsverbesserungen durch Abhängigkeits-Upgrades. Weitere Informationen finden Sie in unserem Migrationsleitfaden.
Discord4J | Unterstützung | Gateway/API | Absichten | Interaktionen |
---|---|---|---|---|
v3.3.x | In der Entwicklung | v9 | Obligatorischer, nicht privilegierter Standard | Vollständig unterstützt |
v3.2.x | Aktuell | v8 | Obligatorischer, nicht privilegierter Standard | Vollständig unterstützt |
v3.1.x | Nur Wartung | v6 | Optional, kein Absichtsstandard | Nur Wartung |
Weitere Informationen zur Kompatibilität finden Sie in unseren Dokumenten.
Wir möchten allen unseren Sponsoren einen besonderen Dank dafür aussprechen, dass sie uns die Mittel für die weitere Entwicklung und Bereitstellung von Repository-Ressourcen sowie für die Förderung von Initiativen für Gemeinschaftsprogramme zur Verfügung gestellt haben. Ganz besonders möchten wir diesen wunderbaren Menschen ein besonderes Lob aussprechen:
Hier sind einige Beispiele aus der Praxis, wie große Bots Discord4J verwenden:
Besitzen Sie einen großen Bot, der Discord4J nutzt? Bitten Sie einen Administrator in unserem Discord oder senden Sie eine Pull-Anfrage, um Ihren Bot zur Liste hinzuzufügen!
Discord4J verwendet Project Reactor als Grundlage für unser asynchrones Framework. Reactor bietet eine einfache, aber äußerst leistungsstarke API, die es Benutzern ermöglicht, Ressourcen zu reduzieren und die Leistung zu steigern.
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 bietet außerdem mehrere Methoden zur Unterstützung besserer reaktiver Kettenkompositionen, z. B. GatewayDiscordClient#withGateway
und EventDispatcher#on
mit einer Fehlerbehandlungsüberladung.
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 ();
}
}
Durch die Verwendung von Reactor verfügt Discord4J über eine native Integration mit Kotlin-Coroutinen, wenn es mit der Bibliothek kotlinx-coroutines-reactor gekoppelt wird.
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()
Ab dem 1. September 2022 verlangt Discord von Bots, dass sie die Absicht „MESSAGE_CONTENT“ aktivieren, um auf den Inhalt von Nachrichten zuzugreifen. Um die Absicht zu aktivieren, gehen Sie zum Discord-Entwicklerportal und wählen Sie Ihren Bot aus. Gehen Sie dann zur Registerkarte „Bot“ und aktivieren Sie die Absicht „Nachrichteninhalt“. Fügen Sie dann beim Erstellen des DiscordClient die Absicht zu Ihrem Bot hinzu:
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 ();
Benutzer bevorzugen normalerweise die Arbeit mit Namen statt mit IDs. In diesem Beispiel wird gezeigt, wie nach allen Mitgliedern gesucht wird, die eine Rolle mit einem bestimmten Namen haben.
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 ;
Alternativ mit Reactor:
Guild guild = ...
return guild . getMembers ()
. filterWhen ( member -> member . getRoles ()
. map ( Role :: getName )
. any ( "Developers" :: equalsIgnoreCase ));
Discord4J bietet volle Unterstützung für Sprachverbindungen und die Möglichkeit, Audio an andere Benutzer zu senden, die mit demselben Kanal verbunden sind. Discord4J kann jede Opus-Audioquelle akzeptieren, wobei LavaPlayer die bevorzugte Lösung zum Herunterladen und Kodieren von Audio von YouTube, SoundCloud und anderen Anbietern ist.
Warnung
Der ursprüngliche LavaPlayer wird nicht mehr gepflegt. Eine neue gepflegte Version finden Sie hier. Wenn Sie Java 8-Unterstützung benötigen, können Sie den LavaPlayer-Fork von Walkyst verwenden, dieser wird jedoch ebenfalls nicht mehr gepflegt!
Um zu beginnen, müssen Sie zunächst einen konventionell globalen AudioPlayerManager
instanziieren und konfigurieren.
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 );
}
Als nächstes müssen wir Discord4J erlauben, von einem AudioPlayer
zu einem AudioProvider
zu lesen.
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 ;
}
}
In der Regel verfügen Audioplayer über Warteschlangen oder interne Wiedergabelisten, damit Benutzer automatisch durch die Titel blättern können, wenn sie fertig sind oder übersprungen werden sollen. Wir können diese Warteschlange extern verwalten und an andere Bereiche unseres Codes übergeben, damit Titel angezeigt, in die Warteschlange gestellt oder übersprungen werden können, indem wir einen AudioTrackScheduler
erstellen.
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 ();
}
}
}
Derzeit erlaubt Discord nur 1 Sprachverbindung pro Server. Innerhalb dieser Einschränkung ist es logisch, sich die drei Komponenten, mit denen wir bisher gearbeitet haben ( AudioPlayer
, LavaPlayerAudioProvider
und AudioTrackScheduler
), einer bestimmten Guild
zuzuordnen, die natürlich für einige Snowflake
einzigartig ist. Logischerweise ist es sinnvoll, diese Objekte zu einem zusammenzufassen, damit sie in eine Map
eingefügt werden können, um sie beim Herstellen einer Verbindung zu einem Sprachkanal oder beim Arbeiten mit Befehlen leichter abrufen zu können.
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
}
Schließlich müssen wir uns mit dem Sprachkanal verbinden. Nach dem Herstellen der Verbindung erhalten Sie ein VoiceConnection
-Objekt, mit dem Sie später die Verbindung zum Sprachkanal trennen können, indem Sie VoiceConnection#disconnect
aufrufen.
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 */ })
Normalerweise sollte der Bot die Verbindung automatisch trennen, nachdem jeder einen Sprachkanal verlassen hat, da Benutzer normalerweise vergessen, die Verbindung zum Bot manuell zu trennen. Dieses Problem kann recht elegant gelöst werden, indem ein reaktiver Ansatz anstelle eines imperativen Ansatzes verwendet wird, wie das folgende Beispiel zeigt.
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 ());
});