client.scm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. ;; Authorization code flow:
  2. ;; 1) Obtain a <client> by using (register-app).
  3. ;; 2) Obtain an authorization code by asking the user
  4. ;; to visit the url obtained from `build-authorize-url'.
  5. ;; This authorization code can only be used once.
  6. ;; 3) Obtain an access token with `get-token' using the authorization code.
  7. ;; 4) Optionally, verify the token with `verify-credentials'.
  8. (define-module (tapris client)
  9. #:use-module (json)
  10. #:use-module (ice-9 format)
  11. #:use-module (ice-9 match)
  12. #:use-module (ice-9 iconv)
  13. #:use-module (srfi srfi-1)
  14. #:use-module (srfi srfi-9)
  15. #:use-module (srfi srfi-11)
  16. #:use-module (srfi srfi-69)
  17. #:use-module (rnrs bytevectors)
  18. #:use-module (web uri)
  19. #:use-module (web response)
  20. #:use-module (web client)
  21. ;; local
  22. #:use-module (tapris util)
  23. #:use-module (tapris json-mapping)
  24. #:export (CLIENT-NAME
  25. <client>
  26. make-client
  27. alist->client
  28. client?
  29. client-instance
  30. client-id
  31. client-secret
  32. client-token set-client-token!
  33. <mastodon-status>
  34. json->status
  35. status?
  36. status-id
  37. status-uri
  38. status-url
  39. status-account
  40. status-in-reply-to-id
  41. status-in-reply-to-account-id
  42. status-reblog
  43. status-content
  44. status-created-at
  45. status-emojis
  46. status-replies-count
  47. status-reblogs-count
  48. status-favourites-count
  49. status-reblogged
  50. status-favourited
  51. status-muted
  52. status-sensitive
  53. status-spoiler-text
  54. status-visibility
  55. status-media-attachments
  56. status-mentions
  57. status-tags
  58. status-card
  59. status-application
  60. status-language
  61. status-pinned
  62. register-app
  63. build-authorize-url
  64. get-token
  65. verify-credentials
  66. client-post
  67. client-get
  68. post-status
  69. post-media
  70. get-home-timeline))
  71. (define CLIENT-NAME "TAPRIS")
  72. (define NO-REDIRECT "urn:ietf:wg:oauth:2.0:oob")
  73. (define BOUNDARY "AYYYLMAOxDxDxDBENIS")
  74. (define (make-boundary)
  75. (let ((n1 (random 100))
  76. (n2 (random 100)))
  77. (format #f "~a~a~a" BOUNDARY n1 n2)))
  78. ;;; DATASTRUCTURES
  79. (define-record-type <client>
  80. (make-client instance id secret token)
  81. client?
  82. (instance client-instance)
  83. (id client-id)
  84. (secret client-secret)
  85. (token client-token set-client-token!))
  86. (define (alist->client instance alist)
  87. "Converts an alist (for instance obtained from `json->scm') to a <client>"
  88. (make-client
  89. instance
  90. (assoc-ref alist "client_id")
  91. (assoc-ref alist "client_secret")
  92. #f))
  93. (define-json-mapping <status>
  94. make-status
  95. status?
  96. json->status
  97. status->json
  98. (id status-id)
  99. (uri status-uri)
  100. (url status-url)
  101. (account status-account)
  102. (in-reply-to-id status-in-reply-to-id "in_reply_to_id")
  103. (in-reply-to-account-id status-in-reply-to-account-id "in_reply_to_account_id")
  104. (reblog status-reblog)
  105. (content status-content)
  106. (created-at status-created-at "created_at")
  107. (emojis status-emojis)
  108. (replies-count status-replies-count "replies_count")
  109. (reblogs-count status-reblogs-count "reblogs_count")
  110. (favourites-count status-favourites-count "favourites_count")
  111. (reblogged status-reblogged)
  112. (favourited status-favourited)
  113. (muted status-muted)
  114. (sensitive status-sensitive)
  115. (spoiler-text status-spoiler-text "spoiler_text")
  116. (visibility status-visibility)
  117. (media-attachments status-media-attachments "media_attachments")
  118. (mentions status-mentions)
  119. (tags status-tags)
  120. (card status-card)
  121. (application status-application)
  122. (language status-language)
  123. (pinned status-pinned))
  124. ;;;;;;;;;; BASIC API FUNCTIONS
  125. (define* (get-data uri #:optional #:key (token #f))
  126. (let-values (((res body)
  127. (http-get uri
  128. #:body #f
  129. #:version '(1 . 1)
  130. #:keep-alive? #f
  131. #:headers (if token
  132. `((Authorization
  133. . ,(string-append "Bearer " token)))
  134. '())
  135. #:decode-body? #t
  136. #:streaming? #f)))
  137. (match (response-code res)
  138. (200
  139. (json-string->scm (bytevector->string body "utf-8")))
  140. (_
  141. ;; Error
  142. (throw 'pleroma `(("request" . ,uri)
  143. ("response-code" . ,(response-code res))
  144. ("response-phrase" .
  145. ,(response-reason-phrase res))
  146. ("response" .
  147. ,(if (bytevector? body)
  148. (bytevector->string body "utf-8")
  149. body))))))))
  150. (define* (post-data uri data #:optional #:key (token #f) (content-type "application/json"))
  151. "Post to URI `uri' with `data' in JSON format."
  152. (let-values (((res body)
  153. (http-post uri
  154. #:body (if (bytevector? data)
  155. data
  156. (string->bytevector data "utf-8"))
  157. #:version '(1 . 1)
  158. #:keep-alive? #f
  159. #:headers (if token
  160. `((Content-Type . ,content-type)
  161. (Authorization
  162. . ,(string-append "Bearer " token)))
  163. `((Content-Type . ,content-type)))
  164. #:decode-body? #t
  165. #:streaming? #f)))
  166. (match (response-code res)
  167. (200
  168. (json-string->scm (bytevector->string body "utf-8")))
  169. (_
  170. ;; Error
  171. (throw 'pleroma `(("request" . ,uri)
  172. ("response-code" . ,(response-code res))
  173. ("response-phrase" .
  174. ,(response-reason-phrase res))
  175. ("response" .
  176. ,(if (bytevector? body)
  177. (bytevector->string body "utf-8")
  178. body))))))))
  179. ;; REGISTERING AN APP
  180. (define (register-app inst-base)
  181. "Register a new application, the first parameter is a uri."
  182. (let ((url (uri-set-path-query inst-base
  183. "/api/v1/apps"
  184. ""))
  185. (data (scm->json-string
  186. `(("client_name" . ,CLIENT-NAME)
  187. ("redirect_uris" . ,NO-REDIRECT)
  188. ("scopes" . "read write follow")))))
  189. (alist->client inst-base (post-data url data))))
  190. ;; GETTING THE AUTH CODE
  191. (define (build-authorize-url client)
  192. "Construct the URL for obtaining the access tokens."
  193. (let ((query (alist->query
  194. `(("client_id" . ,(client-id client))
  195. ("client_secret" . ,(client-secret client))
  196. ("redirect_uri" . ,NO-REDIRECT)
  197. ("response_type" . "code")
  198. ("scope" . "read write follow")))))
  199. (uri-set-path-query (client-instance client) "/oauth/authorize" query)))
  200. ;; GETTING AND VERIFYING THE CLIENT TOKEN
  201. (define (get-token client auth-code)
  202. (let* ((query
  203. (alist->query
  204. `(("client_id" . ,(client-id client))
  205. ("client_secret" . ,(client-secret client))
  206. ("redirect_uri" . ,NO-REDIRECT)
  207. ("grant_type" . "authorization_code")
  208. ("code" . ,auth-code))))
  209. (url (uri-set-path-query (client-instance client)
  210. "/oauth/token"
  211. query)))
  212. (assoc-ref (post-data url "") "access_token")))
  213. (define (verify-credentials client)
  214. (let* ((query
  215. (alist->query '()))
  216. (url (uri-set-path-query (client-instance client)
  217. "/api/v1/accounts/verify_credentials"
  218. query)))
  219. (get-data url #:token (client-token client))))
  220. ;; Wew all of this auth crap is over...
  221. ;; Simpler API functions
  222. (define (data->json data)
  223. (cond
  224. ((string? data) data)
  225. ((bytevector? data) data)
  226. (else (scm->json-string data))))
  227. (define* (client-post client endpoint
  228. #:optional #:key
  229. (data "")
  230. (query '())
  231. (content-type "application/json"))
  232. "A \"generic\" client POST request to an endpoint. Data can be
  233. either a Scheme datastructrure (will be converted to JSON), a
  234. string, or a bytevector; a query is an association list."
  235. (let* ((data (data->json data))
  236. (query (alist->query query))
  237. (uri (uri-set-path-query (client-instance client) endpoint query)))
  238. (post-data uri data #:token (client-token client)
  239. #:content-type content-type)))
  240. (define* (client-get client endpoint
  241. #:optional #:key
  242. (query '()))
  243. "A \"generic\" client GET request to an endpoint. Data can be either
  244. a Scheme datastructrure (will be converted to JSON). The query is an
  245. association list."
  246. (let* ((query (alist->query query))
  247. (uri (uri-set-path-query (client-instance client) endpoint query)))
  248. (get-data uri #:token (client-token client))))
  249. ;; Specifics
  250. (define* (post-status client status #:optional #:key (content-type "text/html"))
  251. (client-post client "/api/v1/statuses" #:data `(("status" . ,status)
  252. ("content_type" . ,content-type))))
  253. ;; TODO: expand file paths
  254. (define* (post-media client file #:optional #:key (status ""))
  255. (let* ((boundary (make-boundary))
  256. (media-endp "/api/v1/media")
  257. (status-endp "/api/v1/statuses")
  258. (media-data (file-encode file boundary))
  259. (attachment
  260. (client-post client media-endp #:data media-data
  261. #:content-type (string-append "multipart/form-data; "
  262. "boundary=" boundary)))
  263. (attachment-id
  264. (assoc-ref attachment "id"))
  265. (data `(("status" . ,status)
  266. ("media_ids" . #(,attachment-id)))))
  267. (client-post client status-endp #:data data)))
  268. (define* (get-home-timeline client #:optional #:key (limit 20))
  269. "Returns a list of <status>es from the home timeline."
  270. (map json->status
  271. (vector->list
  272. (client-get client "/api/v1/timelines/home" #:query `(("limit" . ,limit))))))