Telegraff: Kotlin DSL for Telegram

شعار


على Habré الآلاف من المقالات حول كيفية إنشاء روبوت Telegram للغات ومنصات البرمجة المختلفة. الموضوع أبعد ما يكون عن الجديد.


لكن Telegraff هو أفضل إطار لتنفيذ روبوتات Telegram ، وسأثبت ذلك تحت الخفض.


مقدمة


في عام 2015 ، كان الروبل الروسي في حمى. كان لدي مدخرات بالدولار ودققت السعر حرفيًا كل خمس دقائق لبيع العملة بالسعر الذي أحتاجه. الحمى التي استمرت ، تعبت وكتبت روبوت Telegram ( TinkoffRatesBot ) ، والذي يخطرك إذا وصل سعر الصرف إلى القيمة (المتوقعة).
لقد تأثرت كثيرا بهذه المهمة. كتب بوتا بسرعة كبيرة ، لكنه لم يحصل على الرضا.


التكامل مع Telegram ليس ولم تكن هناك مشاكل. تم حل هذه المشكلة في بضع ساعات. وأنا مندهش من وجود مكتبات بأكملها في Java (ذاتي ، مع رمز مثير للاشمئزاز في الجودة) للتكامل مع Telegrams ، التي كسبت أكثر من ألف نجم على Github.


كان التحدي الرئيسي بالنسبة لي هو نظام البرمجة النصية: يقوم المستخدم باستدعاء أمر ، على سبيل المثال ، "/ taxi" ، يسأله الروبوت سلسلة من الأسئلة ، يتم التحقق من صحة كل إجابة ويمكن أن تؤثر على ترتيب الأسئلة اللاحقة ، يتم تشكيل "النموذج" المعتاد ، ويتم إعطاء طريقة المعالجة النهائية لتشكيل استجابة.
لقد فعلت ذلك ، لكن بنية الطبقات ، ومستويات التجريد ، كان كل شيء غير متجانس لدرجة أنه كان من المرير النظر إليها. لقد تعذبني السؤال: كيف يمكن نقل ذلك بإيجاز وعضوية إلى نموذج وجوه المنحى؟


كنت أرغب في الحصول على شيء بسيط ومريح ، والأهم من ذلك - أن أكون قادرًا على وصف النص بأكمله في ملف واحد معزول حتى لا أحتاج إلى عرض نصف المشروع من أجل فهم سلسلة تفاعل المستخدم.


كي لا نقول أن القضية كانت حادة للغاية ، لأن المهمة قد تم حلها بالفعل. بدلا من ذلك ، في بعض الأحيان فكرت به. كان الفكر هو Groovy DSL ، ولكن عندما وصل Kotlin ، أصبح الاختيار واضحًا. هكذا بدا Telegraff.


نعم ، بالطبع ، لم تكن هناك منافسة سيفوز بها Telegraff. والادعاء بأن Telegraff هو الأفضل لا ينبغي أن يؤخذ حرفيا. لكن Telegraff هو نهج جديد وفريد ​​لهذا التحدي. من السهل أن تكون الأفضل ، كونك الوحيد.


كيفية استخدامها؟


اعتمادا على


الخطوة الأولى هي تحديد مستودع إضافي للتبعيات. ربما في مرحلة ما سوف أنشر Telegraff في Maven Central أو في JCenter ، لكن الآن.


Gradle
repositories { maven { url "https://dl.bintray.com/ruslanys/maven" } } 

مخضرم
 <repositories> <repository> <snapshots> <enabled>false</enabled> </snapshots> <id>bintray-ruslanys-maven</id> <name>bintray</name> <url>https://dl.bintray.com/ruslanys/maven</url> </repository> </repositories> 

يبقى الحال بالنسبة للصغيرة. لاستخدام Telegraff ، تحتاج إلى تحديد تبعية واحدة فقط لبدء تشغيل الربيع:


Gradle
 compile("me.ruslanys.telegraff:telegraff-starter:1.0.0") 

مخضرم
 <dependency> <groupId>me.ruslanys.telegraff</groupId> <artifactId>telegraff-starter</artifactId> <version>1.0.0</version> </dependency> 

ترتيب


تكوين المشروع بسيط ويمكن أن يقتصر على المعلمتين أو الثلاثة الأولى:


application.properties
 telegram.access-key=123 # ① telegram.mode=webhook # ② telegram.webhook-base-url=https://ruslanys.me # ③ telegram.webhook-endpoint-url=/telegram # ④ telegram.handlers-path=handlers # ⑤ telegram.unresolved-filter.enabled=false # ⑥ 

  1. المفتاح الخاص بك إلى Telegram API.
  2. وضع تلقي الرسائل (التحديثات) من Telegram. يمكن أن يكون إما الاقتراع أو webhook.
  3. إذا تم الإشارة إلى طريقة تلقي التحديثات بواسطة "webhook" ، يجب عليك تحديد المسار إلى التطبيق الخاص بك.
  4. إذا كنت ترغب في ذلك ، يمكنك تحديد المسار الخاص بك إلى نقطة النهاية. إذا لم يتم إعادة تعريف هذه المعلمة ، فسيتم إنشاء مسار النموذج التالي: /telegram/${UUID} . قبل بدء التطبيق ، يتم تعيين العنوان المحدد كعنوان ربط الويب. في نهاية العمل ، يتم استبدال عنوان ربط الويب لتكون قادرًا على التبديل إلى الاستقصاء في المرة التالية التي يبدأ فيها.
  5. إذا رغبت في ذلك ، يمكنك تغيير المجلد الذي توجد به البرامج النصية للمعالجات. بشكل افتراضي ، هذا هو مجلد handlers .
  6. UnresolvedFilter تضمين UnresolvedFilter في "التسليم" ويتم تمكينه بشكل افتراضي. في حالة عدم العثور على معالج على رسالة المستخدم ، يستجيب UnresolvedFilter بشيء مثل "عذرًا ، أنا لا أفهمك :(".

حان الوقت لكتابة النصوص!


معالجات


معالجات (البرامج النصية) هي جزء رئيسي من Telegraff. هذا هو المكان الذي يتم فيه تعيين سلسلة تفاعل المستخدم. خلاصة القول هي أن كل أمر ، مثل "/ start" ، "/ taxi" ، "/ help" ، هو برنامج نصي / نصي / معالج / معالج منفصل.


يمكن أن يحتوي البرنامج النصي على مجموعة من الخطوات (الأسئلة) التي يحتاج المستخدم إلى تنفيذها من أجل تنفيذ أمر. بمعنى آخر ، يجب على المستخدم ملء النموذج. ونظرًا لأن messenger من الواجهة ، فأنت بحاجة إلى التحدث وطلب المستخدم.


هل أحتاج إلى توضيح أن استجابات المستخدم يجب التحقق من صحتها؟ أول شيء سيفعله المستخدم هو أنه سوف يستجيب بشكل مختلف عما تتوقعه.


حسنًا ، في النهاية ، يمكن أن يتفرع النص البرمجي ، أي يمكن أن تؤثر كل إجابة على سؤال في ترتيب الإجابات اللاحقة.


على سبيل المثال!


للبدء ، ضع الملف بالملحق .kts في المجلد مع handlers الموارد: src/main/resources/handlers/ExampleHandler.kts .


سيناريو استدعاء سيارة أجرة
 enum class PaymentMethod { CARD, CASH } handler("/taxi", "") { // ① step<String>("locationFrom") { // ② question { // ③ MarkdownMessage(" ?") } } step<String>("locationTo") { question { MarkdownMessage(" ?") } } step<PaymentMethod>("paymentMethod") { question { state -> MarkdownMessage("   ?", "", "") // ④ } validation { // ⑤ when (it.toLowerCase()) { "" -> PaymentMethod.CARD "" -> PaymentMethod.CASH else -> throw ValidationException(",    ") // ⑥ } } next { state -> null // ⑦ } } process { state, answers -> // ⑧ val from = answers["locationFrom"] as String val to = answers["locationTo"] as String val paymentMethod = answers["paymentMethod"] as PaymentMethod // ⑨ // Business logic MarkdownMessage("""     #${state.chat.id}.   $from  $to.  $paymentMethod. """.trimIndent()) // ⑩ } } 

مفاتيح السهوب عن عمد لم تؤخذ في الثوابت. في الإنتاج ، بالطبع ، من الأفضل تجنب هذا.


دعونا معرفة ذلك:


  1. نعلن السيناريو. مطلوب اسم فريق واحد على الأقل. في هذه الحالة ، هناك فريقان: "/ تاكسي" ، "تاكسي". إذا كانت رسالة المستخدم تبدأ بهذه الكلمات ، فسيتم استدعاء المعالج المقابل.
  2. نحدد الخطوات (الأسئلة). مطلوب اسم خطوة فريدة ل بعد ذلك ، يمكن الوصول إلى استجابة المستخدم بدقة بواسطة هذا المفتاح ("locationFrom").
  3. تحتوي كل خطوة على ثلاثة أقسام ، أولها هو السؤال نفسه. السؤال هو قسم إلزامي يجب أن يكون حاضراً في كل خطوة. لا يوجد أي معنى في خطوة دون سؤال.
  4. يمكنك ملء السؤال كما يحلو لك. في هذه الحالة ، سيُطلب من المستخدم من خلال لوحة المفاتيح تحديد أحد الخيارات: "البطاقة" أو "النقدية". نتيجة لاستدعاء هذا الحظر ، يجب أن يكون هناك كائن من النوع TelegramSendRequest . عذرًا ، لم أتمكن من التوصل إلى أي شيء أفضل من لاحقة SendRequest ، التي تصف البنية على أنها طلب صادر في Telegram.
    هيكل الطبقة
  5. الخطوة الثانية الأكثر أهمية هي التحقق من استجابة المستخدم. يتم تحديد نوع كل خطوة (عام) ، وبالتالي ، يجب على كتلة التحقق من الصحة أن ترجع بالضبط النوع الذي يتم به تحديد الخطوة.
  6. إذا كانت استجابة المستخدم غير مرضية ، فيمكنك طرح ValidationException مع توضيح النص ، ولكن نفس لوحة المفاتيح ، إذا تمت الإشارة إليه في السؤال.
  7. قسم الخطوة الأخيرة عبارة عن كتلة تشير إلى الخطوة التالية. بشكل افتراضي ، سيتم تنفيذ الخطوات بترتيب إعلانها ، من أعلى إلى أسفل. ولكن يمكن أن تتأثر هذه العملية بتجاوز الكتلة المقابلة. يمكن إرجاع إما مفتاح الخطوة التالية ( String ) أو "فارغة" نتيجة لتنفيذ هذه الكتلة ، مما يشير إلى أنه لا توجد خطوات أخرى وأنه قد حان الوقت لمتابعة تنفيذ الأمر.
  8. عند إنشاء طلب مستخدم ، تكون المعالجة مطلوبة. الحجج في lambda هي الحالة (هذا يشبه الجلسة) وردود المستخدمين.
  9. لاحظ أن الاستجابة الفاشلة لم تعد سلسلة استجابة المستخدم ، ولكنها كائن تم معالجته بالفعل من النوع المطلوب.
  10. يمكن أن يكون الرد على الأمر مشابهًا للفقرة 4. إذا لم يكن الرد على الأمر مطلوبًا ، فيمكنك إرجاع "خالية".

قد لا يحتوي المعالج على خطوات على الإطلاق. في هذه الحالة ، تحتاج فقط إلى تحديد سلوك المعالج لاستدعاء الأمر.


النصي الترحيب
 handler("/start") { process { _, _ -> MarkdownMessage("!") } } 

محاولة


من أجل المحاولة ، قم بتفرع المستودع ، وقم telegraff-sample إلى الجهاز المحلي telegraff-sample مجلد telegraff-sample . تكوين ، إطلاق ، اللمس!


بشكل عام ، telegraff-sample هو مشروع مستقل بشكل متعمد لا يرتبط بالوالد وحتى لديه Gradle Wrapper. يمكنك ترك هذا المجلد فقط. هذا هو نوع من النموذج الأصلي.


كيف يعمل؟


برقية


التكامل مع Telegram بسيط للغاية ويتم تنفيذه في TelegramApi .


تم تنفيذ كل طريقة بشكل متعمد بشكل فردي بسبب عدد من الظروف: بدءًا من استخدام Spring's RestTemplate (واختباراته) ، إلى خصوصية Telegram API.


كما ترى من التكوين ، هناك نوعان من عملاء واجهة برمجة التطبيقات هذه في Telegraff: PollingClient ، WebhookClient . اعتمادا على التكوين ، سيتم الإعلان عن صندوق معين.


وعلى الرغم من أن طرق تلقي التحديثات (رسائل جديدة) تختلف عن Telegram ، إلا أن جوهرها لم يتغير ويتلخص في شيء واحد - نشر حدث ( TelegramUpdateEvent ) حول الرسائل الجديدة من خلال EventPublisher (نمط "المراقب"). إذا كنت ترغب في ذلك ، يمكنك تنفيذ المستمع الخاص بك عن طريق الاشتراك في هذا النوع من الأحداث. طبقة منطقية ، كما يبدو لي ، تجريدية ، لأنه لا يهم مطلقًا كيف تم استلام الرسالة.


مرشحات


بمجرد استلام رسالة جديدة ، يجب معالجتها والرد على المستخدم. للقيام بذلك ، تحتاج الرسالة إلى الانتقال عبر سلسلة التصفية.


هذا مشابه لمرشحات Java EE المألوفة لمبرمجي Java. الفرق الوحيد هو أن ما يسمى بالمعالجات (إذا رسمنا موازٍ مع Java EE ، فهذه هي Servlets) ليست مستقلة عن المرشحات ، ولكنها جزء منها.


سلسلة مرشح


لذلك ، يتم ترشيح الفلاتر ويمكن أن تسمح للرسائل بالذهاب أبعد من السلسلة ، ربما لا.


LoggingFilter الواضح أن LoggingFilter هو LoggingFilter ذي الأولوية العليا (الأول) الذي سيتم استدعاؤه كجزء من معالجة رسالة جديدة. يسجل المعلومات على رسالة واردة ويرسلها إلى أسفل السلسلة. أضفت عمدا LoggingFilter كمثال. في الواقع ، قد لا يكون ذلك منطقيا ، لأن يتم تسجيل الرسائل الواردة على مستوى العميل.


المرشح التالي هو CancelFilter . إنه يعمل بشكل أساسي مع HandlersFilter وهو مكمل له. مهمته بسيطة: إذا أراد المستخدم التخلي عن البرنامج النصي الحالي ، فيمكنه كتابة "/ إلغاء" أو "إلغاء" ويجب إلغاء حالته (الجلسة). يمكنه بدء أي سيناريو جديد دون إكمال السيناريو السابق. لهذا السبب ، قم CancelFilter "أعلى" (الأولوية).


HandlersFilter هو المرشح الرئيسي في العملية الحالية. هذا المرشح هو الذي يخزن حالة دردشات المستخدم ، ويجد ويدعو المعالج المطلوب (البرنامج النصي) ، ويطبق كتل التحقق من الصحة ، ويحدد ترتيب الخطوات ويستجيب للمستخدم.


إذا لم يعثر HandlersFilter على أي معالجات مناسبة لرسالة المستخدم ، سواء في الجلسة أو في المحتوى ، يتم إرسال الرسالة إلى أسفل السلسلة. مرشح المدقع هو UnresolvedFilter . هذا مرشح يعرف أنه الأخير ، وبالتالي فإن وظيفته بسيطة: إذا وصلوا إلي ، فإن كيفية الرد على رسالة غير واضحة ، سأقول أنني لم أفهم شيئًا. يبدو لي أنه من الأفضل تلقي بعض الرسائل على الأقل من برنامج الروبوت إذا لم يكن يعرف كيفية الرد ، بدلاً من تلقي أي شيء على الإطلاق.


لإضافة عامل التصفية الخاص بك ، يلزمك إعلان Bean من فئة TelegramFilter وتحديد التعليق التوضيحي @TelegramFilterOrder(ORDER_NUMBER) .


مثال مرشح
 @Component @TelegramFilterOrder(Integer.MIN_VALUE) class LoggingFilter : TelegramFilter { override fun handleMessage(message: TelegramMessage, chain: TelegramFilterChain) { log.info("New message from #{}: {}", message.chat.id, message.text) chain.doFilter(message) } companion object { private val log = LoggerFactory.getLogger(LoggingFilter::class.java) } } 

هذه هي الطريقة التي تنفذ بها TinkoffRatesBot "آلة حاسبة". بدون الاتصال بأي برنامج نصي وأمر ، يمكنك إرسال رقم ، على سبيل المثال ، "1000" ، أو حتى تعبير كامل ، على سبيل المثال ، "4500 * 3 - 12000". سوف يحسب الروبوت نتيجة التعبير ، ويطبق أسعار الصرف الحالية على النتيجة ويعرض معلومات عنها. في الواقع ، نتيجة هذه الإجراءات هي تنفيذ CalculationFilter ، والذي يقع في السلسلة أسفل HandlersFilter ، ولكن أعلى UnresolvedFilter .


معالجات


نظام البرمجة النصية Telegraff (معالجات) مبني على Kotlin DSL. باختصار ، يدور حول لامبدا وبناة.


أنا لا أرى وجهة نظر بشكل منفصل عرض Kotlin DSL ، ل هذه محادثة مختلفة تمامًا. هناك وثائق كبيرة من JetBrains وتقرير شامل من i_osipov .


الفروق الدقيقة


هذا القسم مخصص للميزات الحالية. كلهم ، في رأيي ، ليسوا مهمين ، بعضهم يمكن إصلاحه ، البعض الآخر لا يمكن إصلاحه. ولكن عليك أن تعرف عن هذه الجوانب.


إذا كنت ترغب في المشاركة أو معرفة كيفية تصحيح نقطة أو نقطة أخرى من هذا القسم ، سأكون ممتنًا للغاية.


برقية


ربما لم يتم وصف طبقة التكامل مع Telegram بالكامل. فقط تلك الأساليب التي كنت بحاجة إلى تنفيذها. إذا كان هناك شيء تفتقر إليه شخصيًا ، TelegramApi بتصحيح TelegramApi وإرسال PR!


من بين الأجزاء المهمة في الوقت الحالي عدم وجود دعم لوحة المفاتيح المضمنة (وهذا هو عندما تكون لوحة المفاتيح مباشرة أسفل الرسالة الموجودة في الشريط). تتفاقم المهمة بسبب الحاجة إلى "إدخال" لوحات المفاتيح المضمنة بشكل صحيح في الهيكل الحالي بحيث تظل بسيطة ومريحة ومعزولة. هناك بالفعل فكرة جيدة لتنفيذ هذه الوظيفة ، ولكن لم يتم تنفيذها واختبارها بأي شكل من الأشكال.


جرة الدهون


لسوء الحظ ، يمكن أن تواجه بعض المكتبات ، مثل JRuby وربما Kotlin Embedded Compiler (اللازمة لتجميع البرامج النصية) مشاكل كجزء من Fat JAR . Fat JAR هو عندما يتم تعبئة الكود الخاص بك وكل التبعيات في ملف واحد ( *.jar ).


لحل هذه المشكلة ، يمكنك فك التبعيات في وقت التشغيل. وهذا هو ، عند بدء تشغيل التطبيق ، يتم نشر التبعية JAR من الحزمة الرئيسية في مكان ما على القرص ويشار إلى classpath قبل ذلك. هذا سهل جدًا من خلال تكوين bootJar :


التكوين المساعد
 bootJar { requiresUnpack "**/**kotlin**.jar" requiresUnpack "**/**telegraff**.jar" } 

ومع ذلك ، من أجل الرجوع من معالجات (البرامج النصية) إلى حبوبك (الخدمات ، على سبيل المثال) ، يجب أيضًا تفريغها. الذي ، من حيث المبدأ ، يلغي فائدة هذا النهج.


كما أراها ، يظل application المكوّن الإضافي application Gradle الطريقة الأكثر موثوقية وبساطة وملاءمة. علاوة على ذلك ، إذا كنت تقوم بتعبئة التطبيق الخاص بك ، فلن يكون هناك اختلاف حسب النتيجة.


حول كل هذا كتبت بشيء من التفصيل هنا .


ترتيب التهيئة


هنا أود أن أشير إلى حالتين.


أولاً ، إذا نظرت إلى سيناريو استدعاء سيارة أجرة ، يمكنك أن ترى أن فئة enum محددة أعلى من استدعاء handler(...) . يتم فرض هذه الضرورة من خلال حقيقة أن handler هو استدعاء دالة. مكالمة دالة ، والنتيجة التي يجب أن تكون بعض البنية ، والتي سوف تستخدم Telegraff في وقت لاحق. إذا ، وفقًا لنتائج تنفيذ البرنامج النصي الخاص بك ، لا يمكن للمصنع إحضار النتيجة إلى النوع المطلوب ، فسيحدث خطأ في مرحلة التهيئة.


ثانياً ، عليك أن تتذكر أنه يمكن تهيئة البرامج النصية في وقت أبكر من تطبيقك وفولك بالكامل. على سبيل المثال ، إذا وضعت رابطًا لسياق في متغير ثابت وحاولت الحصول على بعض الخدمات على السطر الأول في ملف البرنامج النصي ، فقد يتضح أن السياق لن يحتوي عليه ، لأن لم تتم تهيئته بعد. لتجنب مثل هذه المشكلات ، استخدم طريقة Telegraff. إنه يضمن تهيئة السياق وأن جميع الحبوب اللازمة متوفرة. مثال يمكن رؤيته هنا .


استنتاج


أردت أن أحاول - شوكة ،
أردت إصلاحه - إرسال العلاقات العامة ،
أردت أن أشكر - وضع علامة النجمة في جيثب ، مثل هذا المنصب وأخبر أصدقائك!


مستودع المشروع

Source: https://habr.com/ru/post/ar445072/


All Articles