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

;;; sico.el --- A personal assistant for Emacs and
;; Copyright (C) 2015 Ryan Rix
;; Author: Ryan Rix <>
;; Maintainer: Ryan Rix <>
;; Created: 20 December 2015
;; Keywords: web
;; Homepage:
;; 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 <>.
;;; Commentary:
;; Sico is your personal robot butler. Sico acts as an interface to a running
;; Emacs session over the 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\"
(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."
(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))
(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
(matrix-login-with-password con
(plist-get found :user)
(let ((secret (plist-get found :secret)))
(if (functionp secret)
(funcall secret)
(let ((save-func (plist-get found :save-function)))
(when save-func (funcall save-func)))))))
(defun sico-stop ()
"Tell sico to stop polling."
(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)
(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 "")
(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))
(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