Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions docs/source/web.rst
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,84 @@ Server Replies:


The callback host is now set to ``1.2.3.4`` and the port is now ``80``.


get_user_info
================

Returns all settings and attributes of a user.

Required Form Variables:

* ``security_token``: The password specified in the config as ``-r`` or ``--api-security-token``.
* ``user_name``: The target user.

Returns json:

[ success (boolean) , details (object) ]

Example:

Client Requests URL:

.. sourcecode:: none

/web/get_user_info?security_token=yo&user_name=mcarter


Server Replies:


.. sourcecode:: javascript

[
true,
{
"channels": [
"testing"
],
"connections": [
"467412414c294f1a9d1759ace01455d9"
],
"name": "mcarter",
"options": {
"reflective": true,
"moderated_message": true,
"per_connection_subscriptions": false
}
}
]


set_user_options
===================

Set the options for a user.

Required Form Variables:

* ``security_token``: The password specified in the config as ``-r`` or ``--api-security-token``.
* ``user_name``: The target user.

Optional Form Variables:

* ``reflective``: json boolean - if true, private messages sent by this user will also be sent back to the user
* ``moderated_message``: json boolean - if true, private messages sent by this user will call the message webhook
* ``per_connection_subscriptions``: json boolean - if true, only the user connection (or connections) that sends a subscribe frame will be subscribed to the specified channel. Otherwise, all of a user's connections will share channel subscriptions established by any of the connections.

Example:

Client Requests URL:

.. sourcecode:: none

/web/set_user_options?security_token=yo&user_name=mcarter&reflective=false


Server Replies:

.. sourcecode:: javascript

[ true, {} ]

The ``reflective`` of the user is now `false`.
14 changes: 3 additions & 11 deletions hookbox/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,7 @@ def __init__(self, server, name, **options):
#print 'self._options is', self._options
self.state = {}
self.update_options(**self._options)
self.update_options(**options)

def user_disconnected(self, user):
# TODO: remove this pointless check, it should never happen, right?
if user not in self.subscribers:
return
self.unsubscribe(user, needs_auth=True, force_auth=True)


self.update_options(**options)

def set_history(self, history):
self.history = history
Expand Down Expand Up @@ -192,8 +184,8 @@ def publish(self, user, payload, needs_auth=True, conn=None, **kwargs):
self.prune_history()

def subscribe(self, user, conn=None, needs_auth=True):

if user in self.subscribers:
user.channel_subscribed(self, conn=conn)
return

has_initial_data = False
Expand All @@ -214,7 +206,7 @@ def subscribe(self, user, conn=None, needs_auth=True):
user.send_frame('CHANNEL_INIT', frame)

self.subscribers.append(user)
user.channel_subscribed(self)
user.channel_subscribed(self, conn=conn)
_now = get_now()
frame = {"channel_name": self.name, "user": user.get_name(), "datetime": _now}
self.server.admin.channel_event('subscribe', self.name, frame)
Expand Down
115 changes: 93 additions & 22 deletions hookbox/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,69 @@ def get_now():
return datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')

class User(object):
def __init__(self, server, name):
_options = {
'reflective': True,
'moderated_message': True,
'per_connection_subscriptions': False,
}

def __init__(self, server, name, **options):
self.server = server
self.name = name
self.connections = []
self.channels = []
self.channels = {}
self._temp_cookie = ""
self.update_options(**self._options)
self.update_options(**options)

def serialize(self):
return {
'channels': [ chan.name for chan in self.channels ],
'connections': [ conn.id for conn in self.connections ],
'name': self.name
'name': self.name,
'options': dict([ (key, getattr(self, key)) for key in self._options])
}

def update_options(self, **options):
# TODO: this can't remain so generic forever. At some point we need
# better checks on values, such as the list of dictionaries
# for history, or the polling options.
# TODO: add support for lists (we only have dicts now)
# TODO: Probably should make this whole function recursive... though
# we only really have one level of nesting now.
# TODO: most of this function is duplicated from Channel#update_options
# (including the TODOs above), could be a lot DRYer
for key, val in options.items():
if key not in self._options:
raise ValueError("Invalid keyword argument %s" % (key))
default = self._options[key]
cls = default.__class__
if cls in (unicode, str):
cls = basestring
if not isinstance(val, cls):
raise ValueError("Invalid type for %s (should be %s)" % (key, default.__class__))
if key == 'state':
self.state_replace(val)
continue
if isinstance(val, dict):
for _key, _val in val.items():
if _key not in self._options[key]:
raise ValueError("Invalid keyword argument %s" % (_key))
default = self._options[key][_key]
cls = default.__class__
if isinstance(default, float) and isinstance(_val, int):
_val = float(_val)
if cls in (unicode, str):
cls = basestring
if not isinstance(_val, cls):
raise ValueError("%s is Invalid type for %s (should be %s)" % (_val, _key, default.__class__))
# two loops forces exception *before* any of the options are set.
for key, val in options.items():
# this should create copies of any dicts or lists that are options
if isinstance(val, dict) and hasattr(self, key):
getattr(self, key).update(val)
else:
setattr(self, key, val.__class__(val))

def add_connection(self, conn):
self.connections.append(conn)
Expand All @@ -36,29 +87,43 @@ def _send_initial_subscriptions(self, conn):

def remove_connection(self, conn):
self.connections.remove(conn)

# Remove the connection from the channels it was subscribed to,
# unsubscribing the user from any channels which they no longer
# have open connections to
for (channel, channel_connections) in self.channels.items():
if conn not in channel_connections:
continue
self.channels[channel].remove(conn)
if not self.channels[channel] and self.per_connection_subscriptions:
channel.unsubscribe(self, needs_auth=True, force_auth=True)

if not self.connections:
# each call to user_disconnected might result in an immediate call
# to self.channel_unsubscribed, thus modifying self.channels and
# messing up our loop. So we loop over a copy of self.channels...

for channel in self.channels[:]:
channel.user_disconnected(self)
# print 'tell server to remove user...'
for (channel, connections) in self.channels.items():
channel.unsubscribe(self, needs_auth=True, force_auth=True)
# so the disconnect callback has a cookie
self._temp_cookie = conn.get_cookie()
self.server.remove_user(self.name)

def channel_subscribed(self, channel):
self.channels.append(channel)
def channel_subscribed(self, channel, conn=None):
if channel not in self.channels:
self.channels[channel] = [ conn ]
elif conn not in self.channels[channel]:
self.channels[channel].append(conn)

def channel_unsubscribed(self, channel):
self.channels.remove(channel)
if channel in self.channels:
del self.channels[channel]

def get_name(self):
return self.name

def send_frame(self, name, args={}, omit=None):
for conn in self.connections:
def send_frame(self, name, args={}, omit=None, channel=None):
if not self.per_connection_subscriptions:
channel = None
if channel and channel not in self.channels:
return
for conn in (self.channels[channel] if channel else self.connections):
if conn is not omit:
conn.send_frame(name, args)

Expand All @@ -68,23 +133,29 @@ def get_cookie(self, conn=None):

return self._temp_cookie or ""

def send_message(self, recipient, payload, conn=None, needs_auth=True):
def send_message(self, recipient_name, payload, conn=None, needs_auth=True):
try:
encoded_payload = json.loads(payload)
except:
raise ExpectedException("Invalid json for payload")
payload = encoded_payload
if needs_auth:
form = { 'sender': self.get_name(), 'recipient': recipient.get_name(), 'payload': json.dumps(payload) }
if needs_auth and self.moderated_message:
form = { 'sender': self.get_name(), 'recipient': recipient_name, 'recipient_exists': self.server.exists_user(recipient_name), 'payload': json.dumps(payload) }
success, options = self.server.http_request('message', self.get_cookie(conn), form, conn=conn)
self.server.maybe_auto_subscribe(self, options, conn=conn)
if not success:
raise ExpectedException(options.get('error', 'Unauthorized'))
payload = options.get('override_payload', payload)
recipient_name = options.get('override_recipient_name', recipient_name)
elif not self.server.exists_user(recipient_name):
raise ExpectedException('Invalid user name')

recipient = self.server.get_user(recipient_name) if self.server.exists_user(recipient_name) else None

frame = {"sender": self.get_name(), "recipient": recipient.get_name(), "payload": payload, "datetime": get_now()}
recipient.send_frame('MESSAGE', frame)
if recipient.name != self.name:
frame = {"sender": self.get_name(), "recipient": recipient.get_name() if recipient else "null", "payload": payload, "datetime": get_now()}
if recipient:
recipient.send_frame('MESSAGE', frame)
if self.reflective and (not recipient or recipient.name != self.name):
self.send_frame('MESSAGE', frame)