택시짱의 개발 노트

티스토리에서 작성한 글로 github에 잔디 심기 본문

기타

티스토리에서 작성한 글로 github에 잔디 심기

택시짱 2022. 3. 7. 23:34

왜??

티스토리에서 개발 관련 글을 작성한지도 약3년정도가 되어가고 있으며 지금까지 티스토리에 글을 쓰면서 불편하다고 느낀적은 한번도 없었던거 같다. 

그러나 한가지 단점이 있었으니 github에 기록이 남지 않는다는 것 이였다. 

나는 github는 개발일기장과 같다고 생각하였고 글을 작성하는것도 개발의 일부분 이라고 생각 했기 때문이다.

그래서 github blog도 만들어 보았지만 front개발에 무지했던 나로써는.. github blog를 포기했다.


어떻게?

티스토리에 글을 올리면 github action을 통해 일정시간마다 새롭게 작성된 글이 있는지 확인하여

내가 블로그에 작성한 글을 github issue로 작성을 해주도록 하였다.

새롭게 작성된 블로그 글을 github issue로 작성

프로젝트의 아키텍쳐

프로젝트의 아키텍처

사용 기술

1. github

2. github action

3. tistory openapi

위의 아키텍처를 좀더 설명 하자면 github action에 cron이라는 기능을 이용하였다. 일단 프로젝트를 진행 하기 전에 먼저 tistory

 

cron이란?

소프트웨어 유틸리티로써 유닉스 계열 컴퓨터 운영 체제의 시간 기반 job schedule이다. 소프트웨어 환경을 설정하고 관리하는 사람들은 작업을 고정된 시간, 날짜 간격에 주기적으로 실행할 수 있도록 스케쥴링하는데 cron을 사용한다. 
즉 일정 주기 마다 프로그램을 실행 시키는것 이다.

 

tistory openapi를 만들어야 되는데 아래 링크를 참고하면 된다. 

https://taxijjang.tistory.com/144

 

티스토리 앱 등록 & Access Token 발급

티스토리 앱 등록 https://www.tistory.com/guide/api/manage/register 에 접근 하여 앱 등록 서비스 URL , CallBack 은 본인의 Tistory 주소를 입력 해주세요 앱 등록 하면 App ID, Secret Key를 발급 받을 수 있..

taxijjang.tistory.com

 

프로젝트 구조

프로젝트의 구조는 main, blog_info, github_utils, post로 나뉘어져 있다.

1. main은 여러개로 나뉘어 작성된 module를 실행 시킨다. issue를 작성할 repo가 유효한지, 새롭게 작성된 post가 있는지, 새롭게 작성된 post가 있다면 issue를 작성, post.json을 업데이트

아래는 main.py code

더보기
# main.py


import os
from datetime import datetime
from pytz import timezone
from github import BadCredentialsException

from github_utils import GithubUtil
from post import Post

"""
* 환경 변수 * 
- github
MY_GITHUB_ACCESS_TOKEN: settings에서 발급한 access token
- tistory
APP_ID: tistory 에서 발급된 app_id
SECRET_KEY: tistory secret_key
REDIRECT_URI: 본인 tistory 주소를 입력합니다 ex) https://taxijjang.tistory.com
ACCESS_TOKEN: tistory 에서 발급 받은 access_token
USERNAME: 이슈에 남길 이름 ex)택시짱
REPO_NAME: 해당 프로젝트가 포함되어 있는 github repository의 이름 ex) AutoCommitTistory
"""


def main():
    seoul_timezone = timezone('Asia/Seoul')
    today = datetime.now(seoul_timezone)
    today_date = today.strftime('%Y년 %m월 %d일')
    today_date_eng = today.strftime('%Y/%m/%d')
    issue_title = f'{os.environ.get("USERNAME")} TISTORY 새로운 포스팅 알림({today_date})'

    repository_name = os.environ.get('REPO_NAME')
    path = 'posts.json'
    access_token = os.environ.get('MY_GITHUB_ACCESS_TOKEN')
    github_util = GithubUtil(access_token=access_token)

    # check collect github repo
    try:
        github_util.set_github_repo(os.environ.get('REPO_NAME'))
    except BadCredentialsException:
        print("github repository 유효하지 않습니다.")
        return None

    # set my repository
    github_util.set_github_repo(repository_name=repository_name)

    # post objects
    post = Post(
        access_token=os.environ.get('ACCESS_TOKEN')
    )

    # get new_post
    new_posts, upload_issue_body = post.issue_body()

    # no new posts today
    if not upload_issue_body:
        print(f'{today_date} 블로그 포스팅 목록이 없습니다.')
        return None

    # upload new issue
    github_util.upload_github_issue(title=issue_title, body=upload_issue_body, labels=['new_posting'])
    print(f'{today_date} 블로그 포스팅 목록 Issue 등록 성공!')

    # upload new json file push
    github_util.upload_github_push(message=f'Add new posting {today_date_eng}',
                                   content=new_posts, path=path, branch='master')
    print(f'{today_date} posts.json push 성공!')
    return None


if __name__ == '__main__':
    main()

 

2. python에서 제공하는 github library를 이용하여 github에서 발급한 access token으로 나의 git repo에 접근, issue작성, post.json update

아래는 github_utils.py code

더보기

 

# github_utils.py


import json
import os
from json import JSONDecodeError

from github import Github
from github import UnknownObjectException


class GithubUtil:
    def __init__(self, access_token):
        self._access_token = access_token
        self._repo = None

    def set_github_repo(self, repository_name):
        """
        github repo object를 얻는 함수
        :param repository_name: 해당 repo의 이름
        :return: repo object
        """
        g = Github(self._access_token)
        self._repo = g.get_user().get_repo(repository_name)

    def upload_github_issue(self, title, body, labels=None):
        """
        해당 repo의 issue에 새롭게 작성된 post의 내용을 등록하는 함수
        :param title: 이슈 제목
        :param body: 이슈 내용
        :param labels: 이슈의 labels
        :return: None
        """
        self._repo.create_issue(title=title, body=body, labels=labels)

    def upload_github_push(self, message, content, path, branch):
        """
        해당 repo에 변경된 json file을 push 해주는 함수
        :param message: push message
        :param content: push content
        :param path: push 할 대상의 file 위치
        :param branch: push 할 branch
        :return: None
        """
        try:
            contents = self._repo.get_contents(path, branch)
            data = json.loads(contents.decoded_content.decode('utf-8'))
            data = self._check_json_username(data=data, content=content)
            self._repo.update_file(contents.path, message, data, contents.sha, branch=branch)
        except JSONDecodeError:
            data = self._check_json_username(content=content)
            self._repo.update_file(contents.path, message, data, contents.sha, branch=branch)
        except UnknownObjectException:
            # if old file is not exists make new file
            data = self._check_json_username(content=content)
            self._repo.create_file(path, message, data, branch=branch)

    def _check_json_username(self, content, data=None):
        data = data if data else dict()
        if data.get('username') != os.environ.get('USERNAME'):
            data['username'] = os.environ.get('USERNAME')
            data['posts'] = content
        else:
            data['posts'].update(content)
        data = dict(sorted(data.items(), reverse=True))
        data = json.dumps(data, ensure_ascii=False, indent='\t')
        return data

 

3. tistory openapi를 이용하여 얻는 access token을 이용하여 나의 블로그에 접근하여 정보를 획득

아래는 blog_info.py code

더보기
# blog_info.py


import json

import requests


class Tistory:
    @staticmethod
    def get_blog_name(access_token):
        """
        tistory access_token를 이용하여
        해당 유저 블로그의 이름을 반환하는 함수
        :return: 해당 유저 블로그 이름
        """
        blog_info_url = (
            "https://www.tistory.com/apis/blog/info?"
            f"access_token={access_token}"
            f"&output=json"
        )

        req = requests.get(blog_info_url)
        blog_info_json = json.loads(req.text)
        return blog_info_json['tistory']['item']['blogs'][0].get('name')

 

4. 블로그에서 새롭게 작성된 post가 있는지 확인하고 post.json에 새롭게 작성된 post를 업데이트, issue 작성 준비

아래는 post.py code

더보기
# post.py


import os
import json
from json import JSONDecodeError

import requests

from blog_info import Tistory

BASE_DIR = os.path.dirname(os.path.abspath(__file__))


class Post:
    PRIVATE = 0
    PENDING = 15
    PUBLIC = 20

    def __init__(self, access_token):
        """
        init post class
        :param access_token: tistory에서 발급 받은 access_token
        """
        self._access_token = access_token

    def post_list(self, page=1):
        """
        Tistory open api를 이용하여
        내가 작성한 블로그 글 목록을 가지고 오는 함수
        :param page: 현재 보고자 하는 글 목록의 page 번호
        :return: 현재 글 목록을 가지고 있는 page의 data를 dict로 변환
        """
        blog_name = Tistory.get_blog_name(access_token=self._access_token)
        post_list_url = (
            "https://www.tistory.com/apis/post/list?"
            f"access_token={self._access_token}"
            f"&output=json"
            f"&blogName={blog_name}"
            f"&page={page}"
        )
        req = requests.get(post_list_url)
        return json.loads(req.text)

    def post_max_page(self):
        """
        해당 유저의 access_token을 이용하여 post의 목록 data를 가지고 온 후에
        post 목록 api에서 제공해주는 pagination 최대 page를 구하는 함수
        :return: api에서 제공해주는 pagination의 최대 page
        """
        post_list_json = self.post_list()
        count = int(post_list_json.get('tistory').get('item').get('count'))
        total_count = int(post_list_json.get('tistory').get('item').get('totalCount'))
        return total_count // count if total_count % count == 0 else total_count // count + 1

    def all_post_data(self):
        posts = dict()
        max_page = self.post_max_page()
        for page_cnt in range(max_page, 0, -1):
            now_page_posts = self.post_list(page_cnt).get('tistory').get('item').get('posts')
            for now_page_post in now_page_posts:
                if int(now_page_post.get('visibility')) != self.PUBLIC:
                    continue
                posts[int(now_page_post.get('id'))] = now_page_post
        return dict(sorted(posts.items()))

    def check_new_post(self):
        try:
            with open(os.path.join(BASE_DIR, 'posts.json'), "r") as f:
                json_data = json.load(f)
                if json_data.get('username') != os.environ.get('USERNAME'):
                    # When you are a new author
                    print("기존에 작성된 posts.json의 사용자와 다른 사용자 입니다.")
                    raise FileNotFoundError
        except (FileNotFoundError, JSONDecodeError):
            # json file is empty
            json_data = dict()
            json_data['username'] = os.environ.get('USERNAME')
            json_data['posts'] = dict()

        new_posts = dict()
        tistory_posts = self.all_post_data()
        for post_id, data in tistory_posts.items():
            # post is not visibility
            if int(data.get('visibility')) != self.PUBLIC:
                continue
            post_id = str(post_id)
            if not json_data.get('posts').get(post_id):
                json_data['posts'][post_id] = data
                new_posts[post_id] = data

        # make now posts_data in json file
        print(json_data)
        with open(os.path.join(BASE_DIR,'posts.json'), 'w', encoding='utf-8') as make_file:
            json.dump(json_data, make_file, ensure_ascii=False, indent="\t")
        return new_posts

    def issue_body(self):
        new_posts = self.check_new_post()
        upload_issue_body = ''
        for key, value in new_posts.items():
            if int(value.get('visibility')) != self.PUBLIC:
                continue
            id = value.get('id')
            title = value.get('title')
            post_url = value.get('postUrl')
            create_at = value.get('date')

            content = f'{id} - <a href={post_url}>{title}</a>, {create_at} <br/>\n'
            upload_issue_body += content
        return new_posts, upload_issue_body

 

github repo link -> https://github.com/taxijjang/AutoCommitTistory

 

GitHub - taxijjang/AutoCommitTistory: 티스토리에 작성한 글을 확인하고 자동으로 github에다가 commit 해주

티스토리에 작성한 글을 확인하고 자동으로 github에다가 commit 해주는 봇. Contribute to taxijjang/AutoCommitTistory development by creating an account on GitHub.

github.com

 

개선하면 좋을 점

1. 새로운 글 판별 하는 방법 수정

새로운 글을 판별하기 위해 post.json을 이용하고 있는데 블로그의 글이 엄청 많이 작성이 되었을때는

다른 방법을 이용하여 판별 할 수 있는 방법을 찾아야 겠다. post의 정보를 db로 migration 등등

2. 프로젝트에 작성된 여러 code의 구조 개선

살짝 의식의 흐름대로 code를 작성한 부분이 없지 않아 있어 clean code를 적용하여 더 깔끔한 코드를 작성하고 싶다.

3. 비공개로 작성한 글은 새로운 글에서 제외

현재는 비공개로 작성된 글도 새로운 글로 인식하여 issue에 올리고 있는데 이것을 수정 해야된다.  수정완료

3. 여러 개발자들의 의견을 수용

내가 code를 작성했지만 여러 개발자들이 내 코드를 사용하며 느낀 부족한 부분 또는 잘못된 구조의 코드를 같이 개선하고 싶다!

 

마무리

위의 프로젝트를 진행하면서 tistory에 개발 관련 글을 작성했을때 github에 잔디 심기라는 목표는 달성했다!

확실히 github에 잔디가 심어지는게 블로그에 글을 꾸준하게 작성할 수 있게 하는 원동력이 된것같다.

아마 저와 같은 고민 (tistory에서 글 작성시 github에 기록을 남기고 싶은..)을 하시는분들이 조금은 있지 않을까 생각하고 있고 

그 분들에게 조금이나마 도움이 되었으면 좋겠다!

아래는 현재 진행 중인 issue의 목록이다!

 

반응형
Comments