1
0

server.lisp 14 KB

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