From 43d5d5aadf81167c16d452e4f7fcc753063b8260 Mon Sep 17 00:00:00 2001 From: Ilya Fomko Date: Wed, 19 Apr 2023 13:26:27 +0500 Subject: [PATCH 1/8] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B0=D1=83=D0=B4=D0=B8=D0=BE,=20=D0=BA=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=D1=83=20=D0=B6=D0=B5=20=D1=81=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D0=BB=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C,=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=80=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=80=D0=B5=D1=87=D0=B8?= =?UTF-8?q?=20=D0=BA=20YandexAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 98 ++++++++------ .../VoiceAssistantApplication.java | 6 +- .../voiceAssistant/answer/AnswerDTO.java | 28 ++++ .../voiceAssistant/answer/AnswerService.java | 54 ++++++++ .../voiceAssistant/answer/GetAnswer.java | 26 ++++ .../configurations/VoiceAssistantConfig.java | 25 ++++ .../speech/SpeechSynthesis.java | 5 + .../voiceAssistant/speech/YandexClient.java | 19 +++ .../speech/YandexRecognition.java | 127 ++++++++++++++++++ .../speech/responses/RecognitionResponse.java | 13 ++ .../speech/types/YandexFormat.java | 17 +++ .../speech/types/YandexLanguage.java | 61 +++++++++ .../speech/types/YandexTopic.java | 37 +++++ src/main/resources/application.properties | 2 +- .../VoiceAssistantApplicationTests.java | 6 +- 15 files changed, 475 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/SpeechSynthesis.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/responses/RecognitionResponse.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/types/YandexLanguage.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/types/YandexTopic.java diff --git a/pom.xml b/pom.xml index 8faba20..f0d03d6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,47 +1,61 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.0.5 - - - com.iffomko - Voice-Assistant - 0.0.1-SNAPSHOT - Voice-Assistant - Voice-Assistant - - 17 - - - - org.springframework.boot - spring-boot-starter-web - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.0.5 + + + com.iffomko + Voice-Assistant + 0.0.1-SNAPSHOT + Voice-Assistant + Voice-Assistant + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.google.code.gson + gson + 2.10.1 + + + com.fasterxml.jackson.core + jackson-core + 2.15.0-rc3 + + + org.projectlombok + lombok + + - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + 3.0.5 + + + diff --git a/src/main/java/com/iffomko/voiceAssistant/VoiceAssistantApplication.java b/src/main/java/com/iffomko/voiceAssistant/VoiceAssistantApplication.java index 4ca1014..69659d4 100644 --- a/src/main/java/com/iffomko/voiceAssistant/VoiceAssistantApplication.java +++ b/src/main/java/com/iffomko/voiceAssistant/VoiceAssistantApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class VoiceAssistantApplication { - public static void main(String[] args) { - SpringApplication.run(VoiceAssistantApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(VoiceAssistantApplication.class, args); + } } diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java new file mode 100644 index 0000000..d28b553 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java @@ -0,0 +1,28 @@ +package com.iffomko.voiceAssistant.answer; + +public class AnswerDTO { + private String byteAudio; + + private String encode; + + public AnswerDTO(String byteAudio, String encode) { + this.byteAudio = byteAudio; + this.encode = encode; + } + + public String getByteAudio() { + return byteAudio; + } + + public String getEncode() { + return encode; + } + + public void setByteAudio(String byteAudio) { + this.byteAudio = byteAudio; + } + + public void setEncode(String encode) { + this.encode = encode; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java new file mode 100644 index 0000000..d9aaec5 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java @@ -0,0 +1,54 @@ +package com.iffomko.voiceAssistant.answer; + +import com.iffomko.voiceAssistant.speech.YandexClient; +import com.iffomko.voiceAssistant.speech.responses.RecognitionResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import java.util.Base64; + +@Service +@Component +@Slf4j +public class AnswerService { + @Autowired + private YandexClient yandexClient; + + /** + *

В параметрах передается звук в формате Base64 и на выходе возвращается текстовое содержание этого звука

+ * @param voice - звук в кодировке base64 + * @return - возвращается текст звука или null, если звук не закодирован в Base64 + */ + public String audioToText(String voice, String format, Boolean profanityFilter) { + byte[] bytes = null; + + try { + bytes = Base64.getDecoder().decode(voice); + } catch (IllegalArgumentException e) { + log.error(e.getMessage()); + return null; + } + + if (format != null) { + yandexClient.getRecognition().setFormat(format); + } + + if (profanityFilter != null) { + yandexClient.getRecognition().setProfanityFilter(profanityFilter); + } + + RecognitionResponse response = yandexClient.recognise(bytes); + + if (response == null) { + return null; + } + + return yandexClient.recognise(bytes).getResult(); + } + + public String textToVoice(String text) { + return null; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java b/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java new file mode 100644 index 0000000..04cf822 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java @@ -0,0 +1,26 @@ +package com.iffomko.voiceAssistant.answer; + +import jakarta.annotation.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/answer") +public class GetAnswer { + @Resource(name = "AnswerService") + private AnswerService answerService; + + @GetMapping(produces="application/json") + @ResponseBody() + public ResponseEntity getAnswer( + @RequestParam String audio, + @RequestParam(required = false) String format, + @RequestParam(required = false) Boolean profanityFilter + ) { + audio = audio.replace(" ", "+"); + + String text = answerService.audioToText(audio, format, profanityFilter); + + return ResponseEntity.ok(new AnswerDTO(text, "base64")); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java b/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java new file mode 100644 index 0000000..9f958a4 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java @@ -0,0 +1,25 @@ +package com.iffomko.voiceAssistant.configurations; + +import com.iffomko.voiceAssistant.answer.AnswerService; +import com.iffomko.voiceAssistant.speech.YandexClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +@Configuration +@ComponentScan("com.iffomko.voiceAssistant") +public class VoiceAssistantConfig { + @Bean("AnswerService") + @Scope("prototype") + public AnswerService getAnswerService() { + return new AnswerService(); + } + + @Bean("yandexClient") + public YandexClient getYandexRecognition() { + String apiKey = System.getenv("API_KEY"); + + return new YandexClient(apiKey); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/SpeechSynthesis.java b/src/main/java/com/iffomko/voiceAssistant/speech/SpeechSynthesis.java new file mode 100644 index 0000000..87714e5 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/SpeechSynthesis.java @@ -0,0 +1,5 @@ +package com.iffomko.voiceAssistant.speech; + +public class SpeechSynthesis { + +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java b/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java new file mode 100644 index 0000000..2c17f29 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java @@ -0,0 +1,19 @@ +package com.iffomko.voiceAssistant.speech; + +import com.iffomko.voiceAssistant.speech.responses.RecognitionResponse; + +public class YandexClient { + private final YandexRecognition recognition; + + public YandexClient(String apiKey) { + this.recognition = new YandexRecognition(apiKey); + } + + public YandexRecognition getRecognition() { + return recognition; + } + + public RecognitionResponse recognise(byte[] voice) { + return recognition.recognition(voice); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java b/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java new file mode 100644 index 0000000..c222a16 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java @@ -0,0 +1,127 @@ +package com.iffomko.voiceAssistant.speech; + +import com.iffomko.voiceAssistant.speech.responses.RecognitionResponse; +import com.iffomko.voiceAssistant.speech.types.YandexFormat; +import com.iffomko.voiceAssistant.speech.types.YandexLanguage; +import com.iffomko.voiceAssistant.speech.types.YandexTopic; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; + +@Slf4j +public class YandexRecognition { + private final String apiKey; + private YandexLanguage lang = null; + private YandexTopic topic = null; + private boolean profanityFilter = false; + private String format = "oggopus"; + public final static String URL = "https://stt.api.cloud.yandex.net/speech/v1/stt:recognize"; + + public YandexRecognition(String apiKey) { + this.apiKey = apiKey; + } + + private String getQueryParams() { + StringBuilder queries = new StringBuilder(); + + if (lang != null) { + queries.append("lang="); + queries.append(lang.getLang()); + } + + if (topic != null) { + if (!queries.isEmpty()) { + queries.append("&"); + } + + queries.append("topic="); + queries.append(lang.getLang()); + } + + if (!queries.isEmpty()) { + queries.append("&"); + } + + queries.append("profanityFilter="); + queries.append(profanityFilter); + + if (!queries.isEmpty()) { + queries.append("&"); + } + + queries.append("format="); + queries.append(format); + + return queries.toString(); + } + + protected RecognitionResponse recognition(byte[] voice) { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + headers.set("Authorization", "Api-Key " + apiKey); + + HttpEntity request = new HttpEntity<>(voice, headers); + + ResponseEntity response = null; + + try { + response = restTemplate.exchange( + URL + "?" + getQueryParams(), + HttpMethod.POST, + request, + RecognitionResponse.class + ); + } catch (RestClientException e) { + log.error(e.getMessage()); + } + + if (response == null || response.getBody() == null) { + return null; + } + + log.info( + "The Yandex API text recognition request was executed successfully with the return response: " + + response.getBody().getResult() + ); + + return response.getBody(); + } + + public YandexLanguage getLang() { + return lang; + } + + public void setLang(YandexLanguage lang) { + this.lang = lang; + } + + public YandexTopic getTopic() { + return topic; + } + + public void setTopic(YandexTopic topic) { + this.topic = topic; + } + + public boolean isProfanityFilter() { + return profanityFilter; + } + + public void setProfanityFilter(boolean profanityFilter) { + this.profanityFilter = profanityFilter; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/responses/RecognitionResponse.java b/src/main/java/com/iffomko/voiceAssistant/speech/responses/RecognitionResponse.java new file mode 100644 index 0000000..4ed5ba2 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/responses/RecognitionResponse.java @@ -0,0 +1,13 @@ +package com.iffomko.voiceAssistant.speech.responses; + +public class RecognitionResponse { + private String result; + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java new file mode 100644 index 0000000..759ba4d --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java @@ -0,0 +1,17 @@ +package com.iffomko.voiceAssistant.speech.types; + +public enum YandexFormat { + MP3("mp3"), + OGGOPUS("oggopus"), + LPCM("lpcm"); + + private final String format; + + YandexFormat(String format) { + this.format = format; + } + + public String getFormat() { + return format; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexLanguage.java b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexLanguage.java new file mode 100644 index 0000000..cde9fad --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexLanguage.java @@ -0,0 +1,61 @@ +package com.iffomko.voiceAssistant.speech.types; + +/** + *

Перечисление всех возможных языков доступных для распознавания для запроса

+ */ +public enum YandexLanguage { + /** + *

Русский язык

+ */ + RUSSIAN("ru-RU"), + /** + *

Английский язык

+ */ + ENGLISH("en-US"), + /** + *

Немецкий язык

+ */ + GERMANY("de-DE"), + /** + *

Испанский язык

+ */ + SPANISH("es-ES"), + /** + *

Французский язык

+ */ + FRENCH("fr-FR"), + /** + *

Казахский язык

+ */ + KAZAKH("kk-KK"), + /** + *

Тюркский язык

+ */ + TURKISH("tr-TR"), + /** + *

Польский язык

+ */ + POLISH("pl-PL"), + /** + *

Итальянский язык

+ */ + ITALIAN("it-IT"); + + private final String lang; + + /** + *

Устанавливает язык для распозновательной модели

+ * @param lang - язык + */ + YandexLanguage(String lang) { + this.lang = lang; + } + + /** + *

Возвращает язык для распозновательной модели

+ * @return - язык + */ + public String getLang() { + return lang; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexTopic.java b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexTopic.java new file mode 100644 index 0000000..b65023b --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexTopic.java @@ -0,0 +1,37 @@ +package com.iffomko.voiceAssistant.speech.types; + +/** + *

Перечисление всех версий моделей распознавания доступных для запроса

+ */ +public enum YandexTopic { + /** + *

Основная версия модели

+ */ + GENERAL("general"), + /** + *

Версия-кандидат для релиза, которую вы можете тестировать

+ */ + GENERAL_RC("general:rc"), + /** + *

Предыдущая версия модели

+ */ + GENERAL_DEPRECATED("general:deprecated"); + + private final String topic; + + /** + *

Устанавливает текущее значение распозновательной модели

+ * @param topic - версия модели + */ + YandexTopic(String topic) { + this.topic = topic; + } + + /** + *

Возвращает тег распазновательной модели

+ * @return - тег модели + */ + public String getTopic() { + return topic; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..43bc65f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1 @@ - +server.max-http-header-size=1024KB \ No newline at end of file diff --git a/src/test/java/com/iffomko/voiceAssistant/VoiceAssistantApplicationTests.java b/src/test/java/com/iffomko/voiceAssistant/VoiceAssistantApplicationTests.java index a1a2a4d..c97cfef 100644 --- a/src/test/java/com/iffomko/voiceAssistant/VoiceAssistantApplicationTests.java +++ b/src/test/java/com/iffomko/voiceAssistant/VoiceAssistantApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class VoiceAssistantApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } From 82d866d24bce336336f848c65c8cbead53aa2746 Mon Sep 17 00:00:00 2001 From: Ilya Fomko Date: Fri, 21 Apr 2023 11:30:07 +0500 Subject: [PATCH 2/8] =?UTF-8?q?=D0=94=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB?= =?UTF-8?q?=20=D1=81=D0=B8=D0=BD=D1=82=D0=B5=D0=B7=20=D1=80=D0=B5=D1=87?= =?UTF-8?q?=D0=B8=20=D1=81=20=D0=BF=D0=BE=D0=BC=D0=BE=D1=89=D1=8C=D1=8E=20?= =?UTF-8?q?YandexAPI,=20=D1=82=D0=B0=D0=BA=D0=B6=D0=B5=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=B0=D0=BB=20javadoc=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=B2=D1=81=D0=B5=D1=85=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voiceAssistant/answer/AnswerDTO.java | 31 +++--- .../voiceAssistant/answer/AnswerError.java | 12 +++ .../voiceAssistant/answer/AnswerService.java | 28 ++++- .../voiceAssistant/answer/GetAnswer.java | 21 +++- .../speech/SpeechSynthesis.java | 5 - .../voiceAssistant/speech/YandexClient.java | 43 +++++++- .../speech/YandexRecognition.java | 74 +++++++++++-- .../speech/YandexSynthesis.java | 101 ++++++++++++++++++ .../speech/data/RecognitionResponse.java | 11 ++ .../speech/data/YandexSynthesisJSON.java | 16 +++ .../speech/responses/RecognitionResponse.java | 13 --- .../speech/types/YandexFormat.java | 7 ++ .../speech/types/YandexVoice.java | 42 ++++++++ 13 files changed, 354 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/iffomko/voiceAssistant/answer/AnswerError.java delete mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/SpeechSynthesis.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/data/RecognitionResponse.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/data/YandexSynthesisJSON.java delete mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/responses/RecognitionResponse.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/types/YandexVoice.java diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java index d28b553..e81a7b7 100644 --- a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java +++ b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java @@ -1,28 +1,23 @@ package com.iffomko.voiceAssistant.answer; +import lombok.Data; + +@Data public class AnswerDTO { - private String byteAudio; + private String voice = null; + private String encode = null; + private String format = null; + private AnswerError error; - private String encode; + public AnswerDTO() {} - public AnswerDTO(String byteAudio, String encode) { - this.byteAudio = byteAudio; + public AnswerDTO(String byteAudio, String encode, String format) { + this.voice = byteAudio; this.encode = encode; + this.format = format; } - public String getByteAudio() { - return byteAudio; - } - - public String getEncode() { - return encode; - } - - public void setByteAudio(String byteAudio) { - this.byteAudio = byteAudio; - } - - public void setEncode(String encode) { - this.encode = encode; + public AnswerDTO(AnswerError error) { + this.error = error; } } diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerError.java b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerError.java new file mode 100644 index 0000000..6475cf9 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerError.java @@ -0,0 +1,12 @@ +package com.iffomko.voiceAssistant.answer; + +import lombok.Data; + +/** + *

Объект ошибки. Он сериализуется в JSON, а так же содержит в себе сообщение ошибки и код ошибки

+ */ +@Data +public class AnswerError { + private String message; + private int code; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java index d9aaec5..3354802 100644 --- a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java +++ b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java @@ -1,7 +1,8 @@ package com.iffomko.voiceAssistant.answer; import com.iffomko.voiceAssistant.speech.YandexClient; -import com.iffomko.voiceAssistant.speech.responses.RecognitionResponse; +import com.iffomko.voiceAssistant.speech.data.RecognitionResponse; +import com.iffomko.voiceAssistant.speech.types.YandexVoice; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -9,6 +10,9 @@ import java.util.Base64; +/** + *

Класс бизнес-логики формирования ответа на вопрос

+ */ @Service @Component @Slf4j @@ -48,7 +52,25 @@ public String audioToText(String voice, String format, Boolean profanityFilter) return yandexClient.recognise(bytes).getResult(); } - public String textToVoice(String text) { - return null; + /** + *

Переводит текст в звук

+ * @param text - текст, который нужно озвучить + * @param format - формат выходного звука (mp3, oggopus, lpcm) + * @return - звук, закодированный в Base64 + */ + public String textToVoice(String text, String format) { + if (format != null) { + yandexClient.getSynthesis().setFormat(format); + } + + yandexClient.getSynthesis().setVoice(YandexVoice.FILIPP); + + byte[] voice = yandexClient.synthesis(text); + + if (voice == null) { + return null; + } + + return Base64.getEncoder().encodeToString(voice); } } diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java b/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java index 04cf822..59fa85d 100644 --- a/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java +++ b/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java @@ -1,6 +1,8 @@ package com.iffomko.voiceAssistant.answer; +import com.iffomko.voiceAssistant.speech.types.YandexFormat; import jakarta.annotation.Resource; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -10,6 +12,9 @@ public class GetAnswer { @Resource(name = "AnswerService") private AnswerService answerService; + /** + *

Контроллер, который отвечает за ответ на вопрос, который содержится в аудио

+ */ @GetMapping(produces="application/json") @ResponseBody() public ResponseEntity getAnswer( @@ -17,10 +22,24 @@ public ResponseEntity getAnswer( @RequestParam(required = false) String format, @RequestParam(required = false) Boolean profanityFilter ) { + if ( + format != null && + !format.equals(YandexFormat.OGGOPUS.getFormat()) && + !format.equals(YandexFormat.MP3.getFormat()) && + !format.equals(YandexFormat.LPCM.getFormat()) + ) { + AnswerError error = new AnswerError(); + error.setCode(HttpStatus.BAD_REQUEST.value()); + error.setMessage("Вы ввели некорректный формат аудио: " + format); + + return ResponseEntity.badRequest().body(new AnswerDTO(error)); + } + audio = audio.replace(" ", "+"); String text = answerService.audioToText(audio, format, profanityFilter); + String response = answerService.textToVoice(text, format); - return ResponseEntity.ok(new AnswerDTO(text, "base64")); + return ResponseEntity.ok(new AnswerDTO(response, "base64", format)); } } diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/SpeechSynthesis.java b/src/main/java/com/iffomko/voiceAssistant/speech/SpeechSynthesis.java deleted file mode 100644 index 87714e5..0000000 --- a/src/main/java/com/iffomko/voiceAssistant/speech/SpeechSynthesis.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.iffomko.voiceAssistant.speech; - -public class SpeechSynthesis { - -} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java b/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java index 2c17f29..45ccf2f 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java +++ b/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java @@ -1,19 +1,60 @@ package com.iffomko.voiceAssistant.speech; -import com.iffomko.voiceAssistant.speech.responses.RecognitionResponse; +import com.iffomko.voiceAssistant.speech.data.RecognitionResponse; +/** + *

Клиент, который позволяет обращаться к Yandex API за распознаванием и синтезированием голоса

+ */ public class YandexClient { private final YandexRecognition recognition; + private final YandexSynthesis synthesis; + /** + *

Создает класс YandexClient и заполняет его поля нужными значениями

+ * @param apiKey ключ от Yandex API + */ public YandexClient(String apiKey) { this.recognition = new YandexRecognition(apiKey); + this.synthesis = new YandexSynthesis(apiKey); } + /** + *

+ * Возвращает объект, который занимается распознаванием голоса. С помощью этого метода можно только настроить + * этот объект по свои нужды. + *

+ * @return объект для распознавания голоса + */ public YandexRecognition getRecognition() { return recognition; } + /** + *

+ * Возвращает объект, который занимается синтезированием голоса. С помощью этого метода можно только настроить + * этот объект по свои нужды. + *

+ * @return объект для синтезирования голоса + */ + public YandexSynthesis getSynthesis() { + return synthesis; + } + + /** + *

Метод, который позволяет распознать речь в входящем аудио

+ * @param voice входящее аудио в виде массива байтов + * @return объект RecognitionResponse, в котором содержится в поле result распознанный текст + */ public RecognitionResponse recognise(byte[] voice) { return recognition.recognition(voice); } + + /** + *

Метод, который позволяет синтезировать речь

+ * @param text текст, который надо озвучить + * @return массив байтов синтезированной речи + */ + public byte[] synthesis(String text) { + return synthesis.synthesize(text); + } } diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java b/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java index c222a16..85803ae 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java +++ b/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java @@ -1,9 +1,9 @@ package com.iffomko.voiceAssistant.speech; -import com.iffomko.voiceAssistant.speech.responses.RecognitionResponse; -import com.iffomko.voiceAssistant.speech.types.YandexFormat; +import com.iffomko.voiceAssistant.speech.data.RecognitionResponse; import com.iffomko.voiceAssistant.speech.types.YandexLanguage; import com.iffomko.voiceAssistant.speech.types.YandexTopic; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.web.client.RestClientException; @@ -11,6 +11,9 @@ import java.util.Collections; +/** + *

Класс, который умеет делать запросы к Yandex API и с её помощью синтезировать речь

+ */ @Slf4j public class YandexRecognition { private final String apiKey; @@ -20,10 +23,18 @@ public class YandexRecognition { private String format = "oggopus"; public final static String URL = "https://stt.api.cloud.yandex.net/speech/v1/stt:recognize"; + /** + *

Конструктор, который принимает ключ от Yandex API, чтобы можно авторизоваться с его помощью в системе

+ * @param apiKey ключ от Yandex API для авторизации + */ public YandexRecognition(String apiKey) { this.apiKey = apiKey; } + /** + *

Формирует строчку для Query параметров для запроса

+ * @return строка содержащая query параметры + */ private String getQueryParams() { StringBuilder queries = new StringBuilder(); @@ -38,7 +49,7 @@ private String getQueryParams() { } queries.append("topic="); - queries.append(lang.getLang()); + queries.append(topic.getTopic()); } if (!queries.isEmpty()) { @@ -58,6 +69,11 @@ private String getQueryParams() { return queries.toString(); } + /** + *

Делает запрос к Yandex API за распознаванием речи по входному аудио

+ * @param voice массив байтов аудио, которое нужно распознать + * @return объект RecognitionResponse, содержащий единственное поле result с распознанным текстом + */ protected RecognitionResponse recognition(byte[] voice) { RestTemplate restTemplate = new RestTemplate(); @@ -66,6 +82,8 @@ protected RecognitionResponse recognition(byte[] voice) { headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.set("Authorization", "Api-Key " + apiKey); + log.info(headers.toString()); + HttpEntity request = new HttpEntity<>(voice, headers); ResponseEntity response = null; @@ -82,46 +100,84 @@ protected RecognitionResponse recognition(byte[] voice) { } if (response == null || response.getBody() == null) { + log.error("Failed to recognize speech from Yandex API"); return null; } - log.info( - "The Yandex API text recognition request was executed successfully with the return response: " + - response.getBody().getResult() - ); + log.info("The Yandex API text recognition request was executed successfully"); return response.getBody(); } + /** + *

Возвращает язык, который нужно распознавать, в настройках этого класса

+ * @return объект YandexLanguage, который содержит распознаваемый язык + */ public YandexLanguage getLang() { return lang; } + /** + *

Устанавливает распознаваемый язык для текущих настроек класса

+ * @param lang объект YandexLanguage, содержащий распознаваемый язык + */ public void setLang(YandexLanguage lang) { this.lang = lang; } + /** + *

Возвращает название модели распознавания в настройках этого класса

+ * @return объект YandexTopic, содержащий название модели распознавания + */ public YandexTopic getTopic() { return topic; } + /** + *

Устанавливает модель распознавания для настроек этого класса

+ * @param topic объект YandexTopic, содержащий название модели распознавания + */ public void setTopic(YandexTopic topic) { this.topic = topic; } + /** + *

+ * Возвращает значение поля profanityFilter. + * Если установлен false, то ненормативная лексика не будет исключена из распознавания. + * Если установлен true, то нормативная лекция будет исключена из распознавания. + *

+ * @return возвращает значение поля profanityFilter + */ public boolean isProfanityFilter() { return profanityFilter; } - public void setProfanityFilter(boolean profanityFilter) { + /** + *

+ * Устанавливает значение true или false для поля profanityFilter. + * Если установлен false, то ненормативная лексика не будет исключена из распознавания. + * Если установлен true, то нормативная лекция будет исключена из распознавания. + *

+ * @param profanityFilter true или false - значение, которое нужно установить полю + */ + public void setProfanityFilter(@NonNull boolean profanityFilter) { this.profanityFilter = profanityFilter; } + /** + *

Возвращает формат входного аудио (mp3, oggopus, lpcm)

+ * @return возвращает формат входящего аудио + */ public String getFormat() { return format; } - public void setFormat(String format) { + /** + *

Устанавливает формат входного аудио в настройках этого класса

+ * @param format формат входящего аудио (mp3, oggopus, lpcm) + */ + public void setFormat(@NonNull String format) { this.format = format; } } diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java b/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java new file mode 100644 index 0000000..194d111 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java @@ -0,0 +1,101 @@ +package com.iffomko.voiceAssistant.speech; + +import com.iffomko.voiceAssistant.speech.types.YandexVoice; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +/** + *

Класс, который умеет делать запросы к Yandex API за синтезом речи

+ */ +@Slf4j +public class YandexSynthesis { + private final String apiKey; + private YandexVoice voice = YandexVoice.FILIPP; + private String format = "oggopus"; + + public static final String URL = "https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize\n"; + + /** + *

Конструктор, который принимает ключ от Yandex API, чтобы можно авторизоваться с его помощью в системе

+ * @param apiKey ключ от Yandex API для авторизации + */ + public YandexSynthesis(String apiKey) { + this.apiKey = apiKey; + } + + /** + *

Делает запрос на Yandex API, чтобы синтезировать речь по тексту

+ * @param text текст, который необходимо озвучить + * @return массив байтов озвученного текста + */ + protected byte[] synthesize(@NonNull String text) { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.set("Authorization", "Api-Key " + apiKey); + + log.info(headers.toString()); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("text", text); + body.add("voice", voice.getVoice()); + body.add("lang", voice.getLang().getLang()); + + HttpEntity> request = new HttpEntity<>(body, headers); + + ResponseEntity response = null; + + try { + response = restTemplate.exchange(URL, HttpMethod.POST, request, byte[].class); + } catch (RestClientException e) { + log.error(e.getMessage()); + } + + if (response == null || response.getBody() == null) { + log.error("Failed to synthesize speech by text from Yandex API"); + return null; + } + + log.info("Successfully synthesized speech using Yandex API"); + + return response.getBody(); + } + + /** + *

Возвращает голос для синтеза речи, установленный в настройках этого класса

+ * @return объект YandexVoice, в котором хранится текущий голос в настройках + */ + public YandexVoice getVoice() { + return voice; + } + + /** + *

Устанавливает голос для синтеза речи

+ * @param voice голос для синтеза речи, который нужно установить + */ + public void setVoice(@NonNull YandexVoice voice) { + this.voice = voice; + } + + /** + *

Возвращает формат выходной аудиодорожки (mp3, oggopus, lpcm)

+ * @return формат аудиодорожки + */ + public String getFormat() { + return format; + } + + /** + *

Устанавливает формат для выходной аудиодорожки (mp3, oggopus, lpcm)

+ * @param format формат аудиодорожки + */ + public void setFormat(@NonNull String format) { + this.format = format; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/data/RecognitionResponse.java b/src/main/java/com/iffomko/voiceAssistant/speech/data/RecognitionResponse.java new file mode 100644 index 0000000..a532b8b --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/data/RecognitionResponse.java @@ -0,0 +1,11 @@ +package com.iffomko.voiceAssistant.speech.data; + +import lombok.Data; + +/** + *

Объект ответа, в котором содержится одно единственное поле result, которое содержит распознанный текст

+ */ +@Data +public class RecognitionResponse { + private String result; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/data/YandexSynthesisJSON.java b/src/main/java/com/iffomko/voiceAssistant/speech/data/YandexSynthesisJSON.java new file mode 100644 index 0000000..00518f6 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/data/YandexSynthesisJSON.java @@ -0,0 +1,16 @@ +package com.iffomko.voiceAssistant.speech.data; + +import lombok.Data; + +/** + *

+ * Объект тела запроса синтеза текста. В нем содержатся 3 поля: название голоса синтеза, текст, который надо озвучить, + * и язык озвучки + *

+ */ +@Data +public class YandexSynthesisJSON { + private String voice; + private String text; + private String lang; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/responses/RecognitionResponse.java b/src/main/java/com/iffomko/voiceAssistant/speech/responses/RecognitionResponse.java deleted file mode 100644 index 4ed5ba2..0000000 --- a/src/main/java/com/iffomko/voiceAssistant/speech/responses/RecognitionResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.iffomko.voiceAssistant.speech.responses; - -public class RecognitionResponse { - private String result; - - public String getResult() { - return result; - } - - public void setResult(String result) { - this.result = result; - } -} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java index 759ba4d..8ce9525 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java +++ b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java @@ -1,5 +1,8 @@ package com.iffomko.voiceAssistant.speech.types; +/** + *

Перечисление, которое содержит доступные форматы для аудио

+ */ public enum YandexFormat { MP3("mp3"), OGGOPUS("oggopus"), @@ -11,6 +14,10 @@ public enum YandexFormat { this.format = format; } + /** + *

Возвращает формат для аудио

+ * @return формат + */ public String getFormat() { return format; } diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexVoice.java b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexVoice.java new file mode 100644 index 0000000..bb23f82 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexVoice.java @@ -0,0 +1,42 @@ +package com.iffomko.voiceAssistant.speech.types; + +/** + *

Перечисление всех голосов, которые доступны для синтеза голоса

+ */ +public enum YandexVoice { + FILIPP("filipp", YandexLanguage.RUSSIAN), + ALENA("alena", YandexLanguage.RUSSIAN), + ERMIL("ermil", YandexLanguage.RUSSIAN), + JANE("jane", YandexLanguage.RUSSIAN), + MADIRUS("madirus", YandexLanguage.RUSSIAN), + OMAZH("omazh", YandexLanguage.RUSSIAN), + ZAHAR("zahar", YandexLanguage.RUSSIAN), + LEA("lea", YandexLanguage.GERMANY), + JOHN("john", YandexLanguage.ENGLISH), + AMIRA("amira", YandexLanguage.KAZAKH), + MADI("madi", YandexLanguage.RUSSIAN); + + private final String voice; + private final YandexLanguage lang; + + YandexVoice(String voice, YandexLanguage lang) { + this.lang = lang; + this.voice = voice; + } + + /** + *

Возвращает язык для синтеза речи

+ * @return объект YandexLanguage, содержащий язык для синтеза речи + */ + public YandexLanguage getLang() { + return lang; + } + + /** + *

Возвращает название голоса для синтеза речи

+ * @return название голоса + */ + public String getVoice() { + return voice; + } +} From 4c50109cad2b0d2239f97255aa4d313e4f486a5f Mon Sep 17 00:00:00 2001 From: Ilya Fomko Date: Fri, 21 Apr 2023 11:34:13 +0500 Subject: [PATCH 3/8] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../speech/data/YandexSynthesisJSON.java | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/main/java/com/iffomko/voiceAssistant/speech/data/YandexSynthesisJSON.java diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/data/YandexSynthesisJSON.java b/src/main/java/com/iffomko/voiceAssistant/speech/data/YandexSynthesisJSON.java deleted file mode 100644 index 00518f6..0000000 --- a/src/main/java/com/iffomko/voiceAssistant/speech/data/YandexSynthesisJSON.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.iffomko.voiceAssistant.speech.data; - -import lombok.Data; - -/** - *

- * Объект тела запроса синтеза текста. В нем содержатся 3 поля: название голоса синтеза, текст, который надо озвучить, - * и язык озвучки - *

- */ -@Data -public class YandexSynthesisJSON { - private String voice; - private String text; - private String lang; -} From 14c6441ed87ab63c0630eb02be91ac76e24336d7 Mon Sep 17 00:00:00 2001 From: Ilya Fomko Date: Sat, 22 Apr 2023 21:02:44 +0500 Subject: [PATCH 4/8] =?UTF-8?q?=D0=98=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=20ChatGPT=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5.=20?= =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BB=20=D0=B4=D0=B2=D0=B0?= =?UTF-8?q?=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=B0:=20=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=20=D1=83=D1=80=D0=BE=D0=B2=D0=B5=D0=BD=D1=8C=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D1=8F,?= =?UTF-8?q?=20=D0=B3=D0=B4=D0=B5=20=D0=B4=D0=B5=D0=BB=D0=B0=D1=8E=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B=20?= =?UTF-8?q?=D0=BA=20OpenAI=20API,=20=D0=B0=20=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=83=D1=80=D0=BE=D0=B2=D0=B5=D0=BD=D1=8C=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0,=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=B8=D0=B2=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D1=8B=20=D0=BF=D0=BE=D0=B4=20=D0=BC?= =?UTF-8?q?=D0=BE=D1=8E=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D0=B8=20=D1=83=D0=B6=D0=B5=20?= =?UTF-8?q?=D1=88=D0=BB=D0=B5=D1=82=20=D1=8D=D1=82=D0=B8=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voiceAssistant/answer/AnswerBody.java | 12 ++ .../voiceAssistant/answer/AnswerService.java | 46 ++++++- .../voiceAssistant/answer/Answers.java | 59 +++++++++ .../voiceAssistant/answer/GetAnswer.java | 45 ------- .../configurations/VoiceAssistantConfig.java | 8 ++ .../voiceAssistant/openAI/AIModel.java | 82 ++++++++++++ .../voiceAssistant/openAI/AIService.java | 125 ++++++++++++++++++ .../openAI/ClearMessagesGarber.java | 69 ++++++++++ .../openAI/data/ModelBodyDTO.java | 25 ++++ .../openAI/data/ModelResponse.java | 18 +++ .../openAI/data/OpenAIChoices.java | 24 ++++ .../openAI/data/OpenAIMessage.java | 19 +++ .../openAI/data/OpenAIUsage.java | 21 +++ .../openAI/types/OpenAIModels.java | 25 ++++ .../openAI/types/OpenAIRole.java | 20 +++ .../speech/YandexRecognition.java | 2 - .../speech/YandexSynthesis.java | 2 - 17 files changed, 551 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/iffomko/voiceAssistant/answer/AnswerBody.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/answer/Answers.java delete mode 100644 src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/AIModel.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/AIService.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/ClearMessagesGarber.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelBodyDTO.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelResponse.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIChoices.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIMessage.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIUsage.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIModels.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIRole.java diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerBody.java b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerBody.java new file mode 100644 index 0000000..f1352b0 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerBody.java @@ -0,0 +1,12 @@ +package com.iffomko.voiceAssistant.answer; + +import lombok.Data; + +@Data +public class AnswerBody { + private Integer userId; + private String audio; + private String format; + private Boolean profanityFilter; + private String encode; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java index 3354802..2be622f 100644 --- a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java +++ b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java @@ -1,8 +1,13 @@ package com.iffomko.voiceAssistant.answer; +import com.iffomko.voiceAssistant.openAI.AIService; +import com.iffomko.voiceAssistant.openAI.data.ModelResponse; +import com.iffomko.voiceAssistant.openAI.data.OpenAIChoices; +import com.iffomko.voiceAssistant.openAI.types.OpenAIRole; import com.iffomko.voiceAssistant.speech.YandexClient; import com.iffomko.voiceAssistant.speech.data.RecognitionResponse; import com.iffomko.voiceAssistant.speech.types.YandexVoice; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -19,13 +24,15 @@ public class AnswerService { @Autowired private YandexClient yandexClient; + @Autowired + private AIService chatGPT; /** *

В параметрах передается звук в формате Base64 и на выходе возвращается текстовое содержание этого звука

* @param voice - звук в кодировке base64 * @return - возвращается текст звука или null, если звук не закодирован в Base64 */ - public String audioToText(String voice, String format, Boolean profanityFilter) { + private String audioToText(String voice, String format, Boolean profanityFilter) { byte[] bytes = null; try { @@ -58,7 +65,7 @@ public String audioToText(String voice, String format, Boolean profanityFilter) * @param format - формат выходного звука (mp3, oggopus, lpcm) * @return - звук, закодированный в Base64 */ - public String textToVoice(String text, String format) { + private String textToVoice(String text, String format) { if (format != null) { yandexClient.getSynthesis().setFormat(format); } @@ -73,4 +80,39 @@ public String textToVoice(String text, String format) { return Base64.getEncoder().encodeToString(voice); } + + /** + *

Формирует ответ на вопрос пользователя

+ * @param userId id пользователя + * @param voice аудио-сообщение, в котором содержится вопрос + * @param format формат аудио-сообщения + * @param profanityFilter фильтр + * @return возвращает ответ на вопрос пользователя в виде аудиосообщения + */ + public String getAnswer( + @NonNull Integer userId, + @NonNull String voice, + @NonNull String format, + @NonNull Boolean profanityFilter + ) { + String text = audioToText(voice, format, profanityFilter); + ModelResponse response = chatGPT.ask(userId, text); + + if (response == null) { + return null; + } + + StringBuilder answerBuilder = new StringBuilder(); + + for (OpenAIChoices choice : response.getChoices()) { + if (choice.getMessage().getRole().equals(OpenAIRole.ASSISTANT.getRole())) { + answerBuilder.append(choice.getMessage()); + answerBuilder.append("\n\n"); + } + } + + String answerChatGPT = answerBuilder.toString(); + + return textToVoice(answerChatGPT, format); + } } diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/Answers.java b/src/main/java/com/iffomko/voiceAssistant/answer/Answers.java new file mode 100644 index 0000000..eb94ec2 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/answer/Answers.java @@ -0,0 +1,59 @@ +package com.iffomko.voiceAssistant.answer; + +import com.iffomko.voiceAssistant.speech.types.YandexFormat; +import jakarta.annotation.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/answer") +public class Answers { + @Resource(name = "AnswerService") + private AnswerService answerService; + + /** + *

Контроллер, который отвечает за ответ на вопрос, который содержится в аудио

+ */ + @PostMapping(produces="application/json") + @ResponseBody() + public ResponseEntity getAnswer(@RequestBody AnswerBody body) { + if ( + body.getFormat() != null && + !body.getFormat().equals(YandexFormat.OGGOPUS.getFormat()) && + !body.getFormat().equals(YandexFormat.MP3.getFormat()) && + !body.getFormat().equals(YandexFormat.LPCM.getFormat()) + ) { + AnswerError error = new AnswerError(); + error.setCode(HttpStatus.BAD_REQUEST.value()); + error.setMessage("Вы ввели некорректный формат аудио: " + body.getFormat()); + + return ResponseEntity.badRequest().body(new AnswerDTO(error)); + } + + if (body.getFormat() == null) { + body.setFormat(YandexFormat.OGGOPUS.getFormat()); + } + + if (body.getProfanityFilter() == null) { + body.setProfanityFilter(false); + } + + String response = answerService.getAnswer( + body.getUserId(), + body.getAudio(), + body.getFormat(), + body.getProfanityFilter() + ); + + if (response == null) { + AnswerError error = new AnswerError(); + error.setCode(HttpStatus.BAD_REQUEST.value()); + error.setMessage("Не удалось получить ответ"); + + return ResponseEntity.badRequest().body(new AnswerDTO(error)); + } + + return ResponseEntity.ok(new AnswerDTO(response, "base64", body.getFormat())); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java b/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java deleted file mode 100644 index 59fa85d..0000000 --- a/src/main/java/com/iffomko/voiceAssistant/answer/GetAnswer.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.iffomko.voiceAssistant.answer; - -import com.iffomko.voiceAssistant.speech.types.YandexFormat; -import jakarta.annotation.Resource; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/answer") -public class GetAnswer { - @Resource(name = "AnswerService") - private AnswerService answerService; - - /** - *

Контроллер, который отвечает за ответ на вопрос, который содержится в аудио

- */ - @GetMapping(produces="application/json") - @ResponseBody() - public ResponseEntity getAnswer( - @RequestParam String audio, - @RequestParam(required = false) String format, - @RequestParam(required = false) Boolean profanityFilter - ) { - if ( - format != null && - !format.equals(YandexFormat.OGGOPUS.getFormat()) && - !format.equals(YandexFormat.MP3.getFormat()) && - !format.equals(YandexFormat.LPCM.getFormat()) - ) { - AnswerError error = new AnswerError(); - error.setCode(HttpStatus.BAD_REQUEST.value()); - error.setMessage("Вы ввели некорректный формат аудио: " + format); - - return ResponseEntity.badRequest().body(new AnswerDTO(error)); - } - - audio = audio.replace(" ", "+"); - - String text = answerService.audioToText(audio, format, profanityFilter); - String response = answerService.textToVoice(text, format); - - return ResponseEntity.ok(new AnswerDTO(response, "base64", format)); - } -} diff --git a/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java b/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java index 9f958a4..02270ed 100644 --- a/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java +++ b/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java @@ -1,6 +1,7 @@ package com.iffomko.voiceAssistant.configurations; import com.iffomko.voiceAssistant.answer.AnswerService; +import com.iffomko.voiceAssistant.openAI.AIService; import com.iffomko.voiceAssistant.speech.YandexClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -22,4 +23,11 @@ public YandexClient getYandexRecognition() { return new YandexClient(apiKey); } + + @Bean("AIService") + public AIService getAIService() { + String apiKey = System.getenv("OPEN_AI_API_KEY"); + + return new AIService(apiKey); + } } diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/AIModel.java b/src/main/java/com/iffomko/voiceAssistant/openAI/AIModel.java new file mode 100644 index 0000000..559906e --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/AIModel.java @@ -0,0 +1,82 @@ +package com.iffomko.voiceAssistant.openAI; + +import com.iffomko.voiceAssistant.openAI.data.ModelBodyDTO; +import com.iffomko.voiceAssistant.openAI.data.ModelResponse; +import com.iffomko.voiceAssistant.openAI.data.OpenAIMessage; +import com.iffomko.voiceAssistant.openAI.types.OpenAIModels; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +/** + *

+ * Класс-репозиторий, который умеет подключаться к OpenAI API и задавать вопросы модели + *

+ */ +@Slf4j +public class AIModel { + private OpenAIModels model; + private final String apiKey; + private final static String URL = "https://api.openai.com/v1/chat/completions"; + + /** + *

Создает объект этого класса, который делает запросы к модели

+ * @param apiKey ключ, с помощью которого можно сделать запрос + */ + public AIModel(String apiKey) { + this.apiKey = apiKey; + this.model = OpenAIModels.CHAT_GPT_3_5; + } + + /** + *

Делает запрос к OpenAI API к той модели, которая установлена в соответствующем поле

+ * @param messages предыдущие сообщения в переписке + * @param mess текущий вопрос к модели + * @return возвращает ответ модели типа ModelResponse + */ + protected ModelResponse ask(List messages, String mess) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + apiKey); + + HttpEntity request = new HttpEntity<>( + new ModelBodyDTO(model.getModel(), mess, model.getTemperature(), messages), + headers + ); + + RestTemplate restTemplate = new RestTemplate(); + + ResponseEntity response = null; + + try { + response = restTemplate.exchange( + URL, HttpMethod.POST, request, ModelResponse.class + ); + } catch (RestClientException e) { + log.error(e.getMessage()); + } + + if (response == null || response.getBody() == null) { + log.error("Failed to make a request to ChatGPT"); + return null; + } + + log.info("The request to ChatGPT was successful"); + + return response.getBody(); + } + + public OpenAIModels getModel() { + return model; + } + + public void setModel(@NonNull OpenAIModels model) { + this.model = model; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/AIService.java b/src/main/java/com/iffomko/voiceAssistant/openAI/AIService.java new file mode 100644 index 0000000..76299d3 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/AIService.java @@ -0,0 +1,125 @@ +package com.iffomko.voiceAssistant.openAI; + +import com.iffomko.voiceAssistant.openAI.data.ModelResponse; +import com.iffomko.voiceAssistant.openAI.data.OpenAIChoices; +import com.iffomko.voiceAssistant.openAI.data.OpenAIMessage; +import com.iffomko.voiceAssistant.openAI.types.OpenAIRole; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; + +/** + *

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

+ *

+ * Также класс умеет само очищаться если время последнего взаимодействия пользователя + * модели с пользователем была более часа назад + *

+ */ +@Slf4j +public class AIService { + private final AIModel model; + private final Map> usersMessages; + private final Map modelLastInteraction; + + public AIService(String apiKey) { + model = new AIModel(apiKey); + usersMessages = new HashMap<>(); + modelLastInteraction = new HashMap<>(); + + new Thread(new ClearMessagesGarber(this, 10000)).start(); + } + + public AIModel getModel() { + return model; + } + + /** + *

+ * Перед запросом настраивает модели если это необходимо и подключается к OpenAI API, + * дожидается ответ и возвращает его + *

+ * @param userId id пользователя, который запросил ответ на свой вопрос + * @param message сам вопрос + * @return ответ на вопрос, который хранится в объекте типа ModelResponse + */ + public ModelResponse ask(int userId, String message) { + synchronized (usersMessages) { + synchronized (modelLastInteraction) { + if (usersMessages.get(userId) == null) { + usersMessages.put(userId, new ArrayList<>()); + modelLastInteraction.put(userId, new Date()); + } + } + + if (usersMessages.get(userId).isEmpty()) { + usersMessages.get(userId).add(new OpenAIMessage( + "user", + "Вы являетесь голосовым помощником под названием Jarvis. " + + "Нужно стараться давать ответ как можно короче, но максимально информативным. " + + "Формулировать ответы нужно в манере искусственного интеллекта Jarvis из фильмов " + + "\"Железный человек\". Реагировать на осуждение твоих ответов нужно в стиле Jarvis " + + "и ссылаться на то, что ты всего лишь программа, написанная другим программистом\n" + + "Каждый ответ должен укладываться в 5000 символов") + ); + } + } + + ModelResponse response = model.ask(usersMessages.get(userId), message); + + if (response == null) { + return null; + } + + synchronized (usersMessages) { + synchronized (modelLastInteraction) { + usersMessages.get(userId).add(new OpenAIMessage(OpenAIRole.USER.getRole(), message)); + + for (OpenAIChoices choice : response.getChoices()) { + if (choice.getMessage().getRole().equals(OpenAIRole.ASSISTANT.getRole())) + usersMessages.get(userId).add(choice.getMessage()); + } + + modelLastInteraction.replace(userId, new Date()); + } + } + + return response; + } + + /** + *

Возвращает контекст со всеми пользователями, где ключ это userId, а значение это контекст

+ * @return контекст со всеми пользователями + */ + protected Map> getUsersMessages() { + return usersMessages; + } + + /** + *

+ * Возвращает дату последнего взаимодействия для каждого контекста, + * где ключ это userId, а значение дата последнего взаимодействия + *

+ *

Если контекста для пользователя не существует, то будет возвращено null

+ * @return сло + */ + protected Map getModelLastInteraction() { + return modelLastInteraction; + } + + /** + *

Удаляет контекст для конкретного пользователя

+ * @param userId id пользователя, контекст которого надо удалить + */ + public void clearMessages(int userId) { + synchronized (usersMessages) { + synchronized (modelLastInteraction) { + log.info("For user with id=" + userId + " history chat cleared"); + usersMessages.remove(userId); + modelLastInteraction.remove(userId); + } + } + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/ClearMessagesGarber.java b/src/main/java/com/iffomko/voiceAssistant/openAI/ClearMessagesGarber.java new file mode 100644 index 0000000..0390bd4 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/ClearMessagesGarber.java @@ -0,0 +1,69 @@ +package com.iffomko.voiceAssistant.openAI; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.Map; + +/** + *

+ * Этот класс отвечает за очистку сообщений в объектах AIService. + * Он очищает сообщения если срок последнего взаимодействия в этом чате на текущий момент больше 1 часа. + *

+ *

+ * Этот класс реализуется интерфейс Runnable, следовательно его нужно запускать в отдельном потоке, + * потому что иначе программа просто зависнет. + *

+ */ +@Slf4j +public class ClearMessagesGarber implements Runnable { + private final AIService service; + private final int timeout; + + private final static long TIME_LIVE = 3600000; + + + /** + *

+ * Конструктор класса ClearMessagesGarber. Он устанавливает время через которое этот класс будет + * производить очистку, а также сам сервис, в котором будет производится очистка. + *

+ * @param service сервис, в котором будет производиться очистка + * @param timeout время через которое будет производиться очистка + */ + public ClearMessagesGarber(@NonNull AIService service, @NonNull int timeout) { + this.service = service; + this.timeout = timeout; + } + + /** + *

+ * Когда объект реализуется интерфейс {@code Runnable} этот метод используется + * для создания потока, при запуске потока {@code run} метод этого объекта будет вызван + * в отдельно исполняющем потоке + *

+ */ + @Override + public void run() { + while (true) { + synchronized (service.getUsersMessages()) { + synchronized (service.getModelLastInteraction()) { + for (Map.Entry createdDate : service.getModelLastInteraction().entrySet()) { + Date currentDate = new Date(); + + if (Math.abs(currentDate.getTime() - createdDate.getValue().getTime()) > TIME_LIVE) { + service.clearMessages(createdDate.getKey()); + } + } + } + } + + try { + Thread.sleep(timeout); + } catch (InterruptedException e) { + log.error(e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelBodyDTO.java b/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelBodyDTO.java new file mode 100644 index 0000000..03e2ae8 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelBodyDTO.java @@ -0,0 +1,25 @@ +package com.iffomko.voiceAssistant.openAI.data; + +import lombok.Data; + +import java.util.List; + +/** + *

Объект, который конвертируется в JSON для передачи в теле запроса к OpenAI API

+ */ +@Data +public class ModelBodyDTO { + private String model; + private String prompt; + private Double temperature; + private List messages; + + public ModelBodyDTO() {} + + public ModelBodyDTO(String model, String prompt, Double temperature, List messages) { + this.model = model; + this.prompt = prompt; + this.temperature = temperature; + this.messages = messages; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelResponse.java b/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelResponse.java new file mode 100644 index 0000000..53aefa4 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelResponse.java @@ -0,0 +1,18 @@ +package com.iffomko.voiceAssistant.openAI.data; + +import lombok.Data; + +import java.util.List; + +/** + *

Объект, который хранит в себе ответ от OpenAI API

+ */ +@Data +public class ModelResponse { + private String id; + private String object; + private Integer created; + private String model; + private OpenAIUsage usage; + private List choices; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIChoices.java b/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIChoices.java new file mode 100644 index 0000000..d07e870 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIChoices.java @@ -0,0 +1,24 @@ +package com.iffomko.voiceAssistant.openAI.data; + +import lombok.Data; + +/** + *

+ * Объект, который хранит один ответ на вопрос от OpenAI API: + * сообщение, причина завершения и индекс в массиве таких ответов (ответов может быть несколько) + *

+ */ +@Data +public class OpenAIChoices { + private OpenAIMessage message; + private String finishReason; + private Integer index; + + public OpenAIChoices() {} + + public OpenAIChoices(OpenAIMessage message, String finishReason, Integer index) { + this.message = message; + this.finishReason = finishReason; + this.index = index; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIMessage.java b/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIMessage.java new file mode 100644 index 0000000..ec57f0f --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIMessage.java @@ -0,0 +1,19 @@ +package com.iffomko.voiceAssistant.openAI.data; + +import lombok.Data; + +/** + *

Объект, который хранит в себе информацию о сообщении в ответе на вопрос от OpenAI API

+ */ +@Data +public class OpenAIMessage { + private String role; + private String content; + + public OpenAIMessage() {} + + public OpenAIMessage(String role, String content) { + this.role = role; + this.content = content; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIUsage.java b/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIUsage.java new file mode 100644 index 0000000..3529bf0 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIUsage.java @@ -0,0 +1,21 @@ +package com.iffomko.voiceAssistant.openAI.data; + +import lombok.Data; + +/** + *

Объект, который хранит в себе количество токенов вопроса, ответа и их сумму

+ */ +@Data +public class OpenAIUsage { + private Integer promptTokens; + private Integer completionTokens; + private Integer totalTokens; + + public OpenAIUsage() {} + + public OpenAIUsage(Integer promptTokens, Integer completionTokens, Integer totalTokens) { + this.promptTokens = promptTokens; + this.completionTokens = completionTokens; + this.totalTokens = totalTokens; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIModels.java b/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIModels.java new file mode 100644 index 0000000..0e9abc8 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIModels.java @@ -0,0 +1,25 @@ +package com.iffomko.voiceAssistant.openAI.types; + +/** + *

Перечисление всех моделей доступных для обращение к API OpenAI и их "температура"

+ */ +public enum OpenAIModels { + CHAT_GPT_3_5("gpt-3.5-turbo", 0.6), + CHAT_GPT_4("gpt-4", 0.6); + + private final String model; + private final Double temperature; + + OpenAIModels(String model, Double temperature) { + this.model = model; + this.temperature = temperature; + } + + public String getModel() { + return model; + } + + public Double getTemperature() { + return temperature; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIRole.java b/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIRole.java new file mode 100644 index 0000000..624b2c5 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIRole.java @@ -0,0 +1,20 @@ +package com.iffomko.voiceAssistant.openAI.types; + +/** + *

Перечисление, которое хранит в себе все роли, которые могут быть у сообщения

+ */ +public enum OpenAIRole { + USER("User"), + ASSISTANT("Assistant"), + SYSTEM("System"); + + private final String role; + + OpenAIRole(String role) { + this.role = role; + } + + public String getRole() { + return role; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java b/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java index 85803ae..018bbbf 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java +++ b/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java @@ -82,8 +82,6 @@ protected RecognitionResponse recognition(byte[] voice) { headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.set("Authorization", "Api-Key " + apiKey); - log.info(headers.toString()); - HttpEntity request = new HttpEntity<>(voice, headers); ResponseEntity response = null; diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java b/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java index 194d111..78c2438 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java +++ b/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java @@ -40,8 +40,6 @@ protected byte[] synthesize(@NonNull String text) { headers.setContentType(MediaType.MULTIPART_FORM_DATA); headers.set("Authorization", "Api-Key " + apiKey); - log.info(headers.toString()); - MultiValueMap body = new LinkedMultiValueMap<>(); body.add("text", text); body.add("voice", voice.getVoice()); From b4faaa9b1f6d47d78de0dd9e404c252d23a86681 Mon Sep 17 00:00:00 2001 From: Ilya Fomko Date: Wed, 26 Apr 2023 19:57:38 +0500 Subject: [PATCH 5/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20Hibernate,=20=D1=80=D0=B5=D1=81=D1=82=D1=80=D1=83?= =?UTF-8?q?=D0=BA=D1=82=D1=83=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82,=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BB=20=D1=87?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B1=D0=B4,=20?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D0=BD=D0=BA=D1=80=D0=B5=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8:=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F,=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=20id,=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D0=BE=20id,=20=D0=BF=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D1=80=D0=BE=D0=BB=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=20id,=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B2=D1=81=D0=B5=D1=85=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D1=80=D0=BE=D0=BB=D1=8C=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + pom.xml | 14 ++++ .../{ => APIs}/openAI/AIModel.java | 15 ++-- .../{ => APIs}/openAI/AIService.java | 12 +-- .../openAI/ClearMessagesGarber.java | 2 +- .../{ => APIs}/openAI/data/ModelBodyDTO.java | 6 +- .../{ => APIs}/openAI/data/ModelResponse.java | 2 +- .../{ => APIs}/openAI/data/OpenAIChoices.java | 2 +- .../{ => APIs}/openAI/data/OpenAIMessage.java | 2 +- .../{ => APIs}/openAI/data/OpenAIUsage.java | 2 +- .../{ => APIs}/openAI/types/OpenAIModels.java | 2 +- .../{ => APIs}/openAI/types/OpenAIRole.java | 8 +- .../{ => APIs}/speech/YandexClient.java | 4 +- .../{ => APIs}/speech/YandexRecognition.java | 8 +- .../{ => APIs}/speech/YandexSynthesis.java | 4 +- .../speech/data/RecognitionResponse.java | 2 +- .../{ => APIs}/speech/types/YandexFormat.java | 2 +- .../speech/types/YandexLanguage.java | 2 +- .../{ => APIs}/speech/types/YandexTopic.java | 2 +- .../{ => APIs}/speech/types/YandexVoice.java | 2 +- .../voiceAssistant/answer/AnswerDTO.java | 23 ------ .../VoiceAssistantConfig.java | 8 +- .../AnswerController.java} | 18 +++-- .../data/AnswerRequestDTO.java} | 4 +- .../controllers/data/AnswerResponseDTO.java | 24 ++++++ .../errors}/AnswerError.java | 2 +- .../services}/AnswerService.java | 20 ++--- .../voiceAssistant/db/entities/Role.java | 24 ++++++ .../voiceAssistant/db/entities/User.java | 31 ++++++++ .../db/repositories/UserRepository.java | 8 ++ .../db/services/UserService.java | 15 ++++ .../db/services/dao/UserDao.java | 78 +++++++++++++++++++ src/main/resources/application.properties | 7 +- 33 files changed, 269 insertions(+), 87 deletions(-) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/AIModel.java (82%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/AIService.java (92%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/ClearMessagesGarber.java (98%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/data/ModelBodyDTO.java (69%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/data/ModelResponse.java (87%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/data/OpenAIChoices.java (92%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/data/OpenAIMessage.java (89%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/data/OpenAIUsage.java (91%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/types/OpenAIModels.java (91%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/openAI/types/OpenAIRole.java (73%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/speech/YandexClient.java (95%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/speech/YandexRecognition.java (96%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/speech/YandexSynthesis.java (97%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/speech/data/RecognitionResponse.java (84%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/speech/types/YandexFormat.java (90%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/speech/types/YandexLanguage.java (96%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/speech/types/YandexTopic.java (95%) rename src/main/java/com/iffomko/voiceAssistant/{ => APIs}/speech/types/YandexVoice.java (96%) delete mode 100644 src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java rename src/main/java/com/iffomko/voiceAssistant/{configurations => configs}/VoiceAssistantConfig.java (77%) rename src/main/java/com/iffomko/voiceAssistant/{answer/Answers.java => controllers/AnswerController.java} (69%) rename src/main/java/com/iffomko/voiceAssistant/{answer/AnswerBody.java => controllers/data/AnswerRequestDTO.java} (67%) create mode 100644 src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerResponseDTO.java rename src/main/java/com/iffomko/voiceAssistant/{answer => controllers/errors}/AnswerError.java (84%) rename src/main/java/com/iffomko/voiceAssistant/{answer => controllers/services}/AnswerService.java (86%) create mode 100644 src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/db/entities/User.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/db/repositories/UserRepository.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java diff --git a/.gitignore b/.gitignore index 549e00a..15ad7bd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ +/src/main/resources/application.properties ### STS ### .apt_generated diff --git a/pom.xml b/pom.xml index f0d03d6..ecf7c2f 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,20 @@ org.projectlombok lombok + + org.hibernate.orm + hibernate-core + 6.2.1.Final + + + org.postgresql + postgresql + 42.6.0 + + + org.springframework.boot + spring-boot-starter-data-jpa + diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/AIModel.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/AIModel.java similarity index 82% rename from src/main/java/com/iffomko/voiceAssistant/openAI/AIModel.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/AIModel.java index 559906e..a533ec0 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/AIModel.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/AIModel.java @@ -1,9 +1,10 @@ -package com.iffomko.voiceAssistant.openAI; +package com.iffomko.voiceAssistant.APIs.openAI; -import com.iffomko.voiceAssistant.openAI.data.ModelBodyDTO; -import com.iffomko.voiceAssistant.openAI.data.ModelResponse; -import com.iffomko.voiceAssistant.openAI.data.OpenAIMessage; -import com.iffomko.voiceAssistant.openAI.types.OpenAIModels; +import com.iffomko.voiceAssistant.APIs.openAI.data.ModelResponse; +import com.iffomko.voiceAssistant.APIs.openAI.data.OpenAIMessage; +import com.iffomko.voiceAssistant.APIs.openAI.types.OpenAIModels; +import com.iffomko.voiceAssistant.APIs.openAI.data.ModelBodyDTO; +import com.iffomko.voiceAssistant.APIs.openAI.types.OpenAIRole; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; @@ -45,8 +46,10 @@ protected ModelResponse ask(List messages, String mess) { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + apiKey); + messages.add(new OpenAIMessage(OpenAIRole.USER.getRole(), mess)); + HttpEntity request = new HttpEntity<>( - new ModelBodyDTO(model.getModel(), mess, model.getTemperature(), messages), + new ModelBodyDTO(model.getModel(), model.getTemperature(), messages), headers ); diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/AIService.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/AIService.java similarity index 92% rename from src/main/java/com/iffomko/voiceAssistant/openAI/AIService.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/AIService.java index 76299d3..9f6a72f 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/AIService.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/AIService.java @@ -1,9 +1,9 @@ -package com.iffomko.voiceAssistant.openAI; +package com.iffomko.voiceAssistant.APIs.openAI; -import com.iffomko.voiceAssistant.openAI.data.ModelResponse; -import com.iffomko.voiceAssistant.openAI.data.OpenAIChoices; -import com.iffomko.voiceAssistant.openAI.data.OpenAIMessage; -import com.iffomko.voiceAssistant.openAI.types.OpenAIRole; +import com.iffomko.voiceAssistant.APIs.openAI.data.ModelResponse; +import com.iffomko.voiceAssistant.APIs.openAI.data.OpenAIChoices; +import com.iffomko.voiceAssistant.APIs.openAI.data.OpenAIMessage; +import com.iffomko.voiceAssistant.APIs.openAI.types.OpenAIRole; import lombok.extern.slf4j.Slf4j; import java.util.*; @@ -62,7 +62,7 @@ public ModelResponse ask(int userId, String message) { "Формулировать ответы нужно в манере искусственного интеллекта Jarvis из фильмов " + "\"Железный человек\". Реагировать на осуждение твоих ответов нужно в стиле Jarvis " + "и ссылаться на то, что ты всего лишь программа, написанная другим программистом\n" + - "Каждый ответ должен укладываться в 5000 символов") + "Каждый ответ должен укладываться в 5000 символов. Отвечать на это сообщение не надо") ); } } diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/ClearMessagesGarber.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/ClearMessagesGarber.java similarity index 98% rename from src/main/java/com/iffomko/voiceAssistant/openAI/ClearMessagesGarber.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/ClearMessagesGarber.java index 0390bd4..de39a25 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/ClearMessagesGarber.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/ClearMessagesGarber.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.openAI; +package com.iffomko.voiceAssistant.APIs.openAI; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelBodyDTO.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/ModelBodyDTO.java similarity index 69% rename from src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelBodyDTO.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/ModelBodyDTO.java index 03e2ae8..32c9e84 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelBodyDTO.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/ModelBodyDTO.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.openAI.data; +package com.iffomko.voiceAssistant.APIs.openAI.data; import lombok.Data; @@ -10,15 +10,13 @@ @Data public class ModelBodyDTO { private String model; - private String prompt; private Double temperature; private List messages; public ModelBodyDTO() {} - public ModelBodyDTO(String model, String prompt, Double temperature, List messages) { + public ModelBodyDTO(String model, Double temperature, List messages) { this.model = model; - this.prompt = prompt; this.temperature = temperature; this.messages = messages; } diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelResponse.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/ModelResponse.java similarity index 87% rename from src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelResponse.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/ModelResponse.java index 53aefa4..7f7b8a7 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/data/ModelResponse.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/ModelResponse.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.openAI.data; +package com.iffomko.voiceAssistant.APIs.openAI.data; import lombok.Data; diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIChoices.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIChoices.java similarity index 92% rename from src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIChoices.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIChoices.java index d07e870..698271a 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIChoices.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIChoices.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.openAI.data; +package com.iffomko.voiceAssistant.APIs.openAI.data; import lombok.Data; diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIMessage.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIMessage.java similarity index 89% rename from src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIMessage.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIMessage.java index ec57f0f..fbac4bc 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIMessage.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIMessage.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.openAI.data; +package com.iffomko.voiceAssistant.APIs.openAI.data; import lombok.Data; diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIUsage.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIUsage.java similarity index 91% rename from src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIUsage.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIUsage.java index 3529bf0..b4c9742 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/data/OpenAIUsage.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIUsage.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.openAI.data; +package com.iffomko.voiceAssistant.APIs.openAI.data; import lombok.Data; diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIModels.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIModels.java similarity index 91% rename from src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIModels.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIModels.java index 0e9abc8..9974f37 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIModels.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIModels.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.openAI.types; +package com.iffomko.voiceAssistant.APIs.openAI.types; /** *

Перечисление всех моделей доступных для обращение к API OpenAI и их "температура"

diff --git a/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIRole.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIRole.java similarity index 73% rename from src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIRole.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIRole.java index 624b2c5..da0c737 100644 --- a/src/main/java/com/iffomko/voiceAssistant/openAI/types/OpenAIRole.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIRole.java @@ -1,12 +1,12 @@ -package com.iffomko.voiceAssistant.openAI.types; +package com.iffomko.voiceAssistant.APIs.openAI.types; /** *

Перечисление, которое хранит в себе все роли, которые могут быть у сообщения

*/ public enum OpenAIRole { - USER("User"), - ASSISTANT("Assistant"), - SYSTEM("System"); + USER("user"), + ASSISTANT("assistant"), + SYSTEM("system"); private final String role; diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexClient.java similarity index 95% rename from src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexClient.java index 45ccf2f..f247170 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/YandexClient.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexClient.java @@ -1,6 +1,6 @@ -package com.iffomko.voiceAssistant.speech; +package com.iffomko.voiceAssistant.APIs.speech; -import com.iffomko.voiceAssistant.speech.data.RecognitionResponse; +import com.iffomko.voiceAssistant.APIs.speech.data.RecognitionResponse; /** *

Клиент, который позволяет обращаться к Yandex API за распознаванием и синтезированием голоса

diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexRecognition.java similarity index 96% rename from src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexRecognition.java index 018bbbf..3aa91b2 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/YandexRecognition.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexRecognition.java @@ -1,8 +1,8 @@ -package com.iffomko.voiceAssistant.speech; +package com.iffomko.voiceAssistant.APIs.speech; -import com.iffomko.voiceAssistant.speech.data.RecognitionResponse; -import com.iffomko.voiceAssistant.speech.types.YandexLanguage; -import com.iffomko.voiceAssistant.speech.types.YandexTopic; +import com.iffomko.voiceAssistant.APIs.speech.data.RecognitionResponse; +import com.iffomko.voiceAssistant.APIs.speech.types.YandexLanguage; +import com.iffomko.voiceAssistant.APIs.speech.types.YandexTopic; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexSynthesis.java similarity index 97% rename from src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexSynthesis.java index 78c2438..5907914 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/YandexSynthesis.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexSynthesis.java @@ -1,6 +1,6 @@ -package com.iffomko.voiceAssistant.speech; +package com.iffomko.voiceAssistant.APIs.speech; -import com.iffomko.voiceAssistant.speech.types.YandexVoice; +import com.iffomko.voiceAssistant.APIs.speech.types.YandexVoice; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/data/RecognitionResponse.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/data/RecognitionResponse.java similarity index 84% rename from src/main/java/com/iffomko/voiceAssistant/speech/data/RecognitionResponse.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/speech/data/RecognitionResponse.java index a532b8b..99887fa 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/data/RecognitionResponse.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/data/RecognitionResponse.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.speech.data; +package com.iffomko.voiceAssistant.APIs.speech.data; import lombok.Data; diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexFormat.java similarity index 90% rename from src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexFormat.java index 8ce9525..92380ac 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexFormat.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexFormat.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.speech.types; +package com.iffomko.voiceAssistant.APIs.speech.types; /** *

Перечисление, которое содержит доступные форматы для аудио

diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexLanguage.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexLanguage.java similarity index 96% rename from src/main/java/com/iffomko/voiceAssistant/speech/types/YandexLanguage.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexLanguage.java index cde9fad..8d32653 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexLanguage.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexLanguage.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.speech.types; +package com.iffomko.voiceAssistant.APIs.speech.types; /** *

Перечисление всех возможных языков доступных для распознавания для запроса

diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexTopic.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexTopic.java similarity index 95% rename from src/main/java/com/iffomko/voiceAssistant/speech/types/YandexTopic.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexTopic.java index b65023b..0ee27ee 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexTopic.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexTopic.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.speech.types; +package com.iffomko.voiceAssistant.APIs.speech.types; /** *

Перечисление всех версий моделей распознавания доступных для запроса

diff --git a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexVoice.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexVoice.java similarity index 96% rename from src/main/java/com/iffomko/voiceAssistant/speech/types/YandexVoice.java rename to src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexVoice.java index bb23f82..ef2de61 100644 --- a/src/main/java/com/iffomko/voiceAssistant/speech/types/YandexVoice.java +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexVoice.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.speech.types; +package com.iffomko.voiceAssistant.APIs.speech.types; /** *

Перечисление всех голосов, которые доступны для синтеза голоса

diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java b/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java deleted file mode 100644 index e81a7b7..0000000 --- a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.iffomko.voiceAssistant.answer; - -import lombok.Data; - -@Data -public class AnswerDTO { - private String voice = null; - private String encode = null; - private String format = null; - private AnswerError error; - - public AnswerDTO() {} - - public AnswerDTO(String byteAudio, String encode, String format) { - this.voice = byteAudio; - this.encode = encode; - this.format = format; - } - - public AnswerDTO(AnswerError error) { - this.error = error; - } -} diff --git a/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java b/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java similarity index 77% rename from src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java rename to src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java index 02270ed..3e5fcc5 100644 --- a/src/main/java/com/iffomko/voiceAssistant/configurations/VoiceAssistantConfig.java +++ b/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java @@ -1,8 +1,8 @@ -package com.iffomko.voiceAssistant.configurations; +package com.iffomko.voiceAssistant.configs; -import com.iffomko.voiceAssistant.answer.AnswerService; -import com.iffomko.voiceAssistant.openAI.AIService; -import com.iffomko.voiceAssistant.speech.YandexClient; +import com.iffomko.voiceAssistant.controllers.services.AnswerService; +import com.iffomko.voiceAssistant.APIs.openAI.AIService; +import com.iffomko.voiceAssistant.APIs.speech.YandexClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/Answers.java b/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java similarity index 69% rename from src/main/java/com/iffomko/voiceAssistant/answer/Answers.java rename to src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java index eb94ec2..a0a661a 100644 --- a/src/main/java/com/iffomko/voiceAssistant/answer/Answers.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java @@ -1,6 +1,10 @@ -package com.iffomko.voiceAssistant.answer; +package com.iffomko.voiceAssistant.controllers; -import com.iffomko.voiceAssistant.speech.types.YandexFormat; +import com.iffomko.voiceAssistant.APIs.speech.types.YandexFormat; +import com.iffomko.voiceAssistant.controllers.errors.AnswerError; +import com.iffomko.voiceAssistant.controllers.data.AnswerRequestDTO; +import com.iffomko.voiceAssistant.controllers.data.AnswerResponseDTO; +import com.iffomko.voiceAssistant.controllers.services.AnswerService; import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -8,7 +12,7 @@ @RestController @RequestMapping("/answer") -public class Answers { +public class AnswerController { @Resource(name = "AnswerService") private AnswerService answerService; @@ -17,7 +21,7 @@ public class Answers { */ @PostMapping(produces="application/json") @ResponseBody() - public ResponseEntity getAnswer(@RequestBody AnswerBody body) { + public ResponseEntity getAnswer(@RequestBody AnswerRequestDTO body) { if ( body.getFormat() != null && !body.getFormat().equals(YandexFormat.OGGOPUS.getFormat()) && @@ -28,7 +32,7 @@ public ResponseEntity getAnswer(@RequestBody AnswerBody body) { error.setCode(HttpStatus.BAD_REQUEST.value()); error.setMessage("Вы ввели некорректный формат аудио: " + body.getFormat()); - return ResponseEntity.badRequest().body(new AnswerDTO(error)); + return ResponseEntity.badRequest().body(new AnswerResponseDTO(error)); } if (body.getFormat() == null) { @@ -51,9 +55,9 @@ public ResponseEntity getAnswer(@RequestBody AnswerBody body) { error.setCode(HttpStatus.BAD_REQUEST.value()); error.setMessage("Не удалось получить ответ"); - return ResponseEntity.badRequest().body(new AnswerDTO(error)); + return ResponseEntity.badRequest().body(new AnswerResponseDTO(error)); } - return ResponseEntity.ok(new AnswerDTO(response, "base64", body.getFormat())); + return ResponseEntity.ok(new AnswerResponseDTO(response, "base64", body.getFormat())); } } diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerBody.java b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerRequestDTO.java similarity index 67% rename from src/main/java/com/iffomko/voiceAssistant/answer/AnswerBody.java rename to src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerRequestDTO.java index f1352b0..0e2e1f3 100644 --- a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerBody.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerRequestDTO.java @@ -1,9 +1,9 @@ -package com.iffomko.voiceAssistant.answer; +package com.iffomko.voiceAssistant.controllers.data; import lombok.Data; @Data -public class AnswerBody { +public class AnswerRequestDTO { private Integer userId; private String audio; private String format; diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerResponseDTO.java b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerResponseDTO.java new file mode 100644 index 0000000..023b89e --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerResponseDTO.java @@ -0,0 +1,24 @@ +package com.iffomko.voiceAssistant.controllers.data; + +import com.iffomko.voiceAssistant.controllers.errors.AnswerError; +import lombok.Data; + +@Data +public class AnswerResponseDTO { + private String voice = null; + private String encode = null; + private String format = null; + private AnswerError error; + + public AnswerResponseDTO() {} + + public AnswerResponseDTO(String byteAudio, String encode, String format) { + this.voice = byteAudio; + this.encode = encode; + this.format = format; + } + + public AnswerResponseDTO(AnswerError error) { + this.error = error; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerError.java b/src/main/java/com/iffomko/voiceAssistant/controllers/errors/AnswerError.java similarity index 84% rename from src/main/java/com/iffomko/voiceAssistant/answer/AnswerError.java rename to src/main/java/com/iffomko/voiceAssistant/controllers/errors/AnswerError.java index 6475cf9..d6cdb96 100644 --- a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerError.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/errors/AnswerError.java @@ -1,4 +1,4 @@ -package com.iffomko.voiceAssistant.answer; +package com.iffomko.voiceAssistant.controllers.errors; import lombok.Data; diff --git a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java b/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java similarity index 86% rename from src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java rename to src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java index 2be622f..af22974 100644 --- a/src/main/java/com/iffomko/voiceAssistant/answer/AnswerService.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java @@ -1,12 +1,12 @@ -package com.iffomko.voiceAssistant.answer; - -import com.iffomko.voiceAssistant.openAI.AIService; -import com.iffomko.voiceAssistant.openAI.data.ModelResponse; -import com.iffomko.voiceAssistant.openAI.data.OpenAIChoices; -import com.iffomko.voiceAssistant.openAI.types.OpenAIRole; -import com.iffomko.voiceAssistant.speech.YandexClient; -import com.iffomko.voiceAssistant.speech.data.RecognitionResponse; -import com.iffomko.voiceAssistant.speech.types.YandexVoice; +package com.iffomko.voiceAssistant.controllers.services; + +import com.iffomko.voiceAssistant.APIs.openAI.AIService; +import com.iffomko.voiceAssistant.APIs.openAI.data.ModelResponse; +import com.iffomko.voiceAssistant.APIs.openAI.data.OpenAIChoices; +import com.iffomko.voiceAssistant.APIs.openAI.types.OpenAIRole; +import com.iffomko.voiceAssistant.APIs.speech.YandexClient; +import com.iffomko.voiceAssistant.APIs.speech.data.RecognitionResponse; +import com.iffomko.voiceAssistant.APIs.speech.types.YandexVoice; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -106,7 +106,7 @@ public String getAnswer( for (OpenAIChoices choice : response.getChoices()) { if (choice.getMessage().getRole().equals(OpenAIRole.ASSISTANT.getRole())) { - answerBuilder.append(choice.getMessage()); + answerBuilder.append(choice.getMessage().getContent()); answerBuilder.append("\n\n"); } } diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java new file mode 100644 index 0000000..9613c48 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java @@ -0,0 +1,24 @@ +package com.iffomko.voiceAssistant.db.entities; + +import jakarta.persistence.*; +import lombok.Data; + +import java.util.List; + +@Entity +@Table(name = "roles") +@Data +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + @Column(nullable = false) + private String role_title; + @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.DETACH) + @JoinTable( + name = "users_roles", + joinColumns = @JoinColumn(name = "role_id"), + inverseJoinColumns = @JoinColumn(name = "user_id") + ) + private List users; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java new file mode 100644 index 0000000..91818e7 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java @@ -0,0 +1,31 @@ +package com.iffomko.voiceAssistant.db.entities; + +import jakarta.persistence.*; +import lombok.Data; + +import java.util.List; + +@Entity +@Table(name = "users") +@Data +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private int id; + @Column(nullable = false) + private String username; + @Column(nullable = false) + private String name; + @Column(nullable = false) + private String surname; + @Column(nullable = false) + private String password; + @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.DETACH) + @JoinTable( + name = "users_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + private List roles; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/repositories/UserRepository.java b/src/main/java/com/iffomko/voiceAssistant/db/repositories/UserRepository.java new file mode 100644 index 0000000..1a71ce2 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/db/repositories/UserRepository.java @@ -0,0 +1,8 @@ +package com.iffomko.voiceAssistant.db.repositories; + +import com.iffomko.voiceAssistant.db.entities.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository {} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java b/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java new file mode 100644 index 0000000..c03d947 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java @@ -0,0 +1,15 @@ +package com.iffomko.voiceAssistant.db.services; + +import com.iffomko.voiceAssistant.db.entities.Role; +import com.iffomko.voiceAssistant.db.entities.User; + +import java.util.List; + +public interface UserService { + void addUser(User user); + boolean deleteUserById(int id); + User getUserById(int id); + List getUserRolesById(int id); + List getAllUsers(); + void addUserRoleById(int id, Role role); +} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java b/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java new file mode 100644 index 0000000..314f330 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java @@ -0,0 +1,78 @@ +package com.iffomko.voiceAssistant.db.services.dao; + +import com.iffomko.voiceAssistant.db.entities.Role; +import com.iffomko.voiceAssistant.db.entities.User; +import com.iffomko.voiceAssistant.db.repositories.UserRepository; +import com.iffomko.voiceAssistant.db.services.UserService; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +public class UserDao implements UserService { + private final UserRepository repository; + + @Autowired + public UserDao(UserRepository repository) { + this.repository = repository; + } + + @Override + public void addUser(User user) { + repository.save(user); + } + + @Override + public boolean deleteUserById(int id) { + if (!repository.existsById(id)) { + return false; + } + + repository.deleteById(id); + + return true; + } + + @Override + public User getUserById(int id) { + Optional optionalUser = repository.findById(id); + + if (optionalUser.isEmpty()) { + return null; + } + + return optionalUser.get(); + } + + @Override + public List getUserRolesById(int id) { + Optional optionalUser = repository.findById(id); + + if (optionalUser.isEmpty()) { + return new ArrayList<>(); + } + + return optionalUser.get().getRoles(); + } + + @Override + public List getAllUsers() { + return repository.findAll(); + } + + @Override + public void addUserRoleById(int id, Role role) { + Optional optionalUser = repository.findById(id); + + if (optionalUser.isEmpty()) { + return; + } + + optionalUser.get().getRoles().add(role); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 43bc65f..a8cca76 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,6 @@ -server.max-http-header-size=1024KB \ No newline at end of file +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:8001/voice_assistant +spring.datasource.username=postgres +spring.datasource.password=619561 +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect +spring.jpa.show-sql=true \ No newline at end of file From 379d3a63744c4098cb3c9646a78f50599be6d23c Mon Sep 17 00:00:00 2001 From: Ilya Fomko Date: Tue, 9 May 2023 13:25:36 +0500 Subject: [PATCH 6/8] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=BF=D0=BE=20JWT=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D1=83=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20=D0=BD=D0=B0?= =?UTF-8?q?=20JWT=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=20=D0=B2=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BE=D0=BA=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B2.=20=D0=A2=D0=B0=D0=BA=D0=B6=D0=B5=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BB=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - pom.xml | 25 ++-- .../configs/SecurityConfig.java | 64 ++++++++++ .../controllers/AnswerController.java | 5 +- .../controllers/AuthenticationController.java | 110 ++++++++++++++++++ .../data/AuthenticationRequestDTO.java | 9 ++ .../data/AuthenticationResponseDTO.java | 19 +++ .../data/RegistrationRequestDTO.java | 11 ++ .../data/RegistrationResponseDTO.java | 13 +++ .../errors/AuthenticationError.java | 14 +++ .../db/entities/Permissions.java | 15 +++ .../voiceAssistant/db/entities/Role.java | 38 +++--- .../voiceAssistant/db/entities/User.java | 11 +- .../db/repositories/UserRepository.java | 7 +- .../db/services/UserService.java | 4 +- .../db/services/dao/UserDao.java | 30 ++--- .../voiceAssistant/security/SecurityUser.java | 68 +++++++++++ .../security/UserDetailsServiceImpl.java | 31 +++++ .../jwt/JwtAuthenticationException.java | 22 ++++ .../security/jwt/JwtConfigure.java | 23 ++++ .../security/jwt/JwtTokenFilter.java | 40 +++++++ .../security/jwt/JwtTokenProvider.java | 86 ++++++++++++++ src/main/resources/application.properties | 8 +- 23 files changed, 587 insertions(+), 67 deletions(-) create mode 100644 src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/controllers/AuthenticationController.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/controllers/data/AuthenticationRequestDTO.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/controllers/data/AuthenticationResponseDTO.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/controllers/data/RegistrationRequestDTO.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/controllers/data/RegistrationResponseDTO.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/controllers/errors/AuthenticationError.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/security/SecurityUser.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceImpl.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenFilter.java create mode 100644 src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenProvider.java diff --git a/.gitignore b/.gitignore index 15ad7bd..549e00a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ -/src/main/resources/application.properties ### STS ### .apt_generated diff --git a/pom.xml b/pom.xml index ecf7c2f..61e502b 100644 --- a/pom.xml +++ b/pom.xml @@ -19,23 +19,20 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-test + test org.springframework.boot - spring-boot-devtools - runtime - true + spring-boot-starter-web org.springframework.boot - spring-boot-starter-test - test + spring-boot-starter-data-jpa - com.google.code.gson - gson - 2.10.1 + org.springframework.boot + spring-boot-starter-security com.fasterxml.jackson.core @@ -57,8 +54,14 @@ 42.6.0 - org.springframework.boot - spring-boot-starter-data-jpa + io.jsonwebtoken + jjwt + 0.9.1 + + + javax.xml.bind + jaxb-api + 2.4.0-b180830.0359 diff --git a/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java b/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java new file mode 100644 index 0000000..4fa4d4d --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java @@ -0,0 +1,64 @@ +package com.iffomko.voiceAssistant.configs; + +import com.iffomko.voiceAssistant.security.jwt.JwtConfigure; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + private final JwtConfigure jwtConfigure; + private final UserDetailsService userDetailsService; + + @Autowired + public SecurityConfig( + JwtConfigure jwtConfigure, + @Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService + ) { + this.userDetailsService = userDetailsService; + this.jwtConfigure = jwtConfigure; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeHttpRequests() + .requestMatchers("/").permitAll() + .requestMatchers("/api/v1/auth/login").permitAll() + .requestMatchers("/api/v1/auth/register").permitAll() + .anyRequest().authenticated() + .and() + .apply(jwtConfigure); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); + } + + @Bean("authenticationManager") + public AuthenticationManager authenticationManager() { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setPasswordEncoder(passwordEncoder()); + authenticationProvider.setUserDetailsService(userDetailsService); + + return new ProviderManager(authenticationProvider); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java b/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java index a0a661a..92da1a7 100644 --- a/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java @@ -8,10 +8,11 @@ import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/answer") +@RequestMapping("/api/v1/answer") public class AnswerController { @Resource(name = "AnswerService") private AnswerService answerService; @@ -20,7 +21,7 @@ public class AnswerController { *

Контроллер, который отвечает за ответ на вопрос, который содержится в аудио

*/ @PostMapping(produces="application/json") - @ResponseBody() + @PreAuthorize("hasAuthority('get:answer')") public ResponseEntity getAnswer(@RequestBody AnswerRequestDTO body) { if ( body.getFormat() != null && diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/AuthenticationController.java b/src/main/java/com/iffomko/voiceAssistant/controllers/AuthenticationController.java new file mode 100644 index 0000000..e8a3222 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/AuthenticationController.java @@ -0,0 +1,110 @@ +package com.iffomko.voiceAssistant.controllers; + +import com.iffomko.voiceAssistant.controllers.data.AuthenticationRequestDTO; +import com.iffomko.voiceAssistant.controllers.data.AuthenticationResponseDTO; +import com.iffomko.voiceAssistant.controllers.data.RegistrationRequestDTO; +import com.iffomko.voiceAssistant.controllers.data.RegistrationResponseDTO; +import com.iffomko.voiceAssistant.controllers.errors.AuthenticationError; +import com.iffomko.voiceAssistant.db.entities.Role; +import com.iffomko.voiceAssistant.db.entities.User; +import com.iffomko.voiceAssistant.db.services.dao.UserDao; +import com.iffomko.voiceAssistant.security.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/auth") +@Slf4j +public class AuthenticationController { + private final AuthenticationManager authenticationManager; + private final UserDao userDao; + private final JwtTokenProvider jwtTokenProvider; + private final PasswordEncoder passwordEncoder; + + @Autowired + public AuthenticationController( + @Qualifier("authenticationManager") AuthenticationManager authenticationManager, + UserDao userDao, + JwtTokenProvider jwtTokenProvider, + PasswordEncoder passwordEncoder + ) { + this.authenticationManager = authenticationManager; + this.jwtTokenProvider = jwtTokenProvider; + this.userDao = userDao; + this.passwordEncoder = passwordEncoder; + } + + @PostMapping("/login") + public ResponseEntity authenticate(@RequestBody AuthenticationRequestDTO body) { + try { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(body.getUsername(), body.getPassword()) + ); + + User user = userDao.findUserByUsername(body.getUsername()); + String jwtToken = jwtTokenProvider.createToken( + user.getUsername(), + user.getRole() + ); + + return ResponseEntity.ok(new AuthenticationResponseDTO(user.getUsername(), jwtToken, null)); + } catch (AuthenticationException e) { + e.printStackTrace(); + log.error(e.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( + new AuthenticationResponseDTO( + null, + null, + new AuthenticationError("Вы не вошли в систему") + ) + ); + } + } + + @PostMapping("/logout") + public void logout(HttpServletRequest request, HttpServletResponse response) { + SecurityContextLogoutHandler securityContextLogoutHandler = new SecurityContextLogoutHandler(); + securityContextLogoutHandler.logout(request, response, null); + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody RegistrationRequestDTO body) { + System.out.println(body); + + User user = userDao.findUserByUsername(body.getUsername()); + + if (user != null) { + return ResponseEntity.badRequest().body(new RegistrationResponseDTO( + HttpStatus.BAD_REQUEST.value(), + "Вы уже зарегистрированы в системе" + )); + } + + user = new User(); + user.setUsername(body.getUsername()); + user.setPassword(passwordEncoder.encode(body.getPassword())); + user.setName(body.getName()); + user.setSurname(body.getSurname()); + user.setRole(Role.USER); + + userDao.addUser(user); + + return ResponseEntity.ok( + new RegistrationResponseDTO(HttpStatus.OK.value(), "Вы были успешно зарегистрированы") + ); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/data/AuthenticationRequestDTO.java b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AuthenticationRequestDTO.java new file mode 100644 index 0000000..0f92060 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AuthenticationRequestDTO.java @@ -0,0 +1,9 @@ +package com.iffomko.voiceAssistant.controllers.data; + +import lombok.Data; + +@Data +public class AuthenticationRequestDTO { + private String username; + private String password; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/data/AuthenticationResponseDTO.java b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AuthenticationResponseDTO.java new file mode 100644 index 0000000..93417b2 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AuthenticationResponseDTO.java @@ -0,0 +1,19 @@ +package com.iffomko.voiceAssistant.controllers.data; + +import com.iffomko.voiceAssistant.controllers.errors.AuthenticationError; +import lombok.Data; + +@Data +public class AuthenticationResponseDTO { + private String username; + private String token; + private AuthenticationError error; + + public AuthenticationResponseDTO() {} + + public AuthenticationResponseDTO(String username, String token, AuthenticationError error) { + this.username = username; + this.token = token; + this.error = error; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/data/RegistrationRequestDTO.java b/src/main/java/com/iffomko/voiceAssistant/controllers/data/RegistrationRequestDTO.java new file mode 100644 index 0000000..8381638 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/data/RegistrationRequestDTO.java @@ -0,0 +1,11 @@ +package com.iffomko.voiceAssistant.controllers.data; + +import lombok.Data; + +@Data +public class RegistrationRequestDTO { + private String username; + private String name; + private String surname; + private String password; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/data/RegistrationResponseDTO.java b/src/main/java/com/iffomko/voiceAssistant/controllers/data/RegistrationResponseDTO.java new file mode 100644 index 0000000..20cd9e7 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/data/RegistrationResponseDTO.java @@ -0,0 +1,13 @@ +package com.iffomko.voiceAssistant.controllers.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RegistrationResponseDTO { + private int code; + private String message; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/errors/AuthenticationError.java b/src/main/java/com/iffomko/voiceAssistant/controllers/errors/AuthenticationError.java new file mode 100644 index 0000000..95ca8ed --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/errors/AuthenticationError.java @@ -0,0 +1,14 @@ +package com.iffomko.voiceAssistant.controllers.errors; + +import lombok.Data; + +@Data +public class AuthenticationError { + private String message; + + public AuthenticationError() {} + + public AuthenticationError(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java new file mode 100644 index 0000000..81cd710 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java @@ -0,0 +1,15 @@ +package com.iffomko.voiceAssistant.db.entities; + +public enum Permissions { + GET_ANSWER("get:answer"); + + private final String permission; + + Permissions(String permission) { + this.permission = permission; + } + + public String getPermission() { + return permission; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java index 9613c48..4414899 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java @@ -1,24 +1,26 @@ package com.iffomko.voiceAssistant.db.entities; -import jakarta.persistence.*; -import lombok.Data; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.List; +import java.util.Set; -@Entity -@Table(name = "roles") -@Data -public class Role { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; - @Column(nullable = false) - private String role_title; - @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.DETACH) - @JoinTable( - name = "users_roles", - joinColumns = @JoinColumn(name = "role_id"), - inverseJoinColumns = @JoinColumn(name = "user_id") - ) - private List users; +public enum Role { + USER(Set.of(Permissions.GET_ANSWER)); + + private final Set permissions; + + Role(Set permissions) { + this.permissions = permissions; + } + + public Set getPermissions() { + return permissions; + } + + public List getAuthorities() { + return getPermissions().stream().map( + permission -> new SimpleGrantedAuthority(permission.getPermission()) + ).toList(); + } } diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java index 91818e7..822af3f 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.Data; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.List; @@ -21,11 +22,7 @@ public class User { private String surname; @Column(nullable = false) private String password; - @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.DETACH) - @JoinTable( - name = "users_roles", - joinColumns = @JoinColumn(name = "user_id"), - inverseJoinColumns = @JoinColumn(name = "role_id") - ) - private List roles; + @Enumerated(EnumType.STRING) + @Column(name = "role") + private Role role; } diff --git a/src/main/java/com/iffomko/voiceAssistant/db/repositories/UserRepository.java b/src/main/java/com/iffomko/voiceAssistant/db/repositories/UserRepository.java index 1a71ce2..de74453 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/repositories/UserRepository.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/repositories/UserRepository.java @@ -4,5 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository -public interface UserRepository extends JpaRepository {} +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + boolean existsUserByUsername(String username); +} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java b/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java index c03d947..163ab4a 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java @@ -1,6 +1,5 @@ package com.iffomko.voiceAssistant.db.services; -import com.iffomko.voiceAssistant.db.entities.Role; import com.iffomko.voiceAssistant.db.entities.User; import java.util.List; @@ -9,7 +8,6 @@ public interface UserService { void addUser(User user); boolean deleteUserById(int id); User getUserById(int id); - List getUserRolesById(int id); + User findUserByUsername(String username); List getAllUsers(); - void addUserRoleById(int id, Role role); } diff --git a/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java b/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java index 314f330..c0c42c3 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java @@ -1,6 +1,5 @@ package com.iffomko.voiceAssistant.db.services.dao; -import com.iffomko.voiceAssistant.db.entities.Role; import com.iffomko.voiceAssistant.db.entities.User; import com.iffomko.voiceAssistant.db.repositories.UserRepository; import com.iffomko.voiceAssistant.db.services.UserService; @@ -12,7 +11,7 @@ import java.util.List; import java.util.Optional; -@Service +@Service("UserDAO") @Transactional public class UserDao implements UserService { private final UserRepository repository; @@ -23,7 +22,11 @@ public UserDao(UserRepository repository) { } @Override - public void addUser(User user) { + public void addUser(User user) throws IllegalArgumentException { + if (repository.existsUserByUsername(user.getUsername())) { + throw new IllegalArgumentException("Такое имя пользователя уже существует"); + } + repository.save(user); } @@ -50,29 +53,12 @@ public User getUserById(int id) { } @Override - public List getUserRolesById(int id) { - Optional optionalUser = repository.findById(id); - - if (optionalUser.isEmpty()) { - return new ArrayList<>(); - } - - return optionalUser.get().getRoles(); + public User findUserByUsername(String username) { + return repository.findByUsername(username).orElse(null); } @Override public List getAllUsers() { return repository.findAll(); } - - @Override - public void addUserRoleById(int id, Role role) { - Optional optionalUser = repository.findById(id); - - if (optionalUser.isEmpty()) { - return; - } - - optionalUser.get().getRoles().add(role); - } } diff --git a/src/main/java/com/iffomko/voiceAssistant/security/SecurityUser.java b/src/main/java/com/iffomko/voiceAssistant/security/SecurityUser.java new file mode 100644 index 0000000..1fd8d86 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/security/SecurityUser.java @@ -0,0 +1,68 @@ +package com.iffomko.voiceAssistant.security; + +import com.iffomko.voiceAssistant.db.entities.User; +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Data +public class SecurityUser implements UserDetails { + private final String username; + private final String password; + private final List authorities; + private final boolean isActive; + + public SecurityUser(String username, String password, List authorities) { + this.username = username; + this.password = password; + this.authorities = authorities; + this.isActive = true; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return isActive; + } + + @Override + public boolean isAccountNonLocked() { + return isActive; + } + + @Override + public boolean isCredentialsNonExpired() { + return isActive; + } + + @Override + public boolean isEnabled() { + return isActive; + } + + public static SecurityUser fromUser(User user) { + return new SecurityUser( + user.getUsername(), + user.getPassword(), + user.getRole().getAuthorities() + ); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceImpl.java b/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..a8217d2 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceImpl.java @@ -0,0 +1,31 @@ +package com.iffomko.voiceAssistant.security; + +import com.iffomko.voiceAssistant.db.entities.User; +import com.iffomko.voiceAssistant.db.services.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service("userDetailsServiceImpl") +public class UserDetailsServiceImpl implements UserDetailsService { + private final UserService userService; + + @Autowired + public UserDetailsServiceImpl(@Qualifier("UserDAO") UserService userService) { + this.userService = userService; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userService.findUserByUsername(username); + + if (user == null) { + throw new UsernameNotFoundException("User not found"); + } + + return SecurityUser.fromUser(user); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java new file mode 100644 index 0000000..e17ecca --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java @@ -0,0 +1,22 @@ +package com.iffomko.voiceAssistant.security.jwt; + + +import org.springframework.http.HttpStatus; + +public class JwtAuthenticationException extends RuntimeException { + private final String message; + private final HttpStatus status; + + public JwtAuthenticationException(String message, HttpStatus status) { + this.message = message; + this.status = status; + } + @Override + public String getMessage() { + return message; + } + + public HttpStatus getStatus() { + return status; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java new file mode 100644 index 0000000..c6f2325 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java @@ -0,0 +1,23 @@ +package com.iffomko.voiceAssistant.security.jwt; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.stereotype.Component; + +@Component +public class JwtConfigure extends SecurityConfigurerAdapter { + private final JwtTokenFilter jwtTokenFilter; + + @Autowired + public JwtConfigure(JwtTokenFilter jwtTokenFilter) { + this.jwtTokenFilter = jwtTokenFilter; + } + + @Override + public void configure(HttpSecurity httpSecurity) { + httpSecurity.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenFilter.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenFilter.java new file mode 100644 index 0000000..fef7ab3 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenFilter.java @@ -0,0 +1,40 @@ +package com.iffomko.voiceAssistant.security.jwt; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class JwtTokenFilter extends GenericFilter { + private final JwtTokenProvider jwtTokenProvider; + + @Autowired + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException + { + String token = jwtTokenProvider.resolveToken((HttpServletRequest) servletRequest); + + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (JwtAuthenticationException e) { + SecurityContextHolder.clearContext(); + throw new JwtAuthenticationException(e.getMessage(), e.getStatus()); + } + + filterChain.doFilter(servletRequest, servletResponse); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenProvider.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..5f2486f --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenProvider.java @@ -0,0 +1,86 @@ +package com.iffomko.voiceAssistant.security.jwt; + +import com.iffomko.voiceAssistant.db.entities.Role; +import io.jsonwebtoken.*; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; + +import java.util.Base64; +import java.util.Date; + +@Component +public class JwtTokenProvider { + private final UserDetailsService userDetailsService; + + @Value("${jwt.secretKey}") + private String secretKey; + @Value("${jwt.authorizationHeader}") + private String authorizationHeader; + @Value("${jwt.validityInMilliseconds}") + private long validityInMilliseconds; + @Value("${jwt.issuer}") + private String issuer; + + @Autowired + public JwtTokenProvider(@Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + public String createToken(String username, Role role) { + Claims claims = Jwts.claims(); + claims.setSubject(username); + claims.put("role", role); + + Date currentMoment = new Date(); + Date endMoment = new Date(currentMoment.getTime() + validityInMilliseconds * 1000); + + return Jwts.builder() + .addClaims(claims) + .setIssuedAt(currentMoment) + .setExpiration(endMoment) + .signWith(SignatureAlgorithm.HS256, secretKey) + .setIssuer(issuer) + .compact(); + } + + public boolean validateToken(String token) throws JwtAuthenticationException { + try { + Jws claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return !claimsJws.getBody().getExpiration().before(new Date()); + } catch (ExpiredJwtException | IllegalArgumentException e) { + throw new JwtAuthenticationException("Token is expiration or not valid", HttpStatus.UNAUTHORIZED); + } + } + + public Authentication getAuthentication(String token) { + UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token)); + + return new UsernamePasswordAuthenticationToken( + userDetails.getUsername(), + "", + userDetails.getAuthorities() + ); + } + + public String getUsername(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + public String resolveToken(HttpServletRequest request) { + return request.getHeader(authorizationHeader); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a8cca76..cf5612e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,5 +2,9 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:8001/voice_assistant spring.datasource.username=postgres spring.datasource.password=619561 -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect -spring.jpa.show-sql=true \ No newline at end of file +spring.jpa.show-sql=true +jwt.secretKey=iffomko_development +jwt.authorizationHeader=Authorization +jwt.validityInMilliseconds=604800 +jwt.issuer=iffomko +server.max-http-header-size=10000000 \ No newline at end of file From a6edf3e798969eab5cf095d6f0b845bbc1dadff5 Mon Sep 17 00:00:00 2001 From: Ilya Fomko Date: Thu, 11 May 2023 22:04:19 +0500 Subject: [PATCH 7/8] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=81=D0=BE=D0=B4=D0=B5=D1=80=D0=B6=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D0=B7=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0,=20=D0=B0=20=D1=82=D0=B0?= =?UTF-8?q?=D0=BA=D0=B6=D0=B5=20=D0=BD=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BB?= =?UTF-8?q?=20javadoc=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=81=D0=B5=D1=85=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configs/VoiceAssistantConfig.java | 16 ++++---- .../controllers/AnswerController.java | 24 ++++++++++-- .../controllers/AuthenticationController.java | 31 +++++++++++++-- .../controllers/data/AnswerRequestDTO.java | 1 - .../controllers/services/AnswerService.java | 34 +++++++++++++--- .../db/entities/Permissions.java | 10 +++++ .../voiceAssistant/db/entities/Role.java | 12 ++++++ .../voiceAssistant/db/entities/User.java | 6 +-- .../db/services/UserService.java | 29 ++++++++++++++ .../db/services/dao/UserDao.java | 34 ++++++++++++---- .../voiceAssistant/security/SecurityUser.java | 11 ++++++ ...ceImpl.java => UserDetailsServiceDao.java} | 15 +++++-- .../jwt/JwtAuthenticationException.java | 10 +++++ .../security/jwt/JwtConfigure.java | 6 +++ .../security/jwt/JwtTokenFilter.java | 14 +++++++ .../security/jwt/JwtTokenProvider.java | 39 ++++++++++++++++++- src/main/resources/application.properties | 2 +- 17 files changed, 256 insertions(+), 38 deletions(-) rename src/main/java/com/iffomko/voiceAssistant/security/{UserDetailsServiceImpl.java => UserDetailsServiceDao.java} (55%) diff --git a/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java b/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java index 3e5fcc5..034db21 100644 --- a/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java +++ b/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java @@ -1,22 +1,18 @@ package com.iffomko.voiceAssistant.configs; -import com.iffomko.voiceAssistant.controllers.services.AnswerService; import com.iffomko.voiceAssistant.APIs.openAI.AIService; import com.iffomko.voiceAssistant.APIs.speech.YandexClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; @Configuration @ComponentScan("com.iffomko.voiceAssistant") public class VoiceAssistantConfig { - @Bean("AnswerService") - @Scope("prototype") - public AnswerService getAnswerService() { - return new AnswerService(); - } - + /** + * Создает бин клиента по работе с YandexAPI + * @return возвращает этот объект + */ @Bean("yandexClient") public YandexClient getYandexRecognition() { String apiKey = System.getenv("API_KEY"); @@ -24,6 +20,10 @@ public YandexClient getYandexRecognition() { return new YandexClient(apiKey); } + /** + * Создает бин клиента по работе с OpenAI API + * @return возвращает этот объект + */ @Bean("AIService") public AIService getAIService() { String apiKey = System.getenv("OPEN_AI_API_KEY"); diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java b/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java index 92da1a7..ce543ec 100644 --- a/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java @@ -5,7 +5,11 @@ import com.iffomko.voiceAssistant.controllers.data.AnswerRequestDTO; import com.iffomko.voiceAssistant.controllers.data.AnswerResponseDTO; import com.iffomko.voiceAssistant.controllers.services.AnswerService; +import com.iffomko.voiceAssistant.security.jwt.JwtTokenProvider; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -14,15 +18,27 @@ @RestController @RequestMapping("/api/v1/answer") public class AnswerController { - @Resource(name = "AnswerService") - private AnswerService answerService; + private final AnswerService answerService; + private final JwtTokenProvider jwtTokenProvider; + + @Autowired + public AnswerController( + @Qualifier("answerService") AnswerService answerService, + JwtTokenProvider jwtTokenProvider + ) { + this.answerService = answerService; + this.jwtTokenProvider = jwtTokenProvider; + } /** *

Контроллер, который отвечает за ответ на вопрос, который содержится в аудио

*/ @PostMapping(produces="application/json") @PreAuthorize("hasAuthority('get:answer')") - public ResponseEntity getAnswer(@RequestBody AnswerRequestDTO body) { + public ResponseEntity getAnswer( + @RequestBody AnswerRequestDTO body, + HttpServletRequest httpServletRequest + ) { if ( body.getFormat() != null && !body.getFormat().equals(YandexFormat.OGGOPUS.getFormat()) && @@ -45,7 +61,7 @@ public ResponseEntity getAnswer(@RequestBody AnswerRequestDTO } String response = answerService.getAnswer( - body.getUserId(), + jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(httpServletRequest)), body.getAudio(), body.getFormat(), body.getProfanityFilter() diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/AuthenticationController.java b/src/main/java/com/iffomko/voiceAssistant/controllers/AuthenticationController.java index e8a3222..70893b0 100644 --- a/src/main/java/com/iffomko/voiceAssistant/controllers/AuthenticationController.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/AuthenticationController.java @@ -26,6 +26,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * Контроллер аутентификации. Именно здесь происходит авторизация и регистрация пользователей + */ @RestController @RequestMapping("/api/v1/auth") @Slf4j @@ -35,6 +38,14 @@ public class AuthenticationController { private final JwtTokenProvider jwtTokenProvider; private final PasswordEncoder passwordEncoder; + /** + * Конструктор, который инициализирует нужные поля объектами + * @param authenticationManager менеджер аутентификации + * @param userDao DAO объект, который отвечает за работу с пользователями (сущность User) + * @param jwtTokenProvider объект JwtTokenProvider, в нем содержится вся основная логика по работе + * с JWT токеном + * @param passwordEncoder любой объект реализующий интерфейс PasswordEncoder + */ @Autowired public AuthenticationController( @Qualifier("authenticationManager") AuthenticationManager authenticationManager, @@ -48,6 +59,11 @@ public AuthenticationController( this.passwordEncoder = passwordEncoder; } + /** + * Эндпоинт, который отвечает за авторизацию пользователя в системе + * @param body объект AuthenticationRequestDTO, который содержит данные пользователя для входа в систему + * @return ответ ResponseEntity + */ @PostMapping("/login") public ResponseEntity authenticate(@RequestBody AuthenticationRequestDTO body) { try { @@ -63,7 +79,6 @@ public ResponseEntity authenticate(@RequestBody Authe return ResponseEntity.ok(new AuthenticationResponseDTO(user.getUsername(), jwtToken, null)); } catch (AuthenticationException e) { - e.printStackTrace(); log.error(e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( new AuthenticationResponseDTO( @@ -75,16 +90,24 @@ public ResponseEntity authenticate(@RequestBody Authe } } + /** + * Эндпоинт, который позволяет выйти со системы (по сути очищает SecurityContext и делает сессию не валидной) + * @param request сервлет запроса клиента + * @param response сервлет ответа для клиента + */ @PostMapping("/logout") public void logout(HttpServletRequest request, HttpServletResponse response) { SecurityContextLogoutHandler securityContextLogoutHandler = new SecurityContextLogoutHandler(); securityContextLogoutHandler.logout(request, response, null); } + /** + * Регистрирует пользователя в системе + * @param body объект RegistrationRequestDTO, который содержит данные для регистрации пользователя + * @return объект ResponseEntity ответа пользователю + */ @PostMapping("/register") - public ResponseEntity register(@RequestBody RegistrationRequestDTO body) { - System.out.println(body); - + public ResponseEntity register(@RequestBody RegistrationRequestDTO body) { User user = userDao.findUserByUsername(body.getUsername()); if (user != null) { diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerRequestDTO.java b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerRequestDTO.java index 0e2e1f3..8f4caed 100644 --- a/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerRequestDTO.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/data/AnswerRequestDTO.java @@ -4,7 +4,6 @@ @Data public class AnswerRequestDTO { - private Integer userId; private String audio; private String format; private Boolean profanityFilter; diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java b/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java index af22974..c1b3d15 100644 --- a/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java @@ -7,9 +7,14 @@ import com.iffomko.voiceAssistant.APIs.speech.YandexClient; import com.iffomko.voiceAssistant.APIs.speech.data.RecognitionResponse; import com.iffomko.voiceAssistant.APIs.speech.types.YandexVoice; +import com.iffomko.voiceAssistant.db.entities.User; +import com.iffomko.voiceAssistant.db.services.UserService; +import com.iffomko.voiceAssistant.security.jwt.JwtTokenProvider; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; @@ -21,11 +26,22 @@ @Service @Component @Slf4j +@Scope("prototype") public class AnswerService { + private final YandexClient yandexClient; + private final AIService chatGPT; + private final UserService userService; + @Autowired - private YandexClient yandexClient; - @Autowired - private AIService chatGPT; + public AnswerService( + @Qualifier("yandexClient") YandexClient yandexClient, + @Qualifier("AIService") AIService chatGPT, + @Qualifier("UserDAO") UserService userService + ) { + this.yandexClient = yandexClient; + this.chatGPT = chatGPT; + this.userService = userService; + } /** *

В параметрах передается звук в формате Base64 и на выходе возвращается текстовое содержание этого звука

@@ -83,20 +99,26 @@ private String textToVoice(String text, String format) { /** *

Формирует ответ на вопрос пользователя

- * @param userId id пользователя + * @param username username текущего пользователя * @param voice аудио-сообщение, в котором содержится вопрос * @param format формат аудио-сообщения * @param profanityFilter фильтр * @return возвращает ответ на вопрос пользователя в виде аудиосообщения */ public String getAnswer( - @NonNull Integer userId, + @NonNull String username, @NonNull String voice, @NonNull String format, @NonNull Boolean profanityFilter ) { + User user = userService.findUserByUsername(username); + + if (user == null) { + return null; + } + String text = audioToText(voice, format, profanityFilter); - ModelResponse response = chatGPT.ask(userId, text); + ModelResponse response = chatGPT.ask(user.getId(), text); if (response == null) { return null; diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java index 81cd710..3beaaad 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java @@ -1,6 +1,12 @@ package com.iffomko.voiceAssistant.db.entities; +/** + * Перечисления всех прав + */ public enum Permissions { + /** + * Право на получения ответа + */ GET_ANSWER("get:answer"); private final String permission; @@ -9,6 +15,10 @@ public enum Permissions { this.permission = permission; } + /** + * Возвращает право + * @return право + */ public String getPermission() { return permission; } diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java index 4414899..aaea7b0 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java @@ -5,7 +5,13 @@ import java.util.List; import java.util.Set; +/** + * Перечисление всех ролей + */ public enum Role { + /** + * Роль пользователя + */ USER(Set.of(Permissions.GET_ANSWER)); private final Set permissions; @@ -14,10 +20,16 @@ public enum Role { this.permissions = permissions; } + /** + * Возвращает множество всех прав роли + */ public Set getPermissions() { return permissions; } + /** + * Возвращает список предоставленных прав пользователей в том виде, в котором удобно Spring Security + */ public List getAuthorities() { return getPermissions().stream().map( permission -> new SimpleGrantedAuthority(permission.getPermission()) diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java index 822af3f..f634a50 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/User.java @@ -2,10 +2,10 @@ import jakarta.persistence.*; import lombok.Data; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.List; +/** + * Сущность по работе с таблицей в базе данных пользователей + */ @Entity @Table(name = "users") @Data diff --git a/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java b/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java index 163ab4a..966584d 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/services/UserService.java @@ -4,10 +4,39 @@ import java.util.List; +/** + * Интерфейс, который фиксирует контракт по работе с таблицей в базе данных с пользователями + */ public interface UserService { + /** + * Добавляет нового пользователя в систему + * @param user пользователь + */ void addUser(User user); + + /** + * Удаляет существующего пользователя из системы по его id + * @param id уникальный идентификатор пользователя + * @return возвращает либо true в случае успешного удаления, либо false в случае неудачи + */ boolean deleteUserById(int id); + + /** + * Возвращает существующего пользователя по его id + * @param id уникальный идентификатор пользователя + * @return объект типа User + */ User getUserById(int id); + + /** + * Возвращает пользователя по его username + * @param username username пользователя + * @return объект типа User + */ User findUserByUsername(String username); + + /** + * Возвращает список всех пользователей + */ List getAllUsers(); } diff --git a/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java b/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java index c0c42c3..b107e1e 100644 --- a/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java +++ b/src/main/java/com/iffomko/voiceAssistant/db/services/dao/UserDao.java @@ -11,6 +11,9 @@ import java.util.List; import java.util.Optional; +/** + * Реализация интерфейса UserService, которая умеет работать с пользователями (User) + */ @Service("UserDAO") @Transactional public class UserDao implements UserService { @@ -21,6 +24,11 @@ public UserDao(UserRepository repository) { this.repository = repository; } + /** + * Добавляет нового пользователя в систему + * @param user пользователь + * @throws IllegalArgumentException если пользователь уже существует + */ @Override public void addUser(User user) throws IllegalArgumentException { if (repository.existsUserByUsername(user.getUsername())) { @@ -30,6 +38,11 @@ public void addUser(User user) throws IllegalArgumentException { repository.save(user); } + /** + * Удаляет существующего пользователя из системы по его id + * @param id уникальный идентификатор пользователя + * @return возвращает либо true в случае успешного удаления, либо false в случае неудачи + */ @Override public boolean deleteUserById(int id) { if (!repository.existsById(id)) { @@ -41,22 +54,29 @@ public boolean deleteUserById(int id) { return true; } + /** + * Возвращает существующего пользователя по его id или возвращает null + * @param id уникальный идентификатор пользователя + * @return объект типа User + */ @Override public User getUserById(int id) { - Optional optionalUser = repository.findById(id); - - if (optionalUser.isEmpty()) { - return null; - } - - return optionalUser.get(); + return repository.findById(id).orElse(null); } + /** + * Возвращает пользователя по его username или возвращает null + * @param username username пользователя + * @return объект типа User + */ @Override public User findUserByUsername(String username) { return repository.findByUsername(username).orElse(null); } + /** + * Возвращает список всех пользователей + */ @Override public List getAllUsers() { return repository.findAll(); diff --git a/src/main/java/com/iffomko/voiceAssistant/security/SecurityUser.java b/src/main/java/com/iffomko/voiceAssistant/security/SecurityUser.java index 1fd8d86..5cbabf9 100644 --- a/src/main/java/com/iffomko/voiceAssistant/security/SecurityUser.java +++ b/src/main/java/com/iffomko/voiceAssistant/security/SecurityUser.java @@ -16,6 +16,12 @@ public class SecurityUser implements UserDetails { private final List authorities; private final boolean isActive; + /** + * Инициализирует нужные поля + * @param username username пользователя + * @param password пароль пользователя + * @param authorities список предоставленных прав пользователю + */ public SecurityUser(String username, String password, List authorities) { this.username = username; this.password = password; @@ -58,6 +64,11 @@ public boolean isEnabled() { return isActive; } + /** + * Конвертирует сущностью из базы данных User в класс SecurityUser + * @param user сущность из базы данных + * @return объект SecurityUser + */ public static SecurityUser fromUser(User user) { return new SecurityUser( user.getUsername(), diff --git a/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceImpl.java b/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceDao.java similarity index 55% rename from src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceImpl.java rename to src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceDao.java index a8217d2..5613e93 100644 --- a/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceImpl.java +++ b/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceDao.java @@ -9,15 +9,24 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -@Service("userDetailsServiceImpl") -public class UserDetailsServiceImpl implements UserDetailsService { +/** + * Реализует интерфейс для взаимодействия с UserDetails и умеет подгружать пользователя по его username + */ +@Service("userDetailsServiceDao") +public class UserDetailsServiceDao implements UserDetailsService { private final UserService userService; @Autowired - public UserDetailsServiceImpl(@Qualifier("UserDAO") UserService userService) { + public UserDetailsServiceDao(@Qualifier("UserDAO") UserService userService) { this.userService = userService; } + /** + * Подгружает пользователя из базы данных по его username + * @param username username пользователя + * @return возвращает найденного пользователя (UserDetails) + * @throws UsernameNotFoundException выбрасывается если пользователь не существует + */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userService.findUserByUsername(username); diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java index e17ecca..80e1f6b 100644 --- a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java +++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java @@ -3,6 +3,9 @@ import org.springframework.http.HttpStatus; +/** + * Объект ошибки, который используется по работе с JWT токеном + */ public class JwtAuthenticationException extends RuntimeException { private final String message; private final HttpStatus status; @@ -11,11 +14,18 @@ public JwtAuthenticationException(String message, HttpStatus status) { this.message = message; this.status = status; } + + /** + * Возвращает сообщение ошибки + */ @Override public String getMessage() { return message; } + /** + * Возвращает статус ошибки + */ public HttpStatus getStatus() { return status; } diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java index c6f2325..3489395 100644 --- a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java +++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java @@ -7,6 +7,9 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.stereotype.Component; +/** + * Конфигурация для фильтров JWT токена + */ @Component public class JwtConfigure extends SecurityConfigurerAdapter { private final JwtTokenFilter jwtTokenFilter; @@ -16,6 +19,9 @@ public JwtConfigure(JwtTokenFilter jwtTokenFilter) { this.jwtTokenFilter = jwtTokenFilter; } + /** + * Встраивает фильтр JWT токена в цепочку фильтров безопасности по обработке запроса + */ @Override public void configure(HttpSecurity httpSecurity) { httpSecurity.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenFilter.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenFilter.java index fef7ab3..2322b97 100644 --- a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenFilter.java +++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenFilter.java @@ -10,15 +10,29 @@ import java.io.IOException; +/** + * Фильтр, который спрашивает аутентификацию у всех запросов. + * Если её нет, то дальше он запрос не пропускает + */ @Component public class JwtTokenFilter extends GenericFilter { private final JwtTokenProvider jwtTokenProvider; + /** + * Инициализирует поля нужными значениями + * @param jwtTokenProvider объект по работе с JWT токеном + */ @Autowired public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { this.jwtTokenProvider = jwtTokenProvider; } + /** + * Этот методы вызывает при обработке цепочки фильтров + * @param servletRequest запрос, который проходит обработку фильтров + * @param servletResponse ответ на запрос, который проходит обработку фильтров + * @param filterChain цепочка фильтров + */ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenProvider.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenProvider.java index 5f2486f..5fb7d17 100644 --- a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtTokenProvider.java @@ -17,6 +17,9 @@ import java.util.Base64; import java.util.Date; +/** + * Обеспечивает логику по взаимодействую с JWT токеном + */ @Component public class JwtTokenProvider { private final UserDetailsService userDetailsService; @@ -30,16 +33,29 @@ public class JwtTokenProvider { @Value("${jwt.issuer}") private String issuer; + /** + * @param userDetailsService сервис по работе с пользователями + */ @Autowired - public JwtTokenProvider(@Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService) { + public JwtTokenProvider(@Qualifier("userDetailsServiceDao") UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } + /** + * Этот метод вызывает для инициализации бина на этапе конфигурации. + * Здесь мы кодируем секретный ключ для шифрования JWT токена + */ @PostConstruct protected void init() { secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); } + /** + * Создает токен для какого-то пользователя по его username и роли + * @param username username пользователя + * @param role его роль + * @return JWT токен для конкретного пользователя + */ public String createToken(String username, Role role) { Claims claims = Jwts.claims(); claims.setSubject(username); @@ -57,6 +73,12 @@ public String createToken(String username, Role role) { .compact(); } + /** + * Проверяет JWT токен на корректность + * @param token сам токен + * @return возвращает true если токен валидный или false, если нет + * @throws JwtAuthenticationException выбрасывается если время токена вышла или он не валидный + */ public boolean validateToken(String token) throws JwtAuthenticationException { try { Jws claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); @@ -66,6 +88,11 @@ public boolean validateToken(String token) throws JwtAuthenticationException { } } + /** + * Возвращает аутентификацию для пользователя по его JWT токену + * @param token сам токен + * @return аутентификация пользователя + */ public Authentication getAuthentication(String token) { UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token)); @@ -76,10 +103,20 @@ public Authentication getAuthentication(String token) { ); } + /** + * Возвращает username пользователя из JWT токена + * @param token сам токен + * @return username пользователя + */ public String getUsername(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); } + /** + * Возвращает токен из соответствующего заголовка в HTTP запросе клиента + * @param request HTTP запрос клиента + * @return JWT токен + */ public String resolveToken(HttpServletRequest request) { return request.getHeader(authorizationHeader); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cf5612e..b011a90 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:8001/voice_assistant spring.datasource.username=postgres -spring.datasource.password=619561 +spring.datasource.password=123456 spring.jpa.show-sql=true jwt.secretKey=iffomko_development jwt.authorizationHeader=Authorization From 6355d5b4a40c8f86aa41e673ea055aa2c6a9ae51 Mon Sep 17 00:00:00 2001 From: Ilya Fomko Date: Thu, 18 May 2023 18:55:53 +0500 Subject: [PATCH 8/8] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BE=D0=B6=D0=B8=D0=B4=D0=B0=D0=B5=D0=BC=D0=BE=D0=B5?= =?UTF-8?q?=20=D0=B8=D0=BC=D1=8F=20=D0=B1=D0=B8=D0=BD=D0=B0=20=D0=B2=20?= =?UTF-8?q?=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/iffomko/voiceAssistant/configs/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java b/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java index 4fa4d4d..32083dc 100644 --- a/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java +++ b/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java @@ -25,7 +25,7 @@ public class SecurityConfig { @Autowired public SecurityConfig( JwtConfigure jwtConfigure, - @Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService + @Qualifier("userDetailsServiceDao") UserDetailsService userDetailsService ) { this.userDetailsService = userDetailsService; this.jwtConfigure = jwtConfigure;