@Override
public void onUpdateReceived(Update update) {
UpdatesReceiver.handleUpdates(update);
}
الذي أقوم بنقله إلى المعالج (فئة UpdatesReceiver الخاصة):
public static void handleUpdates(Update update) {
...
if (update.hasMessage() && update.getMessage().hasText()){
log.info("[Update (id {}) типа \"Текстовое сообщение\"]", update.getUpdateId());
new TextMessageHandler(update, replyGenerator).handleTextMessage();
}
else if (update.hasCallbackQuery()) {
//логгирование
new CallbackQueryHandler(update, replyGenerator).handleCallBackQuery();
}
else {
//логгирование
replyGenerator.sendTextMessage(update.getMessage().getChatId(), "Я могу принимать только текстовые messages!");
}
}
UpdatesReceiver هو معالج مركزي ينقل التحكم، اعتمادًا على نوع التحديث، إلى معالج متخصص آخر: TextMessageHandler أو CallbackQueryHandler، الذي أقوم بتمرير التحديث إلى منشئيه في أسفل السلسلة. يعد التحديث هو أهم شيء عند العمل مع الروبوت ولا ينبغي فقدانه، لأنه بمساعدة المعلومات المخزنة في التحديث، نكتشف المستخدم والدردشة التي يجب إرسال الرد إليها. لتوليد ردود على المستخدم، كتبت فئة منفصلة. يمكنه إرسال رسالة نصية عادية، ورسالة باستخدام لوحة مفاتيح مضمنة، ورسالة تحتوي على صورة، ورسالة باستخدام لوحة مفاتيح الرد. تبدو لوحة المفاتيح المضمنة كما يلي: فهي تحدد الأزرار التي، من خلال النقر عليها، يرسل المستخدم رد اتصال إلى الخادم، والذي يمكن معالجته بنفس طريقة معالجة الرسائل العادية تقريبًا. "للحفاظ عليها" تحتاج إلى معالج خاص بك. نقوم بتعيين إجراء لكل زر، والذي يتم كتابته بعد ذلك إلى كائن التحديث. أولئك. بالنسبة لزر "التكلفة"، قمنا بتعيين الوصف "/السعر" لرد الاتصال، والذي يمكننا الحصول عليه لاحقًا من التحديث. بعد ذلك، في فئة منفصلة، يمكنني بالفعل معالجة رد الاتصال هذا:
public void handleCallBackQuery() {
String call_data = update.getCallbackQuery().getData();
long message_id = update.getCallbackQuery().getMessage().getMessageId();
long chat_id = update.getCallbackQuery().getMessage().getChatId();
switch (call_date){
case "/price" :
//тут что-то сделать
break;
...
تبدو لوحة مفاتيح الرد كما يلي: وهي في الأساس تحل محل كتابة المستخدم. سيؤدي النقر على زر "المكتبة" إلى إرسال رسالة "مكتبة" بسرعة إلى الروبوت. لكل نوع من أنواع لوحات المفاتيح، قمت بكتابة فصلي الخاص، مع تطبيق نمط Builder: inline و Response . ونتيجة لذلك، يمكنك بشكل أساسي "رسم" لوحة المفاتيح المطلوبة وفقًا لمتطلباتك. هذا أمر مريح للغاية، نظرا لأن لوحات المفاتيح قد تكون مختلفة، لكن المبدأ يظل هو نفسه. فيما يلي طريقة بديهية لإرسال رسالة باستخدام لوحة المفاتيح المضمنة:
public synchronized void sendInlineKeyboardMessage(long chat_id, String gameTitle) {
SendMessage keyboard = InlineKeyboardMarkupBuilder.create(chat_id)
.setText("Вы может узнать следующую информацию об игре " + gameTitle)
.row()
.button("Стоимость " + "\uD83D\uDCB0", "/price " + gameTitle)
.button("Обновлено " + "\uD83D\uDDD3", "/updated " + gameTitle)
.button("Версия " + "\uD83D\uDEE0", "/version " + gameTitle)
.endRow()
.row()
.button("Требования " + "\uD83D\uDCF5", "/requirements " + gameTitle)
.button("Покупки " + "\uD83D\uDED2", "/iap " + gameTitle)
.button("Размер " + "\uD83D\uDD0E", "/size " + gameTitle)
.endRow()
.row()
.button("Получить всю информацию об игре" + "\uD83D\uDD79", "/all " + gameTitle)
.endRow()
.row()
.button("Скрыть клавиатуру", "close")
.endRow()
.build();
try {
execute(keyboard);
} catch (TelegramApiException e) {
log.error("[Не удалось отправить сообщение с -inline- клавиатурой]: {}", e.getMessage());
}
}
لمنح الروبوت وظائف صارمة، تم اختراع أوامر خاصة باستخدام حرف الشرطة المائلة: /library، /help، /game، وما إلى ذلك. وإلا فسيتعين علينا معالجة أي بيانات غير مرغوب فيها قد يكتبها المستخدم. في الواقع، هذا هو ما تمت كتابة messageHandler من أجله:
if (message.equals(ChatCommands.START.getDescription())) {
replyGenerator.sendTextMessage(chat_id, new StartMessageHandler().reply());
replyGenerator.sendReplyKeyboardMessage(chat_id);
}
else if (message.equals(ChatCommands.HELP.getDescription())
|| message.equalsIgnoreCase("Помощь")) {
replyGenerator.sendTextMessage(chat_id, new HelpMessageHandler().reply());
}
...
وبالتالي، اعتمادًا على الأمر الذي ترسله إلى الروبوت، سيتم تضمين معالج خاص في العمل. دعنا نذهب أبعد من ذلك ونلقي نظرة على عمل المحلل اللغوي والمكتبة. إذا أرسلت للروبوت رابطًا للعبة في متجر Google Play، فسيعمل معالج خاص تلقائيًا . ردًا على ذلك، سيتلقى المستخدم معلومات حول اللعبة بالشكل التالي: في الوقت نفسه، سيتم استدعاء طريقة تحاول إضافة اللعبة إلى مكتبة الروبوت (أولاً إلى الخريطة المحلية، ثم إلى -> ملف json) ). إذا كانت اللعبة موجودة بالفعل في المكتبة، فسيتم إجراء فحص (كما هو الحال في Hashmap المعتاد)، وإذا كانت البيانات الميدانية (على سبيل المثال، تم تغيير رقم الإصدار)، فسيتم الكتابة فوق اللعبة في المكتبة. إذا لم يتم الكشف عن أي تغييرات، فلن يتم إجراء أي إدخالات. إذا لم تكن هناك لعبة في المكتبة على الإطلاق، فسيتم كتابتها أولاً على الخريطة المحلية (كائن مثل tyk )، ثم كتابتها في ملف json، لأنه إذا تم إغلاق التطبيق على الخادم بشكل غير متوقع، فسيتم حفظ البيانات مفقود، ولكن يمكن دائمًا قراءته باستخدام الملف. في الواقع، عند بدء تشغيل البرنامج، يتم دائمًا تحميل المكتبة لأول مرة من ملف من كتلة ثابتة:
static {
TypeFactory typeFactory = mapper.getTypeFactory();
MapType mapType = typeFactory.constructMapType(ConcurrentSkipListMap.class, String.class, GooglePlayGame.class);
try {
Path path = Paths.get(LIBRARY_PATH);
if (!Files.exists(path)) {
Files.createDirectories(path.getParent());
Files.createFile(path);
log.info("[Файл библиотеки создан]");
}
else {
ConcurrentMap<string, googleplaygame=""> temporary = mapper.readValue(new File(LIBRARY_PATH), mapType);
games.putAll(temporary);
log.info("[Количество игр в загруженной библиотеке] = " + games.size());
}
}
catch (IOException e) {
log.error("[Ошибка при чтении/записи file] {}", e.getMessage());
}
}
هنا يتعين عليك أيضًا قراءة البيانات من الملف إلى خريطة مؤقتة، والتي يتم بعد ذلك "نسخها" إلى خريطة كاملة من أجل الحفاظ على عدم حساسية حالة الأحرف عند البحث عن لعبة في الملف (من خلال كتابة tITan QuEST، سيظل الروبوت يعثر على لعبة Titan Quest في المكتبة). ولم يكن من الممكن إيجاد حل آخر، هذه هي مميزات إلغاء التسلسل باستخدام جاكسون. لذلك، مع كل طلب رابط، تتم إضافة اللعبة إلى المكتبة، إن أمكن، وبالتالي توسيع المكتبة. يمكن الحصول على مزيد من المعلومات حول لعبة معينة باستخدام الأمر /libraryGame_Name. يمكنك معرفة معلمة معينة (على سبيل المثال، الإصدار الحالي) وجميع المعلمات في وقت واحد. يتم تنفيذ ذلك باستخدام لوحة المفاتيح المضمنة، والتي تمت مناقشتها سابقًا. أثناء العمل، قمت أيضًا بتطبيق المهارات المكتسبة هنا أثناء حل المشكلات. على سبيل المثال، قائمة بأسماء الألعاب العشوائية الموجودة في المكتبة (الخيار متاح باستخدام الأمر /library):
private String getRandomTitles(){
if (LibraryService.getLibrary().size() < 10){
return String.join("\n", LibraryService.getLibrary().keySet());
}
List<string> keys = new ArrayList<>(LibraryService.getLibrary().keySet());
Collections.shuffle(keys);
List<string> randomKeys = keys.subList(0, 10);
return String.join("\n", randomKeys);
}
كيف يقوم الروبوت بمعالجة الروابط؟ فهو يتحقق منها لمعرفة ما إذا كانت تنتمي إلى Google Play (المضيف، البروتوكول، المنفذ):
private static class GooglePlayCorrectURL {
private static final String VALID_HOST = "play.google.com";
private static final String VALID_PROTOCOL = "https";
private static final int VALID_PORT = -1;
private static boolean isLinkValid(URI link) {
return (isHostExist(link) && isProtocolExist(link) && link.getPort() == VALID_PORT);
}
private static boolean isProtocolExist(URI link) {
if (link.getScheme() != null) {
return link.getScheme().equals(VALID_PROTOCOL);
}
else {
return false;
}
}
private static boolean isHostExist(URI link) {
if (link.getHost() != null) {
return link.getHost().equals(VALID_HOST);
}
else {
return false;
}
}
إذا كان كل شيء على ما يرام، فإن الروبوت يتصل عبر رابط باستخدام مكتبة Jsoup، والذي يسمح لك بالحصول على كود HTML للصفحة، والذي يخضع لمزيد من التحليل والتحليل. لن تتمكن من خداع الروبوت باستخدام رابط غير صحيح أو ضار.
if (GooglePlayCorrectURL.isLinkValid(link)){
if (!link.getPath().contains("apps")){
throw new InvalidGooglePlayLinkException("К сожалению, бот работает исключительно с играми. Введите другую ссылку.");
}
URL = forceToRusLocalization(URL);
document = Jsoup.connect(URL).get();
}
else {
throw new NotGooglePlayLinkException();
}
...
هنا كان علينا حل مشكلة الإعدادات الإقليمية. يتصل الروبوت بمتجر Google Play من خادم موجود في أوروبا، وبالتالي يتم فتح الصفحة في متجر Google Play باللغة المناسبة. اضطررت إلى كتابة عكاز "يعيد التوجيه" بالقوة إلى النسخة الروسية من الصفحة (كان المشروع، بعد كل شيء، يستهدف جمهورنا). للقيام بذلك، في نهاية الرابط، تحتاج إلى إضافة المعلمة بعناية hl: &hl=ru في طلب GET إلى خادم Google Play .
private String forceToRusLocalization(String URL) {
if (URL.endsWith("&hl=ru")){
return URL;
}
else {
if (URL.contains("&hl=")){
URL = URL.replace(
URL.substring(URL.length()-"&hl=ru".length()), "&hl=ru");
}
else {
URL += "&hl=ru";
}
}
return URL;
}
بعد الاتصال الناجح، نتلقى مستند HTML جاهزًا للتحليل والتحليل، ولكن هذا خارج نطاق هذه المقالة. رمز المحلل اللغوي هنا . يقوم المحلل اللغوي نفسه باسترداد المعلومات الضرورية وإنشاء كائن مع اللعبة، والذي تتم إضافته لاحقًا إلى المكتبة إذا لزم الأمر. <h2>للتلخيص</h2>يدعم الروبوت العديد من الأوامر التي تحتوي على وظائف معينة. يتلقى الرسائل من المستخدم ويطابقها مع أوامره. إذا كان رابطًا أو الأمر /game + link، فإنه يتحقق من هذا الرابط لمعرفة ما إذا كان ينتمي إلى Google Play. إذا كان الرابط صحيحًا، فإنه يتصل عبر Jsoup ويتلقى مستند HTML. يتم تحليل هذه الوثيقة بناءً على المحلل اللغوي المكتوب. يتم استخراج المعلومات الضرورية حول اللعبة من المستند، ثم يتم ملء كائن اللعبة بهذه البيانات. بعد ذلك، يتم وضع كائن اللعبة في وحدة التخزين المحلية (إذا لم تكن اللعبة موجودة بعد) ويتم كتابته على الفور في ملف لتجنب فقدان البيانات. يمكن الحصول على اللعبة المسجلة في المكتبة (اسم اللعبة هو مفتاح الخريطة، والكائن الموجود في اللعبة هو قيمة الخريطة) باستخدام الأمر /library Game_name. إذا تم العثور على اللعبة المحددة في مكتبة الروبوت، فسيتم إرجاع لوحة مفاتيح مضمّنة للمستخدم، والتي يمكنه من خلالها الحصول على معلومات حول اللعبة. إذا لم يتم العثور على اللعبة، يجب عليك إما التأكد من كتابة الاسم بشكل صحيح (يجب أن يتطابق تماما مع اسم اللعبة في متجر جوجل بلاي، باستثناء الحالة)، أو إضافة اللعبة إلى المكتبة عن طريق إرسال البوت رابط للعبة. لقد قمت بنشر الروبوت على Heroku ولأولئك الذين يخططون في المستقبل لكتابة الروبوت الخاص بهم واستضافته مجانًا على Heroku، سأقدم بعض التوصيات لحل الصعوبات التي قد تواجهها (بما أنني واجهتها بنفسي). لسوء الحظ، نظرًا لطبيعة Heroku، تتم "إعادة ضبط" مكتبة الروبوتات باستمرار مرة واحدة كل 24 ساعة. لا تدعم خطتي تخزين الملفات على خوادم Heroku، لذا فهي ببساطة تسحب ملف لعبتي من Github. كانت هناك عدة حلول: استخدم قاعدة بيانات، أو ابحث عن خادم آخر يقوم بتخزين هذا الملف مع اللعبة. قررت ألا أفعل أي شيء في الوقت الحالي، نظرًا لأن الروبوت ليس مفيدًا في الأساس. كنت في حاجة إليها بدلاً من اكتساب الخبرة الكاملة، وهو ما حققته في الأساس. لذا، توصيات هيروكو:
-
سيتعين عليك على الأرجح التسجيل في Heroku باستخدام VPN إذا كنت تعيش في روسيا.
-
في جذر المشروع تحتاج إلى وضع ملف بدون امتداد يسمى Procfile. يجب أن يكون محتواه هكذا: https://github.com/miroha/Telegram-Bot/blob/master/Procfile
-
في pom.xml، أضف الأسطر التالية وفقًا للمثال ، حيث تشير علامة mainClass إلى المسار إلى الفئة التي تحتوي على الطريقة الرئيسية: bot.BotApplication (إذا كانت فئة BotApplication موجودة في مجلد bot).
-
لا تقم ببناء أي مشروع باستخدام أوامر حزمة mvn، وما إلى ذلك، فسوف يقوم Heroku بتجميع كل شيء من أجلك.
-
من المستحسن إضافة gitignore إلى المشروع، على سبيل المثال:
# Log file *.log # Compiled resources target # Tests test # IDEA files .idea *.iml
-
قم فعليًا بتحميل المشروع إلى github، ثم قم بتوصيل المستودع بـ Heroku (أو استخدم طرقًا أخرى، هناك 3 منها، إذا لم أكن مخطئًا).
-
إذا كان التنزيل ناجحًا ("نجح البناء")، فتأكد من الانتقال إلى تكوين Dynos:
وقم بتبديل شريط التمرير، ثم تأكد من أنه في وضع التشغيل (نظرًا لحقيقة أنني لم أفعل ذلك، لم يعمل الروبوت الخاص بي وقمت بإرهاق ذهني لبضعة أيام وقمت بالكثير من الحركات غير الضرورية ).
-
إخفاء رمز الروبوت على جيثب. للقيام بذلك، تحتاج إلى الحصول على الرمز المميز من متغير البيئة:
public class Bot extends TelegramLongPollingBot { private static final String BOT_TOKEN = System.getenv("TOKEN"); @Override public String getBotToken() { return BOT_TOKEN; } ... }
ثم بعد نشر الروبوت، قم بتعيين هذا المتغير في لوحة تحكم Heroku في علامة التبويب "الإعدادات" (على يمين الرمز المميز سيكون هناك حقل VALUE، انسخ رمز الروبوت الخاص بك هناك):
- حصلت على مشروع عمل كامل مكتوب بلغة جافا؛
- تعلمت العمل مع واجهة برمجة تطبيقات الطرف الثالث (Telegram Bot API)؛
- في الممارسة العملية، لقد تعمقت في التسلسل، وعملت كثيرًا مع JSON ومكتبة جاكسون (في البداية استخدمت GSON، ولكن كانت هناك مشاكل معها)؛
- عززت مهاراتي عند العمل مع الملفات، وتعرفت على Java NIO؛
- تعلمت العمل مع ملفات التكوين .xml واعتدت على تسجيل الدخول؛
- تحسين الكفاءة في بيئة التطوير (IDEA)؛
- تعلمت العمل مع git وتعلمت قيمة gitignore؛
- المهارات المكتسبة في تحليل صفحات الويب (مكتبة Jsoup)؛
- تعلمت واستخدمت العديد من أنماط التصميم؛
- طور حسًا ورغبة في تحسين الكود (إعادة البناء)؛
- لقد تعلمت العثور على حلول عبر الإنترنت وألا أخجل من طرح الأسئلة التي لم أتمكن من العثور على إجابة لها.
GO TO FULL VERSION