Implement slack

This commit is contained in:
woozu-shin 2024-05-12 23:08:10 +09:00
parent e8ec96ed27
commit 8502b95a7d
34 changed files with 606 additions and 77 deletions

View File

@ -3,6 +3,7 @@ plugins {
id 'idea' id 'idea'
id 'org.springframework.boot' version '2.5.4' id 'org.springframework.boot' version '2.5.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'com.google.cloud.tools.jib' version '3.4.0'
} }
group = 'com.myoa.engineering.crawl.shopping' group = 'com.myoa.engineering.crawl.shopping'
@ -19,14 +20,16 @@ repositories {
mavenCentral() mavenCentral()
} }
allprojects { subprojects {
group = 'com.myoa.engineering.crawl.shopping' group = 'com.myoa.engineering.crawl.shopping'
version = '1.1.1' version = '1.1.1'
sourceCompatibility = JavaVersion.VERSION_17
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'idea' apply plugin: 'idea'
apply plugin: 'org.springframework.boot' apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management' apply plugin: 'io.spring.dependency-management'
apply plugin: "com.google.cloud.tools.jib"
repositories { repositories {
mavenCentral() mavenCentral()
@ -37,8 +40,11 @@ allprojects {
ext { ext {
set('springCloudVersion', "2020.0.4") set('springCloudVersion', "2020.0.4")
set("BASE_IMAGE_REGISTRY_URL", "192.168.0.10:10001")
} }
apply from: "${project.rootDir}/jib.gradle"
dependencyManagement { dependencyManagement {
imports { imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"

View File

@ -4,3 +4,7 @@ distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.parallel.threads=4
org.gradle.daemon=true

28
jib.gradle Normal file
View File

@ -0,0 +1,28 @@
jib.setAllowInsecureRegistries(true)
jib {
from {
image = "eclipse-temurin:17-jdk"
platforms {
platform {
architecture = "amd64"
os = "linux"
}
}
}
to {
image = "${BASE_IMAGE_REGISTRY_URL}/${project.name}"
tags = ["latest"]
}
container {
// entrypoint = ["INHERIT"]
creationTime = "USE_CURRENT_TIMESTAMP"
environment = [
"TZ": "Asia/Seoul",
]
jvmFlags = [
"--add-opens=java.base/java.time=ALL-UNNAMED"
]
}
}

View File

@ -24,7 +24,7 @@ public class H2ConsoleConfiguration {
@EventListener(ContextRefreshedEvent.class) @EventListener(ContextRefreshedEvent.class)
public void start() throws SQLException { public void start() throws SQLException {
log.info("starting h2 console"); log.info("starting h2 console");
this.webServer = Server.createWebServer("-webPort", port, "-tcpAllowOthers").start(); this.webServer = Server.createWebServer("-webPort", port, "-tcpAllowOthers", "-webAllowOthers").start();
} }
@EventListener(ContextClosedEvent.class) @EventListener(ContextClosedEvent.class)

View File

@ -0,0 +1,28 @@
package com.myoa.engineering.crawl.shopping.configuration.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
/*
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
WebMvcConfigurer.super.configureMessageConverters(converters);
addJsonFormUrlEncodedConverter(converters);
}
private void addJsonFormUrlEncodedConverter(List<HttpMessageConverter<?>> converters) {
JsonFormUrlEncodedConverter<?> converter = new JsonFormUrlEncodedConverter<>();
MediaType mediaType = new MediaType(MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8);
converter.setSupportedMediaTypes(List.of(mediaType));
converters.add(converter);
}
*/
}

View File

@ -0,0 +1,65 @@
package com.myoa.engineering.crawl.shopping.controller;
import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackEventSubscriptionDTO;
import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO;
import com.myoa.engineering.crawl.shopping.service.slack.RegisterCommandHandler;
import com.myoa.engineering.crawl.shopping.service.slack.ShoppingCommandHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
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;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/v1/slack")
public class SlackEventSubscriptionAPIController {
private final ShoppingCommandHandler shoppingCommandHandler;
private final RegisterCommandHandler registerCommandHandler;
public SlackEventSubscriptionAPIController(ShoppingCommandHandler shoppingCommandHandler,
RegisterCommandHandler registerCommandHandler) {
this.shoppingCommandHandler = shoppingCommandHandler;
this.registerCommandHandler = registerCommandHandler;
}
@PostMapping(value = {"/subscriptions"})
private ResponseEntity<String> slackEventSubscription(@RequestBody SlackEventSubscriptionDTO requestDTO) {
log.info("requestDTO: {}", requestDTO);
return ResponseEntity.ok(requestDTO.getChallenge());
}
@PostMapping(value = {"/commands/help"}, consumes = {"application/x-www-form-urlencoded"})
private ResponseEntity<String> handleCommandHelp(SlackSlashCommandDTO requestDTO) {
log.info("requestDTO: {}", requestDTO);
return ResponseEntity.ok("""
/쇼핑 목록
/쇼핑 키워드 추가 (쇼핑몰명) (키워드명)
/쇼핑 키워드 삭제 (쇼핑몰명) (키워드명)
/쇼핑 키워드 목록
/등록 (사용자명)
""");
}
@PostMapping(value = {"/commands/shopping"}, consumes = {"application/x-www-form-urlencoded"})
private ResponseEntity<String> handleAddSubscription(SlackSlashCommandDTO requestDTO) {
log.info("requestDTO: {}", requestDTO);
List<String> results = shoppingCommandHandler.handle(requestDTO);
return ResponseEntity.ok(String.join("\n", results));
}
@PostMapping(value = {"/commands/register"}, consumes = {"application/x-www-form-urlencoded"})
private ResponseEntity<String> handleRegisterUser(SlackSlashCommandDTO requestDTO) {
log.info("requestDTO: {}", requestDTO);
List<String> results = registerCommandHandler.handle(requestDTO);
return ResponseEntity.ok(String.join("\n", results));
}
}

View File

@ -1,22 +1,36 @@
package com.myoa.engineering.crawl.shopping.controller; package com.myoa.engineering.crawl.shopping.controller;
import com.myoa.engineering.crawl.shopping.crawlhandler.PpomppuCrawlDomesticHandler; import com.myoa.engineering.crawl.shopping.crawlhandler.PpomppuCrawlDomesticHandler;
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController @RestController
@RequestMapping("/api/v1/exploit") @RequestMapping("/api/v1/exploit")
public class TestAPIController { public class TestAPIController {
private final PpomppuCrawlDomesticHandler ppomppuCrawlDomesticHandler; private final PpomppuCrawlDomesticHandler ppomppuCrawlDomesticHandler;
private final MethodsClient methodsClient;
public TestAPIController(PpomppuCrawlDomesticHandler ppomppuCrawlDomesticHandler) { public TestAPIController(PpomppuCrawlDomesticHandler ppomppuCrawlDomesticHandler,
MethodsClient methodsClient) {
this.ppomppuCrawlDomesticHandler = ppomppuCrawlDomesticHandler; this.ppomppuCrawlDomesticHandler = ppomppuCrawlDomesticHandler;
this.methodsClient = methodsClient;
} }
@GetMapping("/triggers") @GetMapping("/triggers")
public void triggerExploit() { public void triggerExploit() {
ppomppuCrawlDomesticHandler.handle(); ppomppuCrawlDomesticHandler.handle();
} }
@GetMapping("/test-message")
public void testMessage() throws SlackApiException, IOException {
methodsClient.chatPostMessage(req -> req
.channel("notify_shopping")
.text("Hello, World!"));
}
} }

View File

@ -0,0 +1,20 @@
package com.myoa.engineering.crawl.shopping.dto.feed.v1;
import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget;
import lombok.*;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SubscribedKeywordDTO {
private String userId;
private CrawlTarget crawlTarget;
private String keyword;
public String toMessage() {
return crawlTarget.getAlias() + " | " + keyword;
}
}

View File

@ -0,0 +1,35 @@
package com.myoa.engineering.crawl.shopping.dto.slack.v2;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.Map;
@ToString
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SlackEventSubscriptionDTO {
private String token;
private String challenge;
private String type;
@JsonProperty("team_id")
private String teamId;
@JsonProperty("api_app_id")
private String apiAppId;
private Map<String, Object> event;
@JsonProperty("event_id")
private String eventId;
@JsonProperty("event_time")
private long eventTime;
}

View File

@ -0,0 +1,22 @@
package com.myoa.engineering.crawl.shopping.dto.slack.v2;
import lombok.*;
@ToString
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class SlackSlashCommandDTO {
private String token;
private String command;
private String text;
private String response_url;
private String trigger_id;
private String user_id;
private String user_name;
private String team_id;
private String api_app_id;
}

View File

@ -7,7 +7,7 @@ import com.myoa.engineering.crawl.shopping.domain.model.v2.SubscribedKeywordAggr
import com.myoa.engineering.crawl.shopping.event.ArticleUpsertEvent; import com.myoa.engineering.crawl.shopping.event.ArticleUpsertEvent;
import com.myoa.engineering.crawl.shopping.service.AppUserQueryService; import com.myoa.engineering.crawl.shopping.service.AppUserQueryService;
import com.myoa.engineering.crawl.shopping.service.SubscribedKeywordCacheService; import com.myoa.engineering.crawl.shopping.service.SubscribedKeywordCacheService;
import com.myoa.engineering.crawl.shopping.service.UserNotifyService; import com.myoa.engineering.crawl.shopping.service.slack.UserNotifyService;
import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget; import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -44,7 +44,7 @@ public class ArticleUpsertEventListener {
.filter(AppUserModel::getEnabled) .filter(AppUserModel::getEnabled)
.map(user -> { .map(user -> {
List<ArticleModel> filteredArticles = handleAhoCorasick(articleMap) List<ArticleModel> filteredArticles = handleAhoCorasick(articleMap)
.apply(subscribedKeywordCacheService.getSubscribedKeywordsCached(user.getName())); .apply(subscribedKeywordCacheService.getSubscribedKeywordsCached(user.getSlackId()));
return UserNotifyModel.of(user.getSlackId(), filteredArticles); return UserNotifyModel.of(user.getSlackId(), filteredArticles);
}) })
.forEach(this::notifyMessage); .forEach(this::notifyMessage);

View File

@ -4,6 +4,9 @@ import com.myoa.engineering.crawl.shopping.domain.entity.v2.AppUser;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository @Repository
public interface AppUserRepository extends JpaRepository<AppUser, Long> { public interface AppUserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findBySlackId(String userId);
} }

View File

@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
import java.util.Optional;
@Repository @Repository
public interface SubscribedKeywordRepository extends JpaRepository<SubscribedKeyword, Long> { public interface SubscribedKeywordRepository extends JpaRepository<SubscribedKeyword, Long> {
@ -20,4 +21,6 @@ public interface SubscribedKeywordRepository extends JpaRepository<SubscribedKey
List<SubscribedKeyword> findByUserIdAndCrawlTarget(String userId, CrawlTarget crawlTarget); List<SubscribedKeyword> findByUserIdAndCrawlTarget(String userId, CrawlTarget crawlTarget);
List<SubscribedKeyword> findByUserId(String userId); List<SubscribedKeyword> findByUserId(String userId);
Optional<SubscribedKeyword> findByUserIdAndCrawlTargetAndKeyword(String userId, CrawlTarget crawlTarget, String keyword);
} }

View File

@ -0,0 +1,31 @@
package com.myoa.engineering.crawl.shopping.service;
import com.myoa.engineering.crawl.shopping.domain.entity.v2.AppUser;
import com.myoa.engineering.crawl.shopping.infra.repository.v2.AppUserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AppUserCommandService {
private final AppUserRepository appUserRepository;
public AppUserCommandService(AppUserRepository appUserRepository) {
this.appUserRepository = appUserRepository;
}
@Transactional
public void register(String slackId, String userName) {
appUserRepository.findBySlackId(slackId)
.ifPresent(user -> {
throw new IllegalArgumentException("이미 등록된 사용자입니다.");
});
AppUser appUser = AppUser.builder()
.enabled(true)
.slackId(slackId)
.name(userName)
.build();
appUserRepository.save(appUser);
}
}

View File

@ -3,6 +3,7 @@ package com.myoa.engineering.crawl.shopping.service;
import com.myoa.engineering.crawl.shopping.domain.model.v2.AppUserModel; import com.myoa.engineering.crawl.shopping.domain.model.v2.AppUserModel;
import com.myoa.engineering.crawl.shopping.infra.repository.v2.AppUserRepository; import com.myoa.engineering.crawl.shopping.infra.repository.v2.AppUserRepository;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
@ -15,6 +16,7 @@ public class AppUserQueryService {
this.appUserRepository = appUserRepository; this.appUserRepository = appUserRepository;
} }
@Transactional(readOnly = true)
public List<AppUserModel> findAll() { public List<AppUserModel> findAll() {
return appUserRepository.findAll() return appUserRepository.findAll()
.stream() .stream()
@ -22,4 +24,11 @@ public class AppUserQueryService {
.toList(); .toList();
} }
@Transactional(readOnly = true)
public AppUserModel findByUserId(String userId) {
return appUserRepository.findBySlackId(userId)
.map(AppUserModel::from)
.orElseThrow();
}
} }

View File

@ -27,15 +27,15 @@ public class SubscribedKeywordCacheService {
return SubscribedKeywordAggregatedModel.of(userId, crawlTarget, keywords); return SubscribedKeywordAggregatedModel.of(userId, crawlTarget, keywords);
} }
@Cacheable(cacheNames = "subscribe.keywords", key = "#userId") @Cacheable(cacheNames = "subscribe.keywords", key = "#slackId")
public Map<CrawlTarget, SubscribedKeywordAggregatedModel> getSubscribedKeywordsCached(String userId) { public Map<CrawlTarget, SubscribedKeywordAggregatedModel> getSubscribedKeywordsCached(String slackId) {
System.out.println("getSubscribedKeywordsCached"); System.out.println("getSubscribedKeywordsCached");
return subscribedKeywordQueryService.findByUser(userId) return subscribedKeywordQueryService.findByUser(slackId)
.stream() .stream()
.collect(Collectors.groupingBy(SubscribedKeyword::getCrawlTarget, .collect(Collectors.groupingBy(SubscribedKeyword::getCrawlTarget,
Collectors.mapping(SubscribedKeyword::getKeyword, Collectors.toList()))) Collectors.mapping(SubscribedKeyword::getKeyword, Collectors.toList())))
.entrySet().stream() .entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> SubscribedKeywordAggregatedModel.of(userId, e.getKey(), e.getValue()))); .collect(Collectors.toMap(Map.Entry::getKey, e -> SubscribedKeywordAggregatedModel.of(slackId, e.getKey(), e.getValue())));
} }
} }

View File

@ -0,0 +1,46 @@
package com.myoa.engineering.crawl.shopping.service;
import com.myoa.engineering.crawl.shopping.domain.entity.v2.SubscribedKeyword;
import com.myoa.engineering.crawl.shopping.dto.feed.v1.SubscribedKeywordDTO;
import com.myoa.engineering.crawl.shopping.infra.repository.v2.SubscribedKeywordRepository;
import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class SubscribedKeywordCommandService {
private final SubscribedKeywordRepository subscribedKeywordRepository;
public SubscribedKeywordCommandService(SubscribedKeywordRepository subscribedKeywordRepository) {
this.subscribedKeywordRepository = subscribedKeywordRepository;
}
@Transactional
public void add(SubscribedKeyword subscribedKeyword) {
subscribedKeywordRepository.save(subscribedKeyword);
}
@Transactional
public void delete(String userId, CrawlTarget crawlTarget, String keyword) {
SubscribedKeyword subscribedKeyword = subscribedKeywordRepository.findByUserIdAndCrawlTargetAndKeyword(userId, crawlTarget, keyword)
.orElseThrow(() -> new IllegalArgumentException("Not found keyword: " + keyword));
subscribedKeywordRepository.delete(subscribedKeyword);
}
@Transactional(readOnly = true)
public List<SubscribedKeywordDTO> list(String userId) {
return subscribedKeywordRepository.findByUserId(userId)
.stream()
.map(e -> SubscribedKeywordDTO.builder()
.keyword(e.getKeyword())
.userId(e.getUserId())
.crawlTarget(e.getCrawlTarget())
.build())
.toList();
}
}

View File

@ -1,34 +0,0 @@
package com.myoa.engineering.crawl.shopping.service;
import com.myoa.engineering.crawl.shopping.configuration.slack.properties.SlackSecretProperties;
import com.myoa.engineering.crawl.shopping.dto.slack.v1.SlackMessageDTO;
import com.myoa.engineering.crawl.shopping.infra.client.slack.SlackAPIClient;
import org.springframework.stereotype.Service;
@Service
public class UserNotifyService {
private static final String SLACK_PROPERTIES_UNIT_NAME = "shopping-crawler";
// private static final String NOTIFY_CHANNEL_ID = "notify_shopping";
// private static final String NOTIFY_ICON_EMOJI = ":monge_big:";
// private static final String NOTIFY_BOT_NAME = "몽이 탈호구봇";
private final SlackAPIClient slackAPIClient;
private final SlackSecretProperties.SlackSecretPropertiesUnit slackSecretProperties;
public UserNotifyService(SlackAPIClient slackAPIClient,
SlackSecretProperties slackSecretProperties) {
this.slackAPIClient = slackAPIClient;
this.slackSecretProperties = slackSecretProperties.find(SLACK_PROPERTIES_UNIT_NAME);
}
public void notify(String message) {
SlackMessageDTO slackMessageDTO = SlackMessageDTO.builder()
.channel(slackSecretProperties.getChannel())
.text(message)
.iconEmoji(slackSecretProperties.getIconEmoji())
.username(slackSecretProperties.getUsername())
.build();
slackAPIClient.sendMessage(slackMessageDTO, slackSecretProperties.getToken());
}
}

View File

@ -0,0 +1,33 @@
package com.myoa.engineering.crawl.shopping.service.slack;
import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO;
import com.myoa.engineering.crawl.shopping.service.AppUserCommandService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class RegisterCommandHandler implements SlackCommandHandler {
private final AppUserCommandService appUserCommandService;
public RegisterCommandHandler(AppUserCommandService appUserCommandService) {
this.appUserCommandService = appUserCommandService;
}
public List<String> handle(SlackSlashCommandDTO requestDTO) {
if (!requestDTO.getCommand().equals("/등록")) {
throw new IllegalArgumentException("Invalid command: " + requestDTO.getCommand());
}
final String userName = requestDTO.getText();
try {
appUserCommandService.register(requestDTO.getUser_id(), userName);
return List.of("등록 완료");
} catch (Exception e) {
return List.of(e.getMessage());
}
}
}

View File

@ -0,0 +1,34 @@
package com.myoa.engineering.crawl.shopping.service.slack;
import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ShoppingCommandHandler implements SlackCommandHandler {
private final List<ShoppingCommandProcessor> processors;
public ShoppingCommandHandler(List<ShoppingCommandProcessor> processors) {
this.processors = processors;
}
public List<String> handle(SlackSlashCommandDTO requestDTO) {
String[] commands = requestDTO.getText().split("\\s+", 2);
if (!requestDTO.getCommand().equals("/쇼핑")) {
throw new IllegalArgumentException("Invalid command: " + requestDTO.getCommand());
}
final String action = commands[0];
return processors.stream()
.filter(processor -> processor.isAssignable(action))
.map(processor -> processor.process(requestDTO))
.toList();
}
}

View File

@ -0,0 +1,10 @@
package com.myoa.engineering.crawl.shopping.service.slack;
import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO;
public interface ShoppingCommandProcessor {
boolean isAssignable(String menuContext);
String process(SlackSlashCommandDTO requestDTO);
}

View File

@ -0,0 +1,4 @@
package com.myoa.engineering.crawl.shopping.service.slack;
public interface SlackCommandHandler {
}

View File

@ -0,0 +1,33 @@
package com.myoa.engineering.crawl.shopping.service.slack;
import com.myoa.engineering.crawl.shopping.configuration.slack.properties.SlackSecretProperties;
import com.slack.api.methods.MethodsClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class UserNotifyService {
private static final String SLACK_PROPERTIES_UNIT_NAME = "shopping-crawler";
private final SlackSecretProperties.SlackSecretPropertiesUnit slackSecretProperties;
private final MethodsClient methodsClient;
public UserNotifyService(SlackSecretProperties slackSecretProperties,
MethodsClient methodsClient) {
this.slackSecretProperties = slackSecretProperties.find(SLACK_PROPERTIES_UNIT_NAME);
this.methodsClient = methodsClient;
}
public void notify(String message) {
try {
methodsClient.chatPostMessage(req -> req
.channel(slackSecretProperties.getChannel())
.username(slackSecretProperties.getUsername())
.text(message));
} catch (Exception e) {
log.warn("Failed. message: {}", message, e);
}
}
}

View File

@ -0,0 +1,67 @@
package com.myoa.engineering.crawl.shopping.service.slack.shopping;
import com.myoa.engineering.crawl.shopping.domain.entity.v2.SubscribedKeyword;
import com.myoa.engineering.crawl.shopping.dto.feed.v1.SubscribedKeywordDTO;
import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO;
import com.myoa.engineering.crawl.shopping.service.SubscribedKeywordCommandService;
import com.myoa.engineering.crawl.shopping.service.slack.ShoppingCommandProcessor;
import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class KeywordShoppingCommandProcessor implements ShoppingCommandProcessor {
private final SubscribedKeywordCommandService subscribedKeywordCommandService;
public KeywordShoppingCommandProcessor(SubscribedKeywordCommandService subscribedKeywordCommandService) {
this.subscribedKeywordCommandService = subscribedKeywordCommandService;
}
@Override
public boolean isAssignable(String menuContext) {
return menuContext.equals("키워드");
}
@Override
public String process(SlackSlashCommandDTO requestDTO) {
// "키워드 추가 (쇼핑몰명) (키워드명)"
final String[] splited = requestDTO.getText().split("\\s+", 4);
final String action = splited[1];
switch (action) {
case "추가" -> {
final CrawlTarget crawlTarget = CrawlTarget.fromAlias(splited[2]);
final String keyword = splited[3];
SubscribedKeyword subscribedKeyword =
SubscribedKeyword.builder()
.crawlTarget(crawlTarget)
.userId(requestDTO.getUser_id())
.keyword(keyword)
.build();
subscribedKeywordCommandService.add(subscribedKeyword);
return "keyword: [" + keyword + "] 추가완료";
}
case "삭제" -> {
final CrawlTarget crawlTarget = CrawlTarget.fromAlias(splited[2]);
final String keyword = splited[3];
subscribedKeywordCommandService.delete(requestDTO.getUser_id(), crawlTarget, keyword);
return "keyword: [" + keyword + "] 삭제완료";
}
case "목록" -> {
List<SubscribedKeywordDTO> keywords = subscribedKeywordCommandService.list(requestDTO.getUser_id());
if (keywords.isEmpty()) {
return "등록된 키워드가 없습니다.";
}
return keywords.stream().map(SubscribedKeywordDTO::toMessage)
.collect(Collectors.joining("\n"));
}
default -> {
return "지원하지 않는 명령어입니다.";
}
}
}
}

View File

@ -0,0 +1,25 @@
package com.myoa.engineering.crawl.shopping.service.slack.shopping;
import com.myoa.engineering.crawl.shopping.dto.slack.v2.SlackSlashCommandDTO;
import com.myoa.engineering.crawl.shopping.service.slack.ShoppingCommandProcessor;
import com.myoa.engineering.crawl.shopping.support.dto.constant.CrawlTarget;
import org.springframework.stereotype.Component;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
public class ListShoppingCommandProcessor implements ShoppingCommandProcessor {
@Override
public boolean isAssignable(String menuContext) {
return menuContext.equals("목록");
}
@Override
public String process(SlackSlashCommandDTO requestDTO) {
return Stream.of(CrawlTarget.values())
.map(e -> e.getAlias() + " | " + e.isAvailable())
.collect(Collectors.joining("\n"));
}
}

View File

@ -1,12 +0,0 @@
spring:
config:
activate:
on-profile: development
import:
- "configserver:http://192.168.0.100:11080"
server:
port: 20081
# import: optional:configserver:http://localhost:11080 # can be start up even config server was not found.

View File

@ -0,0 +1,23 @@
spring:
config:
activate:
on-profile: prod
import:
- classpath:/datasource/prod.yml
- classpath:/slack/prod.yml
devtools:
livereload:
enabled: false
restart:
enabled: false
server:
port: 20080
# import: optional:configserver:http://localhost:11080 # can be start up even config server was not found.
feign:
client:
config:
default:
loggerLevel: FULL

View File

@ -1,6 +0,0 @@
spring:
config:
activate:
on-profile: production
import:
- "configserver:http://ppn-config-server:20080"

View File

@ -7,8 +7,7 @@ spring:
active: ${SPRING_ACTIVE_PROFILE:local} active: ${SPRING_ACTIVE_PROFILE:local}
group: group:
local: "local,datasource-local,webclient-local" local: "local,datasource-local,webclient-local"
development: "development,datasource-development,webclient-development" prod: "prod"
production: "production,datasource-production,webclient-production"
freemarker: freemarker:
enabled: false enabled: false
cloud: cloud:

View File

@ -2,14 +2,14 @@
<configuration> <configuration>
<springProperty name="DEFAULT_LEVEL_CONFIG" source="log.defaultLevel" /> <springProperty name="DEFAULT_LEVEL_CONFIG" source="log.defaultLevel" />
<springProfile name="local"> <springProfile name="local">
<include resource="logback/logback-development.xml" /> <include resource="logback/logback-dev.xml" />
<logger name="org.apache.kafka" level="INFO" /> <logger name="org.apache.kafka" level="INFO" />
</springProfile> </springProfile>
<springProfile name="development"> <springProfile name="dev">
<include resource="logback/logback-development.xml" /> <include resource="logback/logback-dev.xml" />
<logger name="org.apache.kafka" level="INFO" /> <logger name="org.apache.kafka" level="INFO" />
</springProfile> </springProfile>
<springProfile name="production"> <springProfile name="prod">
<include resource="logback/logback-production.xml" /> <include resource="logback/logback-prod.xml" />
</springProfile> </springProfile>
</configuration> </configuration>

View File

@ -3,15 +3,12 @@
<!-- =========== property BETA ========= --> <!-- =========== property BETA ========= -->
<property name="DEFAULT_LEVEL" value="${DEFAULT_LEVEL_CONFIG:-INFO}"/> <property name="DEFAULT_LEVEL" value="${DEFAULT_LEVEL_CONFIG:-INFO}"/>
<!--file--> <!--file-->
<property name="DIRECTORY" value="/home1/www/logs/supervisor"/>
<property name="IMMEDIATE_FLUSH" value="true"/> <property name="IMMEDIATE_FLUSH" value="true"/>
<!--nelo2--> <!--nelo2-->
<property name="NELO2_LEVEL" value="WARN"/> <property name="NELO2_LEVEL" value="WARN"/>
<!-- =========== include appender =========== --> <!-- =========== include appender =========== -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/> <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<include resource="logback/component/logback-nelo2.xml"/>
<include resource="logback/component/logback-datachain.xml"/>
<!-- =========== root logger ============== --> <!-- =========== root logger ============== -->
<root level="${DEFAULT_LEVEL}"> <root level="${DEFAULT_LEVEL}">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>

View File

@ -3,15 +3,12 @@
<!-- =========== property RELEASE ========= --> <!-- =========== property RELEASE ========= -->
<property name="DEFAULT_LEVEL" value="${DEFAULT_LEVEL_CONFIG:-INFO}"/> <property name="DEFAULT_LEVEL" value="${DEFAULT_LEVEL_CONFIG:-INFO}"/>
<!--file--> <!--file-->
<property name="DIRECTORY" value="/home1/www/logs/supervisor"/>
<property name="IMMEDIATE_FLUSH" value="true"/> <property name="IMMEDIATE_FLUSH" value="true"/>
<!--nelo2--> <!--nelo2-->
<property name="NELO2_LEVEL" value="WARN"/> <property name="NELO2_LEVEL" value="WARN"/>
<!-- =========== include appender =========== --> <!-- =========== include appender =========== -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/> <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<include resource="logback/component/logback-nelo2.xml"/>
<include resource="logback/component/logback-datachain.xml"/>
<!-- =========== root logger ============== --> <!-- =========== root logger ============== -->
<root level="${DEFAULT_LEVEL}"> <root level="${DEFAULT_LEVEL}">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>

View File

@ -0,0 +1,33 @@
package com.myoa.engineering.crawl.shopping.controller;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class SlackEventSubscriptionAPIControllerTest {
@Test
void test_handleAddSubscription_1() {
// given
String sut = "/쇼핑 키워드 추가 키워드1";
// when
String actual = sut.replaceFirst("/.+? ", "");
// then
Assertions.assertEquals("키워드 추가 키워드1", actual);
}
@Test
void test_handleAddSubscription_2() {
// given
String sut = "키워드 추가 키워드 1입니 다";
// when
String[] actual = sut.split("\\s+", 2);
// then
Assertions.assertEquals(2, actual.length);
Assertions.assertEquals("키워드", actual[0]);
Assertions.assertEquals("추가 키워드 1입니 다", actual[1]);
}
}

View File

@ -6,8 +6,20 @@ import lombok.Getter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum CrawlTarget { public enum CrawlTarget {
PPOMPPU_DOMESTIC, PPOMPPU_DOMESTIC("뽐뿌국내", true),
PPOMPPU_OVERSEA, PPOMPPU_OVERSEA("뽐뿌해외", false),
FMKOREA, FMKOREA("펨코", false),
; ;
private final String alias;
private final boolean available;
public static CrawlTarget fromAlias(String alias) {
for (CrawlTarget crawlTarget : values()) {
if (crawlTarget.getAlias().equals(alias)) {
return crawlTarget;
}
}
throw new IllegalArgumentException("Not found alias: " + alias);
}
} }