├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
├── src
└── main
│ ├── java
│ └── ru
│ │ └── taksebe
│ │ └── telegram
│ │ └── writeRead
│ │ ├── WriteReadApplication.java
│ │ ├── api
│ │ ├── dictionaries
│ │ │ ├── DictionaryAdditionService.java
│ │ │ ├── DictionaryExcelService.java
│ │ │ ├── DictionaryRepository.java
│ │ │ ├── DictionaryResourceFileService.java
│ │ │ └── WordService.java
│ │ └── tasks
│ │ │ └── TaskService.java
│ │ ├── config
│ │ ├── RedisConfiguration.java
│ │ ├── SpringConfig.java
│ │ └── TelegramConfig.java
│ │ ├── constants
│ │ ├── bot
│ │ │ ├── BotMessageEnum.java
│ │ │ ├── ButtonNameEnum.java
│ │ │ └── CallbackDataPartsEnum.java
│ │ └── resources
│ │ │ ├── DictionaryResourcePathEnum.java
│ │ │ └── TemplateResourcePathsEnum.java
│ │ ├── converters
│ │ ├── BytesToWordConverter.java
│ │ └── WordToBytesConverter.java
│ │ ├── exceptions
│ │ ├── DictionaryTooBigException.java
│ │ ├── TelegramFileNotFoundException.java
│ │ ├── TelegramFileUploadException.java
│ │ └── UserDictionaryNotFoundException.java
│ │ ├── initialization
│ │ └── InitializingBeanImpl.java
│ │ ├── model
│ │ ├── Dictionary.java
│ │ └── Word.java
│ │ ├── telegram
│ │ ├── TelegramApiClient.java
│ │ ├── WebhookController.java
│ │ ├── WriteReadBot.java
│ │ ├── handlers
│ │ │ ├── CallbackQueryHandler.java
│ │ │ └── MessageHandler.java
│ │ └── keyboards
│ │ │ ├── InlineKeyboardMaker.java
│ │ │ └── ReplyKeyboardMaker.java
│ │ └── utils
│ │ ├── FileUtils.java
│ │ └── ResourceLoader.java
│ └── resources
│ ├── dictionaries
│ ├── 1 grade.xlsx
│ ├── 2 grade.xlsx
│ ├── 3 grade.xlsx
│ └── 4 grade.xlsx
│ └── templates
│ ├── Template.docx
│ └── Template.xlsx
└── system.properties
/.gitignore:
--------------------------------------------------------------------------------
1 | ### IntelliJ IDEA ###
2 | .idea
3 | *.iws
4 | *.iml
5 | *.ipr
6 |
7 | target/
8 | log/
9 | src/main/resources/application.yaml
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {2022} {Sergey Kozyrev}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Что это?
2 | Telegram-бот для генерации готового для печати Word-файла, содержащего задания для запоминания правописания словарных слов русского языка
3 |
4 | Пользователь может использовать встроенные словари для 1-4 классов и формировать собственный, используя инструменты бота
5 |
6 | Бот принесёт пользу родителям, которые хотят подтянуть грамотность своих детей
7 |
8 | [Статья на Хабре](https://habr.com/ru/post/646017/)
9 |
10 | ## Лицензия
11 | Этот проект лицензируется в соответствии с лицензией Apache 2.0
12 |
13 | Подробности в файле ```LICENSE```
14 |
15 | ## Попробовать
16 | [@WriteReadRightBot](https://t.me/WriteReadRightBot) доступен в Telegram
17 |
18 | ## Автор
19 | Сергей Козырев
20 |
21 | ## Контакты для связи
22 | Telegram [@taksebe](https://t.me/taksebe)
23 |
24 | ## Создано с помощью
25 | Java™ SE Development Kit 11.0.5
26 |
27 | [Spring Framework](https://spring.io/)
28 |
29 | Git - управление версиями
30 |
31 | GitHub - репозиторий
32 |
33 | [Redis](https://redis.io/) - СУБД
34 |
35 | [Telegram Bots](https://core.telegram.org/bots) - взаимодействие с Telegram
36 |
37 | [Apache Maven](https://maven.apache.org/) - сборка, управление зависимостями
38 |
39 | [Apache POI](https://poi.apache.org/) - создание файлов Word и Excel
40 |
41 | [Lombok](https://projectlombok.org/) - упрощение кода, замена стандартных java-методов аннотациями
42 |
43 | [Heroku](https://www.heroku.com/) - деплой, хостинг
44 |
45 | Полный список зависимостей и используемые версии компонентов можно найти в ```pom.xml```
46 |
47 | ## Сборка и запуск
48 | Перед сборкой необходимо создать бота с помощью [BotFather](https://t.me/botfather) и сохранить его имя и токен (они понадобятся для запуска)
49 |
50 | Далее
51 | ```
52 | git clone https://github.com/taksebe-official/writeReadRightBot
53 | ```
54 |
55 | Создать в проекте файл ```src/main/resources/application.yaml``` (или ```.properties```, если Вам так привычнее)
56 |
57 | Добавить в него настройки Telegram:
58 | ```
59 | telegram.api-url: "https://api.telegram.org/"
60 | telegram.user: <имя бота, полученное от BotFather>
61 | telegram.token: <токен бота, полученный от BotFather>
62 | telegram.webhook-path: <см чуть ниже>
63 | server.port: <см чуть ниже>
64 | ```
65 | Для получения настроек ```telegram.webhook-path``` и ```server.port``` при локальной отладке можно использовать прекрасную утилиту [ngrok](https://ngrok.com/), здесь [инструкция](https://pavelpage.ru/koderstvo/nastroyka-ngrok-dlya-otladki-telegram-bota.html) конкретно для вебхуков Telegram
66 |
67 | Далее нужно добавить в тот же файл настройки подключения к БД Redis:
68 | ```
69 | spring.redis.database: 0
70 | spring.redis.host: <хост БД Redis>
71 | spring.redis.port: <порт БД Redis>
72 | spring.redis.password: <пароль БД Redis>
73 | spring.redis.ssl: true
74 | ```
75 | Можно уже на этом этапе использовать [Heroku Redis](https://devcenter.heroku.com/articles/heroku-redis). Для создания БД необходимо:
76 |
77 | - Зарегистрироваться на Heroku
78 | - Создать проект - New/Create new app в правой части экрана
79 | - Перейти на вкладку Resources
80 | - В разделе Add-ons ввести в поисковую строку "Heroku Redis", выбрать её в результатах поиска
81 | - Подтвердить подключение БД к проекту
82 | - В правом верхнем углу нажать на иконку в виде квадрата из синих точек, выбрать пункт Data, в открывшемся списке баз нажать на только что созданную. Первые несколько минут после подключения может тормозить и показывать ошибку
83 | - Перейти на вкладку Settings
84 | - Нажать на кнопку View credentials в правой части экрана
85 | - Вуаля, перед Вами настройки подключения к БД. Скопируйте их в Ваш файл application
86 |
87 | Учтите, что эти настройки Heroku периодически меняет, поэтому иногда нужно будет заново копировать их в Ваш проект
88 |
89 | Далее:
90 | ```
91 | mvn clean install
92 | java $JAVA_OPTS -jar target/write-read-1.0-SNAPSHOT.jar
93 | ```
94 |
95 | ## Порядок развёртывания на Heroku
96 | Проект писался для релиза на [Heroku](https://www.heroku.com/) и содержит специфический для этой площадки файл ```system.properties```, в котором нужно указать версию Java, если она отлична от 8. Ещё один специфический для Heroku файл ```Procfile``` в данном случае можно не добавлять, он будет сгенерирован автоматичеки на основе ```pom.xml```
97 |
98 | Сначала нужно обязательно удалить/закомментировать в файле ```src/main/resources/application.yaml``` (или ```.properties```) настройки подключения к БД - она подцепится автоматически, поскольку подключена к проекту на Heroku. Если оставить эти настройки, ничего не заведётся, они нужны только для внешнего подключения к этой БД
99 |
100 | Погнали:
101 | ```
102 | //предварительно зарегистрироваться на Heroku
103 | heroku login
104 | heroku create <имя приложения>
105 | ```
106 |
107 | Далее зайти на [Heroku](https://www.heroku.com/) и добавить в созданный проект БД Heroku Redis (инструкция выше в этом ReadMe, в разделе "Сборка и запуск")
108 |
109 | Далее:
110 | ```
111 | mvn clean install
112 | git push heroku master
113 | //установить количество контейнеров (dynos) для типа процесса web
114 | heroku ps:scale web=1
115 | ```
116 |
117 | В интерфейсе управления приложением в личном кабинете на [Heroku](https://www.heroku.com/) можно перейти к логам (прячутся под кнопкой More в правом верхнем углу) и убедиться, что приложение запущено
118 |
119 | Далее необходимо зарегистрировать webhook в Telegram - для этого нужно отправить в любом браузере ссылку вида:
120 | ```
121 | https://api.telegram.org/bot<токен бота от Botfather>/setWebhook?url=
122 | ```
123 |
124 | URL веб-приложения можно получить, нажав на кноку Open app (в правом верхнем углу) - приложение откроется в новой вкладке, необходимо скопировать URL в адресной строке
125 |
126 | Теперь можно проверять бота непосредственно в Telegram
127 |
128 | При необходимости в интерфейсе управления приложением на вкладке Deploy можно переключить деплой на GitHub-репозиторий (по запросу или автоматически)
129 |
130 | ## Что можно доделать
131 | Как известно, Heroku гасит веб-приложения, которые не используются какое-то время, поэтому на первое сообщение бот может отвечать порядка 8-10 секунд - он ждёт, когда приложение развернётся с нуля. Это позволяет на бесплатном тарифе хостить много редко используемых веб-приложений - в тарифе учитывается только время аптайма
132 |
133 | Чтобы заставить приложение работать постоянно, можно добавить в проект пинг по расписанию условного Google, но нужно понимать, что в этом случае бот будет съедать львиную долю бесплатного тарифа. Я жадный, я так делать не буду:)
134 |
135 | ## Отдельное спасибо
136 | [Владу](https://github.com/itotx), который всё ещё возится со мной, неразумным, хотя, казалось бы, я уже давно должен был повзрослеть
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.2.0.RELEASE
9 |
10 |
11 | 4.0.0
12 |
13 | ru.taksebe.telegram
14 | write-read
15 | 1.0-SNAPSHOT
16 | write-read
17 | Пиши-читай
18 | jar
19 |
20 |
21 | 11
22 | ${java.version}
23 | ${java.version}
24 | UTF-8
25 | UTF-8
26 |
27 |
28 |
29 |
30 | org.springframework.boot
31 | spring-boot-starter-web
32 |
33 |
34 | org.springframework.boot
35 | spring-boot-starter-data-redis
36 |
37 |
38 | org.springframework.data
39 | spring-data-redis
40 | 2.2.0.RELEASE
41 |
42 |
43 | redis.clients
44 | jedis
45 | 3.7.0
46 |
47 |
48 | org.telegram
49 | telegrambots-spring-boot-starter
50 | 5.3.0
51 |
52 |
53 | org.projectlombok
54 | lombok
55 | 1.18.20
56 | compile
57 |
58 |
59 | org.apache.poi
60 | poi
61 | 5.0.0
62 |
63 |
64 | org.apache.poi
65 | poi-ooxml
66 | 5.0.0
67 |
68 |
69 |
70 |
71 |
72 |
73 | org.springframework.boot
74 | spring-boot-maven-plugin
75 |
76 |
77 |
78 | build-info
79 |
80 |
81 |
82 | ${project.build.sourceEncoding}
83 | ${project.reporting.outputEncoding}
84 | ${maven.compiler.source}
85 | ${maven.compiler.target}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/WriteReadApplication.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class WriteReadApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(WriteReadApplication.class, args);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/api/dictionaries/DictionaryAdditionService.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.api.dictionaries;
2 |
3 | import lombok.RequiredArgsConstructor;
4 | import org.apache.poi.ss.usermodel.Cell;
5 | import org.apache.poi.ss.usermodel.Row;
6 | import org.apache.poi.xssf.usermodel.XSSFSheet;
7 | import org.apache.poi.xssf.usermodel.XSSFWorkbook;
8 | import org.springframework.stereotype.Component;
9 | import ru.taksebe.telegram.writeRead.exceptions.DictionaryTooBigException;
10 | import ru.taksebe.telegram.writeRead.model.Dictionary;
11 | import ru.taksebe.telegram.writeRead.model.Word;
12 |
13 | import java.io.File;
14 | import java.io.FileInputStream;
15 | import java.io.IOException;
16 | import java.util.ArrayList;
17 | import java.util.HashSet;
18 | import java.util.Iterator;
19 | import java.util.List;
20 |
21 | @Component
22 | @RequiredArgsConstructor
23 | public class DictionaryAdditionService {
24 | private final DictionaryRepository repository;
25 |
26 | public void addUserDictionary(String userId, File file) throws IOException {
27 | try (FileInputStream fileInputStream = new FileInputStream(file)) {
28 | repository.save(
29 | Dictionary.builder()
30 | .id(userId)
31 | .wordList(createDictionary(new XSSFWorkbook(fileInputStream)))
32 | .build()
33 | );
34 | }
35 | }
36 |
37 | public void addDefaultDictionary(String dictionaryId, XSSFWorkbook workbook) {
38 | repository.save(Dictionary.builder().id(dictionaryId).wordList(createDictionary(workbook)).build());
39 | }
40 |
41 | private List createDictionary(XSSFWorkbook workbook) {
42 | XSSFSheet sheet = workbook.getSheetAt(0);
43 | Iterator rowIterator = sheet.iterator();
44 |
45 | List result = new ArrayList<>();
46 | while (rowIterator.hasNext()) {
47 | result.add(createDictionaryWord(rowIterator.next()));
48 | }
49 | result.remove(0);
50 |
51 | if (result.size() > 1000) {
52 | throw new DictionaryTooBigException();
53 | }
54 | return result;
55 | }
56 |
57 | private Word createDictionaryWord(Row row) {
58 | Iterator cellIterator = row.iterator();
59 |
60 | List line = new ArrayList<>();
61 | while (cellIterator.hasNext()) {
62 | line.add(cellIterator.next().getStringCellValue());
63 | }
64 |
65 | String key = line.get(0);
66 | line.remove(0);
67 |
68 | return new Word(key, new HashSet<>(line));
69 | }
70 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/api/dictionaries/DictionaryExcelService.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.api.dictionaries;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.RequiredArgsConstructor;
5 | import lombok.experimental.FieldDefaults;
6 | import org.apache.poi.ss.usermodel.Cell;
7 | import org.apache.poi.ss.usermodel.Row;
8 | import org.apache.poi.xssf.usermodel.XSSFSheet;
9 | import org.apache.poi.xssf.usermodel.XSSFWorkbook;
10 | import org.springframework.core.io.ByteArrayResource;
11 | import org.springframework.stereotype.Component;
12 | import ru.taksebe.telegram.writeRead.constants.resources.DictionaryResourcePathEnum;
13 | import ru.taksebe.telegram.writeRead.exceptions.UserDictionaryNotFoundException;
14 | import ru.taksebe.telegram.writeRead.model.Dictionary;
15 | import ru.taksebe.telegram.writeRead.model.Word;
16 | import ru.taksebe.telegram.writeRead.utils.FileUtils;
17 | import ru.taksebe.telegram.writeRead.utils.ResourceLoader;
18 |
19 | import java.io.IOException;
20 | import java.util.*;
21 | import java.util.stream.Collectors;
22 |
23 | @Component
24 | @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
25 | @RequiredArgsConstructor
26 | public class DictionaryExcelService {
27 | DictionaryRepository repository;
28 | WordService wordService;
29 | ResourceLoader resourceLoader;
30 |
31 | public boolean isUserDictionaryExist(String id) {
32 | return repository.existsById(id);
33 | }
34 |
35 | public ByteArrayResource getAllDefaultDictionariesWorkbook() throws IOException {
36 | List defaultDictionaryList = Arrays.stream(DictionaryResourcePathEnum.values())
37 | .map(resourcePath -> repository.findById(resourcePath.name()).orElseThrow(UserDictionaryNotFoundException::new))
38 | .collect(Collectors.toList());
39 | return createWorkbookByteArray(defaultDictionaryList, "All grades");
40 | }
41 |
42 | public ByteArrayResource getDictionaryWorkbook(String id) throws IOException {
43 | Dictionary dictionary = repository.findById(id).orElseThrow(UserDictionaryNotFoundException::new);
44 | return createWorkbookByteArray(Collections.singletonList(dictionary), getFileName(id));
45 | }
46 |
47 | private ByteArrayResource createWorkbookByteArray(List dictionaryList, String fileName) throws IOException {
48 | XSSFWorkbook workbook = createWorkbook(dictionaryList);
49 | return FileUtils.createOfficeDocumentResource(workbook, fileName, ".xlsx");
50 | }
51 |
52 | private XSSFWorkbook createWorkbook(List dictionaryList) throws IOException {
53 | XSSFWorkbook workbook = resourceLoader.loadTemplateWorkbook();
54 | if (dictionaryList.isEmpty()) {
55 | return workbook;
56 | }
57 |
58 | List wordList = wordService.getDictionariesWordList(dictionaryList);
59 | wordList.sort(Comparator.comparing(Word::getWord, String::compareToIgnoreCase));
60 | XSSFSheet sheet = workbook.getSheetAt(0);
61 | writeDictionary(sheet, wordList);
62 |
63 | return workbook;
64 | }
65 |
66 | private void writeDictionary(XSSFSheet sheet, List wordList) {
67 | int rowNumber = 1;
68 | for (Word word : wordList) {
69 | Row row = sheet.createRow(rowNumber++);
70 | int cellNum = 0;
71 | List dictionaryWordList = new ArrayList<>(word.getMistakes());
72 | dictionaryWordList.add(0, word.getWord());
73 | for (String value : dictionaryWordList) {
74 | Cell cell = row.createCell(cellNum++);
75 | cell.setCellValue(value);
76 | }
77 | }
78 | }
79 |
80 | private String getFileName(String id) {
81 | List defaultDictionaryNames = Arrays.stream(DictionaryResourcePathEnum.values())
82 | .filter(dictionaryResourcePathEnum -> dictionaryResourcePathEnum.name().equals(id))
83 | .map(DictionaryResourcePathEnum::getFileName)
84 | .collect(Collectors.toList());
85 | return defaultDictionaryNames.isEmpty() ? "Personal dictionary" : defaultDictionaryNames.get(0);
86 | }
87 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/api/dictionaries/DictionaryRepository.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.api.dictionaries;
2 |
3 | import org.springframework.data.repository.CrudRepository;
4 | import org.springframework.stereotype.Repository;
5 | import ru.taksebe.telegram.writeRead.model.Dictionary;
6 |
7 | @Repository
8 | public interface DictionaryRepository extends CrudRepository {
9 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/api/dictionaries/DictionaryResourceFileService.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.api.dictionaries;
2 |
3 | import lombok.RequiredArgsConstructor;
4 | import org.springframework.core.io.ByteArrayResource;
5 | import org.springframework.stereotype.Component;
6 | import ru.taksebe.telegram.writeRead.utils.FileUtils;
7 | import ru.taksebe.telegram.writeRead.utils.ResourceLoader;
8 |
9 | import java.io.IOException;
10 |
11 | @Component
12 | @RequiredArgsConstructor
13 | public class DictionaryResourceFileService {
14 | private final ResourceLoader resourceLoader;
15 |
16 | public ByteArrayResource getTemplateWorkbook() throws IOException {
17 | return FileUtils.createOfficeDocumentResource(
18 | resourceLoader.loadTemplateWorkbook(),
19 | "Template",
20 | "xlsx");
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/api/dictionaries/WordService.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.api.dictionaries;
2 |
3 | import org.springframework.stereotype.Component;
4 | import ru.taksebe.telegram.writeRead.model.Dictionary;
5 | import ru.taksebe.telegram.writeRead.model.Word;
6 |
7 | import java.util.ArrayList;
8 | import java.util.HashSet;
9 | import java.util.List;
10 | import java.util.Set;
11 |
12 | @Component
13 | public class WordService {
14 |
15 | public List getDictionariesWordList(List dictionaryList) {
16 | Set allWordSet = new HashSet<>();
17 | for (Dictionary dictionary : dictionaryList) {
18 | allWordSet.addAll(dictionary.getWordList());
19 | }
20 | return new ArrayList<>(allWordSet);
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/api/tasks/TaskService.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.api.tasks;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.RequiredArgsConstructor;
5 | import lombok.experimental.FieldDefaults;
6 | import org.apache.poi.xwpf.usermodel.XWPFDocument;
7 | import org.apache.poi.xwpf.usermodel.XWPFParagraph;
8 | import org.apache.poi.xwpf.usermodel.XWPFRun;
9 | import org.springframework.core.io.ByteArrayResource;
10 | import org.springframework.stereotype.Component;
11 | import ru.taksebe.telegram.writeRead.api.dictionaries.DictionaryRepository;
12 | import ru.taksebe.telegram.writeRead.api.dictionaries.WordService;
13 | import ru.taksebe.telegram.writeRead.constants.resources.DictionaryResourcePathEnum;
14 | import ru.taksebe.telegram.writeRead.exceptions.UserDictionaryNotFoundException;
15 | import ru.taksebe.telegram.writeRead.model.Dictionary;
16 | import ru.taksebe.telegram.writeRead.model.Word;
17 | import ru.taksebe.telegram.writeRead.utils.FileUtils;
18 | import ru.taksebe.telegram.writeRead.utils.ResourceLoader;
19 |
20 | import java.io.IOException;
21 | import java.text.MessageFormat;
22 | import java.util.ArrayList;
23 | import java.util.Arrays;
24 | import java.util.Collections;
25 | import java.util.List;
26 | import java.util.stream.Collectors;
27 |
28 | @Component
29 | @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
30 | @RequiredArgsConstructor
31 | public class TaskService {
32 | DictionaryRepository repository;
33 | WordService wordService;
34 | ResourceLoader resourceLoader;
35 |
36 | public ByteArrayResource getAllDefaultDictionariesTasksDocument() throws IOException {
37 | List defaultDictionaryList = Arrays.stream(DictionaryResourcePathEnum.values())
38 | .map(resourcePath -> repository.findById(resourcePath.name()).orElseThrow(UserDictionaryNotFoundException::new))
39 | .collect(Collectors.toList());
40 | return createDocumentByteArray(wordService.getDictionariesWordList(defaultDictionaryList), "Tasks (all grades)");
41 | }
42 |
43 | public ByteArrayResource getTasksDocument(String dictionaryId, String fileName) throws IOException {
44 | Dictionary dictionary = repository.findById(dictionaryId).orElseThrow(UserDictionaryNotFoundException::new);
45 | return createDocumentByteArray(dictionary.getWordList(), MessageFormat.format("Tasks ({0})", fileName));
46 | }
47 |
48 | private ByteArrayResource createDocumentByteArray(List wordList, String fileName) throws IOException {
49 | XWPFDocument document = resourceLoader.loadTemplateDocument();
50 | setTasksToDocument(document, wordList);
51 | return FileUtils.createOfficeDocumentResource(document, fileName, ".docx");
52 | }
53 |
54 | private void setTasksToDocument(XWPFDocument document, List wordList) {
55 | Collections.shuffle(wordList);
56 |
57 | List paragraphs = document.getParagraphs();
58 | int i = 0;
59 | for (Word word : wordList) {
60 | XWPFParagraph paragraph = paragraphs.get(i);
61 | setValue(paragraph.createRun(), String.join(", ", getVariants(word)));
62 | i = i + 2;
63 | }
64 |
65 | //адовый костыль, связанный с неработоспособностью метода setKeepNext(boolean keepNext) класса XWPFParagraph
66 | //(соответсвует функции Microsoft Word "Не отрывать от следующего") - признак устанавливается, но в Word
67 | //не срабатывает. Пришлось сделать вручную шаблон и удалять из него лишние строки. Принимаю советы
68 | while (document.getParagraphs().size() > i) {
69 | document.removeBodyElement(document.getPosOfParagraph(document.getLastParagraph()));
70 | }
71 | }
72 |
73 | private List getVariants(Word word) {
74 | List mistakes = new ArrayList<>(word.getMistakes());
75 | mistakes.add(word.getWord());
76 | Collections.shuffle(mistakes);
77 | return mistakes;
78 | }
79 |
80 | private void setValue(XWPFRun run, String text) {
81 | run.setFontSize(14);
82 | run.setFontFamily("Times New Roman");
83 | run.setText(text);
84 | }
85 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/config/RedisConfiguration.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.config;
2 |
3 | import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.data.redis.connection.RedisConnectionFactory;
7 | import org.springframework.data.redis.core.RedisTemplate;
8 | import org.springframework.data.redis.core.convert.RedisCustomConversions;
9 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
10 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
11 | import org.springframework.data.redis.serializer.StringRedisSerializer;
12 | import ru.taksebe.telegram.writeRead.converters.BytesToWordConverter;
13 | import ru.taksebe.telegram.writeRead.converters.WordToBytesConverter;
14 |
15 | import java.util.Arrays;
16 |
17 | @Configuration
18 | @EnableRedisRepositories
19 | public class RedisConfiguration {
20 |
21 | @Bean
22 | public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {
23 | return clientConfigurationBuilder -> {
24 | if (clientConfigurationBuilder.build().isUseSsl()) {
25 | clientConfigurationBuilder.useSsl().disablePeerVerification();
26 | }
27 | };
28 | }
29 |
30 | @Bean
31 | public RedisTemplate, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
32 | RedisTemplate template = new RedisTemplate<>();
33 | template.setConnectionFactory(redisConnectionFactory);
34 | template.setKeySerializer(new StringRedisSerializer());
35 | template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
36 | return template;
37 | }
38 |
39 | @Bean
40 | public RedisCustomConversions redisCustomConversions() {
41 | return new RedisCustomConversions(Arrays.asList(new WordToBytesConverter(),new BytesToWordConverter()));
42 | }
43 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/config/SpringConfig.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.config;
2 |
3 | import lombok.AllArgsConstructor;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook;
7 | import ru.taksebe.telegram.writeRead.telegram.WriteReadBot;
8 | import ru.taksebe.telegram.writeRead.telegram.handlers.CallbackQueryHandler;
9 | import ru.taksebe.telegram.writeRead.telegram.handlers.MessageHandler;
10 |
11 | @Configuration
12 | @AllArgsConstructor
13 | public class SpringConfig {
14 | private final TelegramConfig telegramConfig;
15 |
16 | @Bean
17 | public SetWebhook setWebhookInstance() {
18 | return SetWebhook.builder().url(telegramConfig.getWebhookPath()).build();
19 | }
20 |
21 | @Bean
22 | public WriteReadBot springWebhookBot(SetWebhook setWebhook,
23 | MessageHandler messageHandler,
24 | CallbackQueryHandler callbackQueryHandler) {
25 | WriteReadBot bot = new WriteReadBot(setWebhook, messageHandler, callbackQueryHandler);
26 |
27 | bot.setBotPath(telegramConfig.getWebhookPath());
28 | bot.setBotUsername(telegramConfig.getBotName());
29 | bot.setBotToken(telegramConfig.getBotToken());
30 |
31 | return bot;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/config/TelegramConfig.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.config;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.Getter;
5 | import lombok.experimental.FieldDefaults;
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.stereotype.Component;
8 |
9 | @Component
10 | @Getter
11 | @FieldDefaults(level = AccessLevel.PRIVATE)
12 | public class TelegramConfig {
13 | @Value("${telegram.webhook-path}")
14 | String webhookPath;
15 | @Value("${telegram.bot-name}")
16 | String botName;
17 | @Value("${telegram.bot-token}")
18 | String botToken;
19 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/constants/bot/BotMessageEnum.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.constants.bot;
2 |
3 | /**
4 | * Текстовые сообщения, посылаемые ботом
5 | */
6 | public enum BotMessageEnum {
7 | //ответы на команды с клавиатуры
8 | HELP_MESSAGE("\uD83D\uDC4B Привет, я бот ПишиЧитай, и я помогу Вам создать задания на правописание словарных " +
9 | "слов для Ваших детей\n\n" +
10 | "❗ *Что Вы можете сделать:*\n" +
11 | "✅ скачать Word-файл с заданиями, составленными из списков словарных слов за 1-4 классы (по отдельности " +
12 | "или сразу всех вместе)\n" +
13 | "✅ изменить любой из этих словарей и загрузить его как свой - скачайте нужный словарь в " +
14 | "Excel-файл, внесите в него изменения и отправьте мне\n" +
15 | "✅ создать свой словарь с нуля - скачайте шаблон, заполните его и отправьте мне " +
16 | "(максимальный размер - 1 000 слов)\n\n" +
17 | "В мои словари уже добавлены все слова из программ \"Школа России\" и \"Начальная школа XXI века\", " +
18 | "но если в списке Вашего ребёнка есть другие слова, присылайте их моему создателю @taksebe\n\n" +
19 | "Обратите внимание, что некоторые слова попадают в словари сразу нескольких классов - это следствие " +
20 | "использования списков из разных программ и дополнений от пользователей. Это не страшно, " +
21 | "ведь повторение - мать учения\n\n" +
22 | "Удачи!\n\n" +
23 | "Воспользуйтесь клавиатурой, чтобы начать работу\uD83D\uDC47"),
24 | CHOOSE_DICTIONARY_MESSAGE("Выберите словарь\uD83D\uDC47 "),
25 | UPLOAD_DICTIONARY_MESSAGE("Добавьте файл, соответствующий шаблону. Вы можете сделать это в любой момент"),
26 | NON_COMMAND_MESSAGE("Пожалуйста, воспользуйтесь клавиатурой\uD83D\uDC47"),
27 |
28 | //результаты загрузки словаря
29 | SUCCESS_UPLOAD_MESSAGE("\uD83D\uDC4D Словарь успешно загружен"),
30 | EXCEPTION_TELEGRAM_API_MESSAGE("Ошибка при попытку получить файл из API Telegram"),
31 | EXCEPTION_TOO_LARGE_DICTIONARY_MESSAGE("В словаре больше 1 000 слов. Едва ли такой большой набор словарных " +
32 | "слов действительно нужен, ведь я работаю для обучения детей"),
33 | EXCEPTION_BAD_FILE_MESSAGE("Файл не может быть обработан. Вы шлёте мне что-то не то, балуетесь, наверное"),
34 |
35 | //ошибки при обработке callback-ов
36 | EXCEPTION_BAD_BUTTON_NAME_MESSAGE("Неверное значение кнопки. Крайне странно. Попробуйте позже"),
37 | EXCEPTION_DICTIONARY_NOT_FOUND_MESSAGE("Словарь не найден"),
38 | EXCEPTION_DICTIONARY_WTF_MESSAGE("Нежиданная ошибка при попытке получить словарь. Сам в шоке"),
39 | EXCEPTION_TASKS_WTF_MESSAGE("Нежиданная ошибка при попытке получить задания. Сам в шоке"),
40 | EXCEPTION_TEMPLATE_WTF_MESSAGE("Нежиданная ошибка при попытке получить шаблон. Сам в шоке"),
41 |
42 | //прочие ошибки
43 | EXCEPTION_ILLEGAL_MESSAGE("Нет, к такому меня не готовили! Я работаю или с текстом, или с файлом"),
44 | EXCEPTION_WHAT_THE_FUCK("Что-то пошло не так. Обратитесь к программисту");
45 |
46 | private final String message;
47 |
48 | BotMessageEnum(String message) {
49 | this.message = message;
50 | }
51 |
52 | public String getMessage() {
53 | return message;
54 | }
55 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/constants/bot/ButtonNameEnum.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.constants.bot;
2 |
3 | /**
4 | * Названия кнопок основной клавиатуры
5 | */
6 | public enum ButtonNameEnum {
7 | GET_TASKS_BUTTON("Создать файл с заданиями"),
8 | GET_DICTIONARY_BUTTON("Скачать словарь"),
9 | UPLOAD_DICTIONARY_BUTTON("Загрузить мой словарь"),
10 | HELP_BUTTON("Помощь");
11 |
12 | private final String buttonName;
13 |
14 | ButtonNameEnum(String buttonName) {
15 | this.buttonName = buttonName;
16 | }
17 |
18 | public String getButtonName() {
19 | return buttonName;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/constants/bot/CallbackDataPartsEnum.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.constants.bot;
2 |
3 | /**
4 | * Элементы ответов кнопок инлайн-клавиатур
5 | */
6 | public enum CallbackDataPartsEnum {
7 | TASK_,
8 | DICTIONARY_,
9 | USER_DICTIONARY,
10 | TEMPLATE,
11 | ALL_GRADES
12 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/constants/resources/DictionaryResourcePathEnum.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.constants.resources;
2 |
3 | import lombok.Getter;
4 |
5 | /**
6 | * Расположение файлов словарей по умолчанию в resources
7 | */
8 | public enum DictionaryResourcePathEnum {
9 | CLASS_1("dictionaries/разделитель1 gradeразделитель.xlsx", "1 класс"),
10 | CLASS_2("dictionaries/разделитель2 gradeразделитель.xlsx", "2 класс"),
11 | CLASS_3("dictionaries/разделитель3 gradeразделитель.xlsx", "3 класс"),
12 | CLASS_4("dictionaries/разделитель4 gradeразделитель.xlsx", "4 класс");
13 |
14 | private final String filePath;
15 | @Getter
16 | private final String buttonName;
17 |
18 | DictionaryResourcePathEnum(String filePath, String buttonName) {
19 | this.filePath = filePath;
20 | this.buttonName = buttonName;
21 | }
22 |
23 | public String getFilePath() {
24 | return filePath.replace("разделитель", "");
25 | }
26 |
27 | public String getFileName() {
28 | return filePath.split("разделитель")[1];
29 | }
30 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/constants/resources/TemplateResourcePathsEnum.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.constants.resources;
2 |
3 | /**
4 | * Расположение файлов шаблонов в resources
5 | */
6 | public enum TemplateResourcePathsEnum {
7 | TEMPLATE_TASKS("templates/Template.docx"),
8 | TEMPLATE_DICTIONARY("templates/Template.xlsx");
9 |
10 | private final String filePath;
11 |
12 | TemplateResourcePathsEnum(String filePath) {
13 | this.filePath = filePath;
14 | }
15 |
16 | public String getFilePath() {
17 | return filePath;
18 | }
19 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/converters/BytesToWordConverter.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.converters;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import org.springframework.core.convert.converter.Converter;
5 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
6 | import ru.taksebe.telegram.writeRead.model.Word;
7 |
8 | import javax.annotation.Nullable;
9 |
10 | public class BytesToWordConverter implements Converter {
11 | private final Jackson2JsonRedisSerializer serializer;
12 |
13 | public BytesToWordConverter() {
14 | serializer = new Jackson2JsonRedisSerializer<>(Word.class);
15 | serializer.setObjectMapper(new ObjectMapper());
16 | }
17 |
18 | @Override
19 | public Word convert(@Nullable byte[] value) {
20 | return serializer.deserialize(value);
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/converters/WordToBytesConverter.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.converters;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import org.springframework.core.convert.converter.Converter;
5 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
6 | import ru.taksebe.telegram.writeRead.model.Word;
7 |
8 | import javax.annotation.Nullable;
9 |
10 | public class WordToBytesConverter implements Converter {
11 | private final Jackson2JsonRedisSerializer serializer;
12 |
13 | public WordToBytesConverter() {
14 | serializer = new Jackson2JsonRedisSerializer<>(Word.class);
15 | serializer.setObjectMapper(new ObjectMapper());
16 | }
17 |
18 | @Override
19 | public byte[] convert(@Nullable Word value) {
20 | return serializer.serialize(value);
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/exceptions/DictionaryTooBigException.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.exceptions;
2 |
3 | public class DictionaryTooBigException extends RuntimeException {
4 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/exceptions/TelegramFileNotFoundException.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.exceptions;
2 |
3 | public class TelegramFileNotFoundException extends RuntimeException {
4 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/exceptions/TelegramFileUploadException.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.exceptions;
2 |
3 | public class TelegramFileUploadException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/exceptions/UserDictionaryNotFoundException.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.exceptions;
2 |
3 | public class UserDictionaryNotFoundException extends RuntimeException {
4 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/initialization/InitializingBeanImpl.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.initialization;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.RequiredArgsConstructor;
5 | import lombok.experimental.FieldDefaults;
6 | import org.apache.poi.xssf.usermodel.XSSFWorkbook;
7 | import org.springframework.beans.factory.InitializingBean;
8 | import org.springframework.stereotype.Component;
9 | import ru.taksebe.telegram.writeRead.api.dictionaries.DictionaryAdditionService;
10 | import ru.taksebe.telegram.writeRead.utils.ResourceLoader;
11 |
12 | import java.util.Map;
13 |
14 | /**
15 | * Загрузчик в БД словарей по умолчанию при инициализации приложения
16 | */
17 | @Component
18 | @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
19 | @RequiredArgsConstructor
20 | public class InitializingBeanImpl implements InitializingBean {
21 | DictionaryAdditionService dictionaryAdditionService;
22 | ResourceLoader resourceLoader;
23 |
24 | @Override
25 | public void afterPropertiesSet() {
26 | Map defaultDictionaryMap = resourceLoader.getDefaultDictionaries();
27 | for (Map.Entry pair : defaultDictionaryMap.entrySet()) {
28 | dictionaryAdditionService.addDefaultDictionary(pair.getKey(), pair.getValue());
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/model/Dictionary.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.model;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.Builder;
5 | import lombok.Getter;
6 | import lombok.experimental.FieldDefaults;
7 | import org.springframework.data.annotation.Id;
8 | import org.springframework.data.redis.core.RedisHash;
9 |
10 | import java.util.List;
11 |
12 | @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
13 | @Getter
14 | @RedisHash("dictionary")
15 | @Builder
16 | public class Dictionary {
17 |
18 | /**
19 | * Идентификатор - для пользовательских словарей это id чата с пользователем в Telegram, для предзагруженных -
20 | * элементы перечисления путей до словарей по умочанию (пакет constants)
21 | */
22 | @Id
23 | String id;
24 |
25 | /**
26 | * Список словарных слов
27 | */
28 | List wordList;
29 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/model/Word.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.model;
2 |
3 | import lombok.*;
4 | import lombok.experimental.FieldDefaults;
5 | import org.springframework.data.annotation.Id;
6 | import org.springframework.data.redis.core.RedisHash;
7 |
8 | import java.util.Set;
9 |
10 | @FieldDefaults(level = AccessLevel.PRIVATE)
11 | @Getter
12 | @Setter
13 | @AllArgsConstructor
14 | @NoArgsConstructor
15 | @RedisHash("word")
16 | public class Word {
17 |
18 | /**
19 | * Словарное слово
20 | */
21 | @Id
22 | String word;
23 |
24 | /**
25 | * Ошибочные варианты написания
26 | */
27 | Set mistakes;
28 |
29 | @Override
30 | public boolean equals(Object obj) {
31 | if (obj == null || obj.getClass() != this.getClass()) {
32 | return false;
33 | }
34 | Word word = (Word) obj;
35 | return this.word.equals(word.getWord());
36 | }
37 |
38 | @Override
39 | public int hashCode() {
40 | return this.word.hashCode();
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/telegram/TelegramApiClient.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.telegram;
2 |
3 | import org.springframework.beans.factory.annotation.Value;
4 | import org.springframework.core.ParameterizedTypeReference;
5 | import org.springframework.core.io.ByteArrayResource;
6 | import org.springframework.http.*;
7 | import org.springframework.stereotype.Service;
8 | import org.springframework.util.LinkedMultiValueMap;
9 | import org.springframework.util.StreamUtils;
10 | import org.springframework.web.client.RestTemplate;
11 | import org.telegram.telegrambots.meta.api.objects.ApiResponse;
12 | import ru.taksebe.telegram.writeRead.exceptions.TelegramFileNotFoundException;
13 | import ru.taksebe.telegram.writeRead.exceptions.TelegramFileUploadException;
14 |
15 | import java.io.File;
16 | import java.io.FileOutputStream;
17 | import java.text.MessageFormat;
18 | import java.util.Objects;
19 |
20 | @Service
21 | public class TelegramApiClient {
22 | private final String URL;
23 | private final String botToken;
24 |
25 | private final RestTemplate restTemplate;
26 |
27 | public TelegramApiClient(@Value("${telegram.api-url}") String URL,
28 | @Value("${telegram.bot-token}") String botToken) {
29 | this.URL = URL;
30 | this.botToken = botToken;
31 | this.restTemplate = new RestTemplate();
32 | }
33 |
34 | public void uploadFile(String chatId, ByteArrayResource value) {
35 | LinkedMultiValueMap map = new LinkedMultiValueMap<>();
36 | map.add("document", value);
37 |
38 | HttpHeaders headers = new HttpHeaders();
39 | headers.setContentType(MediaType.MULTIPART_FORM_DATA);
40 |
41 | HttpEntity> requestEntity = new HttpEntity<>(map, headers);
42 |
43 | try {
44 | restTemplate.exchange(
45 | MessageFormat.format("{0}bot{1}/sendDocument?chat_id={2}", URL, botToken, chatId),
46 | HttpMethod.POST,
47 | requestEntity,
48 | String.class);
49 | } catch (Exception e) {
50 | throw new TelegramFileUploadException();
51 | }
52 | }
53 |
54 | public File getDocumentFile(String fileId) {
55 | try {
56 | return restTemplate.execute(
57 | Objects.requireNonNull(getDocumentTelegramFileUrl(fileId)),
58 | HttpMethod.GET,
59 | null,
60 | clientHttpResponse -> {
61 | File ret = File.createTempFile("download", "tmp");
62 | StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(ret));
63 | return ret;
64 | });
65 | } catch (Exception e) {
66 | throw new TelegramFileNotFoundException();
67 | }
68 | }
69 |
70 | private String getDocumentTelegramFileUrl(String fileId) {
71 | try {
72 | ResponseEntity> response = restTemplate.exchange(
73 | MessageFormat.format("{0}bot{1}/getFile?file_id={2}", URL, botToken, fileId),
74 | HttpMethod.GET,
75 | null,
76 | new ParameterizedTypeReference>() {
77 | }
78 | );
79 | return Objects.requireNonNull(response.getBody()).getResult().getFileUrl(this.botToken);
80 | } catch (Exception e) {
81 | throw new TelegramFileNotFoundException();
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/telegram/WebhookController.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.telegram;
2 |
3 | import lombok.AllArgsConstructor;
4 | import org.springframework.web.bind.annotation.PostMapping;
5 | import org.springframework.web.bind.annotation.RequestBody;
6 | import org.springframework.web.bind.annotation.RestController;
7 | import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
8 | import org.telegram.telegrambots.meta.api.objects.Update;
9 |
10 | @RestController
11 | @AllArgsConstructor
12 | public class WebhookController {
13 | private final WriteReadBot writeReadBot;
14 |
15 | @PostMapping("/")
16 | public BotApiMethod> onUpdateReceived(@RequestBody Update update) {
17 | return writeReadBot.onWebhookUpdateReceived(update);
18 | }
19 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/telegram/WriteReadBot.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.telegram;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 | import lombok.experimental.FieldDefaults;
7 | import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
8 | import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
9 | import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook;
10 | import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
11 | import org.telegram.telegrambots.meta.api.objects.Message;
12 | import org.telegram.telegrambots.meta.api.objects.Update;
13 | import org.telegram.telegrambots.starter.SpringWebhookBot;
14 | import ru.taksebe.telegram.writeRead.constants.bot.BotMessageEnum;
15 | import ru.taksebe.telegram.writeRead.telegram.handlers.CallbackQueryHandler;
16 | import ru.taksebe.telegram.writeRead.telegram.handlers.MessageHandler;
17 |
18 | import java.io.IOException;
19 |
20 | @Getter
21 | @Setter
22 | @FieldDefaults(level = AccessLevel.PRIVATE)
23 | public class WriteReadBot extends SpringWebhookBot {
24 | String botPath;
25 | String botUsername;
26 | String botToken;
27 |
28 | MessageHandler messageHandler;
29 | CallbackQueryHandler callbackQueryHandler;
30 |
31 | public WriteReadBot(SetWebhook setWebhook, MessageHandler messageHandler,CallbackQueryHandler callbackQueryHandler) {
32 | super(setWebhook);
33 | this.messageHandler = messageHandler;
34 | this.callbackQueryHandler = callbackQueryHandler;
35 | }
36 |
37 | @Override
38 | public BotApiMethod> onWebhookUpdateReceived(Update update) {
39 | try {
40 | return handleUpdate(update);
41 | } catch (IllegalArgumentException e) {
42 | return new SendMessage(update.getMessage().getChatId().toString(),
43 | BotMessageEnum.EXCEPTION_ILLEGAL_MESSAGE.getMessage());
44 | } catch (Exception e) {
45 | return new SendMessage(update.getMessage().getChatId().toString(),
46 | BotMessageEnum.EXCEPTION_WHAT_THE_FUCK.getMessage());
47 | }
48 | }
49 |
50 | private BotApiMethod> handleUpdate(Update update) throws IOException {
51 | if (update.hasCallbackQuery()) {
52 | CallbackQuery callbackQuery = update.getCallbackQuery();
53 | return callbackQueryHandler.processCallbackQuery(callbackQuery);
54 | } else {
55 | Message message = update.getMessage();
56 | if (message != null) {
57 | return messageHandler.answerMessage(update.getMessage());
58 | }
59 | }
60 | return null;
61 | }
62 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/telegram/handlers/CallbackQueryHandler.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.telegram.handlers;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.RequiredArgsConstructor;
5 | import lombok.experimental.FieldDefaults;
6 | import org.springframework.stereotype.Component;
7 | import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
8 | import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
9 | import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
10 | import ru.taksebe.telegram.writeRead.api.dictionaries.DictionaryExcelService;
11 | import ru.taksebe.telegram.writeRead.api.dictionaries.DictionaryResourceFileService;
12 | import ru.taksebe.telegram.writeRead.api.tasks.TaskService;
13 | import ru.taksebe.telegram.writeRead.constants.bot.BotMessageEnum;
14 | import ru.taksebe.telegram.writeRead.constants.bot.CallbackDataPartsEnum;
15 | import ru.taksebe.telegram.writeRead.constants.resources.DictionaryResourcePathEnum;
16 | import ru.taksebe.telegram.writeRead.exceptions.UserDictionaryNotFoundException;
17 | import ru.taksebe.telegram.writeRead.telegram.TelegramApiClient;
18 |
19 | import java.io.IOException;
20 |
21 | @Component
22 | @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
23 | @RequiredArgsConstructor
24 | public class CallbackQueryHandler {
25 | TelegramApiClient telegramApiClient;
26 | TaskService taskService;
27 | DictionaryExcelService dictionaryExcelService;
28 | DictionaryResourceFileService dictionaryResourceFileService;
29 |
30 | public BotApiMethod> processCallbackQuery(CallbackQuery buttonQuery) throws IOException {
31 | final String chatId = buttonQuery.getMessage().getChatId().toString();
32 |
33 | String data = buttonQuery.getData();
34 |
35 | if (data.equals(CallbackDataPartsEnum.TASK_.name() + CallbackDataPartsEnum.USER_DICTIONARY.name())) {
36 | return getDictionaryTasks(chatId, chatId, "personal dictionary");
37 | } else if (data.equals(CallbackDataPartsEnum.TASK_.name() + CallbackDataPartsEnum.ALL_GRADES.name())) {
38 | return getAllDictionaryTasks(chatId);
39 | } else if (data.equals(CallbackDataPartsEnum.DICTIONARY_.name() + CallbackDataPartsEnum.USER_DICTIONARY.name())) {
40 | return getDictionary(chatId, chatId);
41 | } else if (data.equals(CallbackDataPartsEnum.DICTIONARY_.name() + CallbackDataPartsEnum.ALL_GRADES.name())) {
42 | return getAllDefaultDictionaries(chatId);
43 | }else if (data.equals(CallbackDataPartsEnum.DICTIONARY_.name() + CallbackDataPartsEnum.TEMPLATE.name())) {
44 | return getTemplate(chatId);
45 | } else {
46 | return handleDefaultDictionary(chatId, data);
47 | }
48 | }
49 |
50 | private SendMessage handleDefaultDictionary(String chatId, String data) throws IOException {
51 | if (data.startsWith(CallbackDataPartsEnum.TASK_.name())) {
52 | DictionaryResourcePathEnum resourcePath = DictionaryResourcePathEnum.valueOf(
53 | data.substring(CallbackDataPartsEnum.TASK_.name().length())
54 | );
55 | return getDictionaryTasks(chatId, resourcePath.name(), resourcePath.getFileName());
56 | } else if (data.startsWith(CallbackDataPartsEnum.DICTIONARY_.name())) {
57 | return getDictionary(chatId, data.substring(CallbackDataPartsEnum.DICTIONARY_.name().length()));
58 | } else {
59 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_BAD_BUTTON_NAME_MESSAGE.getMessage());
60 | }
61 | }
62 |
63 | private SendMessage getDictionaryTasks(String chatId, String dictionaryId, String fileName) throws IOException {
64 | try {
65 | telegramApiClient.uploadFile(chatId, taskService.getTasksDocument(dictionaryId, fileName));
66 | } catch (Exception e) {
67 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_TASKS_WTF_MESSAGE.getMessage());
68 | }
69 | return null;
70 | }
71 |
72 | private SendMessage getAllDictionaryTasks(String chatId) throws IOException {
73 | try {
74 | telegramApiClient.uploadFile(chatId, taskService.getAllDefaultDictionariesTasksDocument());
75 | } catch (Exception e) {
76 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_TASKS_WTF_MESSAGE.getMessage());
77 | }
78 | return null;
79 | }
80 |
81 | private SendMessage getDictionary(String chatId, String dictionaryId) {
82 | try {
83 | telegramApiClient.uploadFile(chatId, dictionaryExcelService.getDictionaryWorkbook(dictionaryId));
84 | } catch (UserDictionaryNotFoundException e) {
85 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_DICTIONARY_NOT_FOUND_MESSAGE.getMessage());
86 | } catch (Exception e) {
87 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_DICTIONARY_WTF_MESSAGE.getMessage());
88 | }
89 | return null;
90 | }
91 |
92 | private SendMessage getAllDefaultDictionaries(String chatId) {
93 | try {
94 | telegramApiClient.uploadFile(chatId, dictionaryExcelService.getAllDefaultDictionariesWorkbook());
95 | } catch (UserDictionaryNotFoundException e) {
96 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_DICTIONARY_NOT_FOUND_MESSAGE.getMessage());
97 | } catch (Exception e) {
98 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_DICTIONARY_WTF_MESSAGE.getMessage());
99 | }
100 | return null;
101 | }
102 |
103 | private SendMessage getTemplate(String chatId) {
104 | try {
105 | telegramApiClient.uploadFile(chatId, dictionaryResourceFileService.getTemplateWorkbook());
106 | } catch (Exception e) {
107 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_TEMPLATE_WTF_MESSAGE.getMessage());
108 | }
109 | return null;
110 | }
111 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/telegram/handlers/MessageHandler.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.telegram.handlers;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.RequiredArgsConstructor;
5 | import lombok.experimental.FieldDefaults;
6 | import org.springframework.stereotype.Component;
7 | import org.telegram.telegrambots.meta.api.methods.BotApiMethod;
8 | import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
9 | import org.telegram.telegrambots.meta.api.objects.Message;
10 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.ReplyKeyboardMarkup;
11 | import ru.taksebe.telegram.writeRead.api.dictionaries.DictionaryAdditionService;
12 | import ru.taksebe.telegram.writeRead.api.dictionaries.DictionaryExcelService;
13 | import ru.taksebe.telegram.writeRead.constants.bot.BotMessageEnum;
14 | import ru.taksebe.telegram.writeRead.constants.bot.ButtonNameEnum;
15 | import ru.taksebe.telegram.writeRead.constants.bot.CallbackDataPartsEnum;
16 | import ru.taksebe.telegram.writeRead.exceptions.DictionaryTooBigException;
17 | import ru.taksebe.telegram.writeRead.exceptions.TelegramFileNotFoundException;
18 | import ru.taksebe.telegram.writeRead.telegram.TelegramApiClient;
19 | import ru.taksebe.telegram.writeRead.telegram.keyboards.InlineKeyboardMaker;
20 | import ru.taksebe.telegram.writeRead.telegram.keyboards.ReplyKeyboardMaker;
21 |
22 | @Component
23 | @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
24 | @RequiredArgsConstructor
25 | public class MessageHandler {
26 | DictionaryAdditionService dictionaryAdditionService;
27 | DictionaryExcelService dictionaryExcelService;
28 |
29 | TelegramApiClient telegramApiClient;
30 | ReplyKeyboardMaker replyKeyboardMaker;
31 | InlineKeyboardMaker inlineKeyboardMaker;
32 |
33 | public BotApiMethod> answerMessage(Message message) {
34 | String chatId = message.getChatId().toString();
35 |
36 | if (message.hasDocument()) {
37 | return addUserDictionary(chatId, message.getDocument().getFileId());
38 | }
39 |
40 | String inputText = message.getText();
41 |
42 | if (inputText == null) {
43 | throw new IllegalArgumentException();
44 | } else if (inputText.equals("/start")) {
45 | return getStartMessage(chatId);
46 | } else if (inputText.equals(ButtonNameEnum.GET_TASKS_BUTTON.getButtonName())) {
47 | return getTasksMessage(chatId);
48 | } else if (inputText.equals(ButtonNameEnum.GET_DICTIONARY_BUTTON.getButtonName())) {
49 | return getDictionaryMessage(chatId);
50 | } else if (inputText.equals(ButtonNameEnum.UPLOAD_DICTIONARY_BUTTON.getButtonName())) {
51 | return new SendMessage(chatId, BotMessageEnum.UPLOAD_DICTIONARY_MESSAGE.getMessage());
52 | } else if (inputText.equals(ButtonNameEnum.HELP_BUTTON.getButtonName())) {
53 | SendMessage sendMessage = new SendMessage(chatId, BotMessageEnum.HELP_MESSAGE.getMessage());
54 | sendMessage.enableMarkdown(true);
55 | return sendMessage;
56 | } else {
57 | return new SendMessage(chatId, BotMessageEnum.NON_COMMAND_MESSAGE.getMessage());
58 | }
59 | }
60 |
61 | private SendMessage getStartMessage(String chatId) {
62 | SendMessage sendMessage = new SendMessage(chatId, BotMessageEnum.HELP_MESSAGE.getMessage());
63 | sendMessage.enableMarkdown(true);
64 | sendMessage.setReplyMarkup(replyKeyboardMaker.getMainMenuKeyboard());
65 | return sendMessage;
66 | }
67 |
68 | private SendMessage getTasksMessage(String chatId) {
69 | SendMessage sendMessage = new SendMessage(chatId, BotMessageEnum.CHOOSE_DICTIONARY_MESSAGE.getMessage());
70 | sendMessage.setReplyMarkup(inlineKeyboardMaker.getInlineMessageButtons(
71 | CallbackDataPartsEnum.TASK_.name(),
72 | dictionaryExcelService.isUserDictionaryExist(chatId)
73 | ));
74 | return sendMessage;
75 | }
76 |
77 | private SendMessage getDictionaryMessage(String chatId) {
78 | SendMessage sendMessage = new SendMessage(chatId, BotMessageEnum.CHOOSE_DICTIONARY_MESSAGE.getMessage());
79 | sendMessage.setReplyMarkup(inlineKeyboardMaker.getInlineMessageButtonsWithTemplate(
80 | CallbackDataPartsEnum.DICTIONARY_.name(),
81 | dictionaryExcelService.isUserDictionaryExist(chatId)
82 | ));
83 | return sendMessage;
84 | }
85 |
86 | private SendMessage addUserDictionary(String chatId, String fileId) {
87 | try {
88 | dictionaryAdditionService.addUserDictionary(chatId, telegramApiClient.getDocumentFile(fileId));
89 | return new SendMessage(chatId, BotMessageEnum.SUCCESS_UPLOAD_MESSAGE.getMessage());
90 | } catch (TelegramFileNotFoundException e) {
91 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_TELEGRAM_API_MESSAGE.getMessage());
92 | } catch (DictionaryTooBigException e) {
93 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_TOO_LARGE_DICTIONARY_MESSAGE.getMessage());
94 | } catch (Exception e) {
95 | return new SendMessage(chatId, BotMessageEnum.EXCEPTION_BAD_FILE_MESSAGE.getMessage());
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/telegram/keyboards/InlineKeyboardMaker.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.telegram.keyboards;
2 |
3 | import org.springframework.stereotype.Component;
4 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
5 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
6 | import ru.taksebe.telegram.writeRead.constants.bot.CallbackDataPartsEnum;
7 | import ru.taksebe.telegram.writeRead.constants.resources.DictionaryResourcePathEnum;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | /**
13 | * Клавиатуры, формируемые в ленте Telegram для получения файлов
14 | */
15 | @Component
16 | public class InlineKeyboardMaker {
17 |
18 | public InlineKeyboardMarkup getInlineMessageButtonsWithTemplate(String prefix, boolean isUserDictionaryNeed) {
19 | InlineKeyboardMarkup inlineKeyboardMarkup = getInlineMessageButtons(prefix, isUserDictionaryNeed);
20 | inlineKeyboardMarkup.getKeyboard().add(getButton(
21 | "Шаблон",
22 | prefix + CallbackDataPartsEnum.TEMPLATE.name()
23 | ));
24 | return inlineKeyboardMarkup;
25 | }
26 |
27 | public InlineKeyboardMarkup getInlineMessageButtons(String prefix, boolean isUserDictionaryNeed) {
28 | List> rowList = new ArrayList<>();
29 |
30 | for (DictionaryResourcePathEnum dictionary : DictionaryResourcePathEnum.values()) {
31 | rowList.add(getButton(
32 | dictionary.getButtonName(),
33 | prefix + dictionary.name()
34 | ));
35 | }
36 |
37 | if (!rowList.isEmpty()) {
38 | rowList.add(getButton(
39 | "Все классы",
40 | prefix + CallbackDataPartsEnum.ALL_GRADES.name()
41 | ));
42 | }
43 |
44 | if (isUserDictionaryNeed) {
45 | rowList.add(getButton(
46 | "Ваш словарь",
47 | prefix + CallbackDataPartsEnum.USER_DICTIONARY.name()
48 | ));
49 | }
50 |
51 | InlineKeyboardMarkup inlineKeyboardMarkup = new InlineKeyboardMarkup();
52 | inlineKeyboardMarkup.setKeyboard(rowList);
53 | return inlineKeyboardMarkup;
54 | }
55 |
56 | private List getButton(String buttonName, String buttonCallBackData) {
57 | InlineKeyboardButton button = new InlineKeyboardButton();
58 | button.setText(buttonName);
59 | button.setCallbackData(buttonCallBackData);
60 |
61 | List keyboardButtonsRow = new ArrayList<>();
62 | keyboardButtonsRow.add(button);
63 | return keyboardButtonsRow;
64 | }
65 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/telegram/keyboards/ReplyKeyboardMaker.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.telegram.keyboards;
2 |
3 | import org.springframework.stereotype.Component;
4 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.ReplyKeyboardMarkup;
5 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardButton;
6 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardRow;
7 | import ru.taksebe.telegram.writeRead.constants.bot.ButtonNameEnum;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | /**
13 | * Основная клавиатура, расположенная под строкой ввода текста в Telegram
14 | */
15 | @Component
16 | public class ReplyKeyboardMaker {
17 |
18 | public ReplyKeyboardMarkup getMainMenuKeyboard() {
19 | KeyboardRow row1 = new KeyboardRow();
20 | row1.add(new KeyboardButton(ButtonNameEnum.GET_TASKS_BUTTON.getButtonName()));
21 | row1.add(new KeyboardButton(ButtonNameEnum.GET_DICTIONARY_BUTTON.getButtonName()));
22 |
23 | KeyboardRow row2 = new KeyboardRow();
24 | row2.add(new KeyboardButton(ButtonNameEnum.UPLOAD_DICTIONARY_BUTTON.getButtonName()));
25 | row2.add(new KeyboardButton(ButtonNameEnum.HELP_BUTTON.getButtonName()));
26 |
27 | List keyboard = new ArrayList<>();
28 | keyboard.add(row1);
29 | keyboard.add(row2);
30 |
31 | final ReplyKeyboardMarkup replyKeyboardMarkup = new ReplyKeyboardMarkup();
32 | replyKeyboardMarkup.setKeyboard(keyboard);
33 | replyKeyboardMarkup.setSelective(true);
34 | replyKeyboardMarkup.setResizeKeyboard(true);
35 | replyKeyboardMarkup.setOneTimeKeyboard(false);
36 |
37 | return replyKeyboardMarkup;
38 | }
39 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/utils/FileUtils.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.utils;
2 |
3 | import org.apache.poi.ooxml.POIXMLDocument;
4 | import org.springframework.core.io.ByteArrayResource;
5 |
6 | import java.io.File;
7 | import java.io.FileOutputStream;
8 | import java.io.IOException;
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 | import java.text.MessageFormat;
12 |
13 | public class FileUtils {
14 |
15 | private FileUtils() {
16 | }
17 |
18 | public static ByteArrayResource createOfficeDocumentResource(POIXMLDocument document, String name, String suffix)
19 | throws IOException {
20 | return new ByteArrayResource(Files.readAllBytes(createOfficeDocumentFile(document, name, suffix))) {
21 | @Override
22 | public String getFilename() {
23 | return MessageFormat.format("{0}.{1}", name, suffix);
24 | }
25 | };
26 | }
27 |
28 | private static Path createOfficeDocumentFile(POIXMLDocument document, String name, String suffix) throws IOException {
29 | File file = File.createTempFile(name, suffix);
30 | try (FileOutputStream out = new FileOutputStream(file)) {
31 | document.write(out);
32 | } catch (Exception e) {
33 | e.printStackTrace();
34 | }
35 | return file.toPath();
36 | }
37 | }
--------------------------------------------------------------------------------
/src/main/java/ru/taksebe/telegram/writeRead/utils/ResourceLoader.java:
--------------------------------------------------------------------------------
1 | package ru.taksebe.telegram.writeRead.utils;
2 |
3 | import lombok.Getter;
4 | import org.apache.poi.xssf.usermodel.XSSFWorkbook;
5 | import org.apache.poi.xwpf.usermodel.XWPFDocument;
6 | import org.springframework.stereotype.Component;
7 | import ru.taksebe.telegram.writeRead.constants.resources.DictionaryResourcePathEnum;
8 | import ru.taksebe.telegram.writeRead.constants.resources.TemplateResourcePathsEnum;
9 |
10 | import java.io.IOException;
11 | import java.util.HashMap;
12 | import java.util.Map;
13 | import java.util.Objects;
14 |
15 | /**
16 | * Загрузчик шаблонов документов из resources
17 | */
18 | @Component
19 | public class ResourceLoader {
20 | @Getter
21 | private final Map defaultDictionaries;
22 |
23 | public ResourceLoader() throws IOException {
24 | this.defaultDictionaries = loadAllDefaultDictionaryWorkbooks();
25 | }
26 |
27 | public XWPFDocument loadTemplateDocument() throws IOException {
28 | return new XWPFDocument(
29 | Objects.requireNonNull(
30 | getClass()
31 | .getClassLoader()
32 | .getResourceAsStream(TemplateResourcePathsEnum.TEMPLATE_TASKS.getFilePath())
33 | )
34 | );
35 | }
36 |
37 | public XSSFWorkbook loadTemplateWorkbook() throws IOException {
38 | return loadWorkbook(TemplateResourcePathsEnum.TEMPLATE_DICTIONARY.getFilePath());
39 | }
40 |
41 | public XSSFWorkbook loadDefaultDictionaryWorkbook(DictionaryResourcePathEnum dictionaryResourcePath) throws IOException {
42 | return loadWorkbook(dictionaryResourcePath.getFilePath());
43 | }
44 |
45 | private Map loadAllDefaultDictionaryWorkbooks() throws IOException {
46 | Map defaultDictionaries = new HashMap<>();
47 | for (DictionaryResourcePathEnum path : DictionaryResourcePathEnum.values()) {
48 | defaultDictionaries.put(path.name(), loadWorkbook(path.getFilePath()));
49 | }
50 | return defaultDictionaries;
51 | }
52 |
53 | private XSSFWorkbook loadWorkbook(String filePath) throws IOException {
54 | return new XSSFWorkbook(
55 | Objects.requireNonNull(
56 | getClass()
57 | .getClassLoader()
58 | .getResourceAsStream(filePath)
59 | )
60 | );
61 | }
62 | }
--------------------------------------------------------------------------------
/src/main/resources/dictionaries/1 grade.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taksebe-official/writeReadRightBot/2ece29ae7db9c40257469db2382c0644f4d444c3/src/main/resources/dictionaries/1 grade.xlsx
--------------------------------------------------------------------------------
/src/main/resources/dictionaries/2 grade.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taksebe-official/writeReadRightBot/2ece29ae7db9c40257469db2382c0644f4d444c3/src/main/resources/dictionaries/2 grade.xlsx
--------------------------------------------------------------------------------
/src/main/resources/dictionaries/3 grade.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taksebe-official/writeReadRightBot/2ece29ae7db9c40257469db2382c0644f4d444c3/src/main/resources/dictionaries/3 grade.xlsx
--------------------------------------------------------------------------------
/src/main/resources/dictionaries/4 grade.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taksebe-official/writeReadRightBot/2ece29ae7db9c40257469db2382c0644f4d444c3/src/main/resources/dictionaries/4 grade.xlsx
--------------------------------------------------------------------------------
/src/main/resources/templates/Template.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taksebe-official/writeReadRightBot/2ece29ae7db9c40257469db2382c0644f4d444c3/src/main/resources/templates/Template.docx
--------------------------------------------------------------------------------
/src/main/resources/templates/Template.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taksebe-official/writeReadRightBot/2ece29ae7db9c40257469db2382c0644f4d444c3/src/main/resources/templates/Template.xlsx
--------------------------------------------------------------------------------
/system.properties:
--------------------------------------------------------------------------------
1 | java.runtime.version=11
--------------------------------------------------------------------------------
|