Skip to content
This repository was archived by the owner on Jul 2, 2024. It is now read-only.

Commit c01331f

Browse files
committed
feat: Add new-post-checker lambda
1 parent f9dd4fe commit c01331f

File tree

10 files changed

+470
-1
lines changed

10 files changed

+470
-1
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
VH__add-bookmark-blog,
2626
VH__delete-bookmark-blog,
2727
VH__get-posts,
28+
VH__new-post-checker,
2829
]
2930
include:
3031
- lambda: VH__get-blog
@@ -51,6 +52,8 @@ jobs:
5152
path: ./backend/api/delete_bookmark_blog
5253
- lambda: VH__get-posts
5354
path: ./backend/api/get_posts
55+
- lambda: VH__new-post-checker
56+
path: ./backend/cron/new_post
5457

5558
uses: junah201/velog-helper/.github/workflows/deploy.yml@main
5659
with:

backend/common/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class Post(Base):
117117
nullable=False
118118
)
119119
short_description = Column(
120-
String(200),
120+
String(300),
121121
nullable=True,
122122
default=None
123123
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import new_post
2+
import asyncio
3+
4+
5+
def lambda_handler(event, context):
6+
asyncio.run(new_post.update_new_post())
7+
8+
return {
9+
'statusCode': 200,
10+
'body': "Success"
11+
}

backend/cron/new_post/crawler.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import aiohttp
2+
from typing import List
3+
4+
VELOG_API_URL = "https://v2.velog.io/graphql"
5+
6+
get_new_posts_query = """
7+
query Posts($cursor: ID, $username: String, $temp_only: Boolean, $limit: Int) {
8+
posts(cursor: $cursor, username: $username, temp_only: $temp_only, limit: $limit) {
9+
id
10+
title
11+
short_description
12+
user {
13+
username
14+
profile {
15+
thumbnail
16+
}
17+
}
18+
url_slug
19+
released_at
20+
updated_at
21+
}
22+
}
23+
"""
24+
25+
26+
async def get_new_posts(username: str, limit: int = 10, return_type: str = "List") -> List[dict]:
27+
async with aiohttp.ClientSession() as session:
28+
async with session.post(VELOG_API_URL, json={"query": get_new_posts_query, "variables": {"username": username, "limit": limit}}) as resp:
29+
assert resp.status == 200
30+
data = await resp.json()
31+
32+
if return_type == "List":
33+
return data["data"]["posts"]
34+
35+
if return_type == "Dict":
36+
result = {}
37+
38+
for post in data["data"]["posts"]:
39+
result[post["id"]] = post
40+
41+
return result
42+
43+
return data["data"]["posts"]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
디스코드 채널에 웹훅을 이용해서 로깅
3+
"""
4+
5+
from typing import List
6+
import aiohttp
7+
import datetime
8+
import os
9+
10+
DISCORD_WEBHOOKS_NEW_POST_UPLOAD_LOG = os.environ.get(
11+
"DISCORD_WEBHOOKS_NEW_POST_UPLOAD_LOG")
12+
13+
14+
color = {
15+
"green": 0x2ECC71,
16+
}
17+
18+
19+
async def logging_new_post_upload(total_updated_blog_cnt: int, total_updated_posts: List[str]):
20+
async with aiohttp.ClientSession() as session:
21+
data = {
22+
"content": "",
23+
"embeds": [
24+
{
25+
"title": "새 글 업데이트 로그",
26+
"description": f"블로그 : `{total_updated_blog_cnt}`개\n포스트 : `{len(total_updated_posts)}`개",
27+
"color": color["green"],
28+
"footer": {
29+
"text": f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
30+
}
31+
}
32+
]
33+
}
34+
35+
if total_updated_posts:
36+
tmp = ""
37+
print(total_updated_posts)
38+
for post in total_updated_posts:
39+
tmp += post
40+
data["embeds"][0]["description"] += f"\n\n```{tmp}```"
41+
42+
await session.post(
43+
url=DISCORD_WEBHOOKS_NEW_POST_UPLOAD_LOG,
44+
json=data
45+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import new_post
2+
import asyncio
3+
4+
5+
def lambda_handler(event, context):
6+
asyncio.run(new_post.update_new_post())
7+
8+
return {
9+
'statusCode': 200,
10+
'body': "Success"
11+
}

backend/cron/new_post/mail.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import smtplib
2+
from email.mime.multipart import MIMEMultipart
3+
from email.mime.text import MIMEText
4+
from jinja2 import Environment, FileSystemLoader, select_autoescape
5+
import os
6+
from models import Post
7+
8+
BACKEND_SERVER_URL = os.environ.get("BACKEND_SERVER_URL", None)
9+
MAIL_SENDER = os.environ.get("MAIL_SENDER", None)
10+
MAIL_PASSWARD = os.environ.get("MAIL_PASSWARD", None)
11+
12+
env = Environment(
13+
loader=FileSystemLoader('./'),
14+
autoescape=select_autoescape(['html']),
15+
)
16+
17+
18+
def send_new_post_notice_email(receiver_address: str, post: Post, user_id: str) -> None:
19+
new_post_notice_template = env.get_template("new_post_notice.html")
20+
21+
message = MIMEMultipart()
22+
message['From'] = MAIL_SENDER
23+
message['To'] = receiver_address
24+
message['Subject'] = f"{post.title} | 새 글 알림"
25+
message.attach(MIMEText(new_post_notice_template.render(
26+
user=post.blog_id,
27+
title=post.title,
28+
link=post.link,
29+
BACKEND_SERVER_URL=BACKEND_SERVER_URL,
30+
user_id=user_id,
31+
short_description=post.short_description or "이 글의 요약을 가져오지 못했습니다. (2022.11.21 이전 글 일 가능성이 있습니다.)",
32+
user_img=post.blog_img,
33+
released_year=post.created_at.year,
34+
released_month=post.created_at.month,
35+
released_day=post.created_at.day
36+
), 'html'))
37+
session = smtplib.SMTP('smtp.gmail.com', 587)
38+
session.starttls()
39+
session.login(MAIL_SENDER, MAIL_PASSWARD)
40+
text = message.as_string()
41+
session.sendmail(MAIL_SENDER, receiver_address, text)
42+
session.quit()
43+
print(f'Mail Sent (new_post) "{receiver_address}" "{post.title}"')

backend/cron/new_post/new_post.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from sqlalchemy.orm import Session
2+
import models
3+
import database
4+
from crawler import get_new_posts
5+
from typing import List, Tuple
6+
import discord_logging
7+
import datetime
8+
import mail
9+
10+
VELOG_DEFAULT_PROFILE_IMG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAASbSURBVHgB7Z0tTytBFIYP914BDiQ4cIADB0EhwYFE8ifq7g/hJ2CRSCQ4kOCobF3ruHk3maS5aSnbdnfPOe/7JE0oCTvTnmc+dvbMsNbr9b5M0PLLBDUSgBwJQI4EIEcCkCMByJEA5EgAciQAORKAHAlAjgQgRwKQIwHIkQDkSAByJAA5EoAcCUCOBCBHApAjAciRAORIAHIkADkSgBwJQI4EIEcCkCMByJEA5EgAciQAOX+MhPX1dTs+Prbt7W3b3d21jY2N6ndgPB7bYDCw4XBor6+v9vHxUb1nIL0Ae3t7dn5+XgV9FhABYuC1v79f/Q4SPD8/28vLi2UmrQA/Cfx34O/wwjXu7u7S9gi/z87O/loyELTr62vb2tqyZcFQcXp6Wv2MXiEb6SaBCDwEWDVFqmykEgABOjo6sqbAtbNJkEaAi4uLRoNfQBmXl5eWhRQCIChlnG6Dk5OTVstrkvACYKLXxJg/D5RZ1hEiE14ABGIVs/26IPgZeoHQAiDwbYz7s4AA0XuB0AIsusizKsrycmRCC+Dhyz84OLDIhBUAra/rHgCgDpGHgbAC7OzsmBc81aUuYQXY3Nw0L3iqS13CCtDFrd8sPNWlLsoIIkcCkBNWAE8JGpGTRcIKgPw9L3iqS13CCvD5+Wle8FSXuoQVAJm8HlK0UAfUJSqhJ4Fvb2/WNcgcjkxoAfDld936oieKhhYAwX96erKuwJ6B6Oni4dcBIEAXvQAC//j4aNEJLwCC30UgUGaGzSIpVgLRC7Q5FKCsLFvG0iwFPzw8tBIUlIGyspDqWcD9/X2jEuDaKCMT6R4GIUBNzAlwzWzBByl3ByNYaK23t7dLP6vHfT6u9/7+bhlZ6/V6X5YYpI0jebRu/mD2wBfSHxCBngAv9ASQ4PDwsErhwvvJE0JGo1EV9H6/72KFsS1SCDAZyFngnh2vVUwSUV4WQUILULZnlR06aMGYqDW1QDN56khZho6+Ghh2DoBgXF1dTZ3koZWvcqWubECdtg0NZUQ+QiakAGjxOA9gHhABj4wXeWyMHgX5/j85Zwi9AXoeD4+n6xJOAASk7nbwkjyCGT0meXg/mcWDYOMsIJwShtaO3mWRHT/odaINCaHmAIsEHyCQOP6tHAHXFKVukSQIsxK4aPDbBnWMdG5ACAHwhUYIfgHzEwwjEXAvQFdHwCzLzc1NiC1jrgXA2I31/Ijbr1HnCEfKuRagq/N/VgXuJLzPB9wKgMBnOITJu8RuBUDXnwHvQ4FLAbDkGrnr/x8MBV7vClwKEHHWPw+vn8mdANlaf8FrL+BOgIytv+Dxs7kSAC0kY+sveOwFXAnQ5bGvbdH0A6m6uBLAw8GPTePtaFk3AmTv/gtYF/A0DLgRgKH1Fzx9VjcCIBuHBU89nRsBkKrFgqfNJm5SwpBGVc7fz/CvWKZRUsk9bS1PvzVMfI+OiiVHApAjAciRAORIAHIkADkSgBwJQI4EIEcCkCMByJEA5EgAciQAORKAHAlAjgQgRwKQIwHIkQDkSAByJAA5EoAcCUCOBCBHApAjAciRAORIAHIkADkSgBwJQI4EIOcfGjV2tEfztqEAAAAASUVORK5CYII="
11+
TZ_KST = datetime.timezone(datetime.timedelta(hours=9))
12+
13+
14+
def UTC_to_KST(utc_time: datetime.datetime) -> datetime.datetime:
15+
kst_time = utc_time + datetime.timedelta(hours=9)
16+
# kst_time = kst_time.replace(tzinfo=TZ_KST)
17+
return kst_time
18+
19+
20+
async def update_new_post_by_blog(db: Session, blog: models.Blog, limit: int = 10, is_init: bool = False) -> List[str]:
21+
updated_posts: List[str] = list()
22+
23+
posts = await get_new_posts(username=blog.id, limit=limit, return_type="List")
24+
25+
if is_init:
26+
blog.last_uploaded_at = datetime.datetime(2005, 2, 1)
27+
28+
for post in reversed(posts):
29+
post_uploaded_at = UTC_to_KST(datetime.datetime.strptime(
30+
post["released_at"][:19], "%Y-%m-%dT%H:%M:%S"))
31+
32+
print(post_uploaded_at, blog.last_uploaded_at)
33+
if post_uploaded_at <= blog.last_uploaded_at:
34+
continue
35+
36+
if db.query(models.Post).filter(models.Post.id == post["id"]).first():
37+
continue
38+
39+
updated_posts.append(f"{blog.id} - {post['title']}")
40+
41+
# DB에 추가
42+
db_post = models.Post(
43+
id=post["id"],
44+
title=post["title"],
45+
blog_id=post["user"]["username"],
46+
blog_img=post["user"]["profile"]["thumbnail"] if post["user"]["profile"]["thumbnail"] else VELOG_DEFAULT_PROFILE_IMG,
47+
link=post["url_slug"],
48+
short_description=post["short_description"],
49+
created_at=post_uploaded_at,
50+
updated_at=UTC_to_KST(datetime.datetime.strptime(
51+
post["updated_at"][:19], "%Y-%m-%dT%H:%M:%S")),
52+
)
53+
54+
db.add(db_post)
55+
db.commit()
56+
db.refresh(db_post)
57+
58+
db_users = db.query(models.Bookmark).filter(
59+
models.Bookmark.blog_id == db_post.blog_id).all()
60+
61+
for bookmarked_user in db_users:
62+
db_user = db.query(models.User).filter(
63+
models.User.id == bookmarked_user.user_id).first()
64+
if not db_user.email:
65+
continue
66+
67+
if db_user.is_subscribed and not is_init:
68+
mail.send_new_post_notice_email(
69+
receiver_address=db_user.email, post=db_post, user_id=db_user.id)
70+
71+
last_uploaded_at = datetime.datetime.now()
72+
if posts:
73+
last_uploaded_at = UTC_to_KST(datetime.datetime.strptime(
74+
posts[0]["released_at"][:19], "%Y-%m-%dT%H:%M:%S"))
75+
76+
db.query(models.Blog).filter(
77+
models.Blog.id == blog.id).update(
78+
{"last_uploaded_at": last_uploaded_at,
79+
"updated_at": str(datetime.datetime.now())})
80+
db.commit()
81+
82+
return updated_posts
83+
84+
85+
async def update_new_post() -> None:
86+
db = next(database.get_db())
87+
88+
total_updated_blog_cnt: int = 0
89+
total_updated_posts: List[str] = list()
90+
91+
db_blogs = db.query(models.Blog).all()
92+
for blog in db_blogs:
93+
updated_posts = await update_new_post_by_blog(db=db, blog=blog, limit=10)
94+
if updated_posts:
95+
total_updated_blog_cnt += 1
96+
total_updated_posts.extend(updated_posts)
97+
98+
await discord_logging.logging_new_post_upload(total_updated_blog_cnt, total_updated_posts)

0 commit comments

Comments
 (0)