Discord4J เป็นไลบรารี่แบบโต้ตอบที่รวดเร็ว ทรงพลัง ไม่มีความคิดเห็นใดๆ ซึ่งช่วยให้สามารถพัฒนาบอท Discord สำหรับ Java, Kotlin และภาษา JVM อื่นๆ ได้อย่างรวดเร็วและง่ายดาย โดยใช้ Discord Bot API อย่างเป็นทางการ
ในตัวอย่างนี้สำหรับเวอร์ชัน 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 ที่เรียบง่ายและมีประสิทธิภาพมากขึ้นในการสร้างคำขอ แคชเอนทิตีใหม่ และการปรับปรุงประสิทธิภาพจากการอัพเกรดการพึ่งพา ตรวจสอบคู่มือการย้ายข้อมูลของเราสำหรับรายละเอียดเพิ่มเติม
ดิสคอร์ด4เจ | สนับสนุน | เกตเวย์/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 coroutines เมื่อจับคู่กับไลบรารี 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 ;
หรือใช้เครื่องปฏิกรณ์:
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 fork ของ 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 อนุญาตการเชื่อมต่อด้วยเสียงเพียง 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 ());
});