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
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
|
|
|