;;; rudel-obby.el --- An obby backend for Rudel
;;
;; Copyright (C) 2008, 2009 Jan Moringen
;;
;; Author: Jan Moringen <scymtym@users.sourceforge.net>
;; Keywords: Rudel, obby, backend, implementation
;; X-RCS: $Id:$
;;
;; This file is part of Rudel.
;;
;; Rudel 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.
;;
;; Rudel 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 Rudel. If not, see <http://www.gnu.org/licenses>.
;;; Commentary:
;;
;; This file contains a Rudel protocol backend, which implements the
;; obby protocol (used by the Gobby collaborative editor until version
;; 0.5).
;;; History:
;;
;; 0.2 - Refactored client and server to employ state machine.
;;
;; 0.1 - Initial revision.
;;; Code:
;;
(eval-when-compile
(require 'cl))
(require 'eieio)
(require 'rudel)
(require 'rudel-backend)
(require 'rudel-protocol)
(require 'rudel-util)
(require 'rudel-icons)
(require 'rudel-compat) ;; for `read-color' replacement
;;; Constants
;;
(defconst rudel-obby-version '(0 2)
"Version of the obby backend for Rudel.")
(defconst rudel-obby-protocol-version 8
"Version of the obby protocol this library understands.")
(defvar rudel-obby-long-message-threshold 32768
"Threshold for message size, above which messages are sent in
multiple chunks.")
(defvar rudel-obby-long-message-chunk-size 16384
"Chunk size used, when chunking long messages.")
;;; Class rudel-obby-backend
;;
;;;###autoload
(defclass rudel-obby-backend (rudel-protocol-backend)
((capabilities :initform '(join host
change-color
track-subscriptions)))
"Main class of the Rudel obby backend. Creates obby client
connections and creates obby servers.")
(defmethod initialize-instance ((this rudel-obby-backend) &rest slots)
"Initialize slots of THIS with SLOTS."
(when (next-method-p)
(call-next-method))
(oset this :version rudel-obby-version))
(defmethod rudel-ask-connect-info ((this rudel-obby-backend) &optional info)
"Ask user for the information required to connect to an obby server."
;; Read server host and port.
(let ((host (or (and info (plist-get info :host))
(read-string "Server: ")))
(port (or (and info (plist-get info :port))
(read-number "Port: " 6522)))
;; Read desired username and color
(username (or (and info (plist-get info :username))
(read-string "Username: " user-login-name)))
(color (or (and info (plist-get info :color))
(read-color "Color: " t)))
(encryption (if (and info (member :encryption info))
(plist-get info :encryption)
(y-or-n-p "Use encryption? ")))
(global-password (if (and info (member :global-password info))
(plist-get info :global-password)
(read-string "Global password: " "")))
(user-password (if (and info (member :user-password info))
(plist-get info :user-password)
(read-string "User password: " ""))))
(append (list :host host
:port port
:username username
:color color
:encryption encryption
:global-password (unless (string= global-password "")
global-password)
:user-password (unless (string= user-password "")
user-password))
info))
)
(defmethod rudel-connect ((this rudel-obby-backend) info)
"Connect to an obby server using the information INFO.
Return the connection object."
;; Before we start, load the client functionality.
(require 'rudel-obby-client)
;; Create the network process
(let* ((session (plist-get info :session))
(host (plist-get info :host))
(port (plist-get info :port))
(encryption (plist-get info :encryption))
;; Create the network process
(socket (funcall
(if encryption
(progn
(require 'rudel-tls)
#'rudel-tls-make-process)
#'make-network-process)
:name host
:host host
:service port
;; Install connection filter to redirect data to
;; the connection object
:filter #'rudel-filter-dispatch
;; Install connection sentinel to redirect state
;; changes to the connection object
:sentinel #'rudel-sentinel-dispatch
;; Do not start receiving immediately since the
;; filter function is not yet setup properly.
:stop t))
(connection (rudel-obby-connection
host
:session session
:socket socket
:info info)))
;; Now start receiving and wait until the basic session setup is
;; complete.
(continue-process socket)
;; Wait for the connection to reach one of the states idle,
;; join-failed and they-finalized.
(condition-case error
(lexical-let ((reporter (make-progress-reporter "Joining ")))
(flet ((display-progress (state)
(cond
;; For all states, just spin.
((consp state)
(progress-reporter-force-update
reporter nil (format "Joining (%s)" (car state))))
;; Done
(t
(progress-reporter-force-update reporter nil "Joining ")
(progress-reporter-done reporter)))))
(rudel-state-wait connection
'(idle) '(join-failed they-finalized)
#'display-progress)))
(rudel-entered-error-state
(destructuring-bind (symbol . state) (cdr error)
(if (eq (rudel-find-state connection 'join-failed) state)
(with-slots (error-symbol error-data) state
(signal 'rudel-join-error
(append (list error-symbol) error-data)))
(signal 'rudel-join-error nil)))))
;; The connection is now usable; return it.
connection)
)
(defmethod rudel-ask-host-info ((this rudel-obby-backend))
"Ask user for information required to host an obby session."
(let ((port (read-number "Port: " 6522)))
(list :port port)))
(defmethod rudel-host ((this rudel-obby-backend) info)
"Host an obby session using the information INFO.
Return the created server."
;; Before we start, we load the server functionality.
(require 'rudel-obby-server)
;; Create the network process.
(let* ((port (plist-get info :port))
;; Make a server socket
(socket (make-network-process
:name "obby-server"
:host "0.0.0.0"
:service port
:server t
:filter #'rudel-filter-dispatch
:sentinel #'rudel-sentinel-dispatch
;;
:log
(lambda (server-process client-process message)
(let ((server (rudel-process-object server-process)))
(rudel-add-client server client-process)))))
;; Construct server object.
(server (rudel-obby-server "obby-server"
:backend this
:socket socket)))
;; Return the constructed server.
server)
)
(defmethod rudel-make-document ((this rudel-obby-backend)
name session)
"Make a new document in SESSION named NAME.
Return the new document."
;; Find an unused document id and create a document with that id.
(let ((id (rudel-available-document-id this session)))
(with-slots (user-id) (oref session :self)
(rudel-obby-document name
:session session
:id id
:owner-id user-id
:suffix 1)))
)
(defmethod rudel-available-document-id ((this rudel-obby-backend)
session)
"Return a document id, which is not in use in SESSION."
;; Look through some candidates until an unused id is hit.
(let* ((used-ids (with-slots (documents) session
(mapcar 'rudel-id documents)))
(test-ids (number-sequence 0 (length used-ids))))
(car (sort (set-difference test-ids used-ids) '<)))
)
;;; Class rudel-obby-user
;;
(defclass rudel-obby-user (rudel-user)
((client-id :initarg :client-id
:type (or null integer) ;; We allow nil instead of making
:accessor rudel-client-id ;; the slot unbound, to be able to
:initform nil ;; search with test `rudel-client-id
:documentation ;; without headaches
"Id of the client connection, which the user used to log in.
The value is an integer, if the user is connected, and nil
otherwise.")
(user-id :initarg :user-id
:type integer
:accessor rudel-id
:documentation
"")
(connected :initarg :connected
:type boolean
:accessor rudel-connected
:documentation
"")
(encryption :initarg :encryption ;; TODO maybe we should use unbound when the user is not connected
:type boolean
:documentation
""))
"Class rudel-obby-user ")
(defmethod eieio-speedbar-description ((this rudel-obby-user))
"Provide a speedbar description for THIS."
(let ((connected (oref this :connected))
(encryption (if (slot-boundp this :encryption)
(oref this :encryption)
nil)))
(format "User %s (%s, %s)" (object-name-string this)
(if connected "Online" "Offline")
(if encryption "Encryption" "Plain")))
)
(defmethod eieio-speedbar-object-buttonname ((this rudel-obby-user))
"Return a string to use as a speedbar button for THIS."
(rudel-display-string this))
(defmethod rudel-display-string ((this rudel-obby-user)
&optional use-images align)
"Return a textual representation of THIS for user interface stuff."
(with-slots (connected color) this
(let ((encryption (and (slot-boundp this :encryption)
(oref this :encryption)))
(name-string (call-next-method)))
(concat
;; Name bit
(cond
((numberp align) (format (format "%-%ds" align) name-string))
((eq align t) (format "%-12s" name-string))
(t name-string))
;; Connection status bit
(apply
#'propertize
(if connected "c" "-")
'help-echo (format (if connected
"%s is connected"
"%s is not connected")
name-string)
'face (list :background color)
(when use-images
(list 'display (if connected
rudel-icon-connected
rudel-icon-disconnected))))
;; Encryption bit
(apply
#'propertize
(if encryption "e" "-")
'help-echo (format (if encryption
"%s's connection is encrypted"
"%s's connection is not encrypted")
name-string)
'face (list :background color)
(when use-images
(list 'display (if encryption
rudel-icon-encrypted
rudel-icon-plaintext)))))))
)
;;; Class rudel-obby-document
;;
(defclass rudel-obby-document (rudel-document)
((id :initarg :id
:type integer
:accessor rudel-id
:documentation
"The id of this document.
The id has to be unique only with respect to the other documents
owned by the owner.")
(owner-id :initarg :owner-id
:type integer
:documentation
"")
(suffix :initarg :suffix
:type integer
:documentation
"A counter used to distinguish identically named
documents."))
"Objects of the class rudel-obby-document represent shared
documents in obby sessions.")
(defmethod rudel-both-ids ((this rudel-obby-document))
"Return a list consisting of document and owner id of THIS document."
(with-slots ((doc-id :id) owner-id) this
(list owner-id doc-id)))
(defmethod rudel-unique-name ((this rudel-obby-document))
"Generate a unique name for THIS based on the name and the suffix."
(with-slots (suffix) this
(concat (when (next-method-p)
(call-next-method))
(when (> suffix 1)
(format "<%d>" suffix))))
)
(defmethod eieio-speedbar-description ((this rudel-obby-document))
"Construct a description for from the name of document object THIS."
(format "Document %s" (object-name-string this)))
(defmethod eieio-speedbar-object-buttonname ((this rudel-obby-document))
"Return a string to use as a speedbar button for OBJECT."
(with-slots (subscribed) this
(format "%-12s %s" (object-name-string this)
(if subscribed "s" "-")))
)
;;; Obby message functions
;;
(defun rudel-obby-replace-in-string (string replacements)
"Replace elements of REPLACEMENTS in STRING.
REPLACEMENTS is a list of conses whose car is the pattern and
whose cdr is the replacement for the pattern."
(let ((result string))
(dolist (replacement replacements)
(let ((from (car replacement))
(to (cdr replacement)))
(setq result (replace-regexp-in-string
from to result nil t))))
result)
)
(defun rudel-obby-escape-string (string)
"Replace meta characters in STRING with their escape sequences."
(rudel-obby-replace-in-string
string
'(("\\\\" . "\\b") ("\n" . "\\n") (":" . "\\d")))
)
(defun rudel-obby-unescape-string (string)
"Replace escaped versions of obby meta characters in STRING with the actual meta characters."
(rudel-obby-replace-in-string
string
'(("\\\\n" . "\n") ("\\\\d" . ":") ("\\\\b" . "\\")))
)
(defun rudel-obby-parse-color (color)
"Parse the obby color string COLOR into an Emacs color."
(let* ((color-numeric (string-to-number color 16))
(color-string (format "#%04X%04X%04X"
(lsh (logand #xff0000 color-numeric) -08)
(lsh (logand #x00ff00 color-numeric) -00)
(lsh (logand #x0000ff color-numeric) 08))))
color-string)
)
(defun rudel-obby-format-color (color)
"Format the Emacs color COLOR as obby color string."
(multiple-value-bind (red green blue) (color-values color)
(format "%02x%02x%02x" (lsh red -8) (lsh green -8) (lsh blue -8))))
(defun rudel-obby-assemble-message (name &rest arguments)
""
(concat (mapconcat
(lambda (part)
(if (and (not (null part)) (stringp part))
(rudel-obby-escape-string part)
part))
(cons name arguments) ":")
"\n")
)
(defun rudel-obby-parse-message (message)
"Split MESSAGE at `:' and unescape resulting parts.
The terminating `\n' should be removed from MESSAGE before
calling this function."
(mapcar #'rudel-obby-unescape-string (split-string message ":")))
(defun rudel-obby-send (socket name arguments)
"Send an obby message NAME with arguments ARGUMENTS through SOCKET."
;; First, assemble the message string.
(let ((message (apply #'rudel-obby-assemble-message
name arguments)))
(if (>= (length message) rudel-obby-long-message-threshold)
;; For huge messages, chunk the message data and transmit the
;; chunks
(let ((total (/ (length message)
rudel-obby-long-message-chunk-size))
(current 0)
(reporter (make-progress-reporter "Sending data " 0.0 1.0)))
(rudel-loop-chunks message chunk rudel-obby-long-message-chunk-size
(progress-reporter-update reporter (/ (float current) total))
(process-send-string socket chunk)
(incf current))
(progress-reporter-done reporter))
;; Send small messages in one chunk
(process-send-string socket message)))
)
;;; Autoloading
;;
;;;###autoload
(rudel-add-backend (rudel-backend-get-factory 'protocol)
'obby 'rudel-obby-backend)
;;;###autoload
(eval-after-load 'rudel-zeroconf
'(rudel-zeroconf-register-service "_lobby._tcp" 'obby))
(provide 'rudel-obby)
;;; rudel-obby.el ends here