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.
 
 

310 lines
9.9 KiB

;;; twister.el --- A client for the twister distributed microblogging system
;; Copyright (C) 2014-2015 Marcel van der Boom <marcel@hsdev.com>
;; Author: Marcel van der Boom <marcel@hsdev.com>
;; Maintainer: Marcel van der Boom
;; URL: https://github.com/mrvdb/twister.el
;; Created: 14-05-2014
;; Version: 0.1
;; Keywords: microblogging, json
;;; 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")
;; (twister-create-post)
;;
;;; Credits
;; - The elisp-json-rpc library of Christopher Wellons <wellons@nullprogrma.com>
;; does the real work
;; - some code was mimicked from identica-mode by Gabriel Saldana
;;; Code:
(require 'pcomplete)
(require 'twister-rpc)
;; Configuration variables
(defgroup twister nil
"Using the twister microblogging system"
:tag "Microblogging"
:group 'applications
)
(defgroup twister-faces nil
"Faces for twister mode"
:group 'twister
:group 'faces)
(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-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)
(defcustom twister-preview-formatting t
"Preview formatting specifiers in the post buffer.
Twister has support for *bold*, ~italic~, -strike-through- and
_underlined_ format specifiers."
:type 'boolean
:group 'twister)
(defcustom twister-active-addresses t
"Make URLs and mail addresses clickable.
This uses the standard `goto-address-mode'."
:type 'boolean
:group 'twister)
(defface twister-hashtag
'((default (:inherit link :underline nil)))
"Twister mode face for hash tags"
:group 'twister-faces)
(defface twister-bold
'((default (:inherit bold)))
"Twister mode face for bold text"
:group 'twister-faces)
(defface twister-italic
'((default (:inherit italic)))
"Twister mode face for italic text"
:group 'twister-faces)
(defface twister-underline
'((default :inherit underline))
"Twister mode face for underlined text"
:group 'twister-faces)
(defface twister-nickname
'((default (:inherit font-lock-type-face )))
"Twister mode face for nicknames"
:group 'twister-faces)
(defface twister-strikethrough
'((default (:inherit default :strike-through t)))
"Twister mode face for strike-through text"
:group 'twister-faces)
;; End configuration variables
(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-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 it 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-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 twister-user)))
(defun twister-parse-completion-arguments ()
"Look for completable items between POINT and what is before it.
This includes '@nicknames' and '#hashtags' for the moment."
(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 ()
"Default completion function for twister."
(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)))
(makunbound 'twister-post-font-lock-keywords)
(defvar twister-post-font-lock-keywords
'(("\\(^\\| \\)\\(#[[:alnum:]_]+\\)" . (2 'twister-hashtag))
("\\(^\\| \\)\\(@[[:alnum:]_]+\\)" . (2 'twister-nickname))
("\\*\\([[:alnum:]-_]+\\)\\*" . (1 'twister-bold))
("~\\([[:alnum:]-_]+\\)~" . (1 'twister-italic))
("_\\([[:alnum:]-_]+\\)_" . (1 'twister-underline))
("\\-\\([[:alnum:]-_]+\\)\\-" . (1 'twister-strikethrough)))
"Syntax highlighting keywords for twister mode.")
(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) nil)
(setq font-lock-defaults '(twister-post-font-lock-keywords))
;; Make links clickable
(if twister-active-addresses
(goto-address-mode))
;; Add counter to the modeline, so we can see what we are doing
(setq mode-line-format
(cons (format "%s (%%i/%s) " "Twist:" twister-max-msgsize) mode-line-format)))
(add-hook 'twister-post-mode-hook 'twister-post-mode-setup)
(defun twister-ur1ca-get (api longurl)
"Shortens url through ur1.ca.
API is their endpoint to which LONGURL is posted for shortening."
(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