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 특정 문제부터 일반 프로그래밍 및 웹 개발 도움말, 심지어 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, 새로운 엔터티 캐시 및 종속성 업그레이드를 통한 성능 향상이 포함되어 있습니다. 자세한 내용은 마이그레이션 가이드를 확인하세요.
디스코드4J | 지원하다 | 게이트웨이/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 ();
}
}
Discord4J는 Reactor를 활용하여 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 개발자 포털로 이동하여 봇을 선택하세요. 그런 다음 "Bot" 탭으로 이동하여 "Message Content" 인텐트를 활성화합니다. 그런 다음 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는 YouTube, SoundCloud 및 기타 제공업체에서 오디오를 다운로드하고 인코딩하는 데 선호되는 솔루션인 LavaPlayer를 사용하여 모든 Opus 오디오 소스를 수용할 수 있습니다.
경고
원래 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
)가 일부 Snowflake
에 의해 자연스럽게 고유한 특정 Guild
와 상호 연관되어 있다고 생각하는 것이 논리적입니다. 논리적으로 이러한 개체를 하나로 결합하여 음성 채널에 연결하거나 명령을 사용할 때 더 쉽게 검색할 수 있도록 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#disconnect
호출하여 음성 채널 연결을 끊을 때 활용할 수 있는 VoiceConnection
개체가 제공됩니다.
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 ());
});