An interface to the twister microblogging application from Emacs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

277 lines
8.9 KiB

;;; twister.el --- A client for the twister distribute microblogging system
;; Copyright (C) 2014 Marcel van der Boom <marcel@hsdev.com>
;;; Commentary:
;; The original idea for this client was to minimally implement:
;; ✓ make it possible to post to twister directly from Emacs;
;; ✓ have autocompletion for 'known users' when posting a message
;; ✓ (this probably means defining a 'mode')
;; During the implementation of the above the following nice-to-have
;; came to mind:
;; - get a timeline in a buffer in emacs
;; - allow for replies
;; - allow for direct messages
;; - manage follow lists
;; - have avatars obviously. (including the checkmark, which is brilliant)
;;
;; Requirements
;; - json-rpc : https://github.com/mrvdb/elisp-json-rpc
;; - pcomplete: (built into emacs)
;; Installation
;; (add-to-list 'load-path "/path/to/twister.el")
;; (require 'twister)
;; (setq twister-user "yournick")
;;
;;; Credits
;; - The elisp-json-rpc library of Christopher Wellons <wellons@nullprogrma.com>
;; does the real work
;;; Code:
(require 'json-rpc)
(require 'pcomplete)
;; Configuration variables
(defgroup twister nil
"Using the twister microblogging system"
:tag "Microblogging"
:group 'applications
)
(defcustom twister-user "twister_user"
"The nickname you use on your twister instance.
When posting messages, this will be the name used"
:type 'string
:group 'twister)
(defcustom twister-rpcuser "user"
"The RPC username configured in the twister.conf file."
:type 'string
:group 'twister)
(defcustom twister-rpcpassword "pwd"
"The RPC password for the `twister-rpcuser.
This is configured in the twister.conf file"
:type 'string
:group 'twitter)
(defcustom twister-host "localhost"
"Host where the twister daemon runs."
:type 'string
:group 'twister)
(defcustom twister-port 28332
"Port on which twister daemon runs and serves RPC commands."
:type 'integer
:group 'twister)
(defcustom twister-max-msgsize 140
"Maximum size of messages to post. Default of the server is 140.
If you want larger messages, you will also need to enable
automatic splitting in the twister configuration."
:type 'integer
:group 'twister)
(defcustom twister-post-buffername "New Twister Message"
"Name of the buffer in which Twister Messages can be composed."
:type 'string
:group 'twister)
(defvar twister-post-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "\C-c\C-c" 'twister-post-buffer)
(define-key map "\C-c\C-k" 'twister-close-post)
(define-key map "\C-i" 'completion-at-point) ;; \C-i == TAB
(define-key map "\C-c\C-s" 'twister-shortenurl-replace-at-point)
map) "Keymap for `twister-post-mode'.")
(define-derived-mode twister-post-mode text-mode "twister-post"
"Twister major mode for posting new messages."
;; Reason I want this:
;; ✓ define autompletion on @ sign
;; ✓ define specific key map for posting messages
)
(defun twister-rpc (method &rest params)
"Wrapper for the json-rpc method for twister use.
The connection is closed afer each use. This is not necessarily
the most effective. METHOD is the RPC method we are calling
while PARAMS contain the rest of the parameters."
(let* ((twisterd (json-rpc-connect
twister-host twister-port
twister-rpcuser twister-rpcpassword))
(result (apply 'json-rpc twisterd method params)))
(json-rpc-close twisterd)
result))
(defun twister-get-last-post(user)
"Get the last post of a user"
(let (obj (json-new-object))
(twister-rpc "getposts" 1
(vector (json-add-to-object obj "username" user)))))
(defun twister-get-next-k(user)
"Get the next 'k' for a user; this is a post sequence number.
The data structure that contains is is documented at:
http://twister.net.co/?page_id=21"
(+ 1 (plist-get
(plist-get
(elt (twister-get-last-post user) 0)
:userpost) :k)))
(defun twister-post(msg)
"Post msg to the configured twister daemon"
(interactive)
(twister-rpc "newpostmsg"
twister-user
(twister-get-next-k twister-user) msg))
(defun twister-post-region (begin end)
"Post the current region to twister.
The BEGIN and END arguments are the usual points of the region."
(interactive "r")
(let ((selection (buffer-substring-no-properties begin end)))
(when (or (<= (length selection) twister-max-msgsize)
(y-or-n-p (format (concat "The message is %i characters long. "
"Do you still want to post? ")
(length selection))))
(message "Posting message...")
(twister-post selection))))
(defun twister-post-buffer()
"Post the current buffer to twister"
(interactive)
(twister-post-region (point-min) ( point-max)))
(defun twister-create-post ()
"Create a new buffer for writing a note."
(interactive)
(with-current-buffer (get-buffer-create twister-post-buffername)
(twister-post-mode)
(switch-to-buffer-other-window (current-buffer))
(fit-window-to-buffer (selected-window) 10 10)))
(defun twister-close-post ()
"Hide and kill the posting buffer if it is the special posting buffer.
This is typically used in combination with `twister-create-post'
to end the posting activity."
(interactive)
(when (get-buffer twister-post-buffername)
(with-current-buffer twister-post-buffername
;; if the window is the sole window in its frame, delete-window will error
(if (window-parent) (delete-window))
(kill-buffer twister-post-buffername)
)))
(defun twister-getfollowing (&optional user)
"Get a vector of usernames which are followed by `twister-user'.
The USER parameter is only useful for the locally registered
users. In most cases this will be the same as the `twister-user'
so we use that if user is not specified."
(interactive)
(twister-rpc "getfollowing" (if user user twister-user)))
(defun twister-completion-entries ()
"Produce a list of entries to which completion can be matched.
For now, this is just the nicknames the user follows"
(mapcar (lambda (x) (concat "@" x)) (twister-getfollowing)))
(defun twister-parse-completion-arguments ()
(save-excursion
(let* ((end (point))
(start (search-backward "@" nil t)) ;; Only search @.... stuff
(ptt (if start start end)))
(list (list "dummy" ;; hmm, not liking this
(buffer-substring-no-properties ptt end))
(point-min) ptt))))
(defun twister-default-completion ()
(pcomplete-here (twister-completion-entries)))
;; Define autocompletion for the twister-post-mode
(defun pcomplete-twister-post-setup ()
"Setup `twister-post-mode' to use pcomplete"
(interactive)
(set (make-local-variable 'pcomplete-parse-arguments-function)
'twister-parse-completion-arguments)
(set (make-local-variable 'pcomplete-default-completion-function)
'twister-default-completion))
(defun twister-nick-completion-at-point ()
"Using nicknames, provide a completion table for the text around point."
(interactive)
(let* ((end (point))
(start
(save-excursion
(+ end (skip-syntax-backward "w_."))))) ;; '@' is punctuation???
(list start end (twister-completion-entries) :exclusive t)))
(defvar twister-post-font-lock-keywords
'(("#[[:alnum:]_]+" . font-lock-keyword-face)
("@[[:alnum:]_]+" . font-lock-function-name-face)))
(defun twister-post-mode-setup ()
"Initialize the twister post-mode."
;; Set up pcomplete
;; This really should be left up to to user
(pcomplete-twister-post-setup)
;; Add nick completion to at-point completion
(add-hook 'completion-at-point-functions 'twister-nick-completion-at-point nil t)
(set (make-local-variable 'font-lock-defaults)
'(twister-post-font-lock-keywords))
)
(add-hook 'twister-post-mode-hook 'twister-post-mode-setup)
(defun twister-ur1ca-get (api longurl)
"Shortens url through ur1.ca free service 'as in freedom'"
(let* ((url-request-method "POST")
(url-request-extra-headers
'(("Content-Type" . "application/x-www-form-urlencoded")))
(url-request-data (concat "longurl=" (url-hexify-string longurl)))
(buffer (url-retrieve-synchronously api)))
(with-current-buffer buffer
(goto-char (point-min))
(prog1
(if (search-forward-regexp "Your .* is: .*>\\(http://ur1.ca/[0-9A-Za-z].*\\)</a>" nil t)
(match-string-no-properties 1)
(error "URL shortening service failed: %s" longurl))
(kill-buffer buffer)))))
(defun twister-shortenurl-replace-at-point ()
"Replace the url at point with a shorter version."
(interactive)
(let ((url-bounds (bounds-of-thing-at-point 'url)))
(when url-bounds
(let ((url (twister-ur1ca-get "http://ur1.ca" (thing-at-point 'url))))
(when url
(save-restriction
(narrow-to-region (first url-bounds) (rest url-bounds))
(delete-region (point-min) (point-max))
(insert url)))))))
(provide 'twister)
;;; twister.el ends here