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 ());
});