Discord4J é uma biblioteca rápida, poderosa, reativa e reativa para permitir o desenvolvimento rápido e fácil de bots Discord para Java, Kotlin e outras linguagens JVM usando a API oficial do Discord Bot.
Neste exemplo da v3.2, sempre que um usuário envia uma mensagem !ping
, o bot responderá imediatamente com Pong!
.
Certifique-se de que seu bot tenha a intenção de conteúdo de mensagem habilitada em seu portal do desenvolvedor.
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 ();
}
}
Para um exemplo de projeto completo, confira nosso repositório de projetos de exemplo aqui.
Reativo - Discord4J segue o protocolo de fluxos reativos para garantir que os bots Discord funcionem de maneira suave e eficiente, independentemente do tamanho.
Oficial - Limitação automática de taxa, estratégias de reconexão automática e convenções de nomenclatura consistentes estão entre os muitos recursos que o Discord4J oferece para garantir que seus bots Discord funcionem de acordo com as especificações do Discord e fornecer o mínimo de surpresas ao interagir com nossa biblioteca.
Modular - Discord4J se divide em módulos para permitir que usuários avançados interajam com nossa API em níveis mais baixos para construir tempos de execução mínimos e rápidos ou até mesmo adicionar suas próprias abstrações.
⚔️ Poderoso - Discord4J pode ser usado para desenvolver qualquer bot, grande ou pequeno. Oferecemos muitas ferramentas para o desenvolvimento de bots em grande escala a partir de estruturas de distribuição personalizadas, cache off-heap, e sua interação com o Reactor permite integração completa com estruturas como Spring e Micronaut.
? Comunidade - Orgulhamo-nos da nossa comunidade inclusiva e estamos dispostos a ajudar sempre que surgirem desafios; ou se você quiser apenas bater um papo! Oferecemos ajuda que vai desde problemas específicos do Discord4J até ajuda geral de programação e desenvolvimento web, e até mesmo perguntas específicas do Reactor. Não deixe de nos visitar em nosso servidor 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 inclui APIs mais simples e poderosas para criar solicitações, um novo cache de entidade e melhorias de desempenho a partir de atualizações de dependências. Verifique nosso Guia de Migração para mais detalhes.
Discord4J | Apoiar | Gateway/API | Intenções | Interações |
---|---|---|---|---|
v3.3.x | Em desenvolvimento | v9 | Padrão obrigatório e sem privilégios | Totalmente suportado |
v3.2.x | Atual | v8 | Padrão obrigatório e sem privilégios | Totalmente suportado |
v3.1.x | Somente manutenção | v6 | Opcional, sem intenção padrão | Somente manutenção |
Consulte nossos documentos para obter mais detalhes sobre compatibilidade.
Gostaríamos de agradecer especialmente a todos os nossos patrocinadores por nos fornecerem o financiamento para continuarmos desenvolvendo e hospedando recursos de repositórios, bem como por impulsionarmos iniciativas para programas comunitários. Em particular, gostaríamos de dar uma mensagem especial a estas pessoas maravilhosas:
Aqui estão alguns exemplos reais de grandes bots usando Discord4J:
Você possui um bot grande usando Discord4J? Pergunte a um administrador em nosso Discord ou envie uma solicitação pull para adicionar seu bot à lista!
Discord4J usa o Project Reactor como base para nossa estrutura assíncrona. O Reactor fornece uma API simples, mas extremamente poderosa, que permite aos usuários reduzir recursos e aumentar o desempenho.
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 também fornece vários métodos para auxiliar em melhores composições de cadeia reativa, como GatewayDiscordClient#withGateway
e EventDispatcher#on
com uma sobrecarga de tratamento de erros.
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 ();
}
}
Ao utilizar o Reactor, o Discord4J tem integração nativa com corrotinas Kotlin quando emparelhado com a biblioteca 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()
A partir de 1º de setembro de 2022, o Discord exige que os bots habilitem a intenção “MESSAGE_CONTENT” para acessar o conteúdo das mensagens. Para habilitar a intenção, acesse o Portal do Desenvolvedor Discord e selecione seu bot. Em seguida, vá até a aba “Bot” e habilite a intenção “Conteúdo da Mensagem”. Em seguida, adicione a intenção ao seu bot ao criar o 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 ();
Os usuários normalmente preferem trabalhar com nomes em vez de IDs. Este exemplo demonstrará como pesquisar todos os membros que possuem uma função com um nome específico.
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 ;
Alternativamente, usando o Reactor:
Guild guild = ...
return guild . getMembers ()
. filterWhen ( member -> member . getRoles ()
. map ( Role :: getName )
. any ( "Developers" :: equalsIgnoreCase ));
Discord4J oferece suporte completo para conexões de voz e capacidade de enviar áudio para outros usuários conectados ao mesmo canal. Discord4J pode aceitar qualquer fonte de áudio Opus, sendo LavaPlayer a solução preferida para baixar e codificar áudio do YouTube, SoundCloud e outros provedores.
Aviso
O LavaPlayer original não é mais mantido. Uma nova versão mantida pode ser encontrada aqui. Se precisar de suporte Java 8, você pode usar o fork LavaPlayer do Walkyst, mas ele também não é mais mantido!
Para começar, primeiro você precisa instanciar e configurar um AudioPlayerManager
convencionalmente 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 );
}
Em seguida, precisamos permitir que o Discord4J leia de um AudioPlayer
para um 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 ;
}
}
Normalmente, os reprodutores de áudio terão filas ou listas de reprodução internas para que os usuários possam percorrer automaticamente as músicas à medida que elas são finalizadas ou solicitadas para serem ignoradas. Podemos gerenciar essa fila externamente e passá-la para outras áreas do nosso código para permitir que as faixas sejam visualizadas, enfileiradas ou ignoradas criando um 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 ();
}
}
}
Atualmente, o Discord permite apenas 1 conexão de voz por servidor. Trabalhando dentro desta limitação, é lógico pensar que os 3 componentes com os quais trabalhamos até agora ( AudioPlayer
, LavaPlayerAudioProvider
e AudioTrackScheduler
) estejam correlacionados a um Guild
específico, naturalmente único por algum Snowflake
. Logicamente, faz sentido combinar esses objetos em um só, para que possam ser colocados em um Map
para facilitar a recuperação ao conectar-se a um canal de voz ou ao trabalhar com comandos.
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
}
Finalmente, precisamos nos conectar ao canal de voz. Depois de conectar, você recebe um objeto VoiceConnection
onde pode utilizá-lo posteriormente para se desconectar do canal de voz chamando 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 */ })
Normalmente, depois que todos saem de um canal de voz, o bot deve se desconectar automaticamente, pois os usuários normalmente esquecem de desconectar o bot manualmente. Este problema pode ser resolvido de forma bastante elegante usando uma abordagem reativa em vez de uma abordagem imperativa, como demonstra o exemplo abaixo.
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 ());
});