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 ولتوفير أقل قدر من المفاجآت عند التفاعل مع مكتبتنا.
Modular - يقوم Discord4J بتقسيم نفسه إلى وحدات للسماح للمستخدمين المتقدمين بالتفاعل مع واجهة برمجة التطبيقات (API) الخاصة بنا على مستويات أقل لإنشاء أوقات تشغيل سريعة وسريعة أو حتى إضافة تجريداتهم الخاصة.
⚔️ قوي - يمكن استخدام Discord4J لتطوير أي روبوت، سواء كان كبيرًا أو صغيرًا. نحن نقدم العديد من الأدوات لتطوير الروبوتات واسعة النطاق من أطر التوزيع المخصصة، والتخزين المؤقت خارج الكومة، ويتيح تفاعلها مع Reactor التكامل الكامل مع أطر العمل مثل Spring وMicronaut.
؟ المجتمع - نحن نفخر بمجتمعنا الشامل ومستعدون للمساعدة عند ظهور التحديات؛ أو إذا كنت تريد فقط الدردشة! نحن نقدم المساعدة بدءًا من المشكلات المحددة لـ Discord4J، إلى المساعدة العامة في البرمجة وتطوير الويب، وحتى الأسئلة الخاصة بالمفاعل. تأكد من زيارتنا على خادم 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 واجهات برمجة تطبيقات أبسط وأكثر قوة لإنشاء الطلبات، وذاكرة تخزين مؤقت جديدة للكيان، وتحسينات في الأداء من خلال ترقيات التبعية. تحقق من دليل الهجرة الخاص بنا لمزيد من التفاصيل.
Discord4J | يدعم | البوابة/واجهة برمجة التطبيقات | النوايا | التفاعلات |
---|---|---|---|---|
v3.3.x | في التنمية | v9 | افتراضي إلزامي وغير مميز | مدعوم بالكامل |
v3.2.x | حاضِر | v8 | افتراضي إلزامي وغير مميز | مدعوم بالكامل |
v3.1.x | صيانة فقط | v6 | اختياري، لا يوجد نية افتراضية | صيانة فقط |
راجع مستنداتنا لمزيد من التفاصيل حول التوافق.
نود أن نقدم شكرًا خاصًا لجميع الرعاة لدينا لتزويدنا بالتمويل لمواصلة تطوير واستضافة موارد المستودع بالإضافة إلى دفع المبادرات الخاصة بالبرامج المجتمعية إلى الأمام. وعلى وجه الخصوص، نود أن نوجه تحية خاصة لهؤلاء الأشخاص الرائعين:
فيما يلي بعض الأمثلة الواقعية للروبوتات الكبيرة التي تستخدم Discord4J:
هل تمتلك روبوتًا كبيرًا يستخدم Discord4J؟ اطلب من أحد المسؤولين في Discord لدينا أو أرسل طلب سحب لإضافة الروبوت الخاص بك إلى القائمة!
يستخدم Discord4J مشروع Reactor كأساس لإطار عملنا غير المتزامن. يوفر Reactor واجهة برمجة تطبيقات بسيطة ولكنها قوية للغاية تمكن المستخدمين من تقليل الموارد وزيادة الأداء.
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 بتكامل أصلي مع coroutines Kotlin عند إقرانه بمكتبة kotlinx-coroutines-reacture.
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 Developer Portal وحدد الروبوت الخاص بك. ثم انتقل إلى علامة التبويب "Bot" وقم بتمكين هدف "محتوى الرسالة". ثم أضف النية إلى الروبوت الخاص بك عند إنشاء 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، فيمكنك استخدام شوكة Walkyst's 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 باتصال صوتي واحد فقط لكل خادم. من خلال العمل ضمن هذا القيد، من المنطقي التفكير في المكونات الثلاثة التي عملنا معها حتى الآن ( 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 ());
});