Discord4J 是一個快速、強大、無偏見的反應式函式庫,可使用官方 Discord Bot API 快速輕鬆地開發適用於 Java、Kotlin 和其他 JVM 語言的 Discord 機器人。
在 v3.2 的範例中,每當使用者發送!ping
訊息時,機器人都會立即回應Pong!
。
確保您的機器人在開發者入口網站中啟用了訊息內容意圖。
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 ();
}
}
有關完整的專案範例,請在此處查看我們的範例專案儲存庫。
反應式- Discord4J 遵循反應流協議,以確保 Discord 機器人無論大小如何都能平穩且有效率地運作。
官方- 自動速率限制、自動重新連接策略和一致的命名約定是 Discord4J 提供的眾多功能之一,可確保您的 Discord 機器人按照 Discord 的規範運行,並在與我們的庫交互時提供最少的意外。
模組化- Discord4J 將自身分解為模組,以允許高級用戶在較低級別與我們的 API 交互,以構建最小且快速的運行時,甚至添加他們自己的抽象。
⚔️功能強大- Discord4J 可用於開發任何機器人,無論大小。我們提供了許多用於從自訂分發框架、堆外快取開發大型機器人的工具,並且它與 Reactor 的交互允許與 Spring 和 Micronaut 等框架完全整合。
?社群-我們為我們的包容性社群感到自豪,並願意在出現挑戰時提供幫助;或者如果您只是想聊天!我們提供的幫助範圍從 Discord4J 特定問題到一般程式設計和 Web 開發幫助,甚至是 Reactor 特定問題。請務必造訪我們的 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 包含更簡單、更強大的 API 來建置要求、新的實體快取以及相依性升級所帶來的效能改進。請查看我們的遷移指南以了解更多詳細資訊。
Discord4J | 支援 | 網關/API | 意圖 | 互動 |
---|---|---|---|---|
v3.3.x | 開發中 | v9 | 強制、非特權默認 | 完全支持 |
v3.2.x | 目前的 | v8 | 強制、非特權默認 | 完全支持 |
v3.1.x | 僅維護 | v6 | 可選,預設無意圖 | 僅維護 |
有關相容性的更多詳細信息,請參閱我們的文件。
我們要特別感謝所有贊助商為我們提供資金以繼續開發和託管儲存庫資源以及推動社區計劃的舉措。我們特別要向這些優秀的人士表達特別的讚美:
以下是使用 Discord4J 的大型機器人的一些真實範例:
您是否擁有使用 Discord4J 的大型機器人?在我們的 Discord 中詢問管理員或提交拉取請求以將您的機器人新增至清單!
Discord4J 使用 Project Reactor 作為我們非同步框架的基礎。 Reactor 提供了一個簡單但極其強大的 API,使用戶能夠減少資源並提高效能。
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 也提供了多種方法來幫助實現更好的反應鏈組合,例如具有錯誤處理重載的GatewayDiscordClient#withGateway
和EventDispatcher#on
。
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 ();
}
}
透過利用 Reactor,Discord4J 在與 kotlinx-coroutines-reactor 庫配合使用時可以與 Kotlin 協程進行本機整合。
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()
從 2022 年 9 月 1 日開始,Discord 要求機器人啟用「MESSAGE_CONTENT」意圖來存取訊息內容。若要啟用該意圖,請前往 Discord 開發者入口網站並選擇您的機器人。然後,轉到“機器人”選項卡並啟用“訊息內容”意圖。然後,在創建 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 ();
使用者通常更喜歡使用名稱而不是 ID。此範例將示範如何搜尋具有特定名稱角色的所有成員。
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 ;
或者,使用 Reactor:
Guild guild = ...
return guild . getMembers ()
. filterWhen ( member -> member . getRoles ()
. map ( Role :: getName )
. any ( "Developers" :: equalsIgnoreCase ));
Discord4J 提供對語音連線的全面支援以及向連接到相同頻道的其他使用者發送音訊的能力。 Discord4J 可以接受任何 Opus 音訊來源,LavaPlayer 是從 YouTube、SoundCloud 和其他提供者下載和編碼音訊的首選解決方案。
警告
原始的 LavaPlayer 不再維護。可以在此處找到新的維護版本。如果您需要 Java 8 支持,可以使用 Walkyst 的 LavaPlayer 分支,但它也不再維護!
首先,您首先需要實例化並配置一個傳統上全域的AudioPlayerManager
。
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 );
}
接下來,我們需要允許 Discord4J 從AudioPlayer
讀取到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 ;
}
}
通常,音訊播放器會有隊列或內部播放列表,以便用戶能夠在完成或要求跳過歌曲時自動循環播放歌曲。我們可以在外部管理此佇列並將其傳遞到程式碼的其他區域,以便透過建立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 ();
}
}
}
目前,Discord 僅允許每台伺服器 1 個語音連線。在這個限制範圍內,我們可以合理地認為我們迄今為止使用的 3 個組件( AudioPlayer
、 LavaPlayerAudioProvider
和AudioTrackScheduler
)與特定的Guild
相關,而某些Snowflake
自然是唯一的。從邏輯上講,將這些物件組合成一個是有意義的,這樣它們就可以放入Map
中,以便在連接到語音通道或使用命令時更容易檢索。
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
}
最後,我們需要連接到語音通道。連線後,您將獲得一個VoiceConnection
對象,稍後您可以透過呼叫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 */ })
通常,在每個人都離開語音通道後,機器人應自動斷開連接,因為使用者通常會忘記手動斷開機器人連接。使用反應式方法而不是命令式方法可以相當優雅地解決這個問題,如下例所示。
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 ());
});