├── .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 | 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 --------------------------------------------------------------------------------