;;; mastodon-async.el --- Async streaming functions for mastodon.el -*- lexical-binding: t -*- ;; Copyright (C) 2017 Alex J. Griffith ;; Author: Alex J. Griffith ;; Maintainer: Marty Hiatt ;; Package-Requires: ((emacs "27.1")) ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. ;; This file is part of mastodon.el. ;; mastodon.el is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; mastodon.el is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with mastodon.el. If not, see . ;;; Commentary: ;; Rework sync code so it does not mess up the async-buffer ;;; Code: (require 'mastodon-tl) (require 'json) (require 'url-http) (defvar url-http-end-of-headers) (autoload 'mastodon-auth--access-token "mastodon-auth") (autoload 'mastodon-http--api "mastodon-http") (autoload 'mastodon-http--get-json "mastodon-http") (autoload 'mastodon-mode "mastodon") (autoload 'mastodon-notifications--timeline "mastodon-notifications") (autoload 'mastodon-tl--timeline "mastodon-tl") (defgroup mastodon-async nil "An async module for mastodon streams." :prefix "mastodon-async-" :group 'external) ;;;###autoload (define-minor-mode mastodon-async-mode "Async Mastodon." :lighter " MasA") (defvar mastodon-instance-url) (defvar mastodon-tl--enable-relative-timestamps) (defvar mastodon-tl--display-media-p) (defvar mastodon-tl--buffer-spec) (defvar-local mastodon-async--queue "" ;;"*mastodon-async-queue*" "The intermediate queue buffer name.") (defvar-local mastodon-async--buffer "" ;;"*mastodon-async-buffer*" "User facing output buffer name.") (defvar-local mastodon-async--http-buffer "" ;;"" "Buffer variable bound to http output.") (defun mastodon-async--display-http () "Display the async HTTP input buffer." (display-buffer mastodon-async--http-buffer)) (defun mastodon-async--display-buffer () "Display the async user facing buffer." (interactive) (display-buffer mastodon-async--buffer)) (defun mastodon-async--display-queue () "Display the async queue buffer." (display-buffer mastodon-async--queue)) (defun mastodon-async--stop-http () "Stop the http processs and close the async and http buffer." (interactive) (let ((inhibit-read-only t)) (stop-process (get-buffer-process mastodon-async--http-buffer)) (delete-process (get-buffer-process mastodon-async--http-buffer)) (kill-buffer mastodon-async--http-buffer) (setq mastodon-async--http-buffer "") (when (not (string= "" mastodon-async--queue)) ; error handle on kill async buffer (kill-buffer mastodon-async--queue)))) (defun mastodon-async--stream-notifications () "Open a stream of user notifications." (interactive) (mastodon-async--mastodon "user" "home" "notifications" 'mastodon-async--process-queue-string-notifications)) (defun mastodon-async--stream-home () "Open a stream of the home timeline." (interactive) (mastodon-async--mastodon "user" "home" "home" 'mastodon-async--process-queue-string)) (defun mastodon-async--stream-federated () "Open a stream of Federated." (interactive) (mastodon-async--mastodon "public" "public" "federated" 'mastodon-async--process-queue-string)) (defun mastodon-async--stream-local () "Open a stream of Local." (interactive) ;; Need to add another layer of filtering for this to work ;; apparently it the local flag does not work (mastodon-async--mastodon "public" "public?local=true" "local" 'mastodon-async--process-queue-local-string)) (defun mastodon-async--mastodon (endpoint timeline name filter) "Make sure that the previous async process has been closed. Then start an async stream at ENDPOINT filtering toots using FILTER. TIMELINE is a specific target, such as federated or home. NAME is the center portion of the buffer name for *mastodon-async-buffer and *mastodon-async-queue." (ignore timeline) ;; TODO: figure out what this is meant to be used for (let ((buffer (mastodon-async--start-process endpoint filter name))) (with-current-buffer buffer (mastodon-async--display-buffer) (goto-char (point-max)) (goto-char 1)))) (defun mastodon-async--get (url callback) "An async GET request to URL with CALLBACK." (let ((url-request-method "GET") (url-request-extra-headers `(("Authorization" . ,(concat "Bearer " (mastodon-auth--access-token)))))) (url-retrieve url callback))) (defun mastodon-async--set-http-buffer (buffer http-buffer) "Initialize for BUFFER a local variable `mastodon-async--http-buffer'. HTTP-BUFFER is the initializing value. Use this funcion if HTTP-BUFFER is not known when `mastodon-async--setup-buffer' is called." (with-current-buffer (get-buffer-create buffer) (setq mastodon-async--http-buffer http-buffer))) (defun mastodon-async--set-local-variables (buffer http-buffer buffer-name queue-name) "Set local variables for BUFFER, HTTP-BUFFER, BUFFER-NAME, and QUEUE-NAME." (with-current-buffer (get-buffer-create buffer) (let ((value mastodon-instance-url)) (make-local-variable 'mastodon-instance-url) (setq-local mastodon-instance-url value)) (setq mastodon-async--http-buffer http-buffer) (setq mastodon-async--buffer buffer-name) (setq mastodon-async--queue queue-name))) (defun mastodon-async--setup-http (http-buffer name) "Add local variables to HTTP-BUFFER. NAME is used to generate the display buffer and the queue." (let ((queue-name (concat " *mastodon-async-queue-" name "-" mastodon-instance-url "*")) (buffer-name (concat "*mastodon-async-display-" name "-" mastodon-instance-url "*"))) (mastodon-async--set-local-variables http-buffer http-buffer buffer-name queue-name))) (defun mastodon-async--setup-queue (http-buffer name) "Set up HTTP-BUFFER buffer for the async queue. NAME is used to generate the display buffer and the queue." (let ((queue-name (concat " *mastodon-async-queue-" name "-" mastodon-instance-url "*")) (buffer-name(concat "*mastodon-async-display-" name "-" mastodon-instance-url "*"))) (mastodon-async--set-local-variables queue-name http-buffer buffer-name queue-name) queue-name)) (defun mastodon-async--setup-buffer (http-buffer name endpoint) "Set up the buffer timeline like `mastodon-tl--init'. HTTP-BUFFER the name of the http-buffer, if unknown, set to... NAME is the name of the stream for the buffer name. ENDPOINT is the endpoint for the stream and timeline." (let ((queue-name (concat " *mastodon-async-queue-" name "-" mastodon-instance-url "*")) (buffer-name (concat "*mastodon-async-display-" name "-" mastodon-instance-url "*")) ;; if user stream, we need "timelines/home" not "timelines/user" ;; if notifs, we need "notifications" not "timelines/notifications" (endpoint (cond ((string= name "notifications") "notifications") ((string= name "home") "timelines/home") (t (format "timelines/%s" endpoint))))) (mastodon-async--set-local-variables buffer-name http-buffer buffer-name queue-name) ;; Similar to timeline init. (with-current-buffer (get-buffer-create buffer-name) (setq inhibit-read-only t) ; for home timeline? (make-local-variable 'mastodon-tl--enable-relative-timestamps) (make-local-variable 'mastodon-tl--display-media-p) (message (mastodon-http--api endpoint)) (if (string= name "notifications") (mastodon-notifications--timeline (mastodon-http--get-json (mastodon-http--api "notifications"))) (mastodon-tl--timeline (mastodon-http--get-json (mastodon-http--api endpoint)))) (mastodon-mode) (mastodon-tl--set-buffer-spec buffer-name endpoint (if (string= name "notifications") 'mastodon-notifications--timeline 'mastodon-tl--timeline)) (setq-local mastodon-tl--enable-relative-timestamps nil) (setq-local mastodon-tl--display-media-p t) (current-buffer)))) (defun mastodon-async--start-process (endpoint filter &optional name) "Start an async mastodon stream at ENDPOINT. Filter the toots using FILTER. NAME is used for the queue and display buffer." (let* ((stream (concat "streaming/" endpoint)) (async-queue (mastodon-async--setup-queue "" (or name stream))) (async-buffer (mastodon-async--setup-buffer "" (or name stream) endpoint)) (http-buffer (mastodon-async--get (mastodon-http--api stream) (lambda (status) (ignore status) (message "HTTP SOURCE CLOSED"))))) (mastodon-async--setup-http http-buffer (or name stream)) (mastodon-async--set-http-buffer async-buffer http-buffer) (mastodon-async--set-http-buffer async-queue http-buffer) (set-process-filter (get-buffer-process http-buffer) (mastodon-async--http-hook filter)) http-buffer)) (defun mastodon-async--http-hook (filter) "Return a lambda with a custom FILTER for processing toots." (let ((filter filter)) (lambda (proc data) (with-current-buffer (process-buffer proc) (let* ((string (mastodon-async--stream-filter (mastodon-async--http-layer proc data))) (queue-string (mastodon-async--cycle-queue string))) (when queue-string (mastodon-async--output-toot (funcall filter queue-string)))))))) (defun mastodon-async--process-queue-string (string) "Parse the output STRING of the queue buffer, returning only update events." (let ((split-strings (split-string string "\n" t))) (when split-strings ; do nothing if we get nothing; just postpones the error (let ((event-type (replace-regexp-in-string "^event: " "" (car split-strings))) (data (replace-regexp-in-string "^data: " "" (cadr split-strings)))) (when (string= "update" event-type) ;; in some casses the data is not fully formed ;; for now return nil if malformed using `ignore-errors' (ignore-errors (json-read-from-string data))))))) (defun mastodon-async--process-queue-string-notifications (string) "Parse the output STRING of the queue buffer, returning only notification events." ;; NB notification events in streams include follow requests (let* ((split-strings (split-string string "\n" t)) (event-type (replace-regexp-in-string "^event: " "" (car split-strings))) (data (replace-regexp-in-string "^data: " "" (cadr split-strings)))) (when (string= "notification" event-type) ;; in some casses the data is not fully formed ;; for now return nil if malformed using `ignore-errors' (ignore-errors (json-read-from-string data))))) (defun mastodon-async--process-queue-local-string (string) "Use STRING to limit the public endpoint to displaying local steams only." (let ((json (mastodon-async--process-queue-string string))) (when json (when (mastodon-async--account-local-p json) json)))) (defun mastodon-async--account-local-p (json) "Test JSON to see if account is local." (not (string-match-p "@" (alist-get 'acct (alist-get 'account json))))) (defun mastodon-async--output-toot (toot) "Process TOOT and prepend it to the async user-facing buffer." (if (not (bufferp (get-buffer mastodon-async--buffer))) (mastodon-async--stop-http) (when toot (with-current-buffer mastodon-async--buffer (let* ((inhibit-read-only t) (old-max (point-max)) (previous (point)) (mastodon-tl--enable-relative-timestamps t) (mastodon-tl--display-media-p t)) (goto-char (point-min)) (if (equal (buffer-name) (concat "*mastodon-async-display-notifications-" mastodon-instance-url "*")) (mastodon-notifications--timeline (list toot)) (mastodon-tl--timeline (list toot))) (if (eq previous 1) (goto-char 1) (goto-char (+ previous (- (point-max) old-max))))))))) (defun mastodon-async--cycle-queue (string) "Append the most recent STRING from http buffer to queue buffer. Then determine if a full message has been recived. If so return it. Full messages are seperated by two newlines" (with-current-buffer mastodon-async--queue (goto-char (max-char)) (insert (decode-coding-string string 'utf-8)) (goto-char 0) (let ((next (re-search-forward "\n\n" nil t))) (when next (let ((return-string (buffer-substring 1 next)) (inhibit-read-only t)) (delete-region 1 next) return-string))))) (defun mastodon-async--http-layer (proc data) "Passes PROC and DATA to ‘url-http-generic-filter’. It then processes its output." (with-current-buffer (process-buffer proc) (let ((start (max 1 (- (point-max) 2)))) (url-http-generic-filter proc data) (when (> url-http-end-of-headers start) (setq start url-http-end-of-headers)) (let ((end (- (point-max) 2))) (buffer-substring start end))))) (defun mastodon-async--stream-filter (string) "Remove comments from STRING." (replace-regexp-in-string "^:.*\n" "" string)) (provide 'mastodon-async) ;;; mastodon-async.el ends here