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.
207 lines
7.8 KiB
207 lines
7.8 KiB
;;; sico.el --- A personal assistant for Emacs and Matrix.org
|
|
|
|
;; Copyright (C) 2015 Ryan Rix
|
|
;; Author: Ryan Rix <ryan@whatthefuck.computer>
|
|
;; Maintainer: Ryan Rix <ryan@whatthefuck.computer>
|
|
;; Created: 20 December 2015
|
|
;; Keywords: web
|
|
;; Homepage: http://doc.rix.si/matrix.html
|
|
;; Package-Version: 0.0.1
|
|
;; Package-Requires: ((matrix-client "0.1.0"))
|
|
|
|
;; This file is not part of GNU Emacs.
|
|
|
|
;; sico.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.
|
|
;;
|
|
;; sico.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
|
|
;; this file. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; Sico is your personal robot butler. Sico acts as an interface to a running
|
|
;; Emacs session over the Matrix.org RPC network. Run `sico-start' and it will
|
|
;; jump in to your favorite Matrix rooms and lend a hand when you need it.
|
|
|
|
;; To get started, customize, at the minumum `sico-rooms' and `sico-users'. You
|
|
;; may also have to customize `matrix-homeserver-base-url', if you are
|
|
;; connecting to a non-standard homeserver.
|
|
|
|
;; Right now Sico is in early development, supporting only the ability to
|
|
;; capture single-line notes to your Org-Mode. The docstring for
|
|
;; `sico-capture-note-template' explains how to set this up.
|
|
|
|
;; Future work and plans:
|
|
;; - Render an Agenda as HTML and send it over Matrix
|
|
;; - Schedule timers
|
|
;; - Evaluate functions from a whitelist
|
|
;; - Notify you for anything using the `sauron' library
|
|
;; - Notify you for anything using the `notifications' library
|
|
|
|
;;; Code:
|
|
|
|
(require 'matrix-client)
|
|
(require 'matrix-api)
|
|
|
|
(defgroup sico nil "Customization options for Sico, your
|
|
Matrix/Emacs robot butler."
|
|
:prefix "sico")
|
|
|
|
(defcustom sico-rooms nil
|
|
"Rooms the sico should join."
|
|
:type '(repeat string)
|
|
:group 'sico)
|
|
|
|
(defcustom sico-users nil
|
|
"List of users that are allowed to interact with the sico."
|
|
:type '(repeat string)
|
|
:group 'sico)
|
|
|
|
(defcustom sico-username nil
|
|
"What is the username of sico"
|
|
:type 'string :group 'sico)
|
|
|
|
(defcustom sico-capture-note-regexp "^!note\\b\\(.*\\)"
|
|
"Regular expression defining how to capture notes.
|
|
|
|
Handler expects (match-string 1) to include the note."
|
|
:type 'string
|
|
:group 'sico)
|
|
|
|
(defcustom sico-capture-note-template "A"
|
|
"The org-capture-templates key that will be used to store a note.
|
|
|
|
Templates should assume that the body will be given as the head
|
|
of the kill ring, accessible from inside a template as %c. A
|
|
standard org-capture-template could look like:
|
|
|
|
'(\"A\" \"Automatic note taking using Sico\"
|
|
entry
|
|
(file org-default-notes-file)
|
|
\"* %c :NOTE:NOEXPORT:\"
|
|
:immediate-finish t)")
|
|
|
|
(defcustom sico-listeners (list (cons sico-capture-note-regexp 'sico-capture-note))
|
|
"Alist containing a set of (`regexp' . `function') cells.
|
|
The regexps that match will get the entire line passed to it as a
|
|
single argument."
|
|
:type '(alist :value-type (group function)))
|
|
|
|
(defcustom sico-connection-string
|
|
"Hello, my name is Sico and I am pleased to be of service."
|
|
"The message the bot says upon joining each room.")
|
|
|
|
(defvar sico-active-connection nil)
|
|
|
|
(defclass sico-connection (matrix-connection)
|
|
((running :initarg :running
|
|
:initform nil
|
|
:documentation "BOOL specifiying if the event listener is currently running.")
|
|
(rooms :initarg :rooms
|
|
:initform nil
|
|
:documentation "List of matrix-room objects")
|
|
(end-token :initarg :end-token)))
|
|
|
|
(defun sico-start ()
|
|
"Start Assitant and connect it to the Matrix homeserver."
|
|
(interactive)
|
|
(unless sico-active-connection
|
|
(setq sico-active-connection (sico-connection matrix-homeserver-base-url :base-url matrix-homeserver-base-url))
|
|
(sico-login sico-active-connection sico-username))
|
|
(sico-join-rooms)
|
|
(sico-start-polling)
|
|
(setq sico-running t))
|
|
|
|
(defmethod sico-login ((con sico-connection) username)
|
|
(let* ((auth-source-creation-prompts
|
|
'((username . "Matrix identity: ")
|
|
(secret . "Matrix password for %u (homeserver: %h): ")))
|
|
(found (nth 0 (auth-source-search :max 1
|
|
:host (oref con :base-url)
|
|
:user username
|
|
:require '(:user :secret)
|
|
:create t))))
|
|
(when (and
|
|
found
|
|
(matrix-login-with-password con
|
|
(plist-get found :user)
|
|
(let ((secret (plist-get found :secret)))
|
|
(if (functionp secret)
|
|
(funcall secret)
|
|
secret)))
|
|
(let ((save-func (plist-get found :save-function)))
|
|
(when save-func (funcall save-func)))))))
|
|
|
|
(defun sico-stop ()
|
|
"Tell sico to stop polling."
|
|
(interactive)
|
|
(oset sico-active-connection :running nil)
|
|
(setq sico-active-connection nil))
|
|
|
|
(defun sico-start-polling ()
|
|
"Begin polling for events with the [`sico-poll-callback'] as the handler."
|
|
(let* ((con sico-active-connection)
|
|
(next (and (slot-boundp con :end-token)
|
|
(oref con :end-token))))
|
|
(matrix-sync con next (if next nil t) 10
|
|
(apply-partially #'sico-sync-handler con))))
|
|
|
|
(defun sico-join-rooms ()
|
|
"Join the rooms that Sico is configured to be in."
|
|
(dolist (room sico-rooms)
|
|
(matrix-join-room sico-active-connection room)
|
|
(when (> (length sico-connection-string) 0)
|
|
(matrix-send-message sico-active-connection room sico-connection-string))))
|
|
|
|
(defmethod sico-sync-handler ((con sico-connection) data)
|
|
(mapc
|
|
(lambda (room-data)
|
|
(let* ((room-id (symbol-name (car room-data)))
|
|
(room-events (cdr room-data))
|
|
(handler (lambda (item)
|
|
(dolist (cell sico-listeners)
|
|
(let ((content (matrix-get 'content item))
|
|
(type (matrix-get 'type item)))
|
|
(when (and content
|
|
(string= type "m.room.message")
|
|
(string-match (car cell)
|
|
(matrix-get 'body content)))
|
|
(message "match %s: %s" content cell)
|
|
(funcall (cdr cell) con room-id item)))))))
|
|
(mapc handler
|
|
(matrix-get 'events (matrix-get 'state room-events)))
|
|
(mapc handler
|
|
(matrix-get 'events (matrix-get 'timeline room-events)))))
|
|
(matrix-get 'join (matrix-get 'rooms data)))
|
|
(let ((next (matrix-get 'next_batch data)))
|
|
(oset con :end-token next)
|
|
(when (and (slot-boundp con :running)
|
|
(oref con :running))
|
|
(sico-start-polling))))
|
|
|
|
(defun sico-capture-note (con room-id event)
|
|
"Capture a note from EVENT."
|
|
(let* ((content (matrix-get 'content event))
|
|
(body (matrix-get 'body content))
|
|
(matching (string-match sico-capture-note-regexp body))
|
|
(match (match-string 1 body))
|
|
(user-id (matrix-get 'sender event))
|
|
(room-id (matrix-get 'room_id event)))
|
|
(message "matchdata: %s %s %s" body match sico-capture-note-regexp)
|
|
(when (member user-id sico-users)
|
|
(message "userid %s: %s" user-id sico-users)
|
|
(kill-new match)
|
|
(org-capture nil sico-capture-note-template)
|
|
(matrix-send-message sico-active-connection room-id
|
|
(format "I have captured: \"%s\"" match)))))
|
|
|
|
(provide 'sico)
|
|
;;; sico.el ends here
|
|
|