Skip to content

Commit adc6c99

Browse files
committed
slack(article): send message when article is fetched
1 parent db08193 commit adc6c99

File tree

9 files changed

+195
-5
lines changed

9 files changed

+195
-5
lines changed

backend/build.gradle

+4
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ dependencies {
101101
implementation 'com.rometools:rome:1.18.0'
102102
implementation 'org.commonmark:commonmark:0.18.2'
103103

104+
// slack
105+
implementation 'com.slack.api:slack-api-client:1.39.3'
106+
implementation 'com.slack.api:slack-api-model:1.39.3'
107+
104108
// aws s3
105109
implementation platform('software.amazon.awssdk:bom:2.5.29')
106110
implementation group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.12.319'

backend/src/main/java/wooteco/prolog/article/application/ArticleService.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@ public class ArticleService {
2828

2929
private final ArticleRepository articleRepository;
3030
private final MemberService memberService;
31+
private final SlackService slackService;
3132

3233
@Transactional
3334
public Long create(final ArticleRequest articleRequest, final LoginMember loginMember) {
3435
final Member member = memberService.findById(loginMember.getId());
3536
validateMemberIsCrew(member);
36-
final Article article = articleRequest.toArticle(member);
37-
return articleRepository.save(article).getId();
37+
Article article = articleRepository.save(articleRequest.toArticle(member));
38+
39+
slackService.sendSlackMessage(article);
40+
return article.getId();
3841
}
3942

4043
private void validateMemberIsCrew(final Member member) {
@@ -125,7 +128,11 @@ private List<ArticleResponse> fetchArticleWithRssFeedOf(Member member) {
125128

126129
List<Article> newArticles = rssArticles.findNewArticles(existedArticles);
127130

128-
return articleRepository.saveAll(newArticles).stream()
131+
List<Article> persistNewArticles = articleRepository.saveAll(newArticles);
132+
persistNewArticles.stream()
133+
.forEach(slackService::sendSlackMessage);
134+
135+
return persistNewArticles.stream()
129136
.map(article -> ArticleResponse.of(article, member.getId()))
130137
.collect(toList());
131138
} catch (Exception e) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package wooteco.prolog.article.application;
2+
3+
import com.slack.api.Slack;
4+
import com.slack.api.methods.SlackApiException;
5+
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
6+
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
7+
import com.slack.api.model.block.ActionsBlock;
8+
import com.slack.api.model.block.ContextBlock;
9+
import com.slack.api.model.block.DividerBlock;
10+
import com.slack.api.model.block.ImageBlock;
11+
import com.slack.api.model.block.LayoutBlock;
12+
import com.slack.api.model.block.SectionBlock;
13+
import com.slack.api.model.block.composition.MarkdownTextObject;
14+
import com.slack.api.model.block.composition.PlainTextObject;
15+
import com.slack.api.model.block.element.ButtonElement;
16+
import lombok.RequiredArgsConstructor;
17+
import org.springframework.beans.factory.annotation.Value;
18+
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
import wooteco.prolog.article.domain.Article;
21+
22+
import java.io.IOException;
23+
import java.util.Arrays;
24+
import java.util.Collections;
25+
import java.util.List;
26+
27+
@RequiredArgsConstructor
28+
@Service
29+
@Transactional(readOnly = true)
30+
public class SlackService {
31+
32+
@Value("${slack.article.token}")
33+
private String slackToken;
34+
35+
@Value("${slack.article.channel}")
36+
private String slackChannel;
37+
38+
public void sendSlackMessage(Article article) {
39+
Slack slack = Slack.getInstance();
40+
41+
// Image block
42+
ImageBlock imageBlock = ImageBlock.builder()
43+
.imageUrl(article.getImageUrl().getUrl())
44+
.altText("article image")
45+
.build();
46+
47+
// Title section
48+
SectionBlock titleSection = SectionBlock.builder()
49+
.text(MarkdownTextObject.builder()
50+
.text("*" + article.getTitle().getTitle() + "*")
51+
.build())
52+
.build();
53+
54+
// Context block
55+
String name = article.getMember().getNickname() + "(" + article.getMember().getUsername() + ")";
56+
String date = article.getCreatedAt().toLocalDate().toString();
57+
ContextBlock contextBlock = ContextBlock.builder()
58+
.elements(Collections.singletonList(
59+
MarkdownTextObject.builder()
60+
.text(name + " | " + date)
61+
.build()))
62+
.build();
63+
64+
// Summary section
65+
SectionBlock summarySection = SectionBlock.builder()
66+
.text(MarkdownTextObject.builder()
67+
.text(article.getDescription().getDescription())
68+
.build())
69+
.build();
70+
71+
// Button block
72+
ButtonElement buttonElement = ButtonElement.builder()
73+
.text(PlainTextObject.builder()
74+
.text("자세히 보기")
75+
.build())
76+
.url(article.getUrl().getUrl())
77+
.actionId("button_to_article")
78+
.build();
79+
80+
ActionsBlock actionsBlock = ActionsBlock.builder()
81+
.elements(Arrays.asList(buttonElement))
82+
.build();
83+
84+
// Divider block
85+
DividerBlock dividerBlock = DividerBlock.builder().build();
86+
87+
// Prologue section
88+
SectionBlock prologueSection = SectionBlock.builder()
89+
.text(MarkdownTextObject.builder()
90+
.text("📝 프롤로그 프로필 페이지에서 `RSS Link` 를 설정해보세요.")
91+
.build())
92+
.accessory(ButtonElement.builder()
93+
.text(PlainTextObject.builder()
94+
.text("프롤로그 둘러보기")
95+
.emoji(true)
96+
.build())
97+
.value("click_to_prolog")
98+
.url("https://prolog.techcourse.co.kr/article")
99+
.actionId("button-action")
100+
.build())
101+
.build();
102+
103+
// List of blocks
104+
List<LayoutBlock> blocks = Arrays.asList(
105+
imageBlock,
106+
titleSection,
107+
contextBlock,
108+
summarySection,
109+
actionsBlock,
110+
dividerBlock,
111+
prologueSection,
112+
dividerBlock
113+
);
114+
115+
// Create a message request
116+
ChatPostMessageRequest request = ChatPostMessageRequest.builder()
117+
.channel(slackChannel)
118+
.text("새로운 글이 등록되었습니다.")
119+
.blocks(blocks)
120+
.build();
121+
122+
try {
123+
ChatPostMessageResponse chatPostMessageResponse = slack.methods(slackToken).chatPostMessage(request);
124+
chatPostMessageResponse.getMessage();
125+
chatPostMessageResponse.getChannel();
126+
} catch (IOException | SlackApiException e) {
127+
e.printStackTrace();
128+
// TODO: slack 전송 실패 시 예외 처리
129+
}
130+
}
131+
}

backend/src/main/java/wooteco/prolog/article/domain/Article.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public class Article {
3939
@Embedded
4040
private Title title;
4141

42+
@Embedded
43+
private Description description;
44+
4245
@Embedded
4346
private Url url;
4447

@@ -57,16 +60,21 @@ public class Article {
5760
@Embedded
5861
private ViewCount views;
5962

60-
public Article(final Member member, final Title title, final Url url, final ImageUrl imageUrl) {
63+
public Article(Member member, Title title, Description description, Url url, ImageUrl imageUrl) {
6164
this.member = member;
6265
this.title = title;
66+
this.description = description;
6367
this.url = url;
6468
this.imageUrl = imageUrl;
6569
this.articleBookmarks = new ArticleBookmarks();
6670
this.articleLikes = new ArticleLikes();
6771
this.views = new ViewCount();
6872
}
6973

74+
public Article(Member member, Title title, Url url, ImageUrl imageUrl) {
75+
this(member, title, new Description(), url, imageUrl);
76+
}
77+
7078
public void validateOwner(final Member member) {
7179
if (!member.equals(this.member)) {
7280
throw new BadRequestException(BadRequestCode.INVALID_ARTICLE_AUTHOR_EXCEPTION);

backend/src/main/java/wooteco/prolog/article/domain/Articles.java

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public static Articles fromRssFeedBy(Member member) {
3131
syndFeed.getEntries().stream()
3232
.map(entry -> new Article(member,
3333
new Title(entry.getTitle()),
34+
new Description(entry.getDescription().getValue()),
3435
new Url(entry.getLink()),
3536
new ImageUrl(extractImageUrl(entry.getDescription().getValue()))))
3637
.collect(toList())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package wooteco.prolog.article.domain;
2+
3+
import lombok.AccessLevel;
4+
import lombok.EqualsAndHashCode;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import lombok.ToString;
8+
import org.apache.commons.text.StringEscapeUtils;
9+
import org.jsoup.Jsoup;
10+
import org.jsoup.safety.Safelist;
11+
12+
import javax.persistence.Embeddable;
13+
14+
@Getter
15+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
16+
@EqualsAndHashCode
17+
@ToString
18+
@Embeddable
19+
public class Description {
20+
private String description;
21+
22+
public Description(String description) {
23+
this.description = StringEscapeUtils.unescapeHtml4(Jsoup.clean(description, Safelist.none()));
24+
}
25+
}

backend/src/main/java/wooteco/prolog/article/domain/Title.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import lombok.Getter;
1111
import lombok.NoArgsConstructor;
1212
import lombok.ToString;
13+
import org.apache.commons.text.StringEscapeUtils;
14+
import org.jsoup.Jsoup;
15+
import org.jsoup.safety.Safelist;
1316
import wooteco.prolog.common.exception.BadRequestCode;
1417
import wooteco.prolog.common.exception.BadRequestException;
1518

@@ -34,7 +37,9 @@ public Title(String title) {
3437
}
3538

3639
private String trim(String name) {
37-
return name.trim();
40+
// HTML 태그와 자바스크립트를 제거, HTML 특수 문자를 변환
41+
String result = StringEscapeUtils.unescapeHtml4(Jsoup.clean(name, Safelist.none()));
42+
return result.trim();
3843
}
3944

4045
private void validateNull(String title) {

backend/src/main/resources/application.yml

+6
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@ elasticsearch:
5959

6060
manager:
6161
role: COACH
62+
63+
slack:
64+
article:
65+
token: token
66+
channel: channel
67+
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
ALTER TABLE member
22
ADD COLUMN rss_feed_url VARCHAR(256);
3+
4+
ALTER TABLE article
5+
ADD COLUMN description VARCHAR(256) AFTER title;

0 commit comments

Comments
 (0)