chiark / gitweb /
General-purpose event distribution interface
[disorder] / python / disorder.py.in
index 0f16c1a54c82533b1ffe0c14a2b4857f881e606d..3cc300c27c25bd9171d0d097bfd2f7a78bbb5deb 100644 (file)
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2004, 2005, 2007 Richard Kettlewell
+# Copyright (C) 2004, 2005, 2007, 2008 Richard Kettlewell
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -62,8 +62,8 @@ _userconf = True
 
 # various regexps we'll use
 _ws = re.compile(r"^[ \t\n\r]+")
-_squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])+)'")
-_dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])+)\"")
+_squote = re.compile("'(([^\\\\']|\\\\[\\\\\"'n])*)'")
+_dquote = re.compile("\"(([^\\\\\"]|\\\\[\\\\\"'n])*)\"")
 _unquoted = re.compile("[^\"' \\t\\n\\r][^ \t\n\r]*")
 
 _response = re.compile("([0-9]{3}) ?(.*)")
@@ -276,7 +276,7 @@ class client:
   debug_proto = 0x0001
   debug_body = 0x0002
 
-  def __init__(self):
+  def __init__(self, user=None, password=None):
     """Constructor for DisOrder client class.
 
     The constructor reads the configuration file, but does not connect
@@ -294,6 +294,8 @@ class client:
     self.config = { 'collections': [],
                     'username': pw.pw_name,
                     'home': _dbhome }
+    self.user = user
+    self.password = password
     home = os.getenv("HOME")
     if not home:
       home = pw.pw_dir
@@ -327,8 +329,10 @@ class client:
       sys.stderr.write("\n")
       sys.stderr.flush()
 
-  def connect(self):
-    """Connect to the DisOrder server and authenticate.
+  def connect(self, cookie=None):
+    """c.connect(cookie=None)
+
+    Connect to the DisOrder server and authenticate.
 
     Raises communicationError if connection fails and operationError if
     authentication fails (in which case disconnection is automatic).
@@ -339,6 +343,9 @@ class client:
 
     Other operations automatically connect if we're not already
     connected, so it is not strictly necessary to call this method.
+
+    If COOKIE is specified then that is used to log in instead of
+    the username/password.
     """
     if self.state == 'disconnected':
       try:
@@ -368,11 +375,27 @@ class client:
           s.connect(self.who)
         self.w = s.makefile("wb")
         self.r = s.makefile("rb")
-        (res, challenge) = self._simple()
-        h = sha.sha()
-        h.update(self.config['password'])
-        h.update(binascii.unhexlify(challenge))
-        self._simple("user", self.config['username'], h.hexdigest())
+        (res, details) = self._simple()
+        (protocol, algo, challenge) = _split(details)
+        if protocol != '2':
+          raise communicationError(self.who,
+                                   "unknown protocol version %s" % protocol)
+        if cookie is None:
+          if self.user is None:
+            user = self.config['username']
+          else:
+            user = self.user
+          if self.password is None:
+            password = self.config['password']
+          else:
+            password = self.password
+          # TODO support algorithms other than SHA-1
+          h = sha.sha()
+          h.update(password)
+          h.update(binascii.unhexlify(challenge))
+          self._simple("user", user, h.hexdigest())
+        else:
+          self._simple("cookie", cookie)
         self.state = 'connected'
       except socket.error, e:
         self._disconnect()
@@ -393,16 +416,6 @@ class client:
   ########################################################################
   # Operations
 
-  def become(self, who):
-    """Become another user.
-
-    Arguments:
-    who -- the user to become.
-
-    Only trusted users can perform this operation.
-    """
-    self._simple("become", who)
-
   def play(self, track):
     """Play a track.
 
@@ -466,19 +479,16 @@ class client:
     """
     self._simple("reconfigure")
 
-  def rescan(self, pattern):
+  def rescan(self, *flags):
     """Rescan one or more collections.
 
-    Arguments:
-    pattern -- glob pattern matching collections to rescan.
-
     Only trusted users can perform this operation.
     """
-    self._simple("rescan", pattern)
+    self._simple("rescan", *flags)
 
   def version(self):
     """Return the server's version number."""
-    return self._simple("version")[1]
+    return _split(self._simple("version")[1])[0]
 
   def playing(self):
     """Return the currently playing track.
@@ -609,7 +619,7 @@ class client:
     if ret == 555:
       return None
     else:
-      return details
+      return _split(details)[0]
 
   def prefs(self, track):
     """Get all the preferences for a track.
@@ -755,7 +765,8 @@ class client:
     second the line from the event log.
     
     The callback should return True to continue or False to stop (don't
-    forget this, or your program will mysteriously misbehave).
+    forget this, or your program will mysteriously misbehave).  Once you
+    stop reading the log the connection is useless and should be deleted.
 
     It is suggested that you use the disorder.monitor class instead of
     calling this method directly, but this is not mandatory.
@@ -775,11 +786,6 @@ class client:
         l = l[1:]
       if not callback(self, l):
         break
-    # tell the server to stop sending, eat the remains of the body,
-    # eat the response
-    self._send("version")
-    self._body()
-    self._response()
 
   def pause(self):
     """Pause the current track."""
@@ -800,7 +806,7 @@ class client:
     The return value is the preference 
     """
     ret, details = self._simple("part", track, context, part)
-    return details
+    return _split(details)[0]
 
   def setglobal(self, key, value):
     """Set a global preference value.
@@ -831,7 +837,75 @@ class client:
     if ret == 555:
       return None
     else:
-      return details
+      return _split(details)[0]
+
+  def make_cookie(self):
+    """Create a login cookie"""
+    ret, details = self._simple("make-cookie")
+    return _split(details)[0]
+  
+  def revoke(self):
+    """Revoke a login cookie"""
+    self._simple("revoke")
+
+  def adduser(self, user, password):
+    """Create a user"""
+    self._simple("adduser", user, password)
+
+  def deluser(self, user):
+    """Delete a user"""
+    self._simple("deluser", user)
+
+  def userinfo(self, user, key):
+    """Get user information"""
+    res, details = self._simple("userinfo", user, key)
+    if res == 555:
+      return None
+    return _split(details)[0]
+
+  def edituser(self, user, key, value):
+    """Set user information"""
+    self._simple("edituser", user, key, value)
+
+  def users(self):
+    """List all users
+
+    The return value is a list of all users."""
+    self._simple("users")
+    return self._body()
+
+  def register(self, username, password, email):
+    """Register a user"""
+    res, details = self._simple("register", username, password, email)
+    return _split(details)[0]
+
+  def confirm(self, confirmation):
+    """Confirm a user registration"""
+    res, details = self._simple("confirm", confirmation)
+
+  def schedule_list(self):
+    """Get a list of scheduled events """
+    self._simple("schedule-list")
+    return self._body()
+
+  def schedule_del(self, event):
+    """Delete a scheduled event"""
+    self._simple("schedule-del", event)
+
+  def schedule_get(self, event):
+    """Get the details for an event as a dict (returns None if event not found)"""
+    res, details = self._simple("schedule-get", event)
+    if res == 555:
+      return None
+    d = {}
+    for line in self._body():
+      bits = _split(line)
+      d[bits[0]] = bits[1]
+    return d
+
+  def schedule_add(self, when, priority, action, *rest):
+    """Add a scheduled event"""
+    self._simple("schedule-add", str(when), priority, action, *rest)
 
   ########################################################################
   # I/O infrastructure
@@ -1047,6 +1121,8 @@ class monitor:
     elif keyword == 'scratched':
       if len(bits) == 2:
         return self.scratched(bits[0], bits[1])
+    elif keyword == 'rescanned':
+      return self.rescanned()
     return self.invalid(line)
 
   def completed(self, track):
@@ -1126,6 +1202,10 @@ class monitor:
     line -- line that could not be understood"""
     return True
 
+  def rescanned(self):
+    """Called when a rescan completes"""
+    return True
+
 # Local Variables:
 # mode:python
 # py-indent-offset:2