Confluence API로 가져온 제목으로 org-mode 마크업 링크 삽입 - org-cliplink 기능 확장
org-mode에 웹 링크를 삽입할 일이 있으면 Org-cliplink를 사용하고 있다. 인증이 필요한 Confluence 페이지는 org-cliplink로 가져올 수 없어서 기능 추가를 했다.
Confluence API 테스트
Emacs lisp 코드를 짜기 전에 API 테스트를 해서 원하는 걸 가져올 수 있는지 테스트해 본다. 추출하고 싶은 항목은 title, space name이다. Confluence 버전이 6.x라서 HTTP 기본 인증만 지원한다.
Restclient를 사용하면 org 문서에서 바로 테스트할 수 있다.
#+begin_src restclient
:auth := (format "Basic %s" (base64url-encode-string (format "%s:%s" "ohyecloudy" "SUPERSECRET")))
GET http://confluence/rest/api/content/1234
Authorization: :auth
#+end_src
user:password
스트링을 base64로 인코딩한 문자열 앞에 Basic
을 붙여주면 된다. 소스 블럭에서 C-c C-c
키를 누르면 결과가 ’짠’하고 나타난다.
#+RESULTS:
#+BEGIN_SRC js
{
...
"title": "Confluence Page Title",
"space": {
"name": "MySpace",
...
},
...
}
#+END_SRC
rest/api/content/[ID]
메서드를 사용하면 URL 링크를 삽입할 때, 사용할 스페이스 이름과 페이지 제목을 가져올 수 있다.
auth-source에 id와 password를 저장
Auth-source 파일에 id와 password를 저장한다. 이후에 이 정보로 HTTP Basic Authentication에 사용할 문자열을 만들 예정이다.
my/org-cliplink-confluence--id-password
함수는 id password 리스트를 리턴한다. 찾지 못하면 nil
을 리턴한다.
(defun my/org-cliplink-confluence--id-password (host)
(when-let* ((found (auth-source-search
:host host
:requires '(:secret)))
(first-found (nth 0 found))
(id (plist-get first-found :user))
(password (funcall (plist-get first-found :secret))))
(list id password)))
~/.authinfo.gpg
나 ~/.authinfo
파일에 id와 password를 추가한다.
machine confluence_host login ohyecloudy password SUPERSECRET
confluence_host
를 인자로 넘겨서 리턴 값을 확인한다.
(my/org-cliplink-confluence--id-password "confluence_host")
("ohyecloudy" "SUPERSECRET")
HTTP Basic Authentication 문자열 생성
Confluence API를 호출할 때, 실제 필요한 문자열이다. id:password
문자열을 Base64URL로 인코딩한다.
(defun my/org-cliplink-confluence--auth (host)
(when-let* ((id-password (my/org-cliplink-confluence--id-password host))
(encoded (base64url-encode-string
(format "%s:%s" (car id-password)
(cadr id-password)))))
`("Authorization" . ,(format "Basic %s" encoded))))
결과는 ("Authorization" . "Basic ENCODED_ID_PASSWORD")
형식의 리스트다. url-request 패키지에서 해더로 Dotted Pair Notation 리스트를 받기 때문에 해당 포멧을 리턴한다.
Confluence API를 사용해 스페이스 이름과 페이지 제목을 가져오기
만든 HTTP 기본 인증을 사용해 Confluence API를 호출한다.
(defun my/org-cliplink-confluence--get (url authorization-header filter-fun)
(let ((url-request-extra-headers `(,authorization-header
("Content-type" . "application/json; charset=utf-8")))
(json-key-type 'keyword)
(json-object-type 'plist))
(with-temp-buffer
(url-insert-file-contents url)
(let ((content (json-read)))
(funcall filter-fun content)))))
url은 API 주소다. Conflunce 6.x 기준으로 페이지 정보를 가져오는 API는 http://CONFLUENCE_URL/rest/api/content/PAGEID
형식이다. 세 번째 인자인 filter-fun을 Property List로 변환한 HTTP GET 메서드 응답을 필터링해서 원하는 필드 정보만 뽑는다.
my/org-cliplink-confluence--get
함수에 페이지 제목과 스페이스 이름만 필터링하는 필터 함수를 인자로 넘겨 호출한다.
(defun my/org-cliplink-confluence--retrieve-content (url auth-header)
(let ((filter-fun (lambda (content)
(let ((title (plist-get content :title))
(space (plist-get (plist-get content :space) :name)))
(list :title title :space space)))))
(my/org-cliplink-confluence--get url
auth-header
filter-fun)))
위에 만든 함수를 사용해 authorization header를 만들어 호출해 본다.
(my/confluence--retrieve-content "http://confluence/rest/api/content/1234"
(my/org-cliplink-confluence--auth "confluence_host"))
(:title "Confluence Page Title" :space "MySpace")
페이지 제목과 스페이스 이름을 잘 가져온다.
URL에서 host와 API URL을 매칭하고 pageId
추출
Org-cliplink와 연동할 계획이다. 그래서 URL이 현재 작성 중인 패키지의 입력이다. URL에서 auth-source로부터 id와 password를 가져올 키 역할을 하는 host와 Confluence API를 사용할 page id를 추출해야 한다.
Confluence API를 사용할 host는 인자로 받는다. url-generic-parse-url 함수를 사용해 문자열에서 host를 분리해서 사용할까 생각했지만 host에 추가 패스를 사용하는 경우가 문제다. 예를 들어 confluence 호스트가 confluence_host
이면 문제가 없는데, host/confluence
식으로 host에 추가적인 path가 붙는다면 host와 path를 추출해서 조합해야 한다.
(defun my/org-cliplink-confluence--find-host (url host-api-urls)
(car (seq-filter (lambda (elem)
(let ((host (car elem)))
(string-match-p (regexp-quote host) url)))
host-api-urls)))
(my/org-cliplink-confluence--find-host
"http://host/confluence/pages/viewpage.action?pageId=123456"
'(("host/confluence" . "http://host/confluence")
("host.company.corp/confluence" . "http://host.company.corp/confluence")))
("host/confluence" . "http://host/confluence")
입력으로 host와 api url로 구성한 Association List를 받게 했다. URL에 해당하는 host와 API URL을 얻어온다.
못 찾으면 nil
을 리턴한다. 리턴 값이 nil
이 아니면 이걸로 Authorization header를 만든다.
page id는 URL에서 정규식을 사용해 Query String에서 추출한다.
(defun my/org-cliplink-confluence--page-id (url)
(save-match-data
(if (string-match "\\?pageId=\\([[:digit:]]+\\)" url)
(match-string 1 url))))
(my/org-cliplink-confluence--page-id "http://confluence_host/pages/viewpage.action?pageId=123456")
123456
Confluence API URL 만들기
요청할 URL을 제외한 요청에 필요한 재료는 다 만들었다. http, https를 알아서 처리하는 게 귀찮으니 API URL을 인자로 받게 한다.
(defun my/org-cliplink-confluence--api-content-get (base-url id)
(format "%s/rest/api/content/%s" base-url id))
(my/org-cliplink-confluence--api-content-get "http://host/confluence" 12345)
http://host/confluence/rest/api/content/12345
조립 - 대표 함수 만들기
host와 api url을 짝을 지어서 설정하게 해줘야 한다. 함수 인자보다는 custom 설정으로 노출한다. Config.local.el처럼 버전 컨트롤에서 제외한 파일에서 설정할 예정이라 함수 인자로 전달하는 것보단 더 직관적이다. 만약 함수 인자로 넘긴다고 결정하면 config.local.el
파일에서 심볼에 설정을 바인딩하고 버전 컨트롤하는 config.el
파일에서는 해당 심볼을 함수 인자로 넘겨야하기 때문이다.
(defcustom my/org-cliplink-confluence-host-api-urls '()
"Alist of api url about host
Each element has the form (HOST . API-URL)."
:type '(alist :key-type string :value-type string)
:group 'my/org-cliplink-confluence)
(add-to-list 'my/org-cliplink-confluence-host-api-urls '("host/confluence" . "http://host/confluence"))
이렇게 추가해주면 된다. 이제 모두 조립해서 함수를 정의한다.
(defun my/org-cliplink-confluence-title (url)
(when-let* ((host-api (my/org-cliplink-confluence--find-host url my/org-cliplink-confluence-host-api-urls))
(host (car host-api))
(api-base-url (cdr host-api))
(page-id (my/org-cliplink-confluence--page-id url))
(api-url (my/org-cliplink-confluence--api-content-get api-base-url page-id))
(auth-header (my/org-cliplink-confluence--auth host))
(content (my/org-cliplink-confluence--retrieve-content
(my/org-cliplink-confluence--api-content-get api-base-url page-id)
auth-header)))
(format "%s > %s" (plist-get content :space) (plist-get content :title))))
host와 API base URL을 추출한다. host 정보로 Auth-source에서 id와 password를 가져온다. 가져온 정보로 GET 리퀘스트 header로 사용할 authorization 헤더를 만들어서 Confluence API를 호출한다. 간단한 코드라 실패 메시지 대신 nil을 리턴하게 처리했으며 when-let
을 사용해서 심볼에 바인딩하는 값이 nil이 되면 즉각 중지하게 했다.
(my/org-cliplink-confluence-title "http://confluence/rest/api/content/1234")
MySpace > Confluence Page Title
Org-cliplink과 연동
Org-mode에서 웹페이지 링크를 삽입할 때, URL을 클립보드에 복사하고 SPC m l c
키를 누른다. 바인딩한 키는 my/org-cliplink
함수를 호출하는데, 클립보드에서 URL을 가져와 웹페이지를 방문해 타이틀을 가져온다. 가져온 타이틀로 org-mode의 link 마크업 텍스트를 삽입한다. 별도의 함수를 추가하지 않고 my/org-cliplink
함수에 기능을 추가해 보자. Confluence API를 호출해 웹페이지 정보를 가져오거나 기존처럼 방문해서 웹페이지 타이틀을 가져오게 한다.
my-org-cliplink
라이브러리와 my-org-cliplink-confluence
라이브러리 사이에 의존성을 만들지 않는다. my/org-cliplink
에 타이틀을 가져오는 함수를 정의할 수 있게 변수를 선언한다. 해당 변수로 선언된 함수를 호출해서 nil이 아니면 사용하고 nil이면 기존처럼 웹페이지 타이틀을 가져와서 사용하게 한다.
(defcustom my/org-cliplink-custom-retrieve-title-hook nil
"Used when retrieving the title using a method other than obtaining the title by visiting a web page.
If nil is returned, the web page is visited and the title is obtained.
e.g. A page that obtains the title using the API. jira, confluence."
:type 'hook
:group 'my/org-cliplink)
(defun my/org-cliplink ()
(interactive)
(let* ((url (org-cliplink-clipboard-content))
(title (when my/org-cliplink-custom-retrieve-title-hook
(funcall my/org-cliplink-custom-retrieve-title-hook url))))
(if title
(insert (funcall #'my/org-cliplink-link-transformer url title))
(org-cliplink-insert-transformed-title
url
#'my/org-cliplink-link-transformer))))
my/org-cliplink-custom-retrieve-title-hook
변수에 my-org-cliplink-confluence
라이브러리를 사용해 웹페이지 정보를 가져오는 함수를 정의하면 된다.
(setq my/org-cliplink-custom-retrieve-title-hook
#'my/org-cliplink-confluence-title)
마치며
HTTP 기본 인증만 지원하는 Confluence 6.x API를 호출해서 페이지 정보를 가져오는 라이브러리를 만들었다. Org-cliplink 라이브러리를 확장해서 Confluence API를 사용해야 하는 주소이면 API를 사용하고 아니면 페이지 정보를 긁어서 타이틀을 가져온다. 많이 사용하고 손에 익은 SPC m l c
키바인딩을 그대로 사용할 수 있어서 편리하다.
링크
- org-cliplink 패키지로 title과 url을 편하게 삽입 - (emacsian ohyecloudy) - ohyecloudy.com(archive)
- 컨플루언스 (소프트웨어) - ko.wikipedia.org(archive)
- Base64URL - Base64 Standards - Base64 - base64.guru(archive)
- auth-source와 EasyPG 기본 제공 패키지를 사용한 비밀 번호를 포함한 계정 정보 관리 - (emacsian ohyecloudy…(archive)
- Doom emacs 로컬 설정 파일 config.local.el - (emacsian ohyecloudy) - ohyecloudy.com(archive)
- Confluence REST API Documentation - docs.atlassian.com(archive)
- REST 웹서비스를 테스트하고 싶다면 restclient 패키지를 - (emacsian ohyecloudy) - ohyecloudy.com(archive)
- Association Lists (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
- Dotted Pair Notation (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
- URI Parsing (URL Programmer’s Manual) - gnu.org(archive)
- Query string - en.wikipedia.org(archive)
- Property Lists (GNU Emacs Lisp Reference Manual) - gnu.org(archive)
C-x C-s C-x C-c