Blog

[Python] 티스토리 API 100% 활용해보기 - 00

2023. 12. 28. 15:33
목차
  1. 티스토리 OPEN API 등록
  2. tkinter
  3. 프로그램 클래스 생성
  4. 생성자 추가
  5. OAuth handler 구현
  6. 티스토리 인증 코드 발급
  7. Access Token 발급
  8. 전체 코드
728x90

티스토리에서는 블로그 활동을 조금 더 편하게 하도록 도움을 줄 수 있는 API들을 제공하고 있다.

나는 평소에 글 작성 시 주로 마크다운으로 작성하는데, 미리보기가 불편한 점과 저장 문제 등 불편한 점이 좀 있어서 개선하고싶었다.

이걸 해결해보기 위해 파이썬으로 간단한 마크다운 업로드 프로그램을 만들어보고자 한다.

티스토리 OPEN API 등록

우선, 티스토리 오픈 API를 사용하기 위해서는 앱 등록을 해주어야한다.

등록 링크에 접속한 뒤, 앱 등록을 해준다.

 

Tistory

좀 아는 블로거들의 유용한 이야기

www.tistory.com

서비스 명과 설명은 아무거나 써도 상관 없고, 서비스 URL은 자신의 블로그 주소, callback은 localhost:8000을 써준다. 그 이유는 아래 후술하겠지만, callback을 통해 티스토리에서 인증 코드를 발급받을 수 있는데, 이 때 우리는 로컬 서버를 통해 코드를 발급받기 위함이다.

등록하면 다음과 같은 값들을 얻을 수 있다. 이 값들은 API를 사용하기 위한 필수 값들이다.

tkinter

파이썬 프로그램을 간단하게 만들어보기 위해 tkinter 라이브러리를 사용해보았다. tkinter는 GUI를 구현하기 위한 기본 패키지로, 거의 대부분 파이썬이 설치되어있다면 tkinter도 함께 설치되어있을것이다.

tkinter의 사용법은 이 페이지를 보면 될 것이다.

 

Python tkinter 강좌 : 제 1강 - GUI 생성

tkinter

076923.github.io

프로그램 클래스 생성

import tkinter as tk
from tkinter import filedialog
import tkinter.font
import http.server
import socketserver
import threading
from urllib.parse import urlparse, parse_qs
import tkinter.messagebox as messagebox
import requests
import webbrowser

class TistoryUploaderApp:
    def __init__(self, root):
        # 동작 추가


if __name__ == "__main__":
    root = tk.Tk()
    TistoryUploaderApp(root)
    root.mainloop()

생성자 추가

if __name__ == "__main__":
    root = tk.Tk()
    TistoryUploaderApp(root)
    root.mainloop()

먼저 이 부분이다. python3 main.py 처럼 프로그램을 실행하게된다면, 이 부분의 로직을 수행하겠다는 의미이다.

tk는 tkinter로, 가장 상위 레벨의 윈도우 창을 생성하고, TistoryUploaderApp이라는 클래스를 생성한 뒤, 윈도우 창을 종료될 때 까지 실행시킨다는 의미이다.

    def __init__(self, root):
        self.root = root
        self.root.title("Tistory Markdown Uploader")
        self.root.geometry("800x200")
        self.background_color = "#FEE500"
        self.root.configure(bg=self.background_color)
        self.root.columnconfigure(0, weight=1)
        self.root.columnconfigure(3, weight=1)
        self.font = tkinter.font.Font(family="NanumGothic", size=12)
        self.app_id_label = tk.Label(
            root, text="App ID", bg=self.background_color, font=self.font
        )
        self.app_id_entry = tk.Entry(root)
        self.client_secret_label = tk.Label(
            root, text="Secret Key", bg=self.background_color, font=self.font
        )
        self.client_secret_entry = tk.Entry(root)
        self.authorize_button = tk.Button(
            root, text="Authorize", command=self.get_authorize_code
        )
        self.unauthorized_button = tk.Button(
            root, text="Unauthorize", command=self.unauthorize
        )
        self.authorize_success_label = tk.Label(
            root, text="인증 성공", bg=self.background_color, font=self.font
        )

        self.layout_widgets()
        self.server_thread = threading.Thread(target=self.start_server, daemon=True)
        self.server_thread.start()

    def layout_widgets(self):
        self.app_id_label.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
        self.app_id_entry.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
        self.client_secret_label.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
        self.client_secret_entry.grid(row=1, column=2, padx=10, pady=10, sticky="ew")
        self.authorize_button.grid(row=1, column=3, padx=10, pady=10, sticky="ew")

    def start_server(self):
        handler = lambda *args: self.OAuthHandler(self, *args)
        PORT = 8000
        with socketserver.TCPServer(("", PORT), handler) as httpd:
            print(f"Server started at http://localhost:{PORT}")
            httpd.serve_forever()

TistoryUploaderApp 클래스를 생성하면 실행되는 생성자이다. 여기서는 윈도우의 기본 설정, 로컬 서버 설정 등 초기 설정들이 포함된다.

프로그램을 실행시키면 윈도우 세팅이 완료되고, 로컬 서버까지 열리도록 세팅해주었다.

디자인이 영 별로긴 한데.. 어쨌든 의도대로 동작하는걸 확인해볼 수 있을것이다.

서버 세팅 중, handler라는 것이 포함된다. 이것을 통해 티스토리 서버에서 callback 해주는 응답을 수신하도록 설정할 수 있다.

OAuth handler 구현

    class OAuthHandler(http.server.SimpleHTTPRequestHandler):
        def __init__(self, app, *args, **kwargs):
            self.app = app
            super().__init__(*args, **kwargs)

        def do_GET(self):
            url = urlparse(self.path)
            query_params = parse_qs(url.query)
            # 에러 메시지 처리
            error = query_params.get("error")
            error_description = query_params.get("error_description", [""])[0]

            if error:
                self.send_response(401)
                self.end_headers()
                self.wfile.write(f"Error: {error[0]} - {error_description}".encode())
                return

            # 인증 코드 처리
            auth_code = query_params.get("code", [None])[0]
            if auth_code:
                print("Received OAuth code:", auth_code)
                # 인증 코드 처리 로직
                self.send_response(200)
                self.end_headers()
                self.wfile.write(
                    "Authentication successful. You can close this window.".encode()
                )
                self.app.authorize_success()
                return

            # 기본 응답
            self.send_response(404)
            self.end_headers()
            self.wfile.write("Page not found.".encode())

이 클래스는 TistoryUploadApp 클래스 내부에 위치하며, http.server.SimpleHTTPRequestHandler를 상속받아 구현했다. OAuth 인증 과정에서 발생하는 HTTP 요청을 처리한다.

__init__ 메서드: 이 메서드는 OAuthHandler 인스턴스를 초기화한다. 여기서 app 매개변수는 TistoryUploaderApp 인스턴스를 참조하며, 이를 통해 Tkinter GUI와 상호작용이 가능해진다.

do_GET 메서드: 웹 브라우저나 다른 HTTP 클라이언트로부터 GET 요청을 받을 때 호출된다. 이 메서드는 다음과 같은 두 가지 주요 기능을 수행한다:

  • 에러 처리: 인증 과정에서 오류가 발생한 경우, 적절한 HTTP 응답(401)과 함께 오류 메시지를 클라이언트에 전송한다.
  • 인증 코드 처리: OAuth 인증 과정에서 인증 코드(code)를 성공적으로 받은 경우, authorize_success 메서드를 호출하여 인증 성공을 처리한다.
  • 기본 응답: 해당 URL 경로에서 처리할 수 없는 요청에 대해 404 오류 응답을 반환한다.

이 클래스를 통해 OAuth 인증 과정에서 발생하는 HTTP 요청을 적절하게 처리해줄 수 있고, 인증 코드를 발급받거나 오류를 식별할 수 있다.

티스토리 인증 코드 발급

인증 코드를 발급받기 위해서는 아래 URL을 통해 자신의 정보를 전송해주어야 한다.

https://www.tistory.com/oauth/authorize?
  client_id={client-id}
  &redirect_uri={redirect-uri}
  &response_type=code
  &state={state-param}

매개변수는 다음과 같다.

  • client_id: 클라이언트 정보의 Client ID. 이전에 발급받은 App ID가 이에 해당한다.
  • redirect_uri: 사용자가 인증 후에 리디렉션할 URI. 클라이언트 정보의 Callback 경로로 등록하여야 하며 등록되지 않은 URI를 사용하는 경우 인증이 거부된다. 이전에 작성한 callback을 작성해주면 된다. 우리의 경우 `http://localhost:8000`이 될 것이다.
  • response_type: 항상 code를 사용한다.
  • state: 사이트간 요청 위조 공격을 보호하기 위한 임의의 고유한 문자열이며 리디렉션시 해당 값이 전달된다. (필수아님)

인증코드를 요청하는 코드를 작성했다.

    def get_authorize_code(self):
        app_id = self.app_id_entry.get().strip()
        client_secret = self.client_secret_entry.get().strip()

        if not app_id or not client_secret:
            messagebox.showwarning("경고", "App ID와 Secret Key를 모두 입력해주세요.")
            return

        oauth_url = "https://www.tistory.com/oauth/authorize"
        auth_param = {
            "client_id": self.app_id_entry.get().strip(),
            "redirect_uri": "http://localhost:8000",
            "response_type": "code",
        }
        with requests.session() as s:
            res = s.get(oauth_url, params=auth_param, allow_redirects=False)
            webbrowser.open(res.url)

URL에 맞게 코드를 작성해주었다. 이 메서드에서는 입력창이 비어있는지 확인하는 로직과 요청하는 부분으로 나뉘어있다.

마음같아서는 티스토리, 카카오 로그인까지 한번에 다 붙이고 싶었는데, 보안상의 문제인지 무조건 "https://www.tistory.com/oauth/authorize" 이 URL로 인증 요청을 보낸 뒤, 티스토리 서버에서 직접 로그인 과정을 거쳐야했다.

이런 과정때문에, 요청을 보내면 티스토리 서버에서는 리다이렉션 url을 응답하게 되는데, 이 url을 브라우저에서 열어주면 다음과 같은 화면이 뜬다.

아직 client_secret은 사용하지 않기 때문에 아무거나 넣어줘도 되고, 이전에 발급받은 App ID를 입력해준뒤 버튼을 눌러보자.

로그인 창이 뜬다면, 로그인부터 해주면 된다.

허가하기를 눌러주면 터미널에서 인증 코드가 발급된 것을 확인할 수 있다.

Access Token 발급

인증 코드는 access token을 발급받기 위해 필요한 준비물이라고 생각하면 된다.

GET https://www.tistory.com/oauth/access_token?
  client_id={client-id}
  &client_secret={client-secret}
  &redirect_uri={redirect-uri}
  &code={code}
  &grant_type=authorization_code

이전과 별로 다를건 없지만, 이전에 발급받은 인증 코드와 client-secret이 추가적으로 필요하다.

# TistoryUploaderApp 내부 메서드로 추가
    def get_access_token(self, code):
        access_url = "https://www.tistory.com/oauth/access_token"
        access_param = {
            "client_id": self.app_id_entry.get().strip(),
            "client_secret": self.client_secret_entry.get().strip(),
            "redirect_uri": "http://localhost:8000",
            "code": code,
            "grant_type": "authorization_code",
        }
        with requests.session() as s:
            res = s.get(access_url, params=access_param)
            if res.status_code == 200:
                self.access_token = res.text.split("=")[1]
                print("Access Token:", self.access_token)
                self.authorize_success()
                return True
            return False

    def authorize_success(self):
        self.app_id_label.grid_remove()
        self.app_id_entry.grid_remove()
        self.client_secret_label.grid_remove()
        self.client_secret_entry.grid_remove()
        self.authorize_button.grid_remove()

        self.authorize_success_label.grid(
            row=0, column=1, padx=10, pady=10, sticky="ew"
        )
        self.unauthorized_button.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
# OAuthHandler 클래스 변경사항
            # 인증 코드 처리
            auth_code = query_params.get("code", [None])[0]
            if auth_code:
                print("Received OAuth code:", auth_code)
                # 인증 코드 처리 로직
                if self.app.get_access_token(auth_code):
                    self.send_response(200)
                    self.end_headers()
                    self.wfile.write(
                        "Authentication successful. You can close this window.".encode()
                    )
                else:
                    self.send_response(401)
                    self.end_headers()
                    self.wfile.write("Authentication failed.".encode())
                return

OAuthHandler를 통해 auth_code를 성공적으로 발급받았다면, get_access_token 메서드를 호출해 토큰 발급을 받아주는 코드를 작성했다.

성공적으로 토큰을 발급받았다면 인증 성공을 브라우저에 띄워주고 프로그램의 버튼 레이어를 변경해준다.

이번엔 client_secret도 제대로 입력해준다.

토큰 발급 성공

전체 코드

import tkinter as tk
from tkinter import filedialog
import tkinter.font
import http.server
import socketserver
import threading
from urllib.parse import urlparse, parse_qs
import tkinter.messagebox as messagebox
import requests
import webbrowser


class TistoryUploaderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Tistory Markdown Uploader")
        self.root.geometry("800x200")
        self.background_color = "#FEE500"
        self.root.configure(bg=self.background_color)
        self.root.columnconfigure(0, weight=1)
        self.root.columnconfigure(3, weight=1)
        self.font = tkinter.font.Font(family="NanumGothic", size=12)
        self.access_token = None
        self.app_id_label = tk.Label(
            root, text="App ID", bg=self.background_color, font=self.font
        )
        self.app_id_entry = tk.Entry(root)
        self.client_secret_label = tk.Label(
            root, text="Secret Key", bg=self.background_color, font=self.font
        )
        self.client_secret_entry = tk.Entry(root)
        self.authorize_button = tk.Button(
            root, text="Authorize", command=self.get_authorize_code
        )
        self.unauthorized_button = tk.Button(
            root, text="Unauthorize", command=self.unauthorize
        )
        self.authorize_success_label = tk.Label(
            root, text="인증 성공", bg=self.background_color, font=self.font
        )

        self.layout_widgets()
        self.server_thread = threading.Thread(target=self.start_server, daemon=True)
        self.server_thread.start()

    def layout_widgets(self):
        self.app_id_label.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
        self.app_id_entry.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
        self.client_secret_label.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
        self.client_secret_entry.grid(row=1, column=2, padx=10, pady=10, sticky="ew")
        self.authorize_button.grid(row=1, column=3, padx=10, pady=10, sticky="ew")

    def start_server(self):
        handler = lambda *args: self.OAuthHandler(self, *args)
        PORT = 8000
        with socketserver.TCPServer(("", PORT), handler) as httpd:
            print(f"Server started at http://localhost:{PORT}")
            httpd.serve_forever()

    class OAuthHandler(http.server.SimpleHTTPRequestHandler):
        def __init__(self, app, *args, **kwargs):
            self.app = app
            super().__init__(*args, **kwargs)

        def do_GET(self):
            url = urlparse(self.path)
            query_params = parse_qs(url.query)
            # 에러 메시지 처리
            error = query_params.get("error")
            error_description = query_params.get("error_description", [""])[0]

            if error:
                self.send_response(401)
                self.end_headers()
                self.wfile.write(f"Error: {error[0]} - {error_description}".encode())
                return

            # 인증 코드 처리
            auth_code = query_params.get("code", [None])[0]
            if auth_code:
                # 인증 코드 처리 로직
                if self.app.get_access_token(auth_code):
                    self.send_response(200)
                    self.end_headers()
                    self.wfile.write(
                        "Authentication successful. You can close this window.".encode()
                    )
                else:
                    self.send_response(401)
                    self.end_headers()
                    self.wfile.write("Authentication failed.".encode())
                return

            # 기본 응답
            self.send_response(404)
            self.end_headers()
            self.wfile.write("Page not found.".encode())

    def authorize_success(self):
        self.app_id_label.grid_remove()
        self.app_id_entry.grid_remove()
        self.client_secret_label.grid_remove()
        self.client_secret_entry.grid_remove()
        self.authorize_button.grid_remove()

        self.authorize_success_label.grid(
            row=0, column=1, padx=10, pady=10, sticky="ew"
        )
        self.unauthorized_button.grid(row=0, column=2, padx=10, pady=10, sticky="ew")

    def unauthorize(self):
        self.app_id_label.grid()
        self.app_id_entry.grid()
        self.client_secret_label.grid()
        self.client_secret_entry.grid()
        self.authorize_button.grid()

        self.authorize_success_label.grid_remove()
        self.unauthorized_button.grid_remove()

    def get_authorize_code(self):
        app_id = self.app_id_entry.get().strip()
        client_secret = self.client_secret_entry.get().strip()

        if not app_id or not client_secret:
            messagebox.showwarning("경고", "App ID와 Secret Key를 모두 입력해주세요.")
            return

        oauth_url = "https://www.tistory.com/oauth/authorize"
        auth_param = {
            "client_id": self.app_id_entry.get().strip(),
            "redirect_uri": "http://localhost:8000",
            "response_type": "code",
        }
        with requests.session() as s:
            res = s.get(oauth_url, params=auth_param, allow_redirects=False)
            webbrowser.open(res.url)

    def get_access_token(self, code):
        access_url = "https://www.tistory.com/oauth/access_token"
        access_param = {
            "client_id": self.app_id_entry.get().strip(),
            "client_secret": self.client_secret_entry.get().strip(),
            "redirect_uri": "http://localhost:8000",
            "code": code,
            "grant_type": "authorization_code",
        }
        with requests.session() as s:
            res = s.get(access_url, params=access_param)
            if res.status_code == 200:
                self.access_token = res.text.split("=")[1]
                self.authorize_success()
                return True
            return False


if __name__ == "__main__":
    root = tk.Tk()
    TistoryUploaderApp(root)
    root.mainloop()

발급받은 토큰을 통해 여러 티스토리 API를 사용하는것은 다음에 올려보도록 하겠다.

728x90

'Blog' 카테고리의 다른 글

[Python] 티스토리 API 100% 활용해보기 - 完  (1) 2023.12.29
티스토리로 옮기며...  (0) 2023.02.19
깃허브에 개인 블로그 배포하기  (0) 2023.02.19
next.js로 개인 블로그 만들기  (0) 2023.02.19
  1. 티스토리 OPEN API 등록
  2. tkinter
  3. 프로그램 클래스 생성
  4. 생성자 추가
  5. OAuth handler 구현
  6. 티스토리 인증 코드 발급
  7. Access Token 발급
  8. 전체 코드
'Blog' 카테고리의 다른 글
  • [Python] 티스토리 API 100% 활용해보기 - 完
  • 티스토리로 옮기며...
  • 깃허브에 개인 블로그 배포하기
  • next.js로 개인 블로그 만들기
chanwoong1
chanwoong1
안녕하세요.
250x250
chanwoong1
WOONGTECH
chanwoong1
전체
오늘
어제
  • 분류 전체보기 (231)
    • 42SEOUL (28)
      • Circle0 (1)
      • Circle1 (3)
      • Circle2 (3)
      • Circle3 (2)
      • Circle4 (7)
      • Circle5 (8)
      • Circle6 (4)
    • Algorithm (163)
      • PS (159)
      • Study (4)
    • Blog (5)
    • 우테코 프리코스 (5)
    • Data Science (1)
    • WEB (27)
      • React (18)
      • Recoil (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

최근 댓글

최근 글

hELLO · Designed By 정상우.
chanwoong1
[Python] 티스토리 API 100% 활용해보기 - 00
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.