Discord4J — это быстрая, мощная, самоуверенная, реактивная библиотека, позволяющая быстро и легко разрабатывать ботов Discord для Java, Kotlin и других языков JVM с использованием официального API Discord Bot.
В этом примере для версии 3.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 и заканчивая общим программированием и веб-разработкой, а также вопросами, касающимися 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 имеет встроенную интеграцию с сопрограммами Kotlin в сочетании с библиотекой 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()
Начиная с 1 сентября 2022 года 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 ();
Пользователи обычно предпочитают работать с именами, а не с идентификаторами. В этом примере будет показано, как искать всех участников, имеющих роль с определенным именем.
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, вы можете использовать вилку LavaPlayer от Walkyst, но она также больше не поддерживается!
Для начала вам сначала необходимо создать и настроить обычно глобальный 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 допускает только одно голосовое соединение на сервер. Работая в рамках этого ограничения, логично подумать о том, что три компонента, с которыми мы работали до сих пор ( 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 ());
});