· 6 years ago · Mar 14, 2020, 03:38 AM
1;;; naria2-jsonrpc.el --- Control aria2 in Emacs via WebSocket -*- lexical-binding: t -*-
2
3;; Copyright (C) 2019-2020 Zhu Zihao
4
5;; Author: Zhu Zihao <all_but_last@163.com>
6;; URL: https://github.com/cireu/emacs-naria2
7;; Version: 0.0.1
8;; Package-Requires: ((emacs "25.2") (jsonrpc "1.0.7") (websocket "1.11.1"))
9;; Keywords: conn, lisp
10
11;; This file is NOT part of GNU Emacs.
12
13;; This file is free software; you can redistribute it and/or modify
14;; it under the terms of the GNU General Public License as published by
15;; the Free Software Foundation; either version 3, or (at your option)
16;; any later version.
17
18;; This program is distributed in the hope that it will be useful,
19;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21;; GNU General Public License for more details.
22
23;; For a full copy of the GNU General Public License
24;; see <https://www.gnu.org/licenses/>.
25
26;;; Commentary:
27
28;; * Introduction
29
30;; This package allow you to control your [[https://github.com/aria2/aria2][aria2]] instance via WebSocket and JSONRPC
31;; in Emacs. Some low-level API was wrapped for usage.
32
33;; * Installation
34
35;; The suggested way to install is =package.el=.
36
37;; * Examples
38
39;; #+begin_src emacs-lisp
40;; (require 'naria2-jsonrpc)
41
42;; ;; Sync API
43;; ;; Notice that closing a connection in explicit is *always* unnecessary,
44;; ;; because `naria2-jsonrpc-connection' is a RAII object, so the release of
45;; ;; resource can be delegated to GC.
46
47;; (letrec ((inst (naria2-jsonrpc-connect "localhost" 6800 nil "tokenfoobar"))
48;; (gid (naria2-jsonrpc-sync-call inst "addUri"
49;; [["http://example.org/file"]]))
50;; (cancelback (naria2-jsonrpc-on inst "onDownloadComplete"
51;; (lambda (_data)
52;; (message "Task completed")
53;; (funcall cancelback)))))
54;; (message "The GID of download task is %s" gid))
55
56;; ;; Async API
57;; (let ((inst (naria2-jsonrpc-connect "localhost" 6800 nil nil)))
58;; (naria2-jsonrpc-async-call
59;; inst "addUri"
60;; :success-fn (lambda (data)
61;; (message "The GID of download task is %s" gid))))
62;; #+end_src
63
64;; * Reference
65
66;; - [[https://aria2.github.io/manual/en/html/aria2c.html][API manual of Aria2]]
67
68;;; Code:
69
70(require 'jsonrpc)
71(require 'websocket)
72(require 'eieio)
73
74(eval-when-compile
75 (require 'cl-lib)
76 (require 'subr-x)
77 (require 'pcase))
78
79;;; Utilities
80
81(defun naria2-jsonrpc-parse-string (str)
82 "Parse STR into a JSON object."
83 (with-temp-buffer
84 (insert str)
85 (goto-char (point-min))
86 (jsonrpc--json-read)))
87
88;;; JSONRPC
89
90(defclass naria2-jsonrpc-connection (jsonrpc-connection)
91 ((-secret
92 :initform nil
93 :documentation
94 "The authorization token for RPC call.")
95 (-socket
96 :documentation
97 "The websocket connection.")
98 (-notification-handlers-table
99 :initform (make-hash-table :test #'equal)
100 :documentation
101 "The hash table to store handlers of different notifications from aria2.")
102 (-finalizer
103 :documentation
104 "The object returned by `make-finalizer'."))
105 "The websocket JSONRPC connection to aria2 download server.")
106
107;; JSONRPC Implementation
108
109(cl-defmethod jsonrpc-connection-send ((conn naria2-jsonrpc-connection)
110 &rest args &key method &allow-other-keys)
111 (when method
112 (let* ((msg `(:jsonrpc "2.0" ,@args))
113 (json-str (jsonrpc--json-encode msg))
114 (socket (oref conn -socket)))
115 (websocket-send-text socket json-str)
116 (jsonrpc--log-event conn msg 'client))))
117
118(cl-defmethod jsonrpc-shutdown ((conn naria2-jsonrpc-connection))
119 (websocket-close (oref conn -socket)))
120
121(cl-defmethod jsonrpc-running-p ((conn naria2-jsonrpc-connection))
122 (websocket-openp (oref conn -socket)))
123
124(cl-defmethod jsonrpc-connection-ready-p ((conn naria2-jsonrpc-connection)
125 _what)
126 (eq (websocket-ready-state (oref conn -socket)) 'open))
127
128;;; API
129
130(defun naria2-jsonrpc-connect (host port &optional secure? secret)
131 "Connect to aria2 websocket server on PORT at HOST.
132
133If SECURE? is non-nil, use secure connection instead of plain one.
134
135SECRET must be a string if provided, it should match the `--rpc-secret' option
136of remote aria2c instance."
137 (cl-labels ((make-message-handler (conn-ref)
138 (lambda (_ws frame)
139 (let* ((text (websocket-frame-text frame))
140 (msg (naria2-jsonrpc-parse-string text))
141 (conn (gethash t conn-ref)))
142 (jsonrpc-connection-receive conn msg)))))
143 (let* ((proto (if secure? "wss" "ws"))
144 (base-url (format "%s://%s:%d" proto host port))
145 (name (concat "ARIA2 " base-url))
146 (conn (make-instance 'naria2-jsonrpc-connection
147 :name name
148 :notification-dispatcher
149 #'naria2-jsonrpc--do-notification-dispatch))
150 (weak-ref
151 (let ((ht (make-hash-table :weakness 'value :test #'eq)))
152 (puthash t conn ht)
153 ht))
154 (finalizer (lambda ()
155 (jsonrpc-shutdown conn))))
156 (let* ((handshake-url (concat base-url "/jsonrpc"))
157 (socket (websocket-open
158 handshake-url
159 :on-message (make-message-handler weak-ref))))
160 (setf (oref conn -socket) socket)
161 (setf (oref conn -secret) secret)
162 (setf (oref conn -finalizer) (make-finalizer finalizer)))
163 conn)))
164
165(defun naria2-jsonrpc--do-notification-dispatch (conn event params)
166 "Run all handlers of EVENT from CONN with PARAMS."
167 (let* ((fullname (symbol-name event))
168 (name (string-remove-prefix "aria2." fullname))
169 (method-table (oref conn -notification-handlers-table)))
170 (dolist (fn (gethash name method-table))
171 (funcall fn params))))
172
173(defun naria2-jsonrpc-on (conn event handler)
174 "Listen an EVENT from CONN with HANDLER.
175
176Return a function run with no arguments to cancel the HANDLER."
177 (with-slots ((method-table -notification-handlers-table)) conn
178 (cl-callf nconc (gethash event method-table) (list handler))
179 (lambda ()
180 (cl-callf2 delq handler method-table))))
181
182(defun naria2-jsonrpc--normalize-callbody (conn method params)
183 "Normalize the body of a call via CONN to remote METHOD with PARAMS.
184
185Return (FINAL-METHOD . FINAL-PARAMS).
186
187FINAL-METHOD is a string in format `aria2.%s'. where `%s' will be
188replaced by the stringified METHOD.
189
190FINAL-PARAMS is PARAMS itself by default, but if a secret from CONN
191is provided, a secret token will be inserted at the head position of PARAMS."
192 (cl-flet ((stringify (elem)
193 (cl-etypecase elem
194 (keyword (substring (symbol-name elem) 1))
195 (symbol (symbol-name elem))
196 (string elem))))
197 (let* ((final-method (concat "aria2." (stringify method)))
198 (secret (oref conn -secret))
199 ;; Tho weired, but see `aria2c(1)'.
200 (final-params `[,@(if secret (list (format "token:%s" secret)))
201 ,@params]))
202 (cons final-method final-params))))
203
204(defun naria2-jsonrpc-async-call (conn method params &rest args)
205 "Call remote aria2 METHOD with PARAMS via CONN asynchronously.
206
207See `jsonrpc-async-request' for the documentation of ARGS.
208
209\(fn CONN METHOD PARAMS &key SUCCESS-FN ERROR-FN TIMEOUT-FN TIMEOUT DEFERRED)"
210 (pcase-let ((`(,method . ,params)
211 (naria2-jsonrpc--normalize-callbody conn method params)))
212 (apply #'jsonrpc-async-request conn method params args)))
213
214(defun naria2-jsonrpc-sync-call (conn method params &rest args)
215 "Call remote aria2 METHOD with PARAMS via CONN synchronously.
216
217See `jsonrpc-request' for the documentation of ARGS.
218
219\(fn CONN METHOD PARAMS &key DEFERRED TIMEOUT CANCEL-ON-INPUT \
220CANCEL-ON-INPUT-RETVAL)"
221 (pcase-let ((`(,method . ,params)
222 (naria2-jsonrpc--normalize-callbody conn method params)))
223 (apply #'jsonrpc-request conn method params args)))
224
225(defun naria2-jsonrpc-notify (conn method params)
226 "Call remote aria2 METHOD with PARAMS via CONN, don't expect a return value."
227 (pcase-let ((`(,method . ,params)
228 (naria2-jsonrpc--normalize-callbody conn method params)))
229 (jsonrpc-notify conn method params)))
230
231(provide 'naria2-jsonrpc)
232
233;; Local Variables:
234;; coding: utf-8
235;; End:
236
237;;; naria2-jsonrpc.el ends here