@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، که من به روزرسانی را در ادامه زنجیره به سازنده های آن ارسال می کنم. به روز رسانی مهمترین نکته در کار با ربات است و نباید از بین برود، زیرا با کمک اطلاعات ذخیره شده در آپدیت متوجه می شویم که پاسخ باید به کدام کاربر و به کدام چت ارسال شود. برای ایجاد پاسخ به کاربر، یک کلاس جداگانه نوشتم. این می تواند پیام متنی معمولی، پیام با صفحه کلید درون خطی، پیام با تصویر و پیام با صفحه کلید پاسخ ارسال کند. یک صفحه کلید درون خطی به این شکل است: دکمه هایی را تعریف می کند که با کلیک روی آنها، کاربر یک تماس برگشتی به سرور ارسال می کند که می تواند تقریباً مانند پیام های معمولی پردازش شود. برای "نگهداری" آن به کنترل کننده خود نیاز دارید. برای هر دکمه یک اکشن تنظیم می کنیم که سپس روی شی Update نوشته می شود. آن ها برای دکمه "هزینه"، توضیح "/price" را برای پاسخ به تماس تعیین می کنیم، که بعداً می توانیم از به روز رسانی دریافت کنیم. بعد، در یک کلاس جداگانه، من قبلاً می توانم این callback را پردازش کنم:
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;
...
صفحه کلید Reply به این شکل است: و در اصل جایگزین تایپ کاربر می شود. با کلیک بر روی دکمه "کتابخانه" به سرعت یک پیام "کتابخانه" برای ربات ارسال می شود. برای هر نوع کیبورد، کلاس خودم را نوشتم و الگوی Builder را پیادهسازی کردم: inline و reply . در نتیجه، شما اساساً می توانید بسته به نیاز خود، صفحه کلید مورد نظر را "نقاشی" کنید. این بسیار راحت است، زیرا صفحه کلید ممکن است متفاوت باشد، اما اصل یکسان است. در اینجا یک روش بصری برای ارسال پیام با صفحه کلید درون خطی وجود دارد:
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. ). اگر بازی از قبل در کتابخانه باشد، یک بررسی انجام می شود (مانند یک هشمپ معمولی) و اگر داده های فیلد (به عنوان مثال، شماره نسخه تغییر کرده باشد)، بازی در کتابخانه بازنویسی می شود. اگر هیچ تغییری شناسایی نشد، هیچ ورودی انجام نخواهد شد. اگر اصلاً بازی در کتابخانه وجود نداشت، ابتدا روی نقشه محلی نوشته میشود (یک شی مانند 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، بازی ضبط شده در کتابخانه (نام بازی کلید نقشه است، شیء موجود در بازی مقدار نقشه است) به دست می آید. اگر بازی مشخص شده در کتابخانه ربات پیدا شود، یک صفحه کلید درون خطی به کاربر برگردانده می شود که با آن می تواند اطلاعات بازی را دریافت کند. اگر بازی پیدا نشد، یا باید مطمئن شوید که نام به درستی نوشته شده است (باید کاملاً با نام بازی در فروشگاه Google Play مطابقت داشته باشد، به جز مورد)، یا با ارسال ربات، بازی را به کتابخانه اضافه کنید. لینک بازی من ربات را روی heroku مستقر کردم و برای کسانی که در آینده قصد دارند ربات خود را بنویسند و آن را به صورت رایگان در heroku میزبانی کنند، چند توصیه برای حل مشکلاتی که ممکن است با آن مواجه شوید (از زمانی که خودم با آنها مواجه شدم) ارائه خواهم کرد. متاسفانه، به دلیل ماهیت Heroku، کتابخانه ربات به طور مداوم هر 24 ساعت یک بار "بازنشانی" می شود. طرح من از ذخیره فایل ها در سرورهای Heroku پشتیبانی نمی کند، بنابراین به سادگی فایل بازی من را از Github می کشد. چندین راه حل وجود داشت: از یک پایگاه داده استفاده کنید یا به دنبال سرور دیگری باشید که این فایل را با بازی ذخیره کند. من تصمیم گرفتم فعلاً کاری انجام ندهم، زیرا اساساً ربات آنقدرها مفید نیست. من به آن نیاز داشتم تا یک تجربه کامل به دست بیاورم، که اساساً همان چیزی است که به دست آوردم. بنابراین، توصیه هایی برای Heroku:
-
اگر در روسیه زندگی می کنید به احتمال زیاد مجبور خواهید بود با استفاده از VPN در heroku ثبت نام کنید.
-
در ریشه پروژه باید یک فایل بدون پسوند به نام Procfile قرار دهید. محتوای آن باید اینگونه باشد: https://github.com/miroha/Telegram-Bot/blob/master/Procfile
-
در pom.xml، خطوط زیر را مطابق با مثال اضافه کنید، جایی که در تگ mainClass، مسیر کلاس حاوی متد اصلی را نشان می دهد: bot.BotApplication (اگر کلاس BotApplication در پوشه ربات باشد).
-
با استفاده از دستورات بسته mvn و غیره هیچ پروژه ای نسازید، هروکو همه چیز را برای شما جمع می کند.
-
توصیه می شود یک gitignore به پروژه اضافه کنید، به عنوان مثال:
# Log file *.log # Compiled resources target # Tests test # IDEA files .idea *.iml
-
در واقع پروژه را در github آپلود کنید و سپس مخزن را به Heroku متصل کنید (یا از روش های دیگر استفاده کنید، اگر اشتباه نکنم 3 تا از آنها وجود دارد).
-
اگر دانلود با موفقیت انجام شد ("Build موفق شد")، حتما به Configure Dynos بروید:
و نوار لغزنده را تغییر دهید و سپس مطمئن شوید که در وضعیت ON است (به دلیل اینکه من این کار را انجام ندادم ربات من کار نکرد و چند روزی مغزم را به هم زدم و حرکات غیر ضروری زیادی انجام دادم. ).
-
توکن ربات را در Github مخفی کنید. برای انجام این کار، باید توکن را از متغیر محیطی دریافت کنید:
public class Bot extends TelegramLongPollingBot { private static final String BOT_TOKEN = System.getenv("TOKEN"); @Override public String getBotToken() { return BOT_TOKEN; } ... }
و سپس پس از استقرار ربات، این متغیر را در داشبورد Heroku در تب تنظیمات تنظیم کنید (در سمت راست TOKEN یک فیلد VALUE وجود دارد، توکن ربات خود را در آنجا کپی کنید):
- یک پروژه به طور کامل کار نوشته شده در جاوا دریافت کرد.
- یاد گرفت که با API شخص ثالث (Telegram Bot API) کار کند.
- در عمل، من به سریال سازی عمیق تر پرداختم، با JSON و کتابخانه جکسون بسیار کار کردم (در ابتدا از GSON استفاده می کردم، اما مشکلاتی با آن وجود داشت).
- مهارت های خود را در هنگام کار با فایل ها تقویت کردم، با Java NIO آشنا شدم.
- کار با پیکربندی فایل های xml را یاد گرفتم و به لاگ کردن عادت کردم.
- مهارت بهبود یافته در محیط توسعه (IDEA)؛
- کار با git را یاد گرفت و ارزش gitignore را آموخت.
- کسب مهارت در تجزیه صفحات وب (کتابخانه Jsoup)؛
- چندین الگوی طراحی را یاد گرفت و استفاده کرد.
- ایجاد حس و تمایل به بهبود کد (بازسازی)؛
- من یاد گرفتم که راه حل های آنلاین پیدا کنم و از پرسیدن سوالاتی که نتوانستم پاسخی برای آنها پیدا کنم خجالتی نباشم.
GO TO FULL VERSION