Cписок использованных источников

Федеральное агентство связи Федеральное государственное бюджетное образовательное учреждение высшего образования «Сибирский государственный университет телекоммуникаций и информатики» (СибГУТИ)   ВЫПУСКНАЯ КВАЛИФИКАЦИОННАЯ РАБОТА БАКАЛАВРА Разработка Telegram-бота для агентства недвижимости   Пояснительная записка       Кафедра ПМиК Допустить к защите зав. кафедрой:  проф., д.т.н.   ______________________Фионов А.Н.   Новосибирск 2018г.

Федеральное агентство связи

Федеральное государственное бюджетное образовательное учреждение

Высшего образования

«Сибирский государственный университет телекоммуникаций и информатики»

(СибГУТИ)

 

 

Форма утверждена

научно-методическим советом

ФГОБУ ВПО «СибГУТИ»

протокол № 2 от 04.03. 2014 г.

 

 

КАФЕДРА Прикладной математики и кибернетики

 

 

ЗАДАНИЕ

НА ВЫПУСКНУЮ КВАЛИФИКАЦИОННУЮ РАБОТУ БАКАЛАВРА

 

СТУДЕНТУ    Чердакову К.С. ГРУППЫ ИП-413

 

 

УТВЕРЖДАЮ

«_____» _________________

зав. кафедрой ПМиК

 

___________/ ______________/

 

 

Новосибирск 2018г.


Тема выпускной квалификационной работы бакалавра

Разработка Telegram-бота для агентства недвижимости

 

 

утверждена приказом СибГУТИ от «18» 05 2018 г. №4/720o-18

 

2.Срок сдачи студентом законченной работы « » ______20__ г.

 

Исходные данные к работе

1 Специальная литература

2 Материалы сети интернет

 

4.Содержание пояснительной записки (перечень подлежащих разработке вопросов) Сроки выполнения по разделам
Введение 10.04.2018
Telegram боты 1.05.2018
Бот агентства недвижимости 25.05.2018
Заключение 1.06.2018

 

Дата выдачи задания «  »                                       20      г.

Руководитель _____________________________________

                                            подпись

 

Задание принял к исполнению «  »                          20 г.

Студент __________________________________________

 


Федеральное агентство связи

Федеральное государственное бюджетное образовательное учреждение

Высшего образования

«Сибирский государственный университет телекоммуникаций и информатики»

(СибГУТИ)

Форма утверждена научно-методическим

советом университета

протокол № 3 от 19.02.2015 г.

ОТЗЫВ

 

на выпускную квалификационную работу студента ____________________________________

по теме «________________________________________________________________________»

 

 

Оценка уровней сформированности общекультурных и профессиональных компетенций обучающегося:

Компетенции

Уровень сформированности

компетенции

Высокий Средний Низкий
Общекультурные ОК-7      

Профессиональные

ОПК-4      
ОПК-5      
ПК-1      
ПК-2      
ПК-3      

 

Работа имеет практическую ценность   Тема предложена предприятием  
Работа внедрена   Тема предложена студентом  
Рекомендую работу к внедрению   Тема является фундаментальной  
Рекомендую работу к опубликованию   Рекомендую студента в магистратуру  
Работа выполнена с применением ЭВМ   Рекомендую студента в аспирантуру  

Замечания (УДАЛИТЬ!!): Для руководителей, не работающих в СибГУТИ, на подписи должна стоять печать организации, в которой он работает.

Руководитель бакалаврской работы _________________________________

                                   (должность, уч. степень, подпись, фамилия, имя, отчество (полностью), дата)

 


Пояснения к кодам компетенций (не подшивать!)

 

ОК-7 способность к самоорганизации и самообразованию

ОПК-4 способность участвовать в настройке и наладке программно-аппаратных комплексов

ОПК-5 способность решать стандартные задачи профессиональной деятельности на основе информационной и библиографической культуры с применением информационно-коммуникационных технологий и с учетом основных требований информационной безопасности

ПК-1 способность разрабатывать модели компонентов информационных систем, включая модели баз данных и модели и интерфейсов "человек – электронно-вычислительная машина"

ПК-2 способность разрабатывать компоненты аппаратно-программных комплексов и баз данных, используя современные инструментальные средства и технологии программирования

ПК-3 способностью обосновывать принимаемые проектные решения, осуществлять постановку и выполнять эксперименты по проверке их корректности и эффективности

 

 

           


АННОТАЦИЯ

 

Выпускной квалификационной работы студента Чердакова Кирилла Сергеевича

                                                                                                                                                                         (Фамилия,И.О.)

по теме «Разработка Telegram-бота для агентства недвижимости»

 

Объём работы - 34 страницы, на которых размещены 12рисунков и 14таблиц. При написании работы использовалось 18 источников.

 

Ключевые слова: бот, база данных, сессия, поток, интерфейс,

Работа выполнена                       СибГУТИ, кафедра ПМиК  

                                                                                                (название предприятия, подразделения)

Руководитель         доцент каф. ПМиК, Бах Ольга Анатольевна

                                                   (должность, уч.степень, звание, Фамилия Имя Отчество)

 

 

Целью работы является:

Разработка и тестирование Telegram-бота для агентства недвижимости.

 

Решаемые задачи:

− Анализ существующих решений данной задачи;

− Выбор наиболее подходящих инструментов разработки решения;

− Разработка архитектуры хранилища данных;

− Разработка архитектуры сервера приложения;

− Разработка интерфейса пользователя;

Основные результаты

Реализован Telegram-бот для агентства недвижимости на языке программирования Java, в качестве хранилища использующий локальную базу данных Sqlite.


Содержание

Введение. 3

1 telegram боты.. 4

1.1     Постановка задачи. 4

1.2     Обзор предметной области. 4

1.3     Возможности Telegram ботов. 8

2 Бот агентства недвижимости.. 18

2.1     Инструменты разработки. 18

2.2     Архитектура базы данных. 20

2.3     Архитектура приложения. 22

2.4     Уведомления. 28

2.5     Интерфейс пользователя. 28

Заключение. 34

список использованных источников.. 35

Приложение А Диаграмма классов.. 36

Приложение Б Листинг Программы.. 37

Введение

Разработка бота, интегрированного в популярный мессенджер – задача крайне актуальная в наше время. Не смотря на то, что чат-боты появились более 50 лет назад, востребованными они стали только сейчас. Обусловлено это, в первую очередь тем, что до недавнего времени чат боты рассматривались исключительно как собеседники, а не как сервисы. Но с появлением возможностей и идей автоматизации простых процессов боты стали выполнять часть работы сотрудников крупных компаний. Теперь боты могут предоставлять интерфейс взаимодействия пользователям, заменить программы и сайты, но в то же время и обеспечить интерактивность, как при общении с человеком.

Стоило бы начать с описания, что это такое – бот. Со ссылкой на литературу.

Тенденция развития ботов такова, что большинство сервисов в скором времени будут параллельно предоставляться в популярных мессенджерах, это обеспечит стандартизацию инструментов разработки и использования не только сервисов одной области, но и из разных, совершенно не связанных областей. В наше время любая компания, предоставляющая услуги, уже имеет сайт или программу, но основная проблема заключается в том, что интерфейс взаимодействия везде разный и зачастую не интуитивно понятен. Современные боты предлагают одинаковый, интуитивно понятный интерфейс, подкрепленный интерактивностью, что является огромным преимуществом, по сравнению с другими способами коммуникаций.

В данной работе описывается процесс разработки такого бота, интегрированного в мессенджер «Telegram». Его основной задачей будет предоставление услуг, идентичных услугам, предоставляемым агентством недвижимости. Ввиду того, что конечными потребителями и продавцами в данной области являются физические лица, была предпринята попытка заменить посредника в виде агентства на робота, то есть сделать данный процесс полностью автономным и саморегулируемым. Несмотря на это, данный бот может работать и параллельно, с любым агентством, так как не заточен под логику какой-то конкретной компании. Реализованный сервис позволяет размещать и просматривать объявления о покупке, продаже и аренде квартир и прочих объектов недвижимости, а тот факт, что бот интегрирован в мессенджер, лишает необходимости в поиске средств коммуникации между клиентами.

Проблемы с запятыми. Не везде есть, где они нужны.

 

 

Telegram боты

Постановка задачи

Необходимо разработать Telegram-бота для агентства недвижимости. Бот должен реализовывать следующий функционал: Кого, что? – Винительный падеж. «бот», а не «бота». 

− Добавление новых квартир в базу.

− Поиск в базе подходящих квартир.

− Оповещение пользователей о добавлении в базу потенциально интересующих их данных.

А удаление квартир из базы?

Для реализации данного функционала поставлены следующие задачи:

− Разработка базы данных

− Разработка архитектуры бота

− Разработка пользовательского интерфейса

− Написание unit-тестов покрывающих основной функционал

Обзор предметной области

Telegram — кроссплатформенный мессенджер, позволяющий обмениваться сообщениями и медиа файлами многих форматов. Используются собственническая серверная часть c закрытым кодом, работающая на мощностях нескольких компаний США и Германии. Помимо стандартного обмена сообщениями в диалогах и группах, в мессенджере можно хранить неограниченное количество файлов, вести каналы (микро блоги), создавать и использовать ботов [1].

MTProto— криптографический протокол, используемый в системе обмена сообщениями Telegram для шифрования переписки пользователей. Протокол был разработан Николаем Дуровым и другими программистами Telegram [1].

Боты – это сторонние приложения, которые работают внутри Telegram. Пользователи могут взаимодействовать с ботами, отправляя им сообщения, команды и встроенные запросы. Вы контролируете своих ботов, используя HTTPS-запросы к BotAPI. Ботов можно запрограммировать выполнять различные задачи, а также боты можно использовать для интеграция с другими сервисами. Например, бот может отправлять комментарии или управлять «умным домом». Или, например, отправлять вам уведомления при совершении каком-то действия или события (примеры: GitHub Bot, Image Bot). Бот может отображать погоду, переводить тексты или предупреждать о предстоящих событиях по вашему запросу (например, бот опросов). Бота можно использовать в качестве социального сервиса. Робот может находить вам собеседника, основываясь на ваших общих интересах и увлечениях [2].

API (программный интерфейс приложения, интерфейс прикладного программирования) (англ. application programming interface) — набор готовых классов, процедур, функций, структур и констант, предоставляемых приложением (библиотекой, сервисом) или операционной системой для использования во внешних программных продуктах. Используется программистами при написании всевозможных приложений [3].

Серверное программное обеспечение (сервер) — в информационных технологиях — программный компонент вычислительной системы, выполняющий сервисные (обслуживающие) функции по запросу клиента, предоставляя ему доступ к определённым ресурсам или услугам [4].

HTTP (HyperText Transfer Protocol — «протокол передачи гипертекста») — протокол прикладного уровня передачи данных (изначально — в виде гипертекстовых документов в формате «HTML», в настоящий момент используется для передачи произвольных данных). Основой HTTP является технология «клиент-сервер», то есть предполагается существование:

− Потребителей (клиентов), которые инициируют соединение и посылают запрос;

− Поставщиков (серверов), которые ожидают соединения для получения запроса, производят необходимые действия и возвращают обратно сообщение с результатом.

Что с форматом? Везде отступ только первой строки, а тут – всего абзаца.

Каждое HTTP-сообщение состоит из трёх частей, которые передаются в указанном порядке:

− Стартовая строка (Starting line) — определяет тип сообщения;

− Заголовки (Headers) — характеризуют тело сообщения, параметры передачи и прочие сведения;

− Тело сообщения (Message Body) — непосредственно данные сообщения. Обязательно должно отделяться от заголовков пустой строкой.

Тело сообщения может отсутствовать, но стартовая строка и заголовок являются обязательными элементами. Исключением является версия 0.9 протокола, у которой сообщение запроса содержит только стартовую строку, а сообщения ответа только тело сообщения.

Для версии протокола 1.1 сообщение запроса обязательно должно содержать заголовок Host.

Стартовые строки различаются для запроса и ответа. Строка запроса выглядит так:

GET URI — для версии протокола 0.9;

Метод URI HTTP/Версия — для остальных версий.

Здесь:

Метод (Method) — тип запроса, одно слово заглавными буквами. В версии HTTP 0.9 использовался только метод GET, список методов для версии 1.1 представлен ниже.

URI определяет путь к запрашиваемому документу.

Версия (Version) — пара разделённых точкой цифр. Например, 1.0.

Чтобы запросить страницу данной статьи, клиент должен передать строку (задан всего один заголовок):

GET /wiki/HTTP HTTP/1.0

Host: ru.wikipedia.org

Стартовая строка ответа сервера имеет следующий формат: HTTP/Версия КодСостояния Пояснение, где:

Версия — пара разделённых точкой цифр, как в запросе;

Код состояния (Status Code) — три цифры. По коду состояния определяется дальнейшее содержимое сообщения и поведение клиента;

Пояснение (Reason Phrase) — текстовое короткое пояснение к коду ответа для пользователя. Никак не влияет на сообщение и является необязательным.

Например, стартовая строка ответа сервера на предыдущий запрос может выглядеть так:

HTTP/1.0 200 OK

Метод HTTP (англ. HTTP Method) — последовательность из любых символов, кроме управляющих и разделителей, указывающая на основную операцию над ресурсом. Обычно метод представляет собой короткое английское слово, записанное заглавными буквами. Обратите внимание, что название метода чувствительно к регистру.

Сервер может использовать любые методы, не существует обязательных методов для сервера или клиента. Если сервер не распознал указанный клиентом метод, то он должен вернуть статус 501 (Not Implemented). Если серверу метод известен, но он неприменим к конкретному ресурсу, то возвращается сообщение с кодом 405 (Method Not Allowed). В обоих случаях серверу следует включить в сообщение ответа заголовок Allow со списком поддерживаемых методов.

Кроме методов GET и HEAD, часто применяется метод POST.

OPTIONS используется для определения возможностей веб-сервера или параметров соединения для конкретного ресурса. В ответ серверу следует включить заголовок Allow со списком поддерживаемых методов. Также в заголовке ответа может включаться информация о поддерживаемых расширениях.

Предполагается, что запрос клиента может содержать тело сообщения для указания интересующих его сведений. Формат тела и порядок работы с ним в настоящий момент не определён; сервер пока должен его игнорировать. Аналогичная ситуация и с телом в ответе сервера.

Для того, чтобы узнать возможности всего сервера, клиент должен указать в URI звёздочку — «*». Запросы «OPTIONS * HTTP/1.1» могут также применяться для проверки работоспособности сервера (аналогично «пингованию») и тестирования на предмет поддержки сервером протокола HTTP версии 1.1.

Результат выполнения этого метода не кэшируется.

GET используется для запроса содержимого указанного ресурса. С помощью метода GET можно также начать какой-либо процесс. В этом случае в тело ответного сообщения следует включить информацию о ходе выполнения процесса.

Клиент может передавать параметры выполнения запроса в URI целевого ресурса после символа «?»:

GET /path/resource? param1=value1&param2=value2 HTTP/1.1

Согласно стандарту HTTP, запросы типа GET считаются идемпотентными (при повторном запросе должен давать тот же ответ).

POST применяется для передачи пользовательских данных заданному ресурсу. Например, в блогах посетители обычно могут вводить свои комментарии к записям в HTML-форму, после чего они передаются серверу методом POST и он помещает их на страницу. При этом передаваемые данные (в примере с блогами — текст комментария) включаются в тело запроса. Аналогично с помощью метода POST обычно загружаются файлы на сервер.

В отличие от метода GET, метод POST не считается идемпотентным[4], то есть многократное повторение одних и тех же запросов POST может возвращать разные результаты (например, после каждой отправки комментария будет появляться очередная копия этого комментария).

При результате выполнения 200 (Ok) в тело ответа следует включить сообщение об итоге выполнения запроса. Если был создан ресурс, то серверу следует вернуть ответ 201 (Created) с указанием URI нового ресурса в заголовке Location.

Сообщение ответа сервера на выполнение метода POST не кэшируется [5].

 

JSON – тестовый формат обмена данными, используемый для сериализации. JSON-текст чаще всего представляет собой упорядоченный набор пар ключ: значение. В различных языках это реализовано по-разному, но в общем случае это ассоциативный массив.

Следующий пример показывает JSON-представление объекта, описывающего человека. В объекте есть строковые поля имени и фамилии, объект, описывающий адрес, и массив, содержащий список телефонов. Как видно из примера, значение может представлять собой вложенную структуру [6]:

 

{

"firstName": "Иван",

"lastName": "Иванов",

"address": {

  "streetAddress": "Московское ш., 101, кв.101",

  "city": "Ленинград",

  "postalCode": "101101"

},

"phoneNumbers": [

  "812 123-1234",

  "916 123-4567"

]

}

 

Возможности Telegram ботов

Основные возможности

Почему в содержании присутствуют не все уровни заголовков?

Telegram-боты — специальные аккаунты в Telegram, созданные для того, чтобы автоматически обрабатывать и отправлять сообщения. которые не требуют номера телефона при создании. По сути, эти аккаунты играют роль интерфейса к сервису, который работает на удалённом сервере.

Пользователи могут взаимодействовать с ботами при помощи сообщений, отправляемых через обычные или групповые чаты. Эти сообщения могут содержать не только текст, но и все самые популярные типы данных, такие как фото, аудио, видео, местоположение, контакты и просто файлы. Мессенджер умеет работать с основными типами файлов, например, вы можете посмотреть видео или послушать музыку пользуясь встроенным плеером, вам не нужно скачивать никакого стороннего программного обеспечения. С другой стороны, разработчикам не нужно реализовывать этот функционал в своих программах.

Еще одной ключевой особенностью ботов, интегрированных в мессенджер является надежное шифрование передаваемых данных, предоставляемое разработчиком мессенджера. Разработчикам ботов не нужно изучать низкоуровневые методы работы с MTProto и шифрованием — общение с роботом организовано при помощи обычного HTTPS интерфейса с упрощёнными методами Telegram API.

 

Кастомизированные клавиатуры

Одна из возможностей Bot API — кастомизированные клавиатуры (см. рис. 1.1). При передаче сервером ответа есть возможность передать команду на отображение специальной клавиатуры с предустановленными вариантами ответа Клиент Telegram, получив сообщение, отобразит пользователю вашу клавиатуру. Нажатие на клавишу сразу же отправит на сервер соответствующую команду. Таким образом можно значительно упростить взаимодействие робота с пользователем.

Рисунок 1.1- Кастомизированные клавиатуры

Сделайте интервалы между текстом и рисунками!

Бывают случаи, когда пользователи предпочитают делать что-то без отправки каких-либо сообщений в чат. Например, когда пользователь изменяет настройки или просматривает результаты поиска. В таких случаях можно использовать встроенные клавиатуры, интегрированные непосредственно в сообщения, к которым они принадлежат (см. рис. 1.2).

 

Рисунок 1.2 – Встроенные клавиатуры

 

В отличие от пользовательских клавиатур ответов, нажатие кнопок на встроенных клавиатурах не приводит к отправке сообщений в чат. Вместо этого встроенные клавиатуры поддерживают кнопки, которые работают за кулисами: кнопки обратного вызова, кнопки URL и переключение на встроенные кнопки.

При использовании кнопок обратного вызова ваш бот может обновлять существующие сообщения (или только их клавиатуры), чтобы чат оставался актуальным. [7]

Telegram Bot API

Логика бота контролируется при помощи специальный запросов к API для ботов или Telegram Bot API, который представляет из себя HTTPS-интерфейс для работы с ботами в Telegram.

Все запросы к Telegram Bot API должны осуществляться через HTTPS в следующем виде: https://api.telegram.org/bot<token>/НАЗВАНИЕ_МЕТОДА.

Допускаются GET и POST запросы. Для передачи параметров в Bot API доступны 4 способа:

− запрос в URL,

− application/x-www-form-urlencoded,

− application/json (не подходит для загрузки файлов),

− Multipart/form-data (для загрузки файлов).

Ответ придёт в виде JSON-объекта, в котором всегда будет булево поле ok и опциональное строковое поле description, содержащее человекочитаемое описание результата. Если поле ok истинно, то запрос прошёл успешно и результат его выполнения можно увидеть в поле result. В случае ошибки поле ok будет равно false, а причины ошибки будут описаны в поле description. Кроме того, в ответе будет целочисленное поле error_code. Например, JSON объект типа Update, представленный ниже является ответом Telegram Bot Api на запрос https://api.telegram.org/ bot<token>/getUpdates после того, как пользователь прислал боту сообщение “/start”:

"ok":true,

"result":[ 

    "update_id":519751455,

    "message":{ 

       "message_id":4480,

       "from":{ 

          "id":63059291,

          "is_bot":false,

          "first_name":"\u041a\u0438\u0440\u044f",

          "language_code":"ru-ru"

       },

       "chat":{ 

          "id":63059291,

          "first_name":"Киря",

          "type":"private"

       },

       "date":1528901086,

       "text":"/start

    }

}

]

}

 Основные типы объектов представлены в табл. 1, структура каждого объекта - в табл. 2-12.

Таблица 1 - Основные типы объектов

Объект Описание
Update Этот объект представляет входящее обновление.
User Этот объект представляет бота или пользователя Telegram.
Chat Этот объект представляет собой чат.
Message Этот объект представляет собой сообщение.
PhotoSize Этот объект представляет изображение определённого размера или превью файла / стикера.
CallbackQuery   Этот объект представляет входящий запрос обратной связи от инлайн-кнопки с заданным callback_data. Если кнопка, создавшая этот запрос, была привязана к сообщению, то в запросе будет присутствовать поле message.
Contact   Этот объект представляет контакт с номером телефона.
InlineKeyboardMarkup Этот объект представляет встроенную клавиатуру, которая появляется под соответствующим сообщением.
InlineKeyboardButton Этот объект представляет одну кнопку встроенной клавиатуры. Вы обязательно должны задействовать ровно одно опциональное поле.
ReplyKeyboardMarkup Этот объект представляет клавиатуру с опциями ответа
KeyboardButton Этот объект представляет одну кнопку в клавиатуре ответа. Для обычных текстовых кнопок этот объект может быть заменён на строку, содержащую текст на кнопке.

 

Таблица 2 – Описание объекта User

Поле Тип поля Описание
id Integer Уникальный идентификатор пользователя или бота
is_bot Boolean Флаг, означающий, что пользователь является ботом.
first_name String Имя бота или пользователя
last_name String Опционально. Фамилия бота или пользователя
username String Опционально. Username пользователя или бота

Нельзя отрывать заголовки от таблиц.

Таблица 3 – Описание объекта Chat

Поле

Тип поля

Описание
id Integer

Уникальный идентификатор чата. Абсолютное значение не превышает 1e13

type String

Тип чата: “private”, “group”, “supergroup” или “channel”

title String

Опционально. Название, для каналов или групп

username String

Опционально. Username, для чатов и некоторых каналов

first_name String

Опционально. Имя собеседника в чате

last_name String

Опционально. Фамилия собеседника в чате

photo ChatPhoto

Опционально. Фотография собеседника.

description String

Опционально. Описание чата.

       

 

Таблица 4 – Описание объекта Update

Поле Тип поля Описание
update_id Integer Уникальный идентификатор обновления. Идентификаторы обновлений начинаются с определенного положительного числа и последовательно возрастают. Этот идентификатор становится особенно удобным, если вы используете Webhooks, поскольку он позволяет игнорировать повторяющиеся обновления или восстанавливать правильную последовательность обновлений, если они выходят из строя. Если новых обновлений не будет, по крайней мере, на неделю, то идентификатор следующего обновления будет выбран случайным образом, а не последовательно.
message Message Необязательный. Новое входящее сообщение любого типа - текст, фотография, наклейка и т. Д.
edited_message Message Необязательный. Новая версия сообщения, которая известна боту и была отредактирована
channel_post Message Необязательный. Новое сообщение о входящем канале любого типа - текст, фотография, наклейка и т. Д.
edited_channel_post Message Необязательный. Новая версия сообщения канала, которая известна боту и была отредактирована
inline_query InlineQuery Необязательный. Новый входящий встроенный запрос
chosen_inline_result ChosenInlineResult Необязательный. Результат встроенного запроса, который был выбран пользователем и отправлен его чат-партнеру. Подробнее о том, как включить эти обновления для своего бота, смотрите в нашей документации по сбору отзывов.
callback_query CallbackQuery Необязательный. Новый входящий запрос обратного вызова

 

Таблица 5 – Описание объекта Message

Поле Тип поля Описание
message_id Integer Уникальный идентификатор сообщения
from User Опционально. Отправитель. Может быть пустым в каналах.
date Integer Дата отправки сообщения (Unix time)
chat Chat Диалог, в котором было отправлено сообщение
forward_from User Опционально. Для пересланных сообщений: отправитель оригинального сообщения
forward_date Integer Опционально. Для пересланных сообщений: дата отправки оригинального сообщения
reply_to_message Message Опционально. Для ответов: оригинальное сообщение.
edit_date Integer Опционально. Для измененных сообщений: дата последнего изменения сообщения
text String Опционально. Для текстовых сообщений: текст сообщения, 0-4096 символов
photo Array of PhotoSize Опционально. Если сообщение является фотографией, то содержит массив объектов PhotoSize одной и той же фотографии.
contact Contact Опционально. Информация об отправленном контакте
location Location Опционально. Информация о местоположении

 

Таблица 6 – Описание объекта PhotoSize

Поле Тип поля Описание
file_id String Уникальный идентификатор файла
width Integer Ширина фотографии в пикселах
height Integer Высота фотографии в пикселах
file_size Integer Размер файла фотографии

 

Таблица 7 – Описание объекта Contact

Поле Тип поля Описание
phone_number String Номер телефона
first_name String Имя
last_name String Опционально. Фамилия
user_id Integer Опционально. Идентификатор пользователя в Telegram

 

Таблица 8 – Описание объекта CallbackQuery

Поле Тип поля Описание
id String Уникальный идентификатор запроса
from User Отправитель
message Message Опционально. Сообщение, к которому была привязана вызвавшая запрос кнопка. Обратите внимание: если сообщение слишком старое, содержание сообщения и дата отправки будут недоступны.
inline_message_id String Опционально. Идентификатор сообщения, отправленного через вашего бота во встроенном режиме
data String Данные, связанные с кнопкой. Обратите внимание, что клиенты могут добавлять свои данные в это поле.

 

Таблица 9 – Описание объекта InlineKeyboardMarkup

Поле Тип поля Описание
inline_keyboard Массив массивов с InlineKeyboardButton Массив массивов объектов InlineKeyboardButton.

 

Таблица 10 – Описание объекта InlineKeyboardButton

Поле Тип поля Описание
text String Текст на кнопке
url String Опционально. URL, который откроется при нажатии на кнопку
callback_data String Опционально. Данные, которые будут отправлены в callback_query при нажатии на кнопку

 

Таблица 11 – Описание объекта ReplyKeyboardMarkup

Поле Тип поля Описание
keyboard Массив массивов с KeyboardButton Массив рядов кнопок, каждый из которых является массивом объектов KeyboardButton
resize_keyboard Boolean Опционально. Указывает клиенту подогнать высоту клавиатуры под количество кнопок (сделать её меньше, если кнопок мало). По умолчанию False, то есть клавиатура всегда такого же размера, как и стандартная клавиатура устройства.
one_time_keyboard Boolean Опционально. Указывает клиенту скрыть клавиатуру после использования (после нажатия на кнопку). Её по-прежнему можно будет открыть через иконку в поле ввода сообщения. По умолчанию False.
selective Boolean Опционально. Этот параметр нужен, чтобы показывать клавиатуру только определённым пользователям. Цели: 1) пользователи, которые были @упомянуты в поле text объекта Message; 2) если сообщения бота является ответом (содержит поле reply_to_message_id), авторы этого сообщения. Пример: Пользователь отправляет запрос на смену языка бота. Бот отправляет клавиатуру со списком языков, видимую только этому пользователю.

 

Таблица 12 – Описание объекта KeyboardButton

Поле Тип поля Описание
text String Текст на кнопке. Если ни одно из опциональных полей не использовано, то при нажатии на кнопку этот текст будет отправлен боту как простое сообщение.
request_contact Boolean Опционально. Если значение True, то при нажатии на кнопку боту отправится контакт пользователя с его номером телефона. Доступно только в диалогах с ботом.
request_location Boolean Опционально. Если значение True, то при нажатии на кнопку боту отправится местоположение пользователя. Доступно только в диалогах с ботом.

 

В табл. 13 представлены основные запросы к Telegram Bot Api.

Таблица 13 - Основные запросы

Метод Описание
getMe   Простой метод тестирования токена аутентификации бота. Не требует параметров. Возвращает основную информацию о боте в виде объекта пользователя.
sendMessage Метод для отправки текстовых сообщений
forwardMessage Метод для пересылки Сообщений любого рода
sendPhoto Метод для отправки фотографий
sendAudio Метод для отправки аудиофайлов, если, чтобы клиенты Telegram получали их в музыкальном проигрывателе. Аудиофайл должен быть в формате mp3.
sendDocument Метод для отправки файлов.
sendContact Метод для отправки телефонных контактов.

Методы, представленные в табл. 14, позволяют изменить существующее сообщение в переписке вместо отправки нового сообщения с результатом действия. Это наиболее полезно для сообщений со встроенными клавиатурами, использующими запросы обратного вызова, но также может помочь уменьшить беспорядок в разговорах с обычными чат-ботами.

 

Таблица 14 – Методы, позволяющие редактировать сообщения

Метод Описание
editMessageText Метод для редактирования текстовых сообщений, отправляемых ботом
editMessageReplyMarkup Используйте этот метод для редактирования только replymarkup Сообщений, отправленных ботом

Методы, представленные выше позволяют отправить или изменить сообщение, но чтобы его получить используется метод getUpdates,который возвращает список объектов типа Update. С помощью данного метода мы просто периодически опрашиваем сервера Telegram на наличие обновлений, и когда они есть, они возвращаются нам в качестве ответа на запрос. Но возникает резонный вопрос, как часто необходимо опрашивать сервера, ведь ситуации бывают разными, иногда это необходимо делать очень часто, а иногда это не нужно делать по нескольку часов.

А не разумнее отправлять уведомления о появлении обновлений? Это же так и делается, разве нет?

Решением вышеизложенной задачи выступает используемая технология длительного опроса, называемая Long Polling. При длительном опросе клиент запрашивает информацию с сервера точно так же, как при обычном опросе, но с ожиданием, что сервер может не ответить немедленно. Если у сервера нет новой информации для клиента, когда был получен опрос, вместо отправки пустого ответа сервер держит запрос открытым и ожидает, когда информация о доступе станет доступной. После получения новой информации сервер немедленно отправляет HTTP-ответ клиенту, завершая открытый HTTP-запрос. После получения ответа сервера клиент часто сразу же выдает другой запрос сервера. Таким образом, исключается обычная задержка ответа - время между тем, когда появится новая информация и следующим запросом клиента; грубо говоря, мы посылаем запрос, как только на серверах Telegram появляется новая информация [9].

Бот агентства недвижимости

Инструменты разработки

В качестве средств разработки были выбраны следующие инструменты:

1. Java— сильно типизированный объектно-ориентированный язык программирования. Язык Java был выбран из-за следующих преимуществ:

− Java – полностью объектно-ориентированный язык программирования, позволяющий создавать модульные программы. Объектно-ориентированная парадигма в этом языке не только полная, но еще и сильно развитая. В языке Java присутствуют такие полезные инструменты, как множественное наследование, пакеты, интерфейсы и другие мощные функции. Благодаря таким функциями программист может создавать многопоточные программы, оперируя инструкциями высокого уровня.

− Модульная архитектура позволяет писать модульные тесты, а также использовать один и тот же код многократно. Плюс ко всему объект является в какой-то степени атомарным, что упрощает манипуляции с сущностью, потому что это проще воспринимается человеком, привыкшим к тому, что все то, что окружает нас в некотором роде – объекты.

− Java –достаточно старый язык программирования, поэтому решение проблем достигается банальным поиском в интернете по ключевым словам.

− Java имеет Си подобный синтаксис, знаменитый своей лаконичностью, строгостью и достаточно высокой читабельностью.

− На языке Java написано огромное количество библиотек, позволяющих работать с любыми типами данных не имея глубоких познаний.

− Java Exceptions – это полноценная и сильно развитая система обработки исключительных ситуаций, которые постоянно возникают в коммерческих продуктах предназначенных для неподготовленного конечного пользователя.

− Java – кроссплатформенных язык, потому что программа исполняется не процессором на прямую, а виртуальной машиной, которую можно установить практически на любое устройство.[10]

2. Библиотека TelegramBots – простая библиотека для языка Java, являющаяся оберткой над Telegram Bot API. Данная библиотека, позволяет не форматировать HTTP запросы, а использовать набор методов и объектов представленных классами, а также отлавливать исключительные ситуации, в том числе ошибки сетевого взаимодействия, в виде Java Exceptions. [11] Жаргон!

3. JUnit —самая известная библиотека для модульного тестирования программного обеспечения на языке Java. Помимо стандартных функций assert’ов тесты из данной библиотеки могут выполняться параллельно, а в случае неудачи можно получить максимально удобное описание сложившейся ошибки. Так же в данной библиотеки, есть полезные аннотации, позволяющие отключить, пропустить, выполнить многократно и проводить прочие манипуляции с тестами. [12]

4. IntelliJ IDEA — интегрированная среда разработки программного обеспечения для многих языков программирования, в частности Java, JavaScript, Python, разработанная компанией JetBrains. Данная среда разработки популярна из-за таких полезных функций как, предиктивный ввод, автоформатирование и автодополнение, которые уменьшают время написания кода. Другая среда разработки, имеющая подобный функционал – это Eclipse, но она имеет менее приятный интерфейс и не такую удобную документацию. [13]

5. SQL – самый популярный язык запросов, при желании, перенос базы в другую СУБД будет выполнен почти безболезненно, поэтому в качестве локального хранилища была выбрана база данных управляемая SQLite. [14]

6. SQLite — компактная встраиваемая СУБД. SQLite не использует парадигму клиент-сервер, то есть движок SQLite не является отдельно работающим процессом, с которым взаимодействует программа, а представляет собой библиотеку, с которой программа компонуется, и движок становится составной частью программы. Несколько процессов или потоков могут одновременно без каких-либо проблем читать данные из одной базы. Запись в базу можно осуществить только в том случае, если никаких других запросов в данный момент не обслуживается; в противном случае попытка записи оканчивается неудачей, и в программу возвращается код ошибки. Благодаря архитектуре движка возможно использовать SQLite как на встраиваемых системах, так и на выделенных машинах с гигабайтными массивами данных. [15]

7. SQLiteAdministrator является мощным инструментом, c помощью которого можно создавать, проектировать или администрировать файлы базы данных SQLite. Редактор кода SQL помогает быстро писать запросы sql с такими функциями, как завершение кода и выделение. [16]

8. Git — распределённая система управления версиями. Повсеместно используется разработчиками для облегчения работы с изменяющейся информацией. Система управления версиями позволяет хранить несколько версий, а также копий, одного и того же документа в нескольких местах, локально и удаленно, при необходимости возвращаться к более ранним версиям, определять, кто и когда сделал то или иное изменение, и многое другое. [17]

9. GitHub — крупнейший веб-сервис для хостинга IT-проектов и их совместной разработки. Основан на системе контроля версий Git. [18]

Постарайтесь решить проблемы с запятыми. То лишние, то не там, то не хватает…

Архитектура базы данных

База данных представляет собой комплекс таблиц, связанных между собой логически, по соответствующим полям (см. рис. 2.1).

Рисунок 2.1 – Схема базы данных

Рассмотрим каждый объект схемы:

1. Users – таблица, содержащая данные о пользователях приложения. Помимо служебной информации, данные из этой таблица отправляются пользователям, чтобы те могли связаться в другими пользователям в целях купли или продажи объектов недвижимости. Таблица имеет следующие поля:

− TelegramId – уникальный идентификатор пользователя в мессенджере.

− First Name – имя пользователя, взятое из мессенджера.

− Last Name – фамилия пользователя, взятая из мессенджера.

− User Name – логин пользователя, взятый из мессенджера.

− Phone Number – номер телефона, предназначен для общения с клиентами, указывается непосредственно в приложении.

− Email –адрес электронной почты, предназначен для общения с клиентами, указывается непосредственно в приложении.

2. Apartments – хранит квартиры, которые хотят продать или сдать пользователи. Таблица имеет следующие поля:

− Id – первичный ключ

− Street – улица, на которой находится квартира

− House number – номер дома в котором располагается, например, квартира.

− Price – цена за объект или за месяц аренды.

− Seller – id продавца из таблицы Users

− Square – площадь объекта

− Number – номер объекта в доме, не сообщается покупателю.

− Added – флаг, показывающий был ли добавлен объект в базу, или еще находится на этапе формирования.

Этаж? Прочие необходимые данные (количество комнат, например)? Какие-то дополнительные сведения – 2 санузла, лоджия, балкон… ? Неужели кто-то будет искать квартиру, не конкретизируя, сколько в ней должно быть комнат????

3. Wishes – хранит пожелания, пользователей, сформированные по некоторым критериям, позволяющие предлагать данных пользователям наиболее подходящие варианты. Таблица имеет следующие поля:

− id – первичный ключ

− Buyer – id покупателя из таблицы Users.

− Street – улица, на которой пользователь хотел бы купить или арендовать объект.

− Price – желаемая цена «до».

− Square – желаемая площадь «от».

− Added - флаг, показывающий было ли добавлено пожелание в базу, или еще находится на этапе формирования.

4. Photos – таблица, в которой по идентификатору объекта хранятся изображения, относящиеся к нему. Таблица имеет следующие поля:

− Id – первичный ключ.

− Photo – изображение, представленное строкой-идентификатором, хранящееся на серверах telegram. Используя этот идентификатор можно отправлять изображение объекта потенциальным покупателям.

− Apartment ID – id объекта, к которому относится данное изображение.

5. Sessions – таблица, в которой хранятся сессии пользователей. Предназначение данной таблицы будет описано позднее. Таблица имеет следующие поля:

− Chat Id – id чата в мессенджере, в котором была создана данная сессия.

− State – состояние сессии.

− Action – действие, выполняемое в сессии.

− Type – тип сессии. Сессии бывают двух типов: сессия продавца и сессия покупателя.

− Object Id – id объекта из таблицы wishes или apartments, формируемого в рамках данной сессии.

6. Ignored objects – таблица, в которой хранятся объекты, помеченные пользователями, как «не интересующие». Решение хранить и обрабатывать эту информацию было принято ввиду особенностей интерфейса. Чтобы бот не присылал постоянно большое количество повторяющейся и бесполезной информации, пользователям была предоставлена возможность фильтровать свои запросы самостоятельно, оставляя только интересующие его объекты. Таблица имеет следующие поля:

− Used Id – telegram Id пользователя, который пометил данный объект, как «не интересующий его».

− Object Id – id объекта из таблицы Apartments или Wishes.

− Type – тип объекта. В данный момент может принимать только два значения: “WISH” и “APARTMENT”.

Архитектура приложения

Основные концепции

Разрабатывая архитектуру приложения, стояла задача сделать ее более модульной, чтобы она была расширяемой и масштабируемой. Модульная архитектура так же предполагает более простое тестирование, навигацию и отладку кода. «Ямщик погоняет лошадку, весело мордой крутя». Исправить фразу!!!!!!!

«Также» – в смысле «тоже» – пишется слитно. Так же, [как и …] – раздельно. Исправьте здесь и в других местах в зависимости от смысла.  

Во-первых, было определено множество сущностей и созданы классы для каждого из них (см. приложение А). Каждый класс выполняет свою роль, она может быть сложной, управляющей, как у менеджера (например, MessageManager), или простой, для хранения информации, как у объекта (Apartment). В архитектуре использовались такие подходы объектно-ориентированного программирования, как наследование и композиция, позволившие сделать код предельно компактным и в тоже время гибким. Ниже будут описаны основные классы, их роли и связи между ними.

Bot – класс, наследуемый от TelegramLongPollingBot, описанный в библиотеке TelegramBots. В данном классе присутствуют в качестве полей объекты следующих классов: MessageManager, CallbackManager, Database. Bot является ядром программы, именно он отвечает за сетевое взаимодействие, отправляет сообщения и принимает обновления, решает что с ними делать. В зависимости от содержимого обновления Bot решает кому передать данное обновление на обработку MessageManager’у или CallbackManager’у соответственно. Так же Bot создает соединение с локальной базой данных и передает его обоим менеджерам. Обратиться к боту можно из любого места, благодаря статическому полю currentBot, в котором хранится ссылка на текущий экземпляр этого класса.

Database – класс, осуществляющий взаимодействие менеджеров с базой данных. В данном классе реализованы методы добавления, удаления и изменения данных, проводимых пользователями. Данные методы вызываются менеджерами, но по правилам инкапсуляции, для менеджеров данный класс является черным ящиком, который предоставляет им интерфейс хранения и изменения данных в долговременной памяти. Благодаря такому подходу, в случае необходимости изменить способ хранения, придется реализовать только данный интерфейс.

Заголовок должен иметь бОльший интервал относительно текста ПЕРЕД ним, чем ПОСЛЕ него. А у Вас – наоборот. Он «прилипает» к тексту предыдущего раздела.

Менеджеры

MessageManager – класс, обрабатывающий сообщения, приходящие от пользователя. Этот класс уточняет, в какой роли хочет выступить пользователь в данный момент: покупателя или продавца, и в зависимости от ответа создает соответствующую сессию. Пока пользователь находится в рамках сессии, менеджер просто передает сообщения в сессию, но когда пользователь хочет покинуть сессию и поменять роль, менеджер обрабатывает этот запрос. Помимо контроля за сессиями, данный менеджер так же уточняет общую информацию, такую как номер телефона и email.

CallbackManager – класс, обрабатывающий входящие запросы обратной связи, приходящие с кнопок обратной связи встроенных клавиатур. Этот менеджер обрабатывает запросы, относящиеся к конкретным сообщениям. Среди таких запросов получение контактных данных по объекту, удаление и игнорирование объекта.

Получение контактных данных представляет собой запрос обратной связи, который изменяет связанное с ним сообщение таким образом, что к описанию объекта добавляется описание его владельца, включающее в себя имя, номер телефона и адрес электронной почты.

Удаление объекта необходимо, чтобы пользователи уже продавшие или купившие квартиры, могли удалить свои объекты из общей базы.

Ага, а в постановке задачи не было!!!!

Игнорирование объекта означает тот факт, что после получения такого вида запроса, бот больше не будет присылать данный объект потребителю в качестве потенциально возможного варианта.

Сессии

В отличие от программы, где для каждого пользователя запускается своя копия, один бот отвечает сразу всем. При таком подходе мы не может рассчитывать на какую-либо интерактивность, а хотелось бы, чтобы пользователю не приходилось заполнять длинные бланки, выбирать конкретную опцию, получать огромный ответ и начинать все сначала. Поэтому было решено для каждого активного пользователя создавать и поддерживать сессию, некоторую обособленную область взаимодействия. Для каждой такой сессии хранится состояние, а новое сообщение обрабатывается учитывая предыдущие и другую имеющуюся информацию. Описать логику работы программы, можно с помощью схемы, представленной на рисунке 2.2.

Рисунок 2.2 – Диаграмма состояний

Из каждого состояния есть несколько вариантов перехода в следующее состояние, относительно текущего и действия, выбранного пользователем. Для большинства состояний есть общие команды ведущие к переходам в другое состояние, среди таковых команда «Back» и команда «Cancel», которые расцениваются как переход к предыдущему действию и выход из текущего состояния. В сессиях есть несколько состояний, подчиняющихся общей логике для обоих типов сессий. Среди таких состояний:

− ADD_APARTMENT – состояние в котором происходит какое-то действие, связанное непосредственно с добавлением квартиры или подобного объекта в базу данных. В рамках такого состояния присутсвуют следующие действия, идущие в строгой последовательности:

o SEND_STREET – действие указывает, что от пользователя ожидается, что он следующим сообщением пришлет улицу, на которой располагается объект.

o SEND_HOUSE_NUMBER – ожидается, что пользователь пришлет номер дома, в котором располагается объект.

o SEND_APARTMENT_NUMBER - ожидается, что пользователь пришлет номер объекта.

o SEND_PRICE - ожидается, что пользователь пришлет цену объекта

o SEND_SQUARE - ожидается, что пользователь пришлет площадь объекта

o SEND_PICTURE - ожидается, что пользователь пришлет фотографии объекта

o CONFIRM –действие, выполняя которое пользователь может просмотреть введенную информацию и подтвердить ее, или отказаться.

− ADD_WISH – состояние добавления пожелания, множество действий которого является подножество действия состояния ADD_APARTMENT.

− SHOW_APARTMENTS – состояние в котором пользователь простатривает квартиры. В рамках сессии продавца пользователь просматривает свои квартиры, и может их удалять, а в рамках сессии покупателя, пользователь просматривает чужие квартиры.

− SHOW_WISHES – состояние аналогиченое SHOW_APARTMENTS, но пользователь просматривает пожелания, а не квартиры.

 

Отслеживание активности

В конечном итоге каждая сессия описывается тремя составляющими: состояние, действие и объект. У такого подхода есть существенный недостаток, пока наша сессия не закончилась, нам приходится хранить всю информацию, полученную с начала нашей сессии. Проблема заключается в том что, пользователь может на какое-то время отвлечься, находясь в рамках сессии, а потом и вовсе забыть, что он в ней находится ри существующей вероятности таких событий.мы не можем себе позволить постоянно хранить все открытые сессии в оперативной памяти. Для решения данной проблемы были предприняты следующие действия:

− была создана таблица в базе данных, в которой хранятся сессии (sessions);

− были реализованы методы записи в базу данных сессий, а также промежуточный класс SessionDescription, который может хранить в себе полное состояние сессии;

− был добавлен поток, отслеживающий активность сессий, и выгружающий их в случае необходимости в базу данных, удаляя из оперативной памяти; По какому критерию?

− теперь, когда пользователь возвращается к приложению и посылает какое-то сообщение боту, менеджер сперва проверяет, нет ли у этого пользователя сессии в базе данных, и если она имеется он сперва создает для него сессию из этой записи, а потом продолжает работать в обычном режиме.

 

Многопоточность

Помимо хранения сессий возникла еще одна проблема, связанная с тем, что все пользователи используют одного бота. Дело в том, что разные операции, производимые ботом обладают разной трудоемкостью. Например, если пользователь просто хочет покинуть сессию, то это считается легкой операцией, а если пользователь попросил прислать ему все доступные в базе квартиры, то это сложная операция. Сложная она в двух отношениях: во-первых, получив такой запрос серверу нужно сформировать ответ, получив из базы данных только нужные сведения, а с другой стороны, все эти сведения нужно отправить пользователю через интернет, скорость которого непредсказуема. Учитывая то, что в один момент времени ботом могут пользоваться несколько человек, выполнять простые и сложные действия последовательно нельзя. Если получится так, что один пользователь запросит сложную операцию, а следом за ним другой запросит простую, то второму пользователю придется ждать несоотносимо долго. Такие необоснованные задержки приводят к дискомфорту пользования сервисом, поэтому операции необходимо выполнять параллельно.

В качестве решения данной проблемы каждое действие в рамках сессии будет выполняется в отдельном, специально созданном потоке. Благодаря такому подходу, менеджер будет просто передавать новое сообщение в сессию, и тут же начинать обрабатывать следующее, это слегка увеличит среднее время выполнения действий, но время выполнения каждого отдельно взятого действия будет обоснованным.

Такое решение проблемы привело к другой несерьезной проблеме. Теперь, когда бот не дожидаясь выполнения текущего действия переходит к выполнению следующего, даже если оба этих действия принадлежат одному и тому же пользователю, не обеспеченна сохранность порядка выполнения действий одного пользователя. Данная проблема была решена созданием очереди для каждой сессии, в которую помещаются запросы и выполняются только тогда, когда переходят в начало очереди. Реализовать такой механизм было достаточно просто, используя стандартные инструменты Java 8: в сессии все время выполняется поток, который проверяет есть ли в очереди запросы, и если есть извлекает, дожидается выполнения предыдущего запроса и начинает выполнять его. Благодаря потоко-безопасному контейнеру Java ConcurrentLinkedQueue доступ к данной очереди даже не пришлось синхронизировать, синхронизация уже инкапсулирована в этом классе. [10]

Модульность и тестирование

За счет использования модульной парадигмы, удалось сэкономить достаточное количество времени, так как в процессе разработки часто возникала необходимость изменять архитектуру приложения. Самым ярким примером модульности является класс Keyboards, который содержит только статические поля и методы. В данном классе хранятся как константные клавиатуры, так и методы создания клавиатур из различных объектов, например, массив кнопок.В методах этого класса используются методы этого же класса, создается некоторая иерархия, в то время как программисту доступен, каждый метод из этой цепочки. Такой подход позволяет переиспользовать одни и те же методы в разных контекстах многократно.

С другой стороны, помимо переиспользования кода у модульной архитектуры есть еще одно весомое преимущество. Так как наша программа состоит из независимых модулей, каждый из таких модулей может разрабатываться и тестироваться параллельно, нужно только определить интерфейс каждого модуля, то есть команды, с помощью которых будет осуществляться межмодульное взаимодействие. Когда интерфейс определен, в одном модуле можно использовать методы, а в другом реализовывать их независимо друг от друга. Ярким примером в данном проекте такой архитектуры является класс Database, который обеспечивает взаимодейсвие программы с базой данных. Реализации хранения данных инкапсулированы в данном классе, а разработчику предоставлен только интерфейс взаимодействия. Разработчику, который не связан с разработкой данного класса нет никакой необходимости вникать в тонкости хранения, получения и записи информации. База данных может быть какой угодно, локальной или удаленной, работать на известном движке или быть авторской, все эти детали не важны программисту, который просто использует ее. А кроме того, если вдруго возникнет необходимость изменения способа хранения, например, перевод на облачные технологии, необходимо будет только переписатьи п протестировать данный класс, остальная часть кода никак не видоизмениться впоследствии.

Еще один огромный плюс модульной архитектуры – возможность написания модульных тестов. В процессе разработки часто приходится отлаживать программу, чтобы убедиться, что все работает верно и писать дальше. Чтобы каждый раз не запускать заново программу и проверять корректность вручную, были написаны модульные тесты, покрывающие интерфейс взаимодействия с базой данных. Так как модуль является самостоятельной единицей, мы легко может предсказать, как она поведет себя в той или иной ситуации, и быть уверены, что никакие фнешние факторы не повлияют на результат. Суть эти тестов проста: пишется метод добавления в базу и метод извлечения добавленных данных из базы, а потом проверяется эквивалентность того, что было добавлено и того, что было извлечено; или, например, после удаления из базы какого-то объекта проверяется, что его там действительно нет. Посмотреть на тесты можно в коде в приложении.

Юнит тесты остаются полезными и после того, как модуль отлажен. Каждый раз внося какие-то существенные изменения можно запустить все юнит тесты и проверить, что они выполняются корректно. В случае успеха ничего нельзя сказать наверняка, но вероятность ошибки уменьшается, а вот в случае не успешного выполнения тестов можно быстро найти ошибку, так как изменения, которые были внесены еще не глобальные.

 

Уведомления

В приложении есть еще один тип потоков, выполняющихся параллельно основной программе - это поток уведомлений. Каждый раз, когда в базу данных добавляется новое объявление, происходит уведомление потенциальных клиентов, о том, что потенциально интересное для них уведомление было добавлено. Поток уведомлений предусмотрительно запускается с самым низким приоритетом Java потоков, и может выполняться достаточно долго, если бот загружен, или наоборот практически тут же, когда бот свободен. По типу сообщение, которое приходит как уведомление, ничем не отличается от тех сообщений, которые приходят по прямому запросу пользователя просмотреть объекты.

Уведомления будут приходить на все устройства, подобно обычным сообщениям, благодаря тому, что бот интегрирован в мессенджер. Если пользователь не хочет отвлекаться на уведомления нашего сервиса, он может отключить их в настройках бота, они по-прежнему будут приходить в чат, но пользователь сможет узнать о них только открыв переписку.

 

Интерфейс пользователя

Общий интерфейс

Далее будут представлены скриншоты с мобильной и браузерной версии Telegram. Это сделано для того, чтобы продемонстрировать, что интерфейс будет выглядит очень похоже в любой версии приложения на любом устройстве, что обеспечивает комфорт использования сервиса. История использования синхронизируется и совпадает, это означает что пользователь может даже начать заполнять сведения об объекте на одном устройстве, а закончить на другом.

Как только пользователь открывает бота ему сообщается функционал предлагается совершить только одно действие – начать пользоваться сервисом (см. рис. 2.3).

Рисунок 2.3 – Начальное окно

 

Отправив команду “/start” боту пользователю придет сообщение с кастомизированной клавиатурой, в которой будут перечислены варианты дальнейших действий (см. рис. 2.4).

 

Рисунок 2.4 – Стартовое меню пользователя

 

Если в данный момент пользователь хочет продавать квартиры, он должен послать боту команду “Sell” нажав соответствующую кнопку. В таком случае тут же начнется сессия продавца, в которой пользователь может добавлять и удалять объявления о продаже квартир, которыми он владеет. Аналогично, если пользователь хочет продавать квартиры, ??? он должен послать боту команду “Buy” нажав соответствующую кнопку. В таком случае тут же начнется сессия продавца, в которой пользователь может добавлять и удалять объявления о продаже квартир, которые он хотел бы купить или снять.

Также на этом этапе доступны еще два действия: “Set phone number” и “Set email” которые позволяют пользователю указать свои контактные данные, для связи с ним (см. рис. 2.5).

 

Рисунок 2.5 – Изменение номера телефона пользователем

 

Во всем интерфейсе кнопка “Cancel” необходима для отмены текущего действия, а кнопка “Back” для возврата к предыдущему действию.

 

Интерфейс сессии продавца

Сессия продавца предполагает, что все действия, выполняемые в рамках этой сессии ведутся от лица владельца квартир, которые он хотел бы продать (см. рис. 2.6).

 

Рисунок 2.6 – Выбор действий продавца

 

Нажав кнопку “Add” с пользователем начнется диалог (см. рис. 2.7), в котором бот попросит его прислать необходимую информацию по порядку, а в конце подтвердить правильность введенных данных. Как только пользователь подтвердит данные, диалог закончится, а квартира будет добавлена в базу.  И снова…!!!

Рисунок 2.7 – Диалог добавления квартиры в базу

 

Пользователь может увидеть ее, нажав кнопку “Show apartments”. Под по очереди пришлет пользователю все добавленные им квартиры (см. рис. 2.8).

Рисунок 2.8 – Просмотр квартир продавцом

 

Под каждой квартирой будет интегрированная кнопка, позволяющая удалить квартиру из базы, если пользователь уже продал ее или просто передумал. Если пользователь нажмет на нее, то квартира будет удалена из базы, а в вместе с ней и сообщение, содержащее информацию о ней. Тем самым пользователь тут же получит обратную связь, о том, что квартира была успешно удалена. Такой подход более интуитивен, чем сообщение в чат, о том, что все прошло успешно, помимо того, в том случае, если у пользователя достаточно много квартир, ему придется пролистать переписку, чтобы увидеть вердикт удаления квартиры.

Нажав кнопку “Show wishes” пользователю предложат на выбор два варианта: посмотреть все пожелания других пользователей или только те, которые считаются более подходящими, основываясь на добавленных квартирах.

Под каждый пожеланием располагаются две интегрированных кнопки «Get Contact» и «Not interest». Первая – позволяет получить контактные данные, нажав на нее сообщение изменится таким образом, что кнопка исчезнет, а к описанию пожелания будут добавлены контактные данные пользователя, разместившего это объявление (см. рис. 2.9). Вторая кнопка позволяет пометить данное объявление, как не интересное. После нажатия на эту кнопку соответствующее сообщение удалится, а данное объявление больше не будет присылаться ботом.  Вооот! Здесь правильно сформулировано!!!

 

Рисунок 2.9 – Получение контактных данных

 

Интерфейс сессии покупателя

Интерфейс сессии покупателя аналогичен интерфейсу сессии продавца, основное меню даже совпадает полностью, только теперь те же команды воспринимаются ботом иначе (см. рис. 2.10). В рамках данной сессию добавляются пожелания, а просматриваются квартиры.

 

Рисунок 2.10 – Просмотр квартир покупателем


Заключение

Боты, интегрированные в месседжеры активно используются людьми в повседневной жизни. Они выполняют самые различные задачи, уведомляют о новых событиях и предоставляют удобные интерфейсы использования. При разработке таких ботов мессенжеры предоставляют программистам не просто интерфейсы взаимодействия, а полноценную платформу, снимая с разработчиков сторонних программ задачу реализации стандартных функций, таких как воспроизведение музыки, просмотр видеозаписей и фотографий, установление безопасного сетевого соединения.

Результатом данной работы стал бот, интегрированный в мессенджер Telegram, выполняющий функции агентсва недвижимости. С помощью такого бота можно размещать объявления о покупке, продаже и аренде квартир и других объектов недвижимости, просматривать фотографии, связываться с продавцами и покупателям и получать уведомления о новых подходящих вариантах.

Разработанная программа полностью протестированна с помощью ручного, нагрузочного и модульного тестирования и готова к использованию. Данного бота можно внедрить в любую компанию, предоставляющую услуги агентства недвижимости или обозначить ее самостоятельной доской объявления. Модульная архитектура позволит при необходимости корректировать функционал не внося серьезных изменений в основную логику программы.

Программный продукт коммерческий? Используется? Планируется к использованию?

Cписок использованных источников

1. Telegram Messenger [Электронный ресурс] // Telegram:
a new era of messaging. URL: https://telegram.org (дата обращения: 11.04.18)

2. Telegram Bots [Электронный ресурс] // Bots: An introduction for developers URL : https://core.telegram.org/bots (дата обращения: 12.04.18)

3. API [Электронный ресурс] // Википедия: Свободная энциклопедия. URL: https://ru.wikipedia.org/wiki/API (дата обращения: 25.04.18)

4. Сервер [Электронный ресурс] // Википедия: Свободная энциклопедия. URL: https://ru.wikipedia.org/wiki/Сервер_(программное_обеспечение) (дата обращения: 25.04.18)

5. HTTP [Электронный ресурс] // Википедия: Свободная энциклопедия. URL: https://ru.wikipedia.org/wiki/HTTP (дата обращения: 25.04.18)

6. JSON [Электронный ресурс] // Википедия: Свободная энциклопедия. URL: https://ru.wikipedia.org/wiki/JSON (дата обращения: 25.04.18)

7. API [Электронный ресурс] // Википедия: Свободная энциклопедия. URL: https://ru.wikipedia.org/wiki/API (дата обращения: 25.04.18)

8. Telegram Bot API [Электронный ресурс] // Telegram Bot API. URL : https://core.telegram.org/bots/api (дата обращения: 25.04.18)

9. Push technology [Электронный ресурс] // Википедия: Свободная энциклопедия. URL: https://en.wikipedia.org/wiki/Push_technology#Long_polling (дата обращения: 1.05.18)

10. Шилдт Г. Java 8: Полное руководство, 9-е издание = Java 8. TheCompleteReference, 9th Edition. — М. «Вильямс», 2015. — 1376 с.

11. TelegramBots [Электронный ресурс] // Java library to create bots using Telegram Bots API. URL: https://github.com/rubenlagus/TelegramBots (дата обращения: 12.04.18)

12. Junit5 [Электронный ресурс] //The new major version of the programmer-friendly testing framework for Java 8 and beyond. URL : https://junit.org/junit5 (дата обращения: 1.05.18)

13. IntelliJ IDEA [Электронный ресурс] // IntelliJ IDEA: The Java IDE for Professional Developers by JetBrains. URL : https://www.jetbrains.com/idea (дата обращения: 25.04.18)

14. SQL [Электронный ресурс] // Википедия: Свободная энциклопедия. URL: https://ru.wikipedia.org/wiki/SQL (дата обращения: 5.05.18)

15. SQLite [Электронный ресурс] SQLiteDocumentation. URL: https://www.sqlite.org/docs.html (дата обращения 5.05.18)

16. SQLite Administartor [Электронный ресурс] // SQLite Administrator - International Milestone Beta.URL: https://sqliteadmin.orbmu2k.de (дата обращения 5.05.18)

17. Git [Электронный] // Git --distributed-even-if-your-workflow-isnt. URL: https://git-scm.com (дата обращения 10.04.18)

18. GitHub [Электронный ресурс] // GitHub Built for developers Url: https://github.com (дата обращения 10.04.18)

Приложение А
Диаграмма классов

Приложение Б
Листинг Программы

Main.java

package com.jilfond.bot;

import org.telegram.telegrambots.ApiContextInitializer;
import org.telegram.telegrambots.TelegramBotsApi;
import org.telegram.telegrambots.exceptions.TelegramApiException;

import java.sql.SQLException;

public class Main {

public static void main(String[] args) {
   ApiContextInitializer.init();
   TelegramBotsApi telegramBotsApi = new TelegramBotsApi();
   try {
       Bot bot = new Bot();
       telegramBotsApi.registerBot(bot);
   } catch (TelegramApiException | SQLException e) {
       e.printStackTrace();
   }

}
}

 

Bot.java

package com.jilfond.bot;

import com.jilfond.bot.databases.Database;
import com.jilfond.bot.managers.CallbackManager;
import com.jilfond.bot.managers.MessageManager;
import org.telegram.telegrambots.api.methods.send.SendMessage;
import org.telegram.telegrambots.api.methods.send.SendPhoto;
import org.telegram.telegrambots.api.methods.updatingmessages.DeleteMessage;
import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageCaption;
import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageText;
import org.telegram.telegrambots.api.objects.*;
import org.telegram.telegrambots.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.api.objects.replykeyboard.ReplyKeyboard;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.exceptions.TelegramApiException;

import java.sql.SQLException;

public class Bot extends TelegramLongPollingBot {
private MessageManager messageManager;
private CallbackManager callbackManager;
private Database database = new Database();
private static Bot currentBot;


public Bot() throws SQLException {
   currentBot = this;
   messageManager = new MessageManager(database); //messageManager use static field currentBot!
   callbackManager = new CallbackManager(database); //messageManager use static field currentBot!
}

@Override
public void onUpdateReceived(Update update) {
   if(update.hasMessage()){
       try {
           messageManager.pushMessage(update.getMessage());
       } catch (SQLException e) {
           e.printStackTrace();
       }
   } else if(update.hasCallbackQuery()){
       callbackManager.pushUpdate(update);
   }
}


public void send(Long chatId, String text, ReplyKeyboard keyboard){
   try {
       SendMessage sendMessage = new SendMessage().
               setChatId(chatId).
               setText(text);
       if(keyboard!=null){
           sendMessage.setReplyMarkup(keyboard);
       }
       execute(sendMessage);
   } catch (TelegramApiException e) {
       e.printStackTrace();
   }
}


public void send(Long chatId, String text){
   send(chatId,text,null);
}


public static Bot getCurrentBot() {
   return currentBot;
}

@Override
public void onClosing() {
   System.out.println("onClosing");
}

@Override
public String getBotUsername() {
   return "JilfondBot";
}

@Override
public String getBotToken() {
   return "522474427:AAHsCXHRTz4UYhOQovlGQdNheAA2qBQh-rY";
}

public void sendPicture(Long chatId, String photo, String description, ReplyKeyboard replyKeyboard) {
   SendPhoto sendPhotoRequest = new
           SendPhoto().
           setChatId(chatId).
           setPhoto(photo).
           setCaption(description);
   if (replyKeyboard!=null){
       sendPhotoRequest.setReplyMarkup(replyKeyboard);
   }
   try {
       sendPhoto(sendPhotoRequest);
      } catch (TelegramApiException e) {
       e.printStackTrace();
   }
}
public void deleteMessage(Integer messageId, Long chatId){
   DeleteMessage deleteMessage = new DeleteMessage().setMessageId(messageId).setChatId(chatId);
      try {
       execute(deleteMessage);
   } catch (TelegramApiException e) {
       e.printStackTrace();
   }
}
public void updateText(Long chatId, Integer messageId, String text, InlineKeyboardMarkup replyMarkup){
   EditMessageText editMessageText = new EditMessageText()
           .setChatId(chatId)
           .setMessageId(messageId)
           .setText(text);
   if(replyMarkup!=null){
       editMessageText.setReplyMarkup(replyMarkup);
   }
   try {
       execute(editMessageText);
   } catch (TelegramApiException e) {
       e.printStackTrace();
   }
}
public void updateCaption(Long chatId, Integer messageId, String text, InlineKeyboardMarkup replyMarkup){
   EditMessageCaption editMessageCaption = new EditMessageCaption()
           .setChatId(String.valueOf(chatId))
           .setMessageId(messageId)
           .setCaption(text);
   if(replyMarkup!=null){
       editMessageCaption.setReplyMarkup(replyMarkup);
   }
   try {
       execute(editMessageCaption);
   } catch (TelegramApiException e) {
       e.printStackTrace();
   }
}

}

 

Apartment.java

package com.jilfond.bot.objects;


import java.util.LinkedList;
import java.util.List;

public class Apartment {
public String street = "street";
public String houseNumber = "houseNumber";
public Integer number = 0;
public Integer databaseId = 0;
public Integer price = 0;
public Integer square = 0;
public Integer seller = 0;
public List<String> photos = new LinkedList<>();

public Apartment() {

}

@Override
public String toString() {
   return "Apartment{" +
           "street='" + street + '\'' +
           ", houseNumber='" + houseNumber + '\'' +
           ", number=" + number +
           ", databaseId=" + databaseId +
           ", price=" + price +
           ", square=" + square +
           ", seller=" + seller +
           ", photos=" + photos +
           '}';
}

public String getDescription() {
   return "street = " + street + "\n" +
           "houseNumber = " + houseNumber + "\n" +
           "number = " + number + "\n" +
           "price = " + price + "\n" +
           "square = " + square;
}


public void addPhoto(String fileId) {
   photos.add(fileId);
}
}

 

Wish.java

package com.jilfond.bot.objects;

public class Wish {
public String street = "street";
public int price = 0;
public int square = 0;
public Integer buyer = 0;
public int databaseId = 0;

@Override
public String toString() {
   return "Wish{" +
           "street='" + street + '\'' +
           ", price=" + price +
           ", square=" + square +
           '}';
}

public String getDescriptionForBuyer() {
   return "street = " + street + "\n" +
          "price = " + price + "\n" +
          "square = " + square;
}
}

 

BotUser.java

package com.jilfond.bot.objects;

import org.telegram.telegrambots.api.objects.User;

import java.lang.reflect.Field;

public class BotUser {
public int telegramId;
public String firstName = "null";
public String lastName = "null";
public String userName = "null";
public String phoneNumber = "null";
public String email = "null";

@Override
public String toString() {
   return "BotUser{" +
           "telegramId=" + telegramId +
           ", firstName='" + firstName + '\'' +
           ", lastName='" + lastName + '\'' +
           ", userName='" + userName + '\'' +
           ", phoneNumber='" + phoneNumber + '\'' +
           ", email='" + email + '\'' +
            '}';
}

public String getContact() {
   StringBuilder stringBuilder = new StringBuilder();
   stringBuilder.append("Contact details:\n");
   if (firstName != null && !firstName.equals("null")) {
       stringBuilder.append(firstName + "\n");
   }
   if (lastName != null && !lastName.equals("null")) {
       stringBuilder.append(lastName + "\n");
   }
   if (userName != null && !userName.equals("null")) {
       stringBuilder.append("@" + userName + "\n");
   }
   if (phoneNumber != null && !phoneNumber.equals("null")) {
       stringBuilder.append(phoneNumber + "\n");
   }
   if (email != null && !email.equals("null")) {
       stringBuilder.append(email + "\n");
   }
   return stringBuilder.toString();
}

public BotUser() {

}

public BotUser(User user) {
   if (user.getId() != null) {
       telegramId = user.getId();
   }
   if (user.getFirstName() != null) {
         firstName = user.getFirstName();
   }
   if (user.getLastName() != null) {
       lastName = user.getLastName();
   }
   if (user.getUserName() != null) {
       userName = user.getUserName();
   }
}

}

 

MessageManager.java

package com.jilfond.bot.managers;

import com.jilfond.bot.Bot;
import com.jilfond.bot.objects.BotUser;
import com.jilfond.bot.Keyboards;
import com.jilfond.bot.databases.Database;
import com.jilfond.bot.sessions.BuyerSession;
import com.jilfond.bot.sessions.SellerSession;
import com.jilfond.bot.sessions.Session;
import com.jilfond.bot.sessions.SessionDescription;
import org.telegram.telegrambots.api.objects.Message;
import org.telegram.telegrambots.api.objects.replykeyboard.ReplyKeyboardMarkup;


import java.sql.SQLException;
import java.util.*;


public class MessageManager {
private Database database;
private Map<Long, Session> sessions = new HashMap<>();
private Set<Integer> usersWhoChangingPhoneNumber = new HashSet<Integer>();
private Set<Integer> usersWhoChangingEmail = new HashSet<Integer>();


Bot bot = Bot.getCurrentBot();
ReplyKeyboardMarkup selectActionKeyboardMarkup = createSelectActionKeyboard();
private final String mutex = "mutex";

public MessageManager(Database database) {
   this.database = database;
   //TODO:fix this shit!
   Thread activityObserverThread = new Thread(() -> {
       System.out.println("activityObserverThread start");
       while (true) {
           try {
               Thread.sleep(1000 * 60 * 15);
               Date currentTime = Calendar.getInstance().getTime();
               synchronized (mutex) {
                   for (Long key : sessions.keySet()) {
                       Session session = sessions.get(key);
                       if (currentTime.getTime() - session.getLastActivityTime().getTime() > 1000 * 60 * 60) {

                           System.out.println("activityObserverThread:");
                           System.out.println("remove " + key);
                           try {
                               session.save();
                           } catch (SQLException e) {
                               e.printStackTrace();
                           }
                           sessions.remove(key);
                       }
                   }
               }
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
   });
   activityObserverThread.start();
}


private ReplyKeyboardMarkup createSelectActionKeyboard() {
   LinkedList<String> actions = new LinkedList<>();
   actions.add("Sell");
   actions.add("Buy");
   actions.add("Set phone number");
   actions.add("Set email");
   return Keyboards.make(actions, true);
}

public void pushMessage(Message message) throws SQLException {
   Long chatId = message.getChatId();
   Integer userId = message.getFrom().getId();


   if (message.hasText() && message.getText().equals("/start")) {
       database.deleteSession(chatId);
       sessions.remove(chatId);
       sendSelectActionRequest(chatId);
       try {
           database.addUserIfNotExist(new BotUser(message.getFrom()));
       } catch (SQLException e) {
           e.printStackTrace();
       }
   } else {
       synchronized (mutex) {
           if (!sessions.containsKey(chatId) && database.sessionExist(chatId)) {//TODO:can be optimized
               SessionDescription sessionDescription = database.getSession(chatId);
               switch (sessionDescription.type) {
                   case "SELLER":
                       sessions.put(chatId, new SellerSession(database, sessionDescription));
                       break;
                   case "BUYER":
                       sessions.put(chatId, new BuyerSession(database, sessionDescription));
                       break;
               }
           }
           if (sessions.containsKey(chatId)) {
               Session session = sessions.get(chatId);
               if (session.getState().equals("SELECT_ACTION") && message.getText().equals("Cancel")) {
                   sessions.remove(chatId);
                   database.deleteSession(chatId);
                   sendSelectActionRequest(chatId);
               } else {
                   session.pushMessage(message);
               }
           } else if (usersWhoChangingPhoneNumber.contains(userId)) {
               if (!message.getText().equals("Cancel")) {
                   try {
                       database.updatePhoneNumber(userId, message.getText());
                   } catch (SQLException e) {
                       e.printStackTrace();
                       bot.send(chatId, "Error");
                   }
               }
               usersWhoChangingPhoneNumber.remove(userId);
               sendSelectActionRequest(chatId);
           } else if (usersWhoChangingEmail.contains(userId)) {
               if (!message.getText().equals("Cancel")) {
                   try {
                       database.updateEmail(userId, message.getText());
                   } catch (SQLException e) {
                       e.printStackTrace();
                       bot.send(chatId, "Error");
                   }
               }
               usersWhoChangingEmail.remove(userId);
               sendSelectActionRequest(chatId);
           } else {
               handleMessageAsAction(message);
            }
       }
   }
}


void handleMessageAsAction(Message message) {
   Integer userId = message.getFrom().getId();
   Long chatId = message.getChatId();
   switch (message.getText()) {
       case "Sell":
             sessions.put(chatId, new SellerSession(database, message.getChatId()));
           break;
       case "Buy":
           sessions.put(chatId, new BuyerSession(database, message.getChatId()));
           break;
       case "Set phone number":
           usersWhoChangingPhoneNumber.add(userId);
           bot.send(message.getChatId(), "Send your current phone number, please", Keyboards.cancel);
           break;
       case "Set email":
           usersWhoChangingEmail.add(userId);
           bot.send(message.getChatId(), "Send your current email, please", Keyboards.cancel);
           break;
       default:
           bot.send(message.getChatId(), "to start, send me /start", Keyboards.start);
   }
}

void sendSelectActionRequest(Long chatId) {
   bot.send(chatId, "Select action", selectActionKeyboardMarkup);
}

}

 

CallbackManager.java

package com.jilfond.bot.managers;

import com.jilfond.bot.Bot;
import com.jilfond.bot.databases.Database;
import com.jilfond.bot.objects.BotUser;
import org.telegram.telegrambots.api.objects.Message;
import org.telegram.telegrambots.api.objects.Update;

import java.sql.SQLException;

public class CallbackManager {
Database database;
Bot bot = Bot.getCurrentBot();

public CallbackManager(Database database) {
   this.database = database;
}

public void pushUpdate(Update update) {
   String callback = update.getCallbackQuery().getData();
   String command = callback.split(" ")[0];
   Integer id = Integer.valueOf(callback.split(" ")[1]);
   Message message = update.getCallbackQuery().getMessage();
   try {
       switch (command) {
           case "deleteWish":
               bot.deleteMessage(message.getMessageId(), message.getChatId());
               database.deleteWishById(id);
               break;
           case "deleteApartment":
               database.deleteApartmentById(id);
               bot.deleteMessage(message.getMessageId(), message.getChatId());
               break;
           case "getUser":
               BotUser user = database.getBotUserByTelegramId(id);
               String contactDetails = user.getContact();
               if (!message.hasPhoto()) {
                   bot.updateText(
                           message.getChatId(),
                           message.getMessageId(),
                           message.getText() + "\n" + contactDetails,
                           null);
               } else {
                   bot.updateCaption(
                           message.getChatId(),
                           message.getMessageId(),
                           message.getCaption() + "\n" + contactDetails,
                           null);
               }
               break;
           case "ignoreWish":
               Integer objectId = Integer.valueOf(callback.split(" ")[2]);
               database.ignoreWish(id,objectId);
               bot.deleteMessage(message.getMessageId(), message.getChatId());
               break;
           case "ignoreApartment":
               objectId = Integer.valueOf(callback.split(" ")[2]);
               database.ignoreApartment(id,objectId);
               bot.deleteMessage(message.getMessageId(), message.getChatId());
               break;
       }
   } catch (SQLException e) {
      e.printStackTrace();
   }


}
}

 

Notifier.java

package com.jilfond.bot.managers;

import com.jilfond.bot.Bot;
import com.jilfond.bot.Keyboards;
import com.jilfond.bot.databases.Database;
import com.jilfond.bot.objects.Apartment;
import com.jilfond.bot.objects.BotUser;
import com.jilfond.bot.objects.Wish;

import java.sql.SQLException;
import java.util.LinkedList;

public class Notifier extends Thread {
public Notifier(Object object, String type) {
   super(() -> {
       try {
           Bot bot = Bot.getCurrentBot();
           Database database = new Database();
           LinkedList<BotUser> users;
           System.out.println("Notifier");
           switch (type) {
               case "WISH":
                      Wish wish = (Wish) object;
                   users = database.getUsersWithApartmentsOnStreet(wish.street, wish.buyer);
                   for(BotUser user:users){
                       System.out.println(user.toString());
                       bot.send((long) user.telegramId,wish.toString());
                   }
                   break;
               case "APARTMENT":
                   Apartment apartment = (Apartment) object;
                   users = database.getUsersWithWishesOnStreet(apartment.street, apartment.seller);
                   break;
           }

       } catch (SQLException e) {
           e.printStackTrace();
       }
   });
   setPriority(Thread.MIN_PRIORITY);
}
}

 

Session.java

package com.jilfond.bot.sessions;

import com.jilfond.bot.Bot;
import com.jilfond.bot.Keyboards;
import com.jilfond.bot.annotations.Virtual;
import com.jilfond.bot.databases.Database;
import org.telegram.telegrambots.api.objects.Message;
import org.telegram.telegrambots.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.api.objects.replykeyboard.ReplyKeyboard;
import org.telegram.telegrambots.api.objects.replykeyboard.ReplyKeyboardMarkup;

import java.sql.SQLException;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentLinkedQueue;


public class Session {
protected Database database;
protected String state = "SELECT_ACTION";
protected String action = "NONE";
protected Bot bot;
protected Long chatId;
protected Message currentMessage;
protected Thread currentThreadAction;
protected LinkedList<String> actions = new LinkedList<>();
  protected ReplyKeyboardMarkup selectActionKeyboard = createSelectActionKeyboard();
protected Runnable runnable;
private ConcurrentLinkedQueue<Message> messages = new ConcurrentLinkedQueue<>();
protected String type;
private Date lastActivityTime = Calendar.getInstance().getTime();
private Thread messageHandlerThread = new Thread(new Runnable() {
   @Override
   public void run() {
       while (true) {
           Message message = messages.poll();
           if (message != null) {
               currentMessage = message;
               try {
                   currentThreadAction.join();
               } catch (NullPointerException | InterruptedException e) {
                   //its normal situation
               }
               currentThreadAction = new Thread(runnable);
               currentThreadAction.start();
           }
       }
   }
});

public Session(Database database, Long chatId) {
     this.bot = Bot.getCurrentBot();
   this.chatId = chatId;
   this.database = database;
   sendSelectActionRequest();
   messageHandlerThread.start();
}

public Session(Database database, SessionDescription sessionDescription) {
   this.bot = Bot.getCurrentBot();
   this.chatId = sessionDescription.chatId;
   this.type = sessionDescription.type;
   this.action = sessionDescription.action;
   this.state = sessionDescription.state;
   this.database = database;
   setObject(sessionDescription.object);
   messageHandlerThread.start();
}

void reply(String text, ReplyKeyboard replyKeyboard) {
   bot.send(chatId, text, replyKeyboard);
}

void reply(String text) {
   bot.send(chatId, text, null);
}

void replyWithPhoto(String photo, String description, InlineKeyboardMarkup deleteApartmentKeyboard) {
   bot.sendPicture(this.chatId, photo, description, deleteApartmentKeyboard);
}


private ReplyKeyboardMarkup createSelectActionKeyboard() {
   actions.add("Add");
   actions.add("Show Apartments");
   actions.add("Show Wishes");
   actions.add("Cancel");
   return Keyboards.make(actions, true);
}

public void pushMessage(Message message) {
   messages.offer(message);
   lastActivityTime = Calendar.getInstance().getTime();
}


public String getState() {
   return state;
}

public void sendSelectActionRequest() {
   action = "NONE";
   state = "SELECT_ACTION";
   bot.send(chatId, "Select Action", selectActionKeyboard);
}

public void save() throws SQLException {
   SessionDescription sessionDescription = new SessionDescription();
   sessionDescription.object = this.getObject();
   sessionDescription.state = this.state;
   sessionDescription.action = this.action;
   sessionDescription.type = this.type;
   sessionDescription.chatId = this.chatId;
   database.saveSession(sessionDescription);
}

public void load() {
   try {
       if (database.sessionExist(chatId)) {
           SessionDescription sessionDescription = null;
           try {
               sessionDescription = database.getSession(chatId);
           } catch (SQLException e) {
               e.printStackTrace();
           }
           setObject(sessionDescription.object);
           this.state = sessionDescription.state;
           this.action = sessionDescription.action;
           this.type = sessionDescription.type;
           this.chatId = sessionDescription.chatId;
           System.out.println("loaded session:\n" + sessionDescription.toString());
       }
   } catch (SQLException e) {
        e.printStackTrace();
   }
}

@Virtual
protected void setObject(Object object) {
       }

@Virtual
protected Object getObject() {
   return null;
}

public Date getLastActivityTime() {
   return lastActivityTime;
}
}

 

SellerSession.java

package com.jilfond.bot.sessions;

import com.jilfond.bot.Keyboards;
import com.jilfond.bot.managers.Notifier;
import com.jilfond.bot.objects.Apartment;
import com.jilfond.bot.databases.Database;
import com.jilfond.bot.objects.Wish;
import org.telegram.telegrambots.api.objects.Message;
import org.telegram.telegrambots.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.api.objects.replykeyboard.buttons.InlineKeyboardButton;

import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;

public class SellerSession extends Session {
private Apartment apartment;
private Runnable sellerRunnable = () -> {
   switch (action) {
       case "NONE":
           switch (currentMessage.getText()) {
               case "Add":
                   action = "ADD_APARTMENT";
                   sendSendStreetRequest();
                   break;
               case "Show Apartments":
                   try {
                       sendApartmentsToSeller(currentMessage.getFrom().getId());
                   } catch (SQLException e) {
                       reply("Error!");
                       e.printStackTrace();
                    }
                   break;
               case "Show Wishes":
                   action = "SHOW_WISHES";
                   sendSelectWishesTypeRequest();
                   break;
               case "Cancel":
                   //unreachable because this situation is handled by the manager
                   break;
           }
           break;
       case "ADD_APARTMENT":
           handleAddAction(currentMessage);
           break;
       case "SHOW_APARTMENTS":
           handleShowApartmentsAction(currentMessage);
           break;
       case "SHOW_WISHES":
           handleShowWishesAction(currentMessage);
           break;
       default:
           throw new IllegalStateException();
   }
};


@Override
protected Object getObject() {
   return apartment;
}

@Override
protected void setObject(Object object) {
   apartment = (Apartment) object;
}

public SellerSession(Database database, SessionDescription sessionDescription) {
   super(database, sessionDescription);
   runnable = sellerRunnable;

}

public SellerSession(Database database, Long chatId) {
   super(database, chatId);
   apartment = new Apartment(); //only for this constructor
   type = "SELLER";
   runnable = sellerRunnable;
}

private void sendAllWishesToSeller(Integer sellerId) throws SQLException {
   List<Wish> wishes = database.getAllWishes(sellerId);
   if (wishes.isEmpty()) {
       reply("No good wishes.");
   }
   for (Wish wish : wishes) {
       sendWishToSeller(wish,sellerId);
   }
}

private void sendSmartWishesToSeller(Integer sellerId) throws SQLException {
   List<Wish> wishes = database.getSmartWishesBySellerId(sellerId);
   if (wishes.isEmpty()) {
       reply("No good wishes.");
   }
   for (Wish wish : wishes) {
       sendWishToSeller(wish,sellerId);
   }
}

private void sendWishToSeller(Wish wish, Integer sellerId) {
   LinkedList<InlineKeyboardButton> buttons = new LinkedList<>();
   buttons.add(Keyboards.makeInlineButton("Get Contact", "getUser " + wish.buyer));
   buttons.add(Keyboards.makeInlineButton("Not interest", "ignoreWish " + sellerId + " " + wish.databaseId));
   reply(wish.getDescriptionForBuyer(), Keyboards.makeInlineKeyboardMarkup(buttons));
}

private void sendApartmentsToSeller(Integer sellerId) throws SQLException {
   LinkedList<Apartment> apartments = database.getApartmentsByTelegramId(sellerId);
   if (apartments.isEmpty()) {
       reply("No added apartments.");
   }
   for (Apartment apartment : apartments) {
       String callback = "deleteApartment " + apartment.databaseId;
       InlineKeyboardMarkup deleteApartmentKeyboard =
               Keyboards.makeOneButtonInlineKeyboardMarkup("Delete Apartment", callback);

       if (apartment.photos.isEmpty()) {
           reply(apartment.getDescription(), deleteApartmentKeyboard);
       } else {
           replyWithPhoto(apartment.photos.get(0), apartment.getDescription(), deleteApartmentKeyboard);
       }
  }
}

private void handleShowApartmentsAction(Message message) {

}

private void handleAddAction(Message message) {
   String text = message.getText();
   switch (state) {
       case "SEND_STREET":
           if (text.equals("Cancel")) {
               sendSelectActionRequest();
           } else {
               apartment.street = text;
               sendSendHouseNumberRequest();
           }
           break;
       case "SEND_HOUSE_NUMBER":
           switch (text) {
               case "Cancel":
                   sendSelectActionRequest();
                   break;
               case "Back":
                   sendSendStreetRequest();
                   break;
               default:
                   try {
                       apartment.houseNumber = text;
                       sendSendApartmentNumberRequest();
                   } catch (NumberFormatException e) {
                       reply("It is not number :( try again");
                   }
                   break;
           }
           break;
       case "SEND_APARTMENT_NUMBER":
           switch (text) {
               case "Cancel":
                   sendSelectActionRequest();
                   break;
               case "Back":
                   sendSendHouseNumberRequest();
                   break;
               default:
                   try {
                       apartment.number = Integer.valueOf(text);
                       sendSendPriceRequest();
                   } catch (NumberFormatException e) {
                       reply("It is not number :( try again");
                   }
                   break;
           }
           break;
       case "SEND_PRICE":
           switch (text) {
               case "Cancel":
                   sendSelectActionRequest();
                   break;
               case "Back":
                   sendSendApartmentNumberRequest();
                   break;
               default:
                   try {
                       apartment.price = Integer.parseInt(text);
                       sendSendSquareRequest();
                   } catch (NumberFormatException e) {
                       reply("It is not number :( try again");
                   }
                   break;
           }
           break;
       case "SEND_SQUARE":
           switch (text) {
               case "Cancel":
                   sendSelectActionRequest();
                   break;
               case "Back":
                   sendSendPriceRequest();
                   break;
               default:
                   try {
                       apartment.square = Integer.parseInt(text);
                       apartment.seller = message.getFrom().getId();
                       sendAddPicturesRequest();
                   } catch (NumberFormatException e) {
                       reply("It is not number :( try again");
                   }
                   break;
           }
           break;
       case "SEND_PICTURE":
           if (message.hasPhoto()) {
               apartment.addPhoto(message.getPhoto().get(3).getFileId());
               sendConfirmRequest();
           } else {
               switch (text) {
                   case "Cancel":
                       sendSelectActionRequest();
                       break;
                   case "Back":
                       sendSendSquareRequest();
                       break;
                   case "No":
                       sendConfirmRequest();
                       break;
               }
           }
           break;
       case "CONFIRM":
           switch (text) {
               case "Yes":
                   try {
                       database.addApartment(apartment);
                       reply("Done!");
                       sendSelectActionRequest();
                       apartment.seller = -1;
                       new Notifier(apartment, "APARTMENT").start();
                   } catch (SQLException e) {
                       e.printStackTrace();
                       reply("Error!");
                   }
                   break;
               case "Back":
                   sendAddPicturesRequest();
                   apartment.photos.clear();
                   break;
                 case "Cancel":
                   apartment.photos.clear();
                   sendSelectActionRequest();
                   break;
           }
           break;
       default:
           System.out.println(state);
           System.out.println(message.toString());
           throw new IllegalStateException();
   }
}

private void handleShowWishesAction(Message currentMessage) {
   try {
       switch (currentMessage.getText()) {
           case "All":
               sendAllWishesToSeller(currentMessage.getFrom().getId());
               break;
           case "Smart":
               sendSmartWishesToSeller(currentMessage.getFrom().getId());
              break;
       }
   } catch (SQLException e) {
       reply("Error!");
       e.printStackTrace();
   } finally {
       sendSelectActionRequest();
   }
}

private void sendAddPicturesRequest() {
    reply("Send me pictures, please", Keyboards.backCancelAndNo);
   state = "SEND_PICTURE";
}

private void sendSendSquareRequest() {
   reply("Send me square, please", Keyboards.backAndCancel);
   state = "SEND_SQUARE";
}

private void sendSendPriceRequest() {
   reply("Send me price, please", Keyboards.backAndCancel);
   state = "SEND_PRICE";
}

private void sendSendStreetRequest() {
   reply("Send me street, please", Keyboards.cancel);
   state = "SEND_STREET";
}

private void sendSendHouseNumberRequest() {
   reply("Send me number of house, please", Keyboards.backAndCancel);
   state = "SEND_HOUSE_NUMBER";
}

private void sendSendApartmentNumberRequest() {
   reply("Send me number of apartment, please", Keyboards.backAndCancel);
   state = "SEND_APARTMENT_NUMBER";
}

private void sendConfirmRequest() {
   reply("Confirm information", Keyboards.yesBackAndCancel);
   reply(apartment.toString());
   state = "CONFIRM";
}

private void sendSelectWishesTypeRequest() {
   LinkedList<String> types = new LinkedList<>();
   types.add("All");
   types.add("Smart");
   reply("Select wishes type", Keyboards.make(types));
     state = "SELECT_WISHES_TYPE";
}

}

 

BuyerSession.java

package com.jilfond.bot.sessions;

import com.jilfond.bot.Keyboards;
import com.jilfond.bot.databases.Database;
import com.jilfond.bot.managers.Notifier;
import com.jilfond.bot.objects.Apartment;
import com.jilfond.bot.objects.Wish;
import org.telegram.telegrambots.api.objects.Message;
import org.telegram.telegrambots.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.api.objects.replykeyboard.buttons.InlineKeyboardButton;

import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;

public class BuyerSession extends Session {
private Wish wish;
private Runnable buyerRunnable = () -> {
   switch (action) {
       case "NONE":
           switch (currentMessage.getText()) {
               case "Add":
                   action = "ADD_WISH";
                   sendSendStreetRequest();
                   break;
               case "Show Wishes":
                   try {
                       sendWishesToBuyer(currentMessage.getFrom().getId());
                   } catch (SQLException e) {
                       reply("Error!");
                       e.printStackTrace();
                   }
                   break;
               case "Show Apartments":
                   action = "SHOW_APARTMENTS";
                   sendSelectApartmentsTypeRequest();
                   break;
               case "Cancel":
                   //unreachable because this situation is handled by the manager
                   break;
           }
           break;
       case "ADD_WISH":
           handleAddAction(currentMessage);
           break;
       case "SHOW_WISHES":
           handleShowWishesAction(currentMessage);
           break;
       case "SHOW_APARTMENTS":
           handleShowApartmentsAction(currentMessage);
           break;
       default:
           throw new IllegalStateException();
   }
};

private void sendSelectApartmentsTypeRequest() {
   LinkedList<String> types = new LinkedList<>();
   types.add("All");
   types.add("Smart");
   reply("Select Apartments type", Keyboards.make(types));
   state = "SELECT_APARTMENTS_TYPE";
}

private void handleShowApartmentsAction(Message currentMessage) {
   try {
       switch (currentMessage.getText()) {
           case "All":
               sendAllApartmentsToBuyer(currentMessage.getFrom().getId());
               break;
           case "Smart":
               sendSmartApartmentsToBuyer(currentMessage.getFrom().getId());
               break;
       }
   } catch (SQLException e) {
       reply("Error!");
       e.printStackTrace();
   } finally {
       sendSelectActionRequest();
   }
}

private void sendSmartApartmentsToBuyer(Integer buyerId) throws SQLException {
   List<Apartment> apartments = database.getSmartApartmentsByBuyerId(buyerId);
   if (apartments.isEmpty()) {
       reply("No good apartments.");
   }
   for (Apartment apartment : apartments) {
       sendApartmentToBuyer(apartment,buyerId);
   }
}


private void sendAllApartmentsToBuyer(Integer buyerId) throws SQLException {
   List<Apartment> apartments = database.getAllApartmentsByBuyerId(buyerId);
   if (apartments.isEmpty()) {
       reply("No good apartments.");
   }
   for (Apartment apartment : apartments) {
       sendApartmentToBuyer(apartment,buyerId);
   }
}
private void sendApartmentToBuyer(Apartment apartment,Integer buyerId){
   LinkedList<InlineKeyboardButton> buttons = new LinkedList<>();
   buttons.add(Keyboards.makeInlineButton("Get Contact", "getUser " + apartment.seller));
   buttons.add(Keyboards.makeInlineButton("Not interest", "ignoreApartment " + buyerId + " " + apartment.databaseId));
   InlineKeyboardMarkup inlineKeyboardMarkup = Keyboards.makeInlineKeyboardMarkup(buttons);
   if (apartment.photos.isEmpty()) {
       reply(apartment.getDescription(), inlineKeyboardMarkup);
   } else {
       replyWithPhoto(apartment.photos.get(0), apartment.getDescription(), inlineKeyboardMarkup);
   }
}

@Override
protected Object getObject() {
   return wish;
}

@Override
protected void setObject(Object object) {
   wish = (Wish) object;
}

public BuyerSession(Database database, Long chatId) {
   super(database, chatId);
   type = "BUYER";
   wish = new Wish();//only for this constructor
   runnable = buyerRunnable;
}

public BuyerSession(Database database, SessionDescription sessionDescription) {
   super(database, sessionDescription);
   runnable = buyerRunnable;
}

 

private void handleShowWishesAction(Message message) {
}

private void handleAddAction(Message message) {
   String text = message.getText();
   switch (state) {
       case "SEND_STREET":
           if (text.equals("Cancel")) {
               sendSelectActionRequest();
           } else {
               wish.street = text;
               sendSendPriceRequest();
           }
           break;
       case "SEND_PRICE":
           switch (text) {
               case "Cancel":
                   sendSelectActionRequest();
                   break;
               case "Back":
                   sendSendStreetRequest();
                   break;
               default:
                   try {
                       wish.price = Integer.parseInt(text);
                        sendSendSquareRequest();
                   } catch (NumberFormatException e) {
                       reply("It is not number :( try again");
                   }
                   break;
           }
            break;
       case "SEND_SQUARE":
           switch (text) {
               case "Cancel":
                   sendSelectActionRequest();
                   break;
               case "Back":
                   sendSendPriceRequest();
                   break;
               default:
                   try {
                       wish.square = Integer.parseInt(text);
                       wish.buyer = message.getFrom().getId();
                       sendConfirmRequest();
                   } catch (NumberFormatException e) {
                       reply("It is not number :( try again");
                   }
                   break;
           }
           break;
       case "CONFIRM":
           switch (text) {
               case "Yes":
                   try {
                       database.addWish(wish);
                       reply("Done!");
                       sendSelectActionRequest();
                       new Notifier(wish, "WISH").start();
                   } catch (SQLException e) {
                       e.printStackTrace();
                       reply("Error!");
                   }
                   break;
               case "Back":
                   sendSendSquareRequest();
                   break;
               case "Cancel":
                   sendSelectActionRequest();
                   break;
           }
           break;
       default:
           System.out.println(state);
           System.out.println(message.toString());
           throw new IllegalStateException();
   }
}

private void sendWishesToBuyer(Integer id) throws SQLException {
   LinkedList<Wish> wishes = database.getWishesByTelegramId(id);
   if (wishes.isEmpty()) {
       reply("No added wishes.");
   }
   for (Wish wish : wishes) {
       String callback = "deleteWish " + wish.databaseId;
       InlineKeyboardMarkup deleteWishKeyboard =
               Keyboards.makeOneButtonInlineKeyboardMarkup("Delete Wish", callback);
       reply(wish.getDescriptionForBuyer(), deleteWishKeyboard);
   }
}


private void sendSendSquareRequest() {
   reply("Send me square, please", Keyboards.backAndCancel);
   state = "SEND_SQUARE";
}


private void sendSendPriceRequest() {
   reply("Send me price, please", Keyboards.backAndCancel);
   state = "SEND_PRICE";
}

private void sendSendStreetRequest() {
   reply("Send me street, please", Keyboards.cancel);
   state = "SEND_STREET";
}


private void sendConfirmRequest() {
   reply("Confirm information", Keyboards.yesBackAndCancel);
   reply(wish.toString());
   state = "CONFIRM";
}
}

 

SessionDescription.java

package com.jilfond.bot.sessions;

public class SessionDescription {
public Object object;
public String state;
public String action;
public String type;
public Long chatId;

@Override
public String toString() {
   return "SessionDescription{" +
           "object=" + object +
           ", state='" + state + '\'' +
           ", action='" + action + '\'' +
           ", type='" + type + '\'' +
           ", chatId=" + chatId +
           '}';
}
}

 

Database.java

 

package com.jilfond.bot.databases;

import com.jilfond.bot.objects.BotUser;
import com.jilfond.bot.objects.Apartment;
import com.jilfond.bot.objects.Wish;
import com.jilfond.bot.sessions.SessionDescription;
import org.sqlite.SQLiteConfig;
import org.sqlite.SQLiteOpenMode;

import java.sql.*;
import java.util.LinkedList;
import java.util.List;

public class Database {
static private final String databaseFileName = "database.s3db";
private Connection connection = getConnection();


public Database() throws SQLException {

}


public Integer addApartment(Apartment apartment, boolean added) throws SQLException {
   String sql =
           "insert into apartments (street, houseNumber, number, price, square, seller, added) " +
                   "values (?, ?, ?, ?, ?, ?, ?)";
   PreparedStatement preparedStatement = connection.prepareStatement(sql);
   preparedStatement.setString(1, apartment.street);
   preparedStatement.setString(2, apartment.houseNumber);
   preparedStatement.setInt(3, apartment.number);
   preparedStatement.setInt(4, apartment.price);
   preparedStatement.setInt(5, apartment.square);
   preparedStatement.setInt(6, apartment.seller);
   preparedStatement.setBoolean(7, added);
   preparedStatement.execute();
   apartment.databaseId = preparedStatement.getGeneratedKeys().getInt(1);
   if (apartment.photos != null) {
       for (String photo : apartment.photos) {
           addPhotosToApartment(apartment.databaseId, photo);
       }
   }
   return apartment.databaseId;
}

public Integer addApartment(Apartment apartment) throws SQLException {
   return addApartment(apartment, true);
}

void addPhotosToApartment(Integer apartmentDatabaseId, String photo) throws SQLException {
   Statement statement = connection.createStatement();
   String sql = "insert into photos (apartmentId, photo) " +
           "values (" + apartmentDatabaseId + ", " +
           "'" + photo + "')";
   statement.execute(sql);
}

public void addUserIfNotExist(BotUser user) throws SQLException {
   if (!userExist(user.telegramId)) {
       addUser(user);
   }
}

public BotUser getBotUserByTelegramId(Integer telegramId) throws SQLException {
   BotUser botUser = new BotUser();
   Statement statement = connection.createStatement();
   String sql =
           "select * from users " +
                   "where telegramId = " + telegramId;
   ResultSet resultSet = statement.executeQuery(sql);
   botUser.telegramId = resultSet.getInt("telegramId");
   botUser.firstName = resultSet.getString("firstName");
   botUser.lastName = resultSet.getString("lastName");
   botUser.userName = resultSet.getString("userName");
   botUser.email = resultSet.getString("email");
   botUser.phoneNumber = resultSet.getString("phoneNumber");
   resultSet.close();
   return botUser;
}

public void addUser(BotUser user) throws SQLException {
   String sql =
           "insert into users (telegramId, firstName, lastName, userName, phoneNumber, email) " +
                   "values (?, ?, ?, ?, ?, ?)";
   PreparedStatement preparedStatement = connection.prepareStatement(sql);
   preparedStatement.setInt(1, user.telegramId);
   preparedStatement.setString(2, user.firstName);
   preparedStatement.setString(3, user.lastName);
   preparedStatement.setString(4, user.userName);
   preparedStatement.setString(5, user.phoneNumber);
   preparedStatement.setString(6, user.email);
   preparedStatement.execute();
}

public boolean userExist(Integer telegramId) throws SQLException {
   Statement statement = connection.createStatement();
   String sql =
           "select count(*) from users " +
                   "where telegramId = " + telegramId;
   ResultSet resultSet = statement.executeQuery(sql);
   Integer res = resultSet.getInt(1);
   resultSet.close();
   return res == 1;
}

public void deleteUserByTelegramId(Integer telegramId) throws SQLException {
   Statement statement = connection.createStatement();
   String sql =
           "delete from users " +
                   "where telegramId = " + telegramId;
   statement.execute(sql);
}

Connection getConnection() throws SQLException {
   SQLiteConfig config = new SQLiteConfig();
   config.setOpenMode(SQLiteOpenMode.FULLMUTEX);
   return DriverManager.getConnection("jdbc:sqlite:" + databaseFileName, config.toProperties());
}

public void updatePhoneNumber(Integer userId, String phoneNumber) throws SQLException {
   Statement statement = connection.createStatement();
   String sql =
           "update users " +
                   "set phoneNumber = '" + phoneNumber + "' " +
                   "where telegramId = " + userId;
   statement.execute(sql);
}

public void updateEmail(Integer userId, String email) throws SQLException {
   Statement statement = connection.createStatement();
   String sql =
           "update users " +
                   "set email = '" + email + "' " +
                   "where telegramId = " + userId;
   statement.execute(sql);
}

public void deletePhotosFromApartment(Integer apartmentId) throws SQLException {
   Statement statement = connection.createStatement();
   String sql =
           "delete from photos " +
                   "where apartmentId = " + apartmentId;
     statement.execute(sql);
}

public void deleteApartmentById(Integer apartmentId) throws SQLException {
   deletePhotosFromApartment(apartmentId);
   Statement statement = connection.createStatement();
   String sql =
           "delete from apartments " +
                   "where id = " + apartmentId;
   statement.execute(sql);
}

public LinkedList<Apartment> getApartmentsByTelegramId(Integer telegramId, boolean added) throws SQLException {
   LinkedList<Apartment> apartments = new LinkedList<>();
   String sql =
           "SELECT * FROM apartments " +
                   "WHERE seller = " + telegramId;
   Statement statement = connection.createStatement();
   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       if (resultSet.getBoolean("added") != added) {
           continue;
       }
       Apartment apartment = new Apartment();
       apartment.street = resultSet.getString("street");
       apartment.houseNumber = resultSet.getString("houseNumber");
       apartment.number = resultSet.getInt("number");
       apartment.databaseId = resultSet.getInt("id");
       apartment.price = resultSet.getInt("price");
       apartment.square = resultSet.getInt("square");
       apartment.seller = resultSet.getInt("seller");
       apartment.databaseId = resultSet.getInt("id");
       Statement photoStatement = connection.createStatement();
       String photoSql = "SELECT * FROM photos " +
               "WHERE apartmentId = " + apartment.databaseId;
       ResultSet photoResultSet = photoStatement.executeQuery(photoSql);
       while (photoResultSet.next()) {
           String photo = photoResultSet.getString("photo");
           apartment.photos.add(photo);
       }
       apartments.add(apartment);
       photoResultSet.close();
   }
   resultSet.close();
   return apartments;
}

public LinkedList<Apartment> getApartmentsByTelegramId(Integer telegramId) throws SQLException {
   return getApartmentsByTelegramId(telegramId, true);
}

public boolean sessionExist(Long chatId) throws SQLException {
   String sql = "SELECT count(*) FROM sessions " +
           "where chatId = " + chatId;
   Statement statement = connection.createStatement();
   ResultSet resultSet = statement.executeQuery(sql);
   int count = resultSet.getInt(1);
   resultSet.close();
   return count == 1;
}

public void deleteApartmentByChatId(Long chatId) throws SQLException {
   String sql = "delete from apartments " +
           "where id in (select objectId from sessions where chatId = " + chatId + ")";
   Statement statement = connection.createStatement();
   statement.execute(sql);
}

public void saveSession(SessionDescription session) throws SQLException {
   switch (session.type) {
       case "SELLER":
           deleteApartmentByChatId(session.chatId);
           break;
       case "BUYER":
           deleteWishByChatId(session.chatId);
           break;
   }
   deleteSession(session.chatId);
   Integer foreignKey = null;
   switch (session.type) {
       case "SELLER":
           foreignKey = addApartment((Apartment) session.object, false);
           break;
       case "BUYER":
           foreignKey = addWish((Wish) session.object, false);
           break;
   }
   String sql = "insert into sessions " +
           "(chatId, state, action, type, objectId)" +
           "values (?, ?, ?, ?, ?)";
   PreparedStatement preparedStatement = connection.prepareStatement(sql);
   preparedStatement.setLong(1, session.chatId);
   preparedStatement.setString(2, session.state);
   preparedStatement.setString(3, session.action);
   preparedStatement.setString(4, session.type);
   preparedStatement.setInt(5, foreignKey);
   preparedStatement.execute();
}

private void deleteWishByChatId(Long chatId) throws SQLException {
   String sql = "delete from wishes " +
           "where id in (select objectId from sessions where chatId = " + chatId + ")";
   Statement statement = connection.createStatement();
   statement.execute(sql);
}

public SessionDescription getSession(Long chatId) throws SQLException {
   SessionDescription sessionDescription = new SessionDescription();
   String sql = "select * from sessions " +
           "where chatId = " + chatId;
   Statement statement = connection.createStatement();
   ResultSet resultSet = statement.executeQuery(sql);
   Integer foreignKey = resultSet.getInt("objectId");
   sessionDescription.state = resultSet.getString("state");
   sessionDescription.action = resultSet.getString("action");
   sessionDescription.type = resultSet.getString("type");
   sessionDescription.chatId = resultSet.getLong("chatId");
   switch (sessionDescription.type) {
       case "BUYER":
           sessionDescription.object = getWishByWishDatabaseId(foreignKey);
           break;
       case "SELLER":
           sessionDescription.object = getApartmentByApartmentDatabaseId(foreignKey);
           break;
   }
   resultSet.close();
   return sessionDescription;
}

private Object getWishByWishDatabaseId(Integer wishId) throws SQLException {
   Wish wish = new Wish();
   String sql = "SELECT * FROM wishes " +
           "where added = false and id = " + wishId;
   Statement statement = connection.createStatement();
   ResultSet resultSet = statement.executeQuery(sql);
   wish.street = resultSet.getString("street");
   wish.price = resultSet.getInt("price");
   wish.square = resultSet.getInt("square");
   wish.databaseId = resultSet.getInt("id");
   wish.buyer = resultSet.getInt("buyer");
   resultSet.close();
   return wish;
}

public Apartment getApartmentByApartmentDatabaseId(Integer apartmentId) throws SQLException {
   Apartment apartment = new Apartment();
   String sql = "SELECT * FROM apartments " +
           "where added = false and id = " + apartmentId;
   Statement statement = connection.createStatement();
   ResultSet resultSet = statement.executeQuery(sql);
   apartment.street = resultSet.getString("street");
   apartment.houseNumber = resultSet.getString("houseNumber");
   apartment.number = resultSet.getInt("number");
   apartment.price = resultSet.getInt("price");
   apartment.square = resultSet.getInt("square");
   apartment.seller = resultSet.getInt("seller");
   apartment.databaseId = resultSet.getInt("id");
   apartment.photos = getPhotosByApartmentId(apartment.databaseId);
   resultSet.close();
   return apartment;
}

public void deleteSession(Long chatId) throws SQLException {
   deleteApartmentByChatId(chatId);
   deleteWishByChatId(chatId);
   String deleteSql = "delete from sessions " +
           "where chatId = " + chatId;
   Statement statement = connection.createStatement();
   statement.execute(deleteSql);
}


public LinkedList<Wish> getWishesByTelegramId(Integer telegramId) throws SQLException {
   return getWishesByTelegramId(telegramId, true);

}

public LinkedList<Wish> getWishesByTelegramId(Integer telegramId, boolean added) throws SQLException {
   LinkedList<Wish> wishes = new LinkedList<>();
   String sql =
           "SELECT * FROM wishes " +
                   "WHERE buyer = " + telegramId;
   Statement statement = connection.createStatement();
   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       if (resultSet.getBoolean("added") != added) {
           continue;
       }
       Wish wish = new Wish();
       wish.street = resultSet.getString("street");
       wish.buyer = resultSet.getInt("buyer");
       wish.databaseId = resultSet.getInt("id");
       wish.price = resultSet.getInt("price");
       wish.square = resultSet.getInt("square");
       wishes.add(wish);
   }
   resultSet.close();
   return wishes;
}

public Integer addWish(Wish wish) throws SQLException {
   return addWish(wish, true);
}

public Integer addWish(Wish wish, boolean added) throws SQLException {
   String sql =
           "insert into wishes (street, price, square, buyer, added) " +
                   "values (?, ?, ?, ?, ?)";
   PreparedStatement preparedStatement = connection.prepareStatement(sql);
   preparedStatement.setString(1, wish.street);
   preparedStatement.setInt(2, wish.price);
   preparedStatement.setInt(3, wish.square);
   preparedStatement.setInt(4, wish.buyer);
   preparedStatement.setBoolean(5, added);
   preparedStatement.execute();
   wish.databaseId = preparedStatement.getGeneratedKeys().getInt(1);
   return wish.databaseId;
}

public void deleteWishById(Integer wishId) throws SQLException {
   Statement statement = connection.createStatement();
   String sql =
           "delete from wishes " +
                   "where id = " + wishId;
   statement.execute(sql);
}

public List<Wish> getAllWishes(Integer sellerId) throws SQLException {
   List<Wish> wishes = new LinkedList<>();
   Statement statement = connection.createStatement();
   String sql =
           "SELECT * FROM wishes " +
                   "WHERE added and buyer <> " + sellerId + " " +
                   "AND id not in (SELECT objectId FROM ignoredObjects WHERE type = 'WISH' AND userId = " + sellerId + ")";
   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       Wish wish = new Wish();
       wish.street = resultSet.getString("street");
       wish.buyer = resultSet.getInt("buyer");
       wish.databaseId = resultSet.getInt("id");
       wish.price = resultSet.getInt("price");
       wish.square = resultSet.getInt("square");
       wishes.add(wish);
   }
     resultSet.close();
   return wishes;
}

public List<Wish> getSmartWishesBySellerId(Integer sellerId) throws SQLException {
   List<Wish> wishes = new LinkedList<>();
   Statement statement = connection.createStatement();
   String sql =
           "SELECT DISTINCT w.* FROM apartments a, wishes w " +
                   "WHERE w.street = a.street AND " +
                   "w.buyer <> " + sellerId + " AND " +
                   "w.added AND a.added AND " +
                   "w.price >= a.price AND w.square<=a.price " +
                   "AND w.id not in (SELECT objectId FROM ignoredObjects WHERE type = 'WISH' AND userId = " + sellerId + ")";
   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       Wish wish = new Wish();
       wish.street = resultSet.getString("street");
       wish.buyer = resultSet.getInt("buyer");
       wish.databaseId = resultSet.getInt("id");
       wish.price = resultSet.getInt("price");
       wish.square = resultSet.getInt("square");
       wishes.add(wish);
   }
   resultSet.close();
   return wishes;
}

public LinkedList<BotUser> getUsersWithApartmentsOnStreet(String street, Integer telegramId) throws SQLException {
   LinkedList<BotUser> users = new LinkedList<>();
   Statement statement = connection.createStatement();

   String sql = "SELECT * FROM users " +
           "WHERE telegramId in (SELECT seller FROM apartments WHERE street = \'" + street + "\' and seller <> " + telegramId + ")";

   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       BotUser botUser = new BotUser();
       botUser.telegramId = resultSet.getInt("telegramId");
       botUser.firstName = resultSet.getString("firstName");
       botUser.lastName = resultSet.getString("lastName");
       botUser.userName = resultSet.getString("userName");
       botUser.email = resultSet.getString("email");
       botUser.phoneNumber = resultSet.getString("phoneNumber");
       users.add(botUser);
   }
   resultSet.close();
   return users;
}

public List<Apartment> getSmartApartmentsByBuyerId(Integer buyerId) throws SQLException {
   List<Apartment> apartments = new LinkedList<>();
   Statement statement = connection.createStatement();
   String sql =
           "SELECT DISTINCT a.* FROM apartments a, wishes w " +
                    "WHERE w.street = a.street AND " +
                   "a.seller <> " + buyerId + " AND " +
                   "w.added AND a.added AND " +
                   "w.price >= a.price AND w.square<=a.square " +
                   "AND a.id not in (SELECT objectId FROM ignoredObjects WHERE type = 'APARTMENT' AND userId = " + buyerId + ")";
   System.out.println(sql);
   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       Apartment apartment = new Apartment();
       apartment.street = resultSet.getString("street");
       apartment.houseNumber = resultSet.getString("houseNumber");
       apartment.number = resultSet.getInt("number");
       apartment.price = resultSet.getInt("price");
       apartment.square = resultSet.getInt("square");
       apartment.seller = resultSet.getInt("seller");
       apartment.databaseId = resultSet.getInt("id");
       apartments.add(apartment);
    }
   resultSet.close();
   return apartments;
}

public List<Apartment> getAllApartmentsByBuyerId(Integer buyerId) throws SQLException {
   List<Apartment> apartments = new LinkedList<>();
   Statement statement = connection.createStatement();
   String sql =
           "SELECT * FROM apartments a " +
                   "WHERE seller <> " + buyerId +" " +
                   "AND id not in (SELECT objectId FROM ignoredObjects WHERE type = 'APARTMENT' AND userId = " + buyerId + ")";;
   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       Apartment apartment = new Apartment();
       apartment.street = resultSet.getString("street");
       apartment.houseNumber = resultSet.getString("houseNumber");
       apartment.number = resultSet.getInt("number");
       apartment.price = resultSet.getInt("price");
       apartment.square = resultSet.getInt("square");
       apartment.seller = resultSet.getInt("seller");
       apartment.databaseId = resultSet.getInt("id");
       apartment.photos = getPhotosByApartmentId(apartment.databaseId);
       apartments.add(apartment);
   }
   resultSet.close();
   return apartments;
}

List<String> getPhotosByApartmentId(Integer apartmentId) throws SQLException {
   List<String> photos = new LinkedList<>();
   String sql = "SELECT * FROM photos " +
           "where apartmentId = " + apartmentId;
    Statement statement = connection.createStatement();
   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       String photo = resultSet.getString("photo");
       photos.add(photo);
   }
   resultSet.close();
   return photos;
}

public LinkedList<BotUser> getUsersWithWishesOnStreet(String street, Integer telegramId) throws SQLException {
   LinkedList<BotUser> users = new LinkedList<>();
   Statement statement = connection.createStatement();
   String sql =
           "SELECT * FROM users " +
                   "WHERE telegramId in (SELECT buyer FROM wishes WHERE street = '" + street + "' and buyer <> " + telegramId + ")";
   ResultSet resultSet = statement.executeQuery(sql);
   while (resultSet.next()) {
       BotUser botUser = new BotUser();
       botUser.telegramId = resultSet.getInt("telegramId");
       botUser.firstName = resultSet.getString("firstName");
       botUser.lastName = resultSet.getString("lastName");
       botUser.userName = resultSet.getString("userName");
       botUser.email = resultSet.getString("email");
       botUser.phoneNumber = resultSet.getString("phoneNumber");
       users.add(botUser);
   }
   resultSet.close();
   return users;
}

public void ignoreWish(Integer userId, Integer objectId) throws SQLException {
   String sql = "INSERT INTO ignoredObjects (userId, objectId, type) " +
           "VALUES (?, ?, 'WISH')";
   PreparedStatement preparedStatement = connection.prepareStatement(sql);
   preparedStatement.setInt(1, userId);
   preparedStatement.setInt(2, objectId);
   preparedStatement.execute();
   System.out.println(sql);
}

public void ignoreApartment(Integer userId, Integer objectId) throws SQLException {
   String sql = "INSERT INTO ignoredObjects (userId, objectId, type) " +
           "VALUES (?, ?, 'APARTMENT')";
   PreparedStatement preparedStatement = connection.prepareStatement(sql);
   preparedStatement.setInt(1, userId);
   preparedStatement.setInt(2, objectId);
   preparedStatement.execute();
   System.out.println(sql);
}
}

 

Keyboards.java

package com.jilfond.bot;

import org.telegram.telegrambots.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.api.objects.replykeyboard.ReplyKeyboard;
import org.telegram.telegrambots.api.objects.replykeyboard.ReplyKeyboardMarkup;
import org.telegram.telegrambots.api.objects.replykeyboard.buttons.InlineKeyboardButton;
import org.telegram.telegrambots.api.objects.replykeyboard.buttons.KeyboardRow;

import java.util.LinkedList;
import java.util.List;

public class Keyboards {
public static ReplyKeyboardMarkup backCancelAndNo = createBackCancelAndNo();
public static ReplyKeyboardMarkup cancel = createOneButtonKeyboard("Cancel");
public static ReplyKeyboardMarkup backAndCancel = createBackAndCancel();
public static ReplyKeyboardMarkup yesBackAndCancel = createYesBackAndCancel();
public static ReplyKeyboardMarkup start = createOneButtonKeyboard("/start");

public static InlineKeyboardButton makeInlineButton(String text, String callback){
   return new InlineKeyboardButton().setText(text).setCallbackData(callback);
}

private static ReplyKeyboardMarkup createOneButtonKeyboard(String text) {
   LinkedList<String> buttons = new LinkedList<>();
   buttons.add(text);
   return make(buttons);
}

public static InlineKeyboardMarkup makeInlineKeyboardMarkup(List<InlineKeyboardButton> buttons){
   List<List<InlineKeyboardButton>> listButtons = new LinkedList<>();
   listButtons.add(buttons);
   return new InlineKeyboardMarkup().setKeyboard(listButtons);
}

public static InlineKeyboardMarkup makeOneButtonInlineKeyboardMarkup(String buttonText, String callbackData){
   LinkedList<InlineKeyboardButton> buttons = new LinkedList<>();
   buttons.add(makeInlineButton(buttonText,callbackData));
   return makeInlineKeyboardMarkup(buttons);
}

private static ReplyKeyboardMarkup createBackCancelAndNo() {
   LinkedList<String> buttons = new LinkedList<>();
   buttons.add("Back");
   buttons.add("Cancel");
   buttons.add("No");
   return make(buttons);
}

private static ReplyKeyboardMarkup createBackAndCancel() {
   LinkedList<String> buttons = new LinkedList<>();
   buttons.add("Back");
   buttons.add("Cancel");
   return make(buttons);
}

private static ReplyKeyboardMarkup createYesBackAndCancel() {
   LinkedList<String> buttons = new LinkedList<>();
   buttons.add("Yes");
   buttons.add("Back");
   buttons.add("Cancel");
   return make(buttons);
}

public static ReplyKeyboardMarkup make(LinkedList<String> buttons, boolean vertical){
   ReplyKeyboardMarkup replyKeyboardMarkup = new ReplyKeyboardMarkup();
   LinkedList<KeyboardRow> keyboardRows = new LinkedList<>();
   if(vertical) {
       for(String button: buttons){
           KeyboardRow keyboardButtons = new KeyboardRow();
           keyboardButtons.add(button);
           keyboardRows.add(keyboardButtons);
       }
   } else{
      KeyboardRow keyboardButtons = new KeyboardRow();
       for (String button : buttons) {
           keyboardButtons.add(button);
       }
       keyboardRows.add(keyboardButtons);
   }
   replyKeyboardMarkup.setKeyboard(keyboardRows);
   replyKeyboardMarkup.setResizeKeyboard(true);
   return replyKeyboardMarkup;
}
public static ReplyKeyboardMarkup make(LinkedList<String> buttons){
   return make(buttons,false);
}
}

DatabaseTest.java

package com.jilfond.bot.tests;

import com.jilfond.bot.Bot;
import com.jilfond.bot.objects.BotUser;
import com.jilfond.bot.databases.Database;
import com.jilfond.bot.objects.Apartment;
import com.jilfond.bot.sessions.SellerSession;
import org.junit.jupiter.api.Test;

import java.sql.SQLException;
import java.util.LinkedList;

import static org.junit.jupiter.api.Assertions.*;

class DatabaseTest {
@Test
void addGetExistRemoveUser() {
   BotUser botUser = new BotUser();
   botUser.phoneNumber = "+79131112233";
   botUser.email = "email@gmail.com";
   botUser.userName = "AlexDarkStalker98";
   botUser.firstName = "Иван";
   botUser.lastName = "Петров";
   botUser.telegramId = 123454321;
   try {
       Database database = new Database();
       //database.deleteUserByTelegramId(botUser.telegramId);
       database.addUser(botUser);
       assertEquals(true, database.userExist(botUser.telegramId));
       assertEquals(botUser.toString(), database.getBotUserByTelegramId(botUser.telegramId).toString());
       database.deleteUserByTelegramId(botUser.telegramId);
       assertEquals(false, database.userExist(botUser.telegramId));
   } catch (SQLException e) {
       e.printStackTrace();
   }
}

@Test
void addGetExistRemoveApartment() {
   Apartment apartment = new Apartment();
   apartment.street = "Chekhov";
   apartment.houseNumber = "12/3";
   apartment.number = 488;
   apartment.square = 35;
   apartment.price = 15000;
   apartment.seller = 123454321;
   try {
       Database database = new Database();
       database.addApartment(apartment);
   } catch (SQLException e) {
       e.printStackTrace();
   }
}

@Test
void updatePhoneNumberAndEmail() {
   BotUser botUser = new BotUser();
   botUser.phoneNumber = "+79131112233";
   botUser.email = "email@gmail.com";
   botUser.userName = "AlexDarkStalker98";
   botUser.firstName = "Иван";
   botUser.lastName = "Петров";
   botUser.telegramId = 123454321;
   try {
       String newPhoneNumber = "+79130001122";
       String newEmail = "yandex@yandex.ru";
       Database database = new Database();
       database.addUserIfNotExist(botUser);
       database.updatePhoneNumber(botUser.telegramId, newPhoneNumber);
       database.updateEmail(botUser.telegramId, newEmail);
       BotUser databaseBotUser = database.getBotUserByTelegramId(botUser.telegramId);
       assertEquals(newPhoneNumber, databaseBotUser.phoneNumber);
       assertEquals(newEmail, databaseBotUser.email);
       database.deleteUserByTelegramId(botUser.telegramId);
   } catch (SQLException e) {
         e.printStackTrace();
   }

}

@Test
void addApartmentTest() {
   Apartment apartment = new Apartment();
   try {
       Database database = new Database();
       database.addApartment(apartment);
       database.deleteApartmentById(apartment.databaseId);
   } catch (SQLException e) {
       e.printStackTrace();
   }
}

@Test
void getApartmentsByTelegramIdTest() {
   Integer telegramId = 63059291;
   try {
       Database database = new Database();
       LinkedList<Apartment> apartments = database.getApartmentsByTelegramId(telegramId);
       for (Apartment apartment : apartments) {
           System.out.println(apartment.toString());
           System.out.println("photos:");
           for (String photo : apartment.photos) {
               System.out.println(photo);
           }
       }
   } catch (SQLException e) {
       e.printStackTrace();
   }
}

@Test
void saveAndLoadSessionTest() {
   Long chatId = Long.valueOf(123456);
   Database database = null;
   SellerSession sellerSession = null;
   try {
       Bot bot = new Bot();
       database = new Database();
       sellerSession = new SellerSession(database, chatId);
       sellerSession.save();
   } catch (SQLException e) {
       e.printStackTrace();
   }
   try {
       assertEquals(true, database.sessionExist(chatId));
   } catch (SQLException e) {
       e.printStackTrace();
   }
   SellerSession loadedSellerSession = new SellerSession(database, chatId);
   loadedSellerSession.load();
   assert (sellerSession.equals(loadedSellerSession));
}
}

 


Дата добавления: 2018-08-06; просмотров: 94; Мы поможем в написании вашей работы!

Поделиться с друзьями:




Мы поможем в написании ваших работ!