@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. הָהֵן. עבור כפתור "עלות" אנו מגדירים את התיאור "/מחיר" עבור ההתקשרות חזרה, אותו נוכל לקבל מאוחר יותר מהעדכון. בשלב הבא, בכיתה נפרדת, אני כבר יכול לעבד את ההתקשרות חזרה:
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 : מוטבע ותשובה . כתוצאה מכך, אתה בעצם יכול "לצייר" את המקלדת הרצויה בהתאם לדרישות שלך. זה נורא נוח, מכיוון שהמקלדות עשויות להיות שונות, אבל העיקרון נשאר זהה. הנה שיטה אינטואיטיבית לשליחת הודעה עם מקלדת מוטבעת:
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. במידה והמשחק שצוין נמצא בספריית הבוט, תוחזר למשתמש מקלדת מוטבעת, איתה יוכל לקבל מידע על המשחק. אם המשחק לא נמצא, עליך לוודא שהשם מאויית נכון (הוא חייב להתאים לחלוטין לשם המשחק בחנות Google Play, למעט המקרה), או להוסיף את המשחק לספרייה על ידי שליחת הבוט קישור למשחק. פרסתי את הבוט ב-heroku ולמי שמתכננים בעתיד לכתוב בוט משלהם ולארח אותו בחינם ב-heroku, אתן כמה המלצות לפתרון הקשיים שעלולים להיתקל בהם (מאחר שנתקלתי בהם בעצמי). לרוע המזל, בשל אופיו של Heroku, ספריית הבוטים "מאפסת" כל הזמן אחת ל-24 שעות. התוכנית שלי לא תומכת באחסון קבצים בשרתי Heroku, אז היא פשוט מושכת את קובץ המשחק שלי מ-Github. היו כמה פתרונות: השתמש במסד נתונים, או חפש שרת אחר שיאחסן את הקובץ הזה עם המשחק. החלטתי לא לעשות כלום בינתיים, מכיוון שבעצם הבוט לא כל כך שימושי. הייתי צריך את זה דווקא כדי לצבור חוויה מלאה, וזה בעצם מה שהשגתי. אז, המלצות על Heroku:
-
סביר להניח שתצטרך להירשם ב-heroku באמצעות VPN אם אתה גר ברוסיה.
-
בשורש הפרוייקט צריך לשים קובץ ללא סיומת בשם Procfile. התוכן שלו צריך להיות כזה: https://github.com/miroha/Telegram-Bot/blob/master/Procfile
-
ב-pom.xml, הוסף את השורות הבאות לפי הדוגמה , כאשר בתגית mainClass ציינו את הנתיב למחלקה המכילה את השיטה הראשית: bot.BotApplication (אם המחלקה BotApplication נמצאת בתיקיית הבוט).
-
אל תבנו שום פרויקט באמצעות פקודות חבילת mvn וכו', Heroku מרכיב עבורכם הכל.
-
רצוי להוסיף gitignore לפרויקט, למשל זה:
# Log file *.log # Compiled resources target # Tests test # IDEA files .idea *.iml
-
למעשה העלה את הפרויקט ל-github, ואז חבר את המאגר להרוקו (או השתמש בשיטות אחרות, יש 3 כאלה, אם אני לא טועה).
-
אם ההורדה הצליחה ("הבנייה הצליחה"), הקפד לעבור אל הגדר את 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, העתק את האסימון של הבוט שלך לשם):
- קיבל פרויקט עובד במלואו כתוב ב-Java;
- למד לעבוד עם API של צד שלישי (Telegram Bot API);
- בפועל, התעמקתי בהמשכיות, עבדתי הרבה עם JSON ועם ספריית ג'קסון (בהתחלה השתמשתי ב-GSON, אבל היו עם זה בעיות);
- חיזקתי את כישורי בעבודה עם קבצים, הכרתי את Java NIO;
- למדתי לעבוד עם קובצי .xml תצורה והרגיל את עצמי לרישום;
- שיפור מיומנות בסביבת הפיתוח (IDEA);
- למד לעבוד עם git ולמד את הערך של gitignore;
- צבר מיומנויות בניתוח דפי אינטרנט (ספריית Jsoup);
- למד והשתמש במספר דפוסי עיצוב;
- פיתח חוש ורצון לשפר קוד (refactoring);
- למדתי למצוא פתרונות באינטרנט ולא להתבייש לשאול שאלות שלא הצלחתי למצוא עליהן תשובה.
GO TO FULL VERSION