1
0

server.lisp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. (in-package :cl-user)
  2. (defpackage chad-music.server
  3. (:use :cl #:alexandria #:chad-music.db #:chad-music.utils #:jonathan)
  4. (:export #:main))
  5. (in-package :chad-music.server)
  6. (defvar *path-url-mappings* nil "alist map database paths to urls")
  7. ;; Server database
  8. (defstruct server
  9. (token-user (make-hash-table :test #'equal) :type hash-table))
  10. (defvar *server* (make-server) "Server database")
  11. (defvar *server-path* "db-server.sexp")
  12. (defun save-server (&optional (file-name *server-path*))
  13. (declare #.*standard-optimize-settings*)
  14. (with-output-to-file (s file-name :if-exists :supersede)
  15. (labels ((print-table (table)
  16. (print (hash-table-plist table) s)))
  17. (print-table (server-token-user *server*)))))
  18. (defun load-server (&optional (file-name *server-path*))
  19. (declare #.*standard-optimize-settings*)
  20. (with-input-from-file (s file-name)
  21. (make-server
  22. :token-user (plist-hash-table (read s nil) :test #'equal))))
  23. (defun get-url (path)
  24. (declare #.*standard-optimize-settings*)
  25. (when (and path (pathnamep path))
  26. (let ((path (uiop:unix-namestring path)))
  27. (loop for (path-prefix . url-prefix) in *path-url-mappings*
  28. do (multiple-value-bind (foundp suffix)
  29. (starts-with-subseq path-prefix path :return-suffix t)
  30. (when foundp
  31. (return-from get-url
  32. (format nil "~A~{~A~^/~}" url-prefix
  33. (mapcar #'quri:url-encode
  34. (split-sequence:split-sequence #\/ suffix))))))))))
  35. (defmacro maybe-key-value (key value)
  36. `(when ,value
  37. (write-key-value ,key ,value)))
  38. (defmethod %to-json ((album album))
  39. (with-object
  40. (maybe-key-value "id" (album-id album))
  41. (maybe-key-value "artist" (album-artist album))
  42. (maybe-key-value "year" (album-year album))
  43. (maybe-key-value "album" (album-album album))
  44. (maybe-key-value "original_date" (album-original-date album))
  45. (maybe-key-value "publisher" (album-publisher album))
  46. (maybe-key-value "country" (album-country album))
  47. (maybe-key-value "genre" (album-genre album))
  48. (maybe-key-value "type" (or (album-type album) "release"))
  49. (maybe-key-value "status" (album-status album))
  50. (maybe-key-value "mb_id" (album-mb-id album))
  51. (maybe-key-value "track_count" (album-track-count album))
  52. (maybe-key-value "total_duration" (album-total-duration album))
  53. (maybe-key-value "cover" (get-url (album-cover album)))))
  54. (defmethod %to-json ((track track))
  55. (let ((album-artist (album-artist (track-album track)))
  56. (track-artist (track-artist track)))
  57. (with-object
  58. (maybe-key-value "artist" track-artist)
  59. (maybe-key-value "album_artist" (unless (equal track-artist album-artist) album-artist))
  60. (maybe-key-value "album" (album-album (track-album track)))
  61. (maybe-key-value "year" (album-year (track-album track)))
  62. (maybe-key-value "no" (clear-track-no (track-no track)))
  63. (maybe-key-value "title" (track-title track))
  64. (maybe-key-value "bit_rate" (track-bit-rate track))
  65. (maybe-key-value "vbr" (if (track-is-vbr track) :true :false))
  66. (maybe-key-value "duration" (track-duration track))
  67. (maybe-key-value "url" (get-url (track-path track)))
  68. (maybe-key-value "cover" (get-url (album-cover (track-album track)))))))
  69. (defparameter +400+ '(400 nil #.(trivial-utf-8:string-to-utf-8-bytes "Bad Request")))
  70. (defparameter +401+ '(401 nil #.(trivial-utf-8:string-to-utf-8-bytes "Unauthorized")))
  71. (defparameter +404+ '(404 nil #.(trivial-utf-8:string-to-utf-8-bytes "Not found")))
  72. (defparameter +200-empty+ '(200 (:content-type "application/json")
  73. #.(trivial-utf-8:string-to-utf-8-bytes "{}")))
  74. (defun 200-json (data &optional (dumper #'to-json))
  75. (declare #.*standard-optimize-settings*
  76. (type function dumper))
  77. `(200 (:content-type "application/json")
  78. ,(trivial-utf-8:string-to-utf-8-bytes (funcall dumper data))))
  79. (let ((db-package (find-package :chad-music.db)))
  80. (defun getsym (place indicator)
  81. (declare #.*standard-optimize-settings*
  82. (type list place)
  83. (type symbol indicator))
  84. (intern (string-upcase (getf place indicator)) db-package)))
  85. (defun aget (place indicator &key (test #'equal))
  86. (declare #.*standard-optimize-settings*
  87. (type list place)
  88. (type (or string symbol) indicator)
  89. (type function test))
  90. (cdr (assoc indicator place :test test)))
  91. (defun may-integer (string)
  92. (declare #.*standard-optimize-settings*
  93. (type (or null string) string))
  94. (when string
  95. (parse-integer string :junk-allowed t)))
  96. (defun get-restrictions (query-params)
  97. (declare #.*standard-optimize-settings*
  98. (type list query-params))
  99. (loop for key in '(artist year album publisher country genre type status)
  100. for value = (aget query-params (string-downcase (symbol-name key)))
  101. when value collect (cons key (case key
  102. (year (parse-integer value :junk-allowed t))
  103. (otherwise value)))))
  104. (defparameter +max-limit+ 10000)
  105. (defmacro with-category ((params category filter restrictions offset limit latest) &body body)
  106. (with-gensyms (query-string query-params)
  107. `(let ((,category (getsym ,params :category)))
  108. (case ,category
  109. ((artist year album publisher country genre type status)
  110. (let* ((,query-string (getf myway:*env* :query-string))
  111. (,query-params (and ,query-string (quri:url-decode-params ,query-string)))
  112. (,filter (aget ,query-params "filter"))
  113. (,restrictions (get-restrictions ,query-params))
  114. (,offset (or (may-integer (aget ,query-params "offset")) 0))
  115. (,limit (min (the fixnum +max-limit+)
  116. (the fixnum (or (may-integer (aget ,query-params "limit")) +max-limit+))))
  117. (,latest (aget ,query-params "latest")))
  118. ,@body))
  119. (otherwise +404+)))))
  120. (defmacro with-user ((info) &body body)
  121. (with-gensyms (auth bearer headers)
  122. `(let* ((,headers (getf myway:*env* :headers))
  123. (,auth (ignore-errors (gethash "authorization" ,headers)))
  124. (,bearer (when (and ,auth
  125. (> (length (the string ,auth)) 7)
  126. (equal "Bearer " (subseq (the string ,auth) 0 7)))
  127. (subseq (the string ,auth) 7)))
  128. (,info (when ,bearer (gethash ,bearer (server-token-user *server*)))))
  129. (when (and (null ,info) (null (gethash "x-real-ip" ,headers)))
  130. (setf ,info `(:|username| "admin" :|id| 0 :|first_name| "cli" :|last_name| "admin")))
  131. (if ,info (handler-case (progn ,@body) (error (e) (log:error e) (print e)))
  132. +401+))))
  133. (defun get-category-list (params)
  134. (declare #.*standard-optimize-settings* (ignore params))
  135. (with-user (info)
  136. (200-json '("artist" "year" "album" "publisher" "country" "genre" "type" "status"))))
  137. (defun get-category-size (params)
  138. (declare #.*standard-optimize-settings*)
  139. (with-user (info)
  140. (with-category (params category filter restrictions offset limit latest)
  141. (declare (ignore offset limit latest))
  142. (200-json (query-category category
  143. :filter filter :restrictions restrictions
  144. :count-only t)))))
  145. (defun dumps-category-result (results)
  146. (with-output-to-string*
  147. (with-array
  148. (loop for (cat . count) in results
  149. do (write-item
  150. (with-object
  151. (write-key-value "item" cat)
  152. (write-key-value "count" count)))))))
  153. (defun get-category (params)
  154. (declare #.*standard-optimize-settings*)
  155. (with-user (info)
  156. (with-category (params category filter restrictions offset limit latest)
  157. (200-json (query-category category
  158. :filter filter :restrictions restrictions
  159. :limit limit :offset offset :latest latest)
  160. (case category
  161. (album #'to-json)
  162. (t #'dumps-category-result))))))
  163. (defun get-album-tracks (params)
  164. (declare #.*standard-optimize-settings*)
  165. (with-user (info)
  166. (200-json (album-tracks (getf params :id)))))
  167. (defun file-server (root)
  168. (lambda (params)
  169. (declare #.*standard-optimize-settings*)
  170. (let ((file (probe-file (cl-fad:merge-pathnames-as-file
  171. root (uiop:parse-unix-namestring (car (getf params :splat)))))))
  172. (if file (list 200 nil file) +404+))))
  173. ;; Admin tools
  174. (defvar *rescans* nil)
  175. (defun update-db ()
  176. (let (added updated removed)
  177. (sb-impl::call-with-timing #'(lambda (&rest args)
  178. (push (append args
  179. (list :timestamp (get-universal-time)
  180. :added added
  181. :updated updated
  182. :removed removed))
  183. *rescans*))
  184. #'(lambda ()
  185. (multiple-value-bind (a u r)
  186. (rescan (mapcar 'car *path-url-mappings*))
  187. (save-db)
  188. (setf added a updated u removed r))))))
  189. (defvar *rescan-lock* (bt:make-lock "Rescan lock"))
  190. (defvar *rescan-cond* (bt:make-condition-variable :name "Rescan requested"))
  191. (defvar *rescan-thread* nil)
  192. (defvar *rescan-active* nil)
  193. (defun rescanner ()
  194. (loop
  195. (bt:with-lock-held (*rescan-lock*)
  196. (bt:condition-wait *rescan-cond* *rescan-lock*)
  197. (setf *rescan-active* t)
  198. (handler-case (update-db)
  199. (error (e) (format t "Error updating db: ~a" e)))
  200. (setf *rescan-active* nil))))
  201. (defun request-rescan (params)
  202. (declare #.*standard-optimize-settings* (ignorable params))
  203. (with-user (info)
  204. (bt:condition-notify *rescan-cond*)
  205. +200-empty+))
  206. (defun stats (params)
  207. (declare #.*standard-optimize-settings* (ignorable params))
  208. (with-user (info)
  209. (let ((stats (db-stats)))
  210. (setf (getf stats :|duration|)
  211. (format-interval (getf stats :|duration|)))
  212. (200-json (append stats
  213. (list :|rescans| (subseq (the list *rescans*)
  214. 0 (min (length (the list *rescans*)) 10))))))))
  215. (defvar *bot-token* nil "Login bot token")
  216. (defun validate-user (info)
  217. (let ((hash (prog1 (getf info :|hash|)
  218. (remf info :|hash|)))
  219. (check-string (format nil "~{~a=~a~^~%~}"
  220. (alist-plist
  221. (sort (plist-alist info) #'string-lessp
  222. :key #'(lambda (p) (symbol-name (car p)))))))
  223. (hmac (crypto:make-hmac
  224. (crypto:digest-sequence :sha256 (crypto:ascii-string-to-byte-array *bot-token*)) :sha256)))
  225. (crypto:update-hmac hmac (trivial-utf-8:string-to-utf-8-bytes check-string))
  226. (string-equal hash (crypto:byte-array-to-hex-string (crypto:hmac-digest hmac)))))
  227. (defparameter +telegram-api-format+ "https://api.telegram.org/bot~A/~A")
  228. (defun telegram-request (method params)
  229. (json-request (format nil +telegram-api-format+ *bot-token* method)
  230. :method :post
  231. :content (trivial-utf-8:string-to-utf-8-bytes (to-json params))))
  232. (defvar *bot-auth-chat-id* nil "Authentication chat id")
  233. (defparameter +authorized-statuses+ '("creator" "administrator" "member"))
  234. (defun authorize-user (info)
  235. (ignore-errors
  236. (let* ((response (telegram-request "getChatMember"
  237. `(:|chat_id| ,*bot-auth-chat-id*
  238. :|user_id| ,(getf info :|id|))))
  239. (chat-member (getf response :|result|))
  240. (status (getf chat-member :|status|)))
  241. (member status +authorized-statuses+ :test #'equal))))
  242. (defparameter +token-length+ 16)
  243. (defun user-token (info)
  244. (let ((token (crypto:byte-array-to-hex-string
  245. (crypto:random-data +token-length+))))
  246. (setf (gethash token (server-token-user *server*)) info)
  247. (save-server)
  248. token))
  249. (defun login (params)
  250. (declare #.*standard-optimize-settings* (ignorable params))
  251. (handler-case
  252. (let* ((body (trivial-utf-8:read-utf-8-string
  253. (getf myway:*env* :raw-body)
  254. :stop-at-eof t))
  255. (info (parse (coerce (the string body) 'simple-string))))
  256. (unless (validate-user info)
  257. (error "Bad user info"))
  258. (if (authorize-user info)
  259. (200-json `(:|token| ,(user-token info)))
  260. +401+))
  261. (error (e) (log:error e) +400+)))
  262. (defun user-info (params)
  263. (declare #.*standard-optimize-settings* (ignorable params))
  264. (with-user (info)
  265. (200-json info)))
  266. (defvar *mapper* (myway:make-mapper))
  267. (myway:connect *mapper* "/api/cat/:category/size" 'get-category-size)
  268. (myway:connect *mapper* "/api/cat/:category" 'get-category)
  269. (myway:connect *mapper* "/api/cat" 'get-category-list)
  270. (myway:connect *mapper* "/api/album/:id/tracks" 'get-album-tracks)
  271. (myway:connect *mapper* "/api/stats" 'stats)
  272. (myway:connect *mapper* "/api/rescan" 'request-rescan :method :POST)
  273. (myway:connect *mapper* "/api/login" 'login :method :POST)
  274. (myway:connect *mapper* "/api/user" 'user-info)
  275. (defun main (&rest args &key (port 5000) (debug nil) (use-thread t) (serve-files nil) &allow-other-keys)
  276. ;; Load config file
  277. (when-let (file (probe-file
  278. (merge-pathnames "config.lisp"
  279. (asdf:component-pathname
  280. (asdf:find-system '#:chad-music)))))
  281. (load file))
  282. ;; Load server
  283. (when-let (file (probe-file *server-path*))
  284. (setf *server* (load-server file)))
  285. ;; Load database
  286. (when-let (file (probe-file *db-path*))
  287. (let ((*package* (find-package :chad-music.db)))
  288. (setf *db* (load-db file))))
  289. ;; Set up debug file server
  290. (when serve-files
  291. (loop for (path-prefix . url-prefix) in *path-url-mappings*
  292. do (myway:connect *mapper* (concatenate 'string url-prefix "*")
  293. (file-server path-prefix))))
  294. ;; Start rescan processor
  295. (setf *rescan-thread* (bt:make-thread 'rescanner :name "DB rescanner"))
  296. ;; Start application
  297. (apply #'clack:clackup
  298. (myway:to-app *mapper*)
  299. :server :woo
  300. :port port
  301. :debug debug
  302. :use-default-middlewares nil
  303. :use-thread use-thread
  304. (alexandria:remove-from-plist args :data :port :debug :use-thread)))