Example #1
0
    def __init__(self, username, passhash):
        self.username = username
        self.passhash = passhash

        self.user_id = None
        self.rooms_by_id = {}
        self.rooms_by_name = {}
        self.sub_ids = {}

        self.client = DDPClient(RC_URL, debug=DEBUG)

        self.connected_handlers = {}
        self.changed_handlers = {
            "stream-room-messages": self.on_stream_room_messages
        }
        self.added_handlers = {
            "rocketchat_subscription": self.on_rocketchat_subscription,
            "stream-room-messages": self.on_stream_room_messages
        }

        self.client.on("connected", self.login)
        self.client.on("changed", self.on_changed)
        self.client.on("added", self.on_added)

        self.sent_messages = []
        self.client.connect()
Example #2
0
 def _start_connect(self):
     up = parse.urlparse(settings.ROCKETCHAT_URL)
     if up.scheme == 'http':
         ws_proto = 'ws'
     else:
         ws_proto = 'wss'
     self.rc = DDPClient('{}://{}/websocket'.format(ws_proto, up.netloc), auto_reconnect=True, auto_reconnect_timeout=1)
     self.rc.on('connected', self._realtime_login)
     self.rc.on('changed', self._changed_callback)
     self.rc.connect()
Example #3
0
 def __init__(self, url, auto_reconnect=True, auto_reconnect_timeout=0.5, debug=False):
     EventEmitter.__init__(self)
     self.collection_data = CollectionData()
     self.ddp_client = DDPClient(url, auto_reconnect=auto_reconnect,
                                 auto_reconnect_timeout=auto_reconnect_timeout, debug=debug)
     self.ddp_client.on('connected', self.connected)
     self.ddp_client.on('socket_closed', self.closed)
     self.ddp_client.on('failed', self.failed)
     self.ddp_client.on('added', self.added)
     self.ddp_client.on('changed', self.changed)
     self.ddp_client.on('removed', self.removed)
     self.ddp_client.on('reconnected', self._reconnected)
     self.connected = False
     self.subscriptions = {}
     self._login_data = None
class DDPCiaoClient:
    def __init__(self, ddp_params, ciao_queue):
        # validate params - START
        missing_params = []
        required_params = ["host", "port"]
        for p in required_params:
            if not p in ddp_params:
                missing_params.append(p)

        if len(missing_params) > 0:
            raise RuntimeError("DDP configuration error, missing: %s" % ",".join(missing_params))

            # validate params - END

            # reference to Queue for exchanging data with CiaoCore
        self.ciao_queue = ciao_queue

        # local instance of DDP Client
        self.handle = DDPClient("ws://" + ddp_params["host"] + ":" + str(ddp_params["port"]) + "/websocket")
        self.handle.on("connected", self.on_connect)

        self.host = ddp_params["host"]
        self.port = ddp_params["port"]

        self.logger = logging.getLogger("ddp.client")

    def on_connect():
        self.logger.info("Connected to DDP Server %s" % self.host)

    """
	TODO from METEOR to SKETCH
	def on_message(self, client, userdata, msg):
		self.logger.debug("Got new message. Topic: %s Message: %s" % (str(msg.topic), str(msg.payload)))
		entry = {
			"data" : [str(msg.topic), str(msg.payload)]
		}
		self.ciao_queue.put(entry)
	"""

    def connect(self):
        try:
            self.handle.connect()
            return True
        except:
            return False

    def disconnect(self):
        self.handle.close()

    def callback_function(a, b):
        self.logger.debug("Callback Function. Param1: %s Param2: %s" % (str(a), str(b)))

    def publish(self, function, parameter):
        self.logger.debug("Call function: %s Parameter: %s" % (function, str(parameter)))
        try:
            self.handle.call(function, json.loads(parameter), None)
        except Exception, e:
            self.logger.error("Failed handle call: " + str(e))
Example #5
0
    def __init__(self, username, passhash):
        self.username = username
        self.passhash = passhash

        self.user_id = None
        self.rooms_by_id = {}
        self.rooms_by_name = {}
        self.sub_ids = {}

        self.client = DDPClient(RC_URL, debug=DEBUG)

        self.connected_handlers = {}
        self.changed_handlers = {
            "stream-room-messages": self.on_stream_room_messages
        }
        self.added_handlers = {
            "rocketchat_subscription": self.on_rocketchat_subscription,
            "stream-room-messages": self.on_stream_room_messages
        }

        self.client.on("connected", self.login)
        self.client.on("changed", self.on_changed)
        self.client.on("added", self.on_added)

        self.sent_messages = []
        self.client.connect()
Example #6
0
    def __init__(self, ddp_endpoint):
        Cmd.__init__(self)
        self.ddp_client = DDPClient('ws://' + ddp_endpoint + '/websocket')

        self.subscriptions = {}
        self.pending_results = {}

        # Showing a fancy prompt string if we're interactive
        if sys.stdin.isatty():
            self.prompt = ddp_endpoint + '> '
        else:
            self.prompt = ''

        self.ddp_client.on('connected', self.connected)
        self.ddp_client.on('socket_closed', self.closed)
        self.ddp_client.on('failed', self.failed)
        self.ddp_client.on('added', self.added)
        self.ddp_client.on('changed', self.changed)
        self.ddp_client.on('removed', self.removed)
        self.ddp_client.connect()
Example #7
0
    def ddp_connect(self):
        print '++ DDP connection attempt...'
        self.ddp_client = DDPClient(self.server_url)
        self.ddp_client.debug = self.debug

        # Some events to handle from DDP
        self.ddp_client.on('added', self.added)
        self.ddp_client.on('connected', self.connected)
        self.ddp_client.on('socket_closed', self.closed)
        self.ddp_client.on('failed', self.failed)

        # Connect to DDP server
        self.ddp_client.connect()
Example #8
0
 def __init__(self, url, auto_reconnect=True, auto_reconnect_timeout=0.5, debug=False):
     EventEmitter.__init__(self)
     self.collection_data = CollectionData()
     self.ddp_client = DDPClient(url, auto_reconnect=auto_reconnect,
                                 auto_reconnect_timeout=auto_reconnect_timeout, debug=debug)
     self.ddp_client.on('connected', self.connected)
     self.ddp_client.on('socket_closed', self.closed)
     self.ddp_client.on('failed', self.failed)
     self.ddp_client.on('added', self.added)
     self.ddp_client.on('changed', self.changed)
     self.ddp_client.on('removed', self.removed)
     self.ddp_client.on('reconnected', self._reconnected)
     self.connected = False
     self.subscriptions = {}
     self._login_data = None
    def __init__(self, ddp_endpoint):
        Cmd.__init__(self)
        self.ddp_client = DDPClient(
            'ws://' + ddp_endpoint + '/websocket'
        )

        self.subscriptions = {}
        self.pending_results = {}

        # Showing a fancy prompt string if we're interactive
        if sys.stdin.isatty():
            self.prompt = ddp_endpoint + '> '
        else:
            self.prompt = ''

        self.ddp_client.on('connected', self.connected)
        self.ddp_client.on('socket_closed', self.closed)
        self.ddp_client.on('failed', self.failed)
        self.ddp_client.on('added', self.added)
        self.ddp_client.on('changed', self.changed)
        self.ddp_client.on('removed', self.removed)
        self.ddp_client.connect()
    def __init__(self, ddp_params, ciao_queue):
        # validate params - START
        missing_params = []
        required_params = ["host", "port"]
        for p in required_params:
            if not p in ddp_params:
                missing_params.append(p)

        if len(missing_params) > 0:
            raise RuntimeError("DDP configuration error, missing: %s" % ",".join(missing_params))

            # validate params - END

            # reference to Queue for exchanging data with CiaoCore
        self.ciao_queue = ciao_queue

        # local instance of DDP Client
        self.handle = DDPClient("ws://" + ddp_params["host"] + ":" + str(ddp_params["port"]) + "/websocket")
        self.handle.on("connected", self.on_connect)

        self.host = ddp_params["host"]
        self.port = ddp_params["port"]

        self.logger = logging.getLogger("ddp.client")
class App(Cmd):
    """Main input loop"""
    def __init__(self, ddp_endpoint):
        Cmd.__init__(self)
        self.ddp_client = DDPClient(
            'ws://' + ddp_endpoint + '/websocket'
        )

        self.subscriptions = {}
        self.pending_results = {}

        # Showing a fancy prompt string if we're interactive
        if sys.stdin.isatty():
            self.prompt = ddp_endpoint + '> '
        else:
            self.prompt = ''

        self.ddp_client.on('connected', self.connected)
        self.ddp_client.on('socket_closed', self.closed)
        self.ddp_client.on('failed', self.failed)
        self.ddp_client.on('added', self.added)
        self.ddp_client.on('changed', self.changed)
        self.ddp_client.on('removed', self.removed)
        self.ddp_client.connect()

    def subscribe(self, name, params):
        if name in self.subscriptions:
            self._log('* ALREADY SUBSCRIBED - {}'.format(name))
            return
        sub_id = self.ddp_client.subscribe(name, params, self.subscribed)
        self.subscriptions[name] = sub_id

    def unsubscribe(self, name):
        if name not in self.subscriptions:
            self._log('* NOT SUBSCRIBED - {}'.format(name))
            return
        self.ddp_client.unsubscribe(self.subscriptions[name])
        del self.subscriptions[name]
        self._log('* UNSUBSCRIBED - {}'.format(name))

    def call(self, method_name, params):
        self.ddp_client.call(method_name, params, self.result)

    #
    # Callbacks
    #

    def added(self, collection, id, fields):
        self._log('* ADDED {} {}'.format(collection, id), True)
        for key, value in fields.items():
            self._log('  - FIELD {} {}'.format(key, value))


    def changed(self, collection, id, fields, cleared):
        self._log('* CHANGED {} {}'.format(collection, id), True)
        for key, value in fields.items():
            self._log('  - FIELD {} {}'.format(key, value))
        for key, value in cleared.items():
            self._log('  - CLEARED {} {}'.format(key, value))

    def removed(self, collection, id):
        self._log('* REMOVED {} {}'.format(collection, id), True)

    def subscribed(self, error, sub_id):
        if error:
            self._remove_sub_by_id(sub_id)
            self._log('* SUBSCRIPTION FAILED - {}'.format(error.get('reason')), True)
            return
        self._log('* READY', True)

    def result(self, error, result):
        if error:
            self._log('* METHOD FAILED - {}'.format(error.get('reason')), True)
            return
        self._log('* METHOD RESULT - {}'.format(str(result)), True)

    def connected(self):
        self._log('* CONNECTED', True)

    def closed(self, code, reason):
        """Called when the connection is closed"""
        self._log('* CONNECTION CLOSED {} {}'.format(code, reason))

    def failed(self, data):
        self._log('* FAILED - data: {}'.format(str(data)))

    #
    # Commands 
    #

    def do_sub(self, params):
        """The `sub` command"""
        try:
            sub_name, sub_params = self._parse_command(params)
        except ValueError:
            self._log('Error parsing parameter list - try `help sub`')
            return

        self.subscribe(sub_name, sub_params)

    def do_unsub(self, params):
        """The `unsub` command"""
        try:
            sub_name, sub_params = self._parse_command(params)
        except ValueError:
            self._log('Error parsing parameter list - try `help unsub`')
            return

        self.unsubscribe(sub_name)

    def do_call(self, params):
        """The `call` command"""
        try:
            method, params = self._parse_command(params)
        except ValueError:
            self._log('Error parsing parameter list - try `help call`')
            return

        self.call(method, params)

    def do_login(self, params):
        """The `login` command"""
        self._log('Not Implemented Yet -- SRP is in progress')
        self._log('However you can still calling the method `login` (WARNING: password sent in plaintext)')
        #self.ddp_client._login({'username': '******'}, 'password123')

    def do_EOF(self, line):
        """The `EOF` "command"

        It's here to support `cat file | python ddpclient.py`
        """
        return True

    def do_help(self, line):
        """The `help` command"""

        msgs = {
            'call': (
                'call <method name> <json array of parameters>\n'
                '  Calls a remote method\n'
                '  Example: call vote ["foo.meteor.com"]'),
            'sub': (
                'sub <subscription name> [<json array of parameters>]\n'
                '  Subscribes to a remote dataset\n'
                '  Examples: `sub allApps` or `sub myApp '
                '["foo.meteor.com"]`'),
            'unsub': (
                'unsub <subscription name>\n'
                '  Unsubscribes from a remote dataset\n'
                '  Examples: `unsub allApps` or `unsub myApp '
                '["foo.meteor.com"]`'),
        }

        line = line.strip()
        if line and line in msgs:
            return self._log('\n' + msgs[line])

        for msg in msgs.values():
            self._log('\n' + msg)

    #
    # Utils
    #

    def _log(self, msg, server_msg=False):
        if server_msg:
            sys.stderr.write('\n')
        sys.stderr.write('{}\n'.format(msg))
        if server_msg:
            sys.stderr.write('{}'.format(self.prompt))

    def _parse_command(self, params):
        """Parses a command with a first string param and a second
        json-encoded param"""
        name, args = (params + ' ').split(' ', 1)
        return name, args and json.loads(args) or []

    def _remove_sub_by_id(self, sub_id):
        for name, cur_sub_id in self.subscriptions.items():
            if cur_sub_id == sub_id:
                del self.subscriptions[name]
def main(url):
    character = {}
    client = DDPClient('ws://dicecloud.com/websocket', auto_reconnect=False)
    client.is_connected = False
    client.connect()

    def connected():
        client.is_connected = True

    client.on('connected', connected)
    while not client.is_connected:
        time.sleep(1)
    client.subscribe('singleCharacter', [url])

    def update_character(collection, _id, fields):
        if character.get(collection) is None:
            character[collection] = []
        fields['id'] = _id
        character.get(collection).append(fields)

    client.on('added', update_character)
    time.sleep(10)
    client.close()
    character['id'] = url
    return character
Example #13
0
class User:
    def __init__(self, username, passhash):
        self.username = username
        self.passhash = passhash

        self.user_id = None
        self.rooms_by_id = {}
        self.rooms_by_name = {}
        self.sub_ids = {}

        self.client = DDPClient(RC_URL, debug=DEBUG)

        self.connected_handlers = {}
        self.changed_handlers = {
            "stream-room-messages": self.on_stream_room_messages
        }
        self.added_handlers = {
            "rocketchat_subscription": self.on_rocketchat_subscription,
            "stream-room-messages": self.on_stream_room_messages
        }

        self.client.on("connected", self.login)
        self.client.on("changed", self.on_changed)
        self.client.on("added", self.on_added)

        self.sent_messages = []
        self.client.connect()

    def login(self):
        self.client.call(
            "login",
            [{
                "user": {"username": self.username},
                "password": {
                    "digest": self.passhash,
                    "algorithm": "sha-256"
                }
            }],
            callback=self.login_callback
        )

    def login_callback(self, u1, user_data):
        print(user_data)
        self.user_id = user_data["id"]
        self.sub_ids["subscription"] = self.client.subscribe("subscription", [])
        # self.join_room("GENERAL", "c")

    def join_room(self, room_id, room_type):
        self.sub_ids[room_id] = {}
        room = None
        while room is None:
            try:
                room = self.rooms_by_id[room_id]
            except KeyError:
                print(self.rooms_by_id)
                time.sleep(1)

        self.sub_ids[room_id]["room"] = self.client.subscribe("room", [room["type"]+room["name"]], callback=print)
        self.sub_ids[room_id]["stream-room-messages"] = self.client.subscribe("stream-room-messages", [room_id, True])

    def on_stream_room_messages(self, collection, id, fields, *args):
        if "args" not in fields:
            return

        data = fields["args"][0]
        user = data["u"]
        if user["_id"] != self.user_id:
            return

        # print(data)
        if "t" in data and data["t"] == "rm":
            self.on_message_removed(data)
        elif "editedAt" in data:
            self.on_message_edited(data)
        elif data["msg"] != "":
            #ignore messages i sent from this client
            if data["_id"] in self.sent_messages:
                return
            self.on_message(data)
        else:
            print("Got unhandled message.")


    def on_changed(self, collection, id, fields, u1):
        if collection in self.changed_handlers:
            try:
                self.changed_handlers[collection](collection, id, fields, u1)
            except Exception as e:
                print("UNHANDLED EXCEPTION:", e)
        else:
            print("unhandled: ", collection, fields)

    def on_added(self, collection, id, fields):
        if collection in self.added_handlers:
            try:
                self.added_handlers[collection](collection, id, fields)
            except Exception as e:
                print("UNHANDLED EXCEPTION:", e)
        else:
            print("unhandled:", collection, fields)

    def on_rocketchat_subscription(self, collection, id, fields):
        data = fields
        room = {
            "name": data["name"],
            "rid": data["rid"],
            "type": data["t"]
        }
        self.rooms_by_id[room["rid"]] = room
        self.rooms_by_name[room["name"]] = room
        self.join_room(room["rid"], room["type"])
        return

    def on_message_removed(self, data):
        print("My message was deleted!")
        print("The message %s was deleted by %s" % (data["_id"], data["editedBy"]["username"]))

    def on_message_edited(self, data):
        print("My message was edited!")
        print("The message %s was edited by %s and now reads '%s'" % (data["_id"], data["editedBy"]["username"], data["msg"]))

    def on_message(self, data):
        print("I sent a message!")
        print("'%s'" % data["msg"])


    def send_message(self, room_id, msg):
        msg_id = make_id()
        print("attempting to send message with id '%s' and text '%s'" % (msg_id, msg))
        self.sent_messages.append(msg_id)
        self.client.call(
            "sendMessage",
            [{
                "_id": msg_id,
                "rid": room_id,
                "msg": msg
            }]
        )
Example #14
0
class RocketChatBackend(IOBackend, StorageMixin):
    friendly_name = "RocketChat"
    internal_name = "will.backends.io_adapters.rocketchat"
    required_settings = [
        {
            "name":
            "ROCKETCHAT_USERNAME",
            "obtain_at":
            """1. Go to your rocket.chat instance (i.e. your-name.rocket.chat)
2. Create a new normal account for Will.
3. Set this value to the username, just like you'd use to log in with it.""",
        },
        {
            "name":
            "ROCKETCHAT_PASSWORD",
            "obtain_at":
            """1. Go to your rocket.chat instance (i.e. your-name.rocket.chat)
2. Create a new normal account for Will, and note the password you use.
3. Set this value to that password, just like you'd use to log in with it.""",
        },
        {
            "name":
            "ROCKETCHAT_URL",
            "obtain_at":
            ("This is your rocket.chat url - typically either your-name.rocket.chat for "
             "Rocket.Chat cloud, or something like http://localhost:3000 for local installations."
             ),
        },
    ]

    pp = pprint.PrettyPrinter(indent=4)

    def normalize_incoming_event(self, event):
        logging.info('Normalizing incoming Rocket.Chat event')
        logging.debug('event: {}'.format(self.pp.pformat(event)))
        if event["type"] == "message":

            # Were we mentioned?
            will_is_mentioned = False
            for mention in event['mentions']:
                if mention['username'] == self.me.handle:
                    will_is_mentioned = True
                    break

            # Handle direct messages, which in Rocket.Chat are a rid
            # made up of both users' _ids.
            is_private_chat = False
            if self.me.id in event["rid"]:
                is_private_chat = True

                # Create a "Channel" to align with Rocket.Chat DM
                # paradigm. There might well be a better way of doing
                # this. See TODO in _rest_channels_list.
                sender_id = event['u']['_id']
                ids = [sender_id, self.me.id]
                ids.sort()
                channel_id = '{}{}'.format(*ids)
                sender = self.people[sender_id]
                channel_members = {}
                channel_members[sender_id] = sender
                channel_members[self.me.id] = self.me
                channel = Channel(id=channel_id,
                                  name=channel_id,
                                  source=clean_for_pickling(channel_id),
                                  members=channel_members)
            else:
                if "rid" in event and event["rid"] in self.channels:
                    channel = clean_for_pickling(self.channels[event["rid"]])
                else:
                    # Private channel, unknown members.  Just do our best and try to route it.
                    if "rid" in event:
                        channel = Channel(id=event["rid"],
                                          name=event["rid"],
                                          source=clean_for_pickling(
                                              event["rid"]),
                                          members={})
            logging.debug('channel: {}'.format(channel))

            # Set various variables depending on whether @handle was
            # part of the message.
            interpolated_handle = "@%s " % self.handle
            logging.debug(
                'interpolated_handle: {}'.format(interpolated_handle))
            is_direct = False
            if is_private_chat or event['msg'].startswith(interpolated_handle):
                is_direct = True

            # Strip my handle from the start. NB Won't strip it from
            # elsewhere in the text, and won't strip other mentions.
            # This will stop regexes from working, not sure if it's a
            # feature or a bug.
            if event['msg'].startswith(interpolated_handle):
                event['msg'] = event['msg'][len(interpolated_handle):].strip()

            if interpolated_handle in event['msg']:
                will_is_mentioned = True

            # Determine if Will said it.
            logging.debug('self.people: {}'.format(self.pp.pformat(
                self.people)))
            sender = self.people[event['u']['_id']]
            logging.debug('sender: {}'.format(sender))
            if sender['handle'] == self.me.handle:
                logging.debug('Will said it')
                will_said_it = True
            else:
                logging.debug('Will didnt say it')
                will_said_it = False

            m = Message(content=event['msg'],
                        type=event.type,
                        is_direct=is_direct,
                        is_private_chat=is_private_chat,
                        is_group_chat=not is_private_chat,
                        backend=self.internal_name,
                        sender=sender,
                        channel=channel,
                        will_is_mentioned=will_is_mentioned,
                        will_said_it=will_said_it,
                        backend_supports_acl=True,
                        original_incoming_event=clean_for_pickling(event))
            return m
        else:
            logging.debug(
                'Passing, I dont know how to normalize this event of type ',
                event["type"])
            pass

    def handle_outgoing_event(self, event):
        # Print any replies.
        logging.info('Handling outgoing Rocket.Chat event')
        logging.debug('event: {}'.format(self.pp.pformat(event)))

        if event.type in ["say", "reply"]:

            if "kwargs" in event and "html" in event.kwargs and event.kwargs[
                    "html"]:
                event.content = html2text.html2text(event.content)

            self.send_message(event)
            if hasattr(event, "source_message") and event.source_message:
                pass
            else:
                # Backend needs to provide ways to handle and properly route:
                # 1. 1-1 messages
                # 2. Group (channel) messages
                # 3. Ad-hoc group messages (if they exist)
                # 4. Messages that have a channel/room explicitly specified that's different than
                #    where they came from.
                # 5. Messages without a channel (Fallback to ROCKETCHAT_DEFAULT_CHANNEL) (messages that don't have a room )
                kwargs = {}
                if "kwargs" in event:
                    kwargs.update(**event.kwargs)

        if event.type in [
                "topic_change",
        ]:
            self.set_topic(event.content)
        elif (event.type == "message.no_response" and event.data.is_direct
              and event.data.will_said_it is False):
            event.content = random.choice(UNSURE_REPLIES)
            self.send_message(event)

    def set_topic(self, event):
        logging.warn(
            "Rocket.Chat doesn't support topics yet: https://github.com/RocketChat/Rocket.Chat/issues/328"
        )
        event.content(
            "Hm. Looks like Rocket.Chat doesn't support topics yet: https://github.com/RocketChat/Rocket.Chat/issues/328"
        )
        self.send_message(event)

    def send_message(self, event):
        logging.info('Sending message to Rocket.Chat')
        logging.debug('event: {}'.format(self.pp.pformat(event)))
        data = {}
        if hasattr(event, "kwargs"):
            logging.debug('event.kwargs: {}'.format(event.kwargs))
            data.update(event.kwargs)

            # TODO: Go through the possible attachment parameters at
            # https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage
            # - this is a bare minimum inspired by slack.py
            if 'color' in event.kwargs:
                data.update({
                    "attachments": [{
                        'color': event.kwargs["color"],
                        'text': event.content,
                    }],
                })
            else:
                data.update({
                    'text': event.content,
                })
        else:
            # I haven't seen this yet, not sure when it's relevant.
            # 'text' was wrongly set to 'msg' and nothing blew up. ;)
            logging.debug("event doesn't have kwargs")
            data.update({
                'text': event.content,
            })

        if "source_message" in event:
            if hasattr(event.source_message, "data"):
                data['roomId'] = event.source_message.data.channel.id
            else:
                data['roomId'] = event.source_message.channel.id
        else:
            data['roomId'] = event.data['source'].data.channel.id

        self._rest_post_message(data)

    def _get_rest_metadata(self):
        self._rest_users_list()
        self._rest_channels_list()

    def _get_realtime_metadata(self):
        self._realtime_get_rooms()

    # REST API functions, documented at
    # https://rocket.chat/docs/developer-guides/rest-api/

    def _rest_login(self):
        params = {
            'username': settings.ROCKETCHAT_USERNAME,
            'password': settings.ROCKETCHAT_PASSWORD
        }
        r = requests.post('{}login'.format(self.rocketchat_api_url),
                          data=params)
        resp_json = r.json()
        self._token = resp_json['data']['authToken']
        self.save("WILL_ROCKETCHAT_TOKEN", self._token)
        self._userid = resp_json['data']['userId']
        self.save("WILL_ROCKETCHAT_USERID", self._userid)

    def _rest_users_list(self):
        logging.debug('Getting users list from Rocket.Chat')

        # Remember to paginate. ;)
        count = 50
        passes = 0
        headers = {'X-Auth-Token': self.token, 'X-User-Id': self.userid}
        fetched = 0
        total = 0

        self.handle = settings.ROCKETCHAT_USERNAME
        self.mention_handle = "@%s" % settings.ROCKETCHAT_USERNAME

        people = {}

        while fetched <= total:
            params = {'count': count, 'offset': fetched}
            r = requests.get('{}users.list'.format(self.rocketchat_api_url),
                             headers=headers,
                             params=params)
            resp_json = r.json()
            if resp_json['success'] is False:
                logging.exception('resp_json: {}'.format(resp_json))
            total = resp_json['total']

            for user in resp_json['users']:
                # TODO: Unlike slack.py, no timezone support at present.
                # RC returns utcOffset, but this isn't enough to
                # determine timezone.
                # TODO: Pickle error if timezone set to UTC, and I didn't
                # have a chance to report it. Using GMT as a poor substitute.
                person = Person(id=user['_id'],
                                handle=user['username'],
                                mention_handle="@%s" % user["username"],
                                source=clean_for_pickling(user)['username'],
                                name=user['name'],
                                timezone='GMT')

                people[user['_id']] = person
                if user['username'] == self.handle:
                    self.me = person

            passes += 1
            fetched = count * passes

        self.people = people

    def _get_userid_from_username(self, username):
        if username is None:
            raise TypeError("No username given")

        for id, data in self.people.items():
            if data['handle'] == username:
                return id

    def _rest_channels_list(self):
        logging.debug('Getting channel list from Rocket.Chat')

        # Remember to paginate. ;)
        count = 50
        passes = 0
        headers = {'X-Auth-Token': self.token, 'X-User-Id': self.userid}
        fetched = 0
        total = 0

        channels = {}

        while fetched <= total:
            r = requests.get('{}channels.list'.format(self.rocketchat_api_url),
                             headers=headers)
            resp_json = r.json()

            total = resp_json['total']

            for channel in resp_json['channels']:
                members = {}
                for username in channel['usernames']:
                    userid = self._get_userid_from_username(username)
                    members[userid] = self.people[userid]

                channels[channel['_id']] = Channel(
                    id=channel['_id'],
                    name=channel['name'],
                    source=clean_for_pickling(channel),
                    members=members)

            passes += 1
            fetched = count * passes

        self.channels = channels

    def _rest_post_message(self, data):
        logging.info('Posting message to Rocket.Chat REST API')
        logging.debug('data: {}'.format(data))
        headers = {'X-Auth-Token': self.token, 'X-User-Id': self.userid}
        logging.debug('headers: {}'.format(headers))
        r = requests.post(
            '{}chat.postMessage'.format(self.rocketchat_api_url),
            headers=headers,
            data=data,
        )
        resp_json = r.json()

        # TODO: Necessary / useful to check return codes?
        if not 'success' in resp_json:
            logging.debug('resp_json: {}'.format(resp_json))
            assert resp_json['success']

    # Realtime API functions, documented at
    # https://rocket.chat/docs/developer-guides/realtime-api/

    def _start_connect(self):
        up = parse.urlparse(settings.ROCKETCHAT_URL)
        if up.scheme == 'http':
            ws_proto = 'ws'
        else:
            ws_proto = 'wss'
        self.rc = DDPClient('{}://{}/websocket'.format(ws_proto, up.netloc),
                            auto_reconnect=True,
                            auto_reconnect_timeout=1)
        self.rc.on('connected', self._realtime_login)
        self.rc.on('changed', self._changed_callback)
        self.rc.connect()

    def _realtime_login(self):
        params = [{
            'user': {
                'username': settings.ROCKETCHAT_USERNAME
            },
            'password': settings.ROCKETCHAT_PASSWORD
        }]
        self.rc.call('login', params, self._login_callback)

    def _login_callback(self, error, result):
        logging.debug('_login_callback')
        if error:
            logging.exception('error: {}'.format(error))
            return

        logging.debug('result: {}'.format(result))
        logging.debug('self.token: {}'.format(self.token))
        logging.debug('self.userid: {}'.format(self.userid))

        # Use dummy to make it a Thread, otherwise DDP events don't
        # get back to the right place. If there is a real need to make
        # it a real Process, it is probably just a matter of using
        # multiprocessing.Value(s) in the right place(s).
        # TODO: Could this be the reason for the 100% CPU usage?
        # Have asked in #development.
        self.update_thread = Process(target=self._get_updates)
        self.update_thread.start()

    def _changed_callback(self, collection, _id, fields, cleared):
        logging.debug('_changed_callback')
        logging.debug('collection: {}'.format(collection))
        logging.debug('id: {}'.format(_id))
        logging.debug('fields: {}'.format(self.pp.pformat(fields)))
        logging.debug('cleared: {}'.format(cleared))
        event = Event(type='message', version=1, **fields['args'][0])
        self.handle_incoming_event(event)

    def _stream_room_message_callback(self, error, event):
        logging.debug('_stream_room_message_callback')
        if error:
            logging.exception('error: {}'.format(error))
            return

    @property
    def token(self):
        if not hasattr(self, "_token") or not self._token:
            self._token = self.load("WILL_ROCKETCHAT_TOKEN", None)
            if not self._token:
                self._rest_login()
        return self._token

    @property
    def userid(self):
        if not hasattr(self, "_userid") or not self._userid:
            self._userid = self.load("WILL_ROCKETCHAT_USERID", None)
            if not self._userid:
                self._rest_login()
        return self._userid

    @property
    def rocketchat_api_url(self):
        if settings.ROCKETCHAT_URL.endswith("/"):
            return settings.ROCKETCHAT_URL + 'api/v1/'
        else:
            return settings.ROCKETCHAT_URL + '/api/v1/'

    # Gets updates from REST and Realtime APIs.
    def _get_updates(self):
        try:
            polling_interval_seconds = 5
            self._get_rest_metadata()

            while True:
                # Update channels/people/me/etc.
                self._get_rest_metadata()
                self._get_realtime_metadata()

                time.sleep(polling_interval_seconds)
        except (KeyboardInterrupt, SystemExit):
            pass
        except:
            logging.critical("Error in watching RocketChat API: \n%s" %
                             traceback.format_exc())

    # Use this to get a list of all rooms that we are in.
    # https://rocket.chat/docs/developer-guides/realtime-api/the-room-object
    def _realtime_get_rooms(self):
        params = [{'$date': 0}]
        self.rc.call('rooms/get', params, self._get_rooms_callback)

    def _get_rooms_callback(self, error, result):
        logging.debug('_get_rooms_callback')
        if error:
            logging.exception('_get_rooms_callback error: {}'.format(error))
            return

        # TODO: When we leave a room, we don't delete it from
        # self.subscribed_rooms. Not a problem in practice -
        # subscriptions to the room won't fire, but messy.
        for room in result:
            logging.debug('room: {}'.format(room))
            if room['_id'] not in self.subscribed_rooms:
                self.rc.subscribe('stream-room-messages', [room['_id']],
                                  self._stream_room_message_callback)
            self.subscribed_rooms[room['_id']] = True

    def bootstrap(self):
        # Bootstrap must provide a way to to have:
        # a) self.normalize_incoming_event fired, or incoming events put into self.incoming_queue
        # b) any necessary threads running for a)
        # c) self.me (Person) defined, with Will's info
        # d) self.people (dict of People) defined, with everyone in an organization/backend
        # e) self.channels (dict of Channels) defined, with all available channels/rooms.
        #    Note that Channel asks for users, a list of People.
        # f) A way for self.handle, self.me, self.people, and self.channels to be kept accurate,
        #    with a maximum lag of 60 seconds.

        self.subscribed_rooms = {}

        # Gets and stores token and ID.
        self._rest_login()
        # Kicks off listeners and REST room polling.
        self._start_connect()
Example #15
0
from DDPClient import DDPClient
import time
from random import randint

client = DDPClient("ws://vestee.herokuapp.com/websocket")
client.connect()

while True:
  # api = "a72cc00489d7acda461724ae01ab2d"
  api = "01a66de93638c40cd87786cf2c9647"
  client.call('add_data', [api, 1, 'Temperatura zewnetrzna', '*C', randint(20,30)], [])
  client.call('add_data', [api, 2, 'Temperatura piwka', '*C', randint(10, 20)], [])
  client.call('add_data', [api, 3, 'Wilgotnosc', '%', randint(20,80)], [])

  time.sleep(2)
Example #16
0
class MeteorClient(EventEmitter):
    def __init__(self, url, auto_reconnect=True, auto_reconnect_timeout=0.5, debug=False, headers=None):
        EventEmitter.__init__(self)
        self.collection_data = CollectionData()
        self.ddp_client = DDPClient(url, auto_reconnect=auto_reconnect,
                                    auto_reconnect_timeout=auto_reconnect_timeout, debug=debug, headers=headers)
        self.ddp_client.on('connected', self.connected)
        self.ddp_client.on('socket_closed', self.closed)
        self.ddp_client.on('failed', self.failed)
        self.ddp_client.on('added', self.added)
        self.ddp_client.on('changed', self.changed)
        self.ddp_client.on('removed', self.removed)
        self.ddp_client.on('reconnected', self._reconnected)
        self.connected = False
        self.subscriptions = {}
        self._login_data = None
        self._login_token = None

    def connect(self):
        """Connect to the meteor server"""
        self.ddp_client.connect()

    def close(self):
        """Close connection with meteor server"""
        self.ddp_client.close()

    def _reconnected(self):
        """Reconnect

        Currently we get a new session every time so we have
        to clear all the data an resubscribe"""

        if self._login_data or self._login_token:

            def reconnect_login_callback(error, result):
                if error:
                    if self._login_token:
                        self._login_token = None
                        self._login(self._login_data,
                                    callback=reconnect_login_callback)
                        return
                    else:
                        raise MeteorClientException(
                            'Failed to re-authenticate during reconnect')

                self.connected = True
                self._resubscribe()

            if self._login_token:
                self._resume(self._login_token,
                             callback=reconnect_login_callback)
            else:
                self._login(self._login_data,
                            callback=reconnect_login_callback)
        else:
            self._resubscribe()

    def _resubscribe(self):
        self.collection_data.data = {}
        cur_subs = self.subscriptions.items()
        self.subscriptions = {}
        for name, value in cur_subs:
            self.subscribe(name, value['params'])
        self.emit('reconnected')
    #
    # Account Management
    #

    def login(self, user, password, token=None, callback=None):
        """Login with a username and password

        Arguments:
        user - username or email address
        password - the password for the account

        Keyword Arguments:
        token - meteor resume token
        callback - callback function containing error as first argument and login data"""
        # TODO: keep the tokenExpires around so we know the next time
        #       we need to authenticate

        # hash the password
        hashed = hashlib.sha256(password).hexdigest()
        # handle username or email address
        if '@' in user:
            user_object = {
                'email': user
            }
        else:
            user_object = {
                'username': user
            }
        password_object = {
            'algorithm': 'sha-256',
            'digest': hashed
        }

        self._login_token = token
        self._login_data = {'user': user_object, 'password': password_object}

        if token:
            self._resume(token, callback=callback)
        else:
            self._login(self._login_data, callback=callback)

    def _resume(self, token, callback=None):
        login_data = {'resume': token}
        self._login(login_data, callback=callback)

    def _login(self, login_data, callback=None):
        self.emit('logging_in')

        def logged_in(error, data):
            if error:
                if self._login_token:
                    self._login_token = None
                    self._login(self._login_data, callback=callback)
                    return
                if callback:
                    callback(error, None)
                return

            self._login_token = data['token']

            if callback:
                callback(None, data)
            self.emit('logged_in', data)

        self.ddp_client.call('login', [login_data], callback=logged_in)

    def logout(self, callback=None):
        """Logout a user

        Keyword Arguments:
        callback - callback function called when the user has been logged out"""
        self.ddp_client.call('logout', [], callback=callback)
        self.emit('logged_out')

    #
    # Meteor Method Call
    #

    def call(self, method, params, callback=None):
        """Call a remote method

        Arguments:
        method - remote method name
        params - remote method parameters

        Keyword Arguments:
        callback - callback function containing return data"""
        self._wait_for_connect()
        self.ddp_client.call(method, params, callback=callback)

    #
    # Subscription Management
    #

    def subscribe(self, name, params=[], callback=None):
        """Subscribe to a collection
        Arguments:
        name - the name of the publication
        params - the subscription parameters

        Keyword Arguments:
        callback - a function callback that returns an error (if exists)"""
        self._wait_for_connect()

        def subscribed(error, sub_id):
            if error:
                self._remove_sub_by_id(sub_id)
                if callback:
                    callback(error.get('reason'))
                return
            if callback:
                callback(None)
            self.emit('subscribed', name)

        if name in self.subscriptions:
            raise MeteorClientException('Already subcribed to {}'.format(name))

        sub_id = self.ddp_client.subscribe(name, params, subscribed)
        self.subscriptions[name] = {
            'id': sub_id,
            'params': params
        }

    def unsubscribe(self, name):
        """Unsubscribe from a collection

        Arguments:
        name - the name of the publication"""
        self._wait_for_connect()
        if name not in self.subscriptions:
            raise MeteorClientException('No subscription for {}'.format(name))
        self.ddp_client.unsubscribe(self.subscriptions[name]['id'])
        del self.subscriptions[name]
        self.emit('unsubscribed', name)

    #
    # Collection Management
    #

    def find(self, collection, selector={}):
        """Find data in a collection

        Arguments:
        collection - collection to search

        Keyword Arguments:
        selector - the query (default returns all items in a collection)"""
        results = []
        for _id, doc in self.collection_data.data.get(collection, {}).items():
            doc.update({'_id': _id})
            if selector == {}:
                results.append(doc)
            for key, value in selector.items():
                if key in doc and doc[key] == value:
                    results.append(doc)
        return results

    def find_one(self, collection, selector={}):
        """Return one item from a collection

        Arguments:
        collection - collection to search

        Keyword Arguments:
        selector - the query (default returns first item found)"""
        for _id, doc in self.collection_data.data.get(collection, {}).items():
            doc.update({'_id': _id})
            if selector == {}:
                return doc
            for key, value in selector.items():
                if key in doc and doc[key] == value:
                    return doc
        return None

    def insert(self, collection, doc, callback=None):
        """Insert an item into a collection

        Arguments:
        collection - the collection to be modified
        doc - The document to insert. May not yet have an _id attribute,
        in which case Meteor will generate one for you.

        Keyword Arguments:
        callback - Optional. If present, called with an error object as the first argument and,
        if no error, the _id as the second."""
        self.call("/" + collection + "/insert", [doc], callback=callback)

    def update(self, collection, selector, modifier, callback=None):
        """Insert an item into a collection

        Arguments:
        collection - the collection to be modified
        selector - specifies which documents to modify
        modifier - Specifies how to modify the documents

        Keyword Arguments:
        callback - Optional. If present, called with an error object as the first argument and,
        if no error, the number of affected documents as the second."""
        self.call("/" + collection + "/update", [selector, modifier], callback=callback)

    def remove(self, collection, selector, callback=None):
        """Remove an item from a collection

        Arguments:
        collection - the collection to be modified
        selector - Specifies which documents to remove

        Keyword Arguments:
        callback - Optional. If present, called with an error object as its argument."""
        self.call("/" + collection + "/remove", [selector], callback=callback)

    #
    # Event Handlers
    #

    def connected(self):
        self.connected = True
        self.emit('connected')

    def closed(self, code, reason):
        self.connected = False
        self.emit('closed', code, reason)

    def failed(self, data):
        self.emit('failed', str(data))

    def added(self, collection, id, fields):
        self.collection_data.add_data(collection, id, fields)
        self.emit('added', collection, id, fields)

    def changed(self, collection, id, fields, cleared):
        self.collection_data.change_data(collection, id, fields, cleared)
        self.emit('changed', collection, id, fields, cleared)

    def removed(self, collection, id):
        self.collection_data.remove_data(collection, id)
        self.emit('removed', collection, id)

    #
    # Helper functions
    #

    def _time_from_start(self, start):
        now = datetime.datetime.now()
        return now - start

    def _remove_sub_by_id(self, sub_id):
        for name, cur_sub_id in self.subscriptions.items():
            if cur_sub_id == sub_id:
                del self.subscriptions[name]

    def _wait_for_connect(self):
        start = datetime.datetime.now()
        while not self.connected and self._time_from_start(start).seconds < 5:
            time.sleep(0.1)

        if not self.connected:
            raise MeteorClientException('Could not subscribe because a connection has not been established')
Example #17
0
class User:
    def __init__(self, username, passhash):
        self.username = username
        self.passhash = passhash

        self.user_id = None
        self.rooms_by_id = {}
        self.rooms_by_name = {}
        self.sub_ids = {}

        self.client = DDPClient(RC_URL, debug=DEBUG)

        self.connected_handlers = {}
        self.changed_handlers = {
            "stream-room-messages": self.on_stream_room_messages
        }
        self.added_handlers = {
            "rocketchat_subscription": self.on_rocketchat_subscription,
            "stream-room-messages": self.on_stream_room_messages
        }

        self.client.on("connected", self.login)
        self.client.on("changed", self.on_changed)
        self.client.on("added", self.on_added)

        self.sent_messages = []
        self.client.connect()

    def login(self):
        self.client.call("login", [{
            "user": {
                "username": self.username
            },
            "password": {
                "digest": self.passhash,
                "algorithm": "sha-256"
            }
        }],
                         callback=self.login_callback)

    def login_callback(self, u1, user_data):
        print(user_data)
        self.user_id = user_data["id"]
        self.sub_ids["subscription"] = self.client.subscribe(
            "subscription", [])
        # self.join_room("GENERAL", "c")

    def join_room(self, room_id, room_type):
        self.sub_ids[room_id] = {}
        room = None
        while room is None:
            try:
                room = self.rooms_by_id[room_id]
            except KeyError:
                print(self.rooms_by_id)
                time.sleep(1)

        self.sub_ids[room_id]["room"] = self.client.subscribe(
            "room", [room["type"] + room["name"]], callback=print)
        self.sub_ids[room_id]["stream-room-messages"] = self.client.subscribe(
            "stream-room-messages", [room_id, True])

    def on_stream_room_messages(self, collection, id, fields, *args):
        if "args" not in fields:
            return

        data = fields["args"][0]
        user = data["u"]
        if user["_id"] != self.user_id:
            return

        # print(data)
        if "t" in data and data["t"] == "rm":
            self.on_message_removed(data)
        elif "editedAt" in data:
            self.on_message_edited(data)
        elif data["msg"] != "":
            #ignore messages i sent from this client
            if data["_id"] in self.sent_messages:
                return
            self.on_message(data)
        else:
            print("Got unhandled message.")

    def on_changed(self, collection, id, fields, u1):
        if collection in self.changed_handlers:
            try:
                self.changed_handlers[collection](collection, id, fields, u1)
            except Exception as e:
                print("UNHANDLED EXCEPTION:", e)
        else:
            print("unhandled: ", collection, fields)

    def on_added(self, collection, id, fields):
        if collection in self.added_handlers:
            try:
                self.added_handlers[collection](collection, id, fields)
            except Exception as e:
                print("UNHANDLED EXCEPTION:", e)
        else:
            print("unhandled:", collection, fields)

    def on_rocketchat_subscription(self, collection, id, fields):
        data = fields
        room = {"name": data["name"], "rid": data["rid"], "type": data["t"]}
        self.rooms_by_id[room["rid"]] = room
        self.rooms_by_name[room["name"]] = room
        self.join_room(room["rid"], room["type"])
        return

    def on_message_removed(self, data):
        print("My message was deleted!")
        print("The message %s was deleted by %s" %
              (data["_id"], data["editedBy"]["username"]))

    def on_message_edited(self, data):
        print("My message was edited!")
        print("The message %s was edited by %s and now reads '%s'" %
              (data["_id"], data["editedBy"]["username"], data["msg"]))

    def on_message(self, data):
        print("I sent a message!")
        print("'%s'" % data["msg"])

    def send_message(self, room_id, msg):
        msg_id = make_id()
        print("attempting to send message with id '%s' and text '%s'" %
              (msg_id, msg))
        self.sent_messages.append(msg_id)
        self.client.call("sendMessage", [{
            "_id": msg_id,
            "rid": room_id,
            "msg": msg
        }])
Example #18
0
class App(Cmd):
    """Main input loop"""
    def __init__(self, ddp_endpoint):
        Cmd.__init__(self)
        self.ddp_client = DDPClient('ws://' + ddp_endpoint + '/websocket')

        self.subscriptions = {}
        self.pending_results = {}

        # Showing a fancy prompt string if we're interactive
        if sys.stdin.isatty():
            self.prompt = ddp_endpoint + '> '
        else:
            self.prompt = ''

        self.ddp_client.on('connected', self.connected)
        self.ddp_client.on('socket_closed', self.closed)
        self.ddp_client.on('failed', self.failed)
        self.ddp_client.on('added', self.added)
        self.ddp_client.on('changed', self.changed)
        self.ddp_client.on('removed', self.removed)
        self.ddp_client.connect()

    def subscribe(self, name, params):
        if name in self.subscriptions:
            self._log('* ALREADY SUBSCRIBED - {}'.format(name))
            return
        sub_id = self.ddp_client.subscribe(name, params, self.subscribed)
        self.subscriptions[name] = sub_id

    def unsubscribe(self, name):
        if name not in self.subscriptions:
            self._log('* NOT SUBSCRIBED - {}'.format(name))
            return
        self.ddp_client.unsubscribe(self.subscriptions[name])
        del self.subscriptions[name]
        self._log('* UNSUBSCRIBED - {}'.format(name))

    def call(self, method_name, params):
        self.ddp_client.call(method_name, params, self.result)

    #
    # Callbacks
    #

    def added(self, collection, id, fields):
        self._log('* ADDED {} {}'.format(collection, id), True)
        for key, value in fields.items():
            self._log('  - FIELD {} {}'.format(key, value))

    def changed(self, collection, id, fields, cleared):
        self._log('* CHANGED {} {}'.format(collection, id), True)
        for key, value in fields.items():
            self._log('  - FIELD {} {}'.format(key, value))
        for key, value in cleared.items():
            self._log('  - CLEARED {} {}'.format(key, value))

    def removed(self, collection, id):
        self._log('* REMOVED {} {}'.format(collection, id), True)

    def subscribed(self, error, sub_id):
        if error:
            self._remove_sub_by_id(sub_id)
            self._log('* SUBSCRIPTION FAILED - {}'.format(error.get('reason')),
                      True)
            return
        self._log('* READY', True)

    def result(self, error, result):
        if error:
            self._log('* METHOD FAILED - {}'.format(error.get('reason')), True)
            return
        self._log('* METHOD RESULT - {}'.format(str(result)), True)

    def connected(self):
        self._log('* CONNECTED', True)

    def closed(self, code, reason):
        """Called when the connection is closed"""
        self._log('* CONNECTION CLOSED {} {}'.format(code, reason))

    def failed(self, data):
        self._log('* FAILED - data: {}'.format(str(data)))

    #
    # Commands
    #

    def do_sub(self, params):
        """The `sub` command"""
        try:
            sub_name, sub_params = self._parse_command(params)
        except ValueError:
            self._log('Error parsing parameter list - try `help sub`')
            return

        self.subscribe(sub_name, sub_params)

    def do_unsub(self, params):
        """The `unsub` command"""
        try:
            sub_name, sub_params = self._parse_command(params)
        except ValueError:
            self._log('Error parsing parameter list - try `help unsub`')
            return

        self.unsubscribe(sub_name)

    def do_call(self, params):
        """The `call` command"""
        try:
            method, params = self._parse_command(params)
        except ValueError:
            self._log('Error parsing parameter list - try `help call`')
            return

        self.call(method, params)

    def do_login(self, params):
        """The `login` command"""
        self._log('Not Implemented Yet -- SRP is in progress')
        self._log(
            'However you can still calling the method `login` (WARNING: password sent in plaintext)'
        )
        #self.ddp_client._login({'username': '******'}, 'password123')

    def do_EOF(self, line):
        """The `EOF` "command"

        It's here to support `cat file | python ddpclient.py`
        """
        return True

    def do_help(self, line):
        """The `help` command"""

        msgs = {
            'call': ('call <method name> <json array of parameters>\n'
                     '  Calls a remote method\n'
                     '  Example: call vote ["foo.meteor.com"]'),
            'sub': ('sub <subscription name> [<json array of parameters>]\n'
                    '  Subscribes to a remote dataset\n'
                    '  Examples: `sub allApps` or `sub myApp '
                    '["foo.meteor.com"]`'),
            'unsub': ('unsub <subscription name>\n'
                      '  Unsubscribes from a remote dataset\n'
                      '  Examples: `unsub allApps` or `unsub myApp '
                      '["foo.meteor.com"]`'),
        }

        line = line.strip()
        if line and line in msgs:
            return self._log('\n' + msgs[line])

        for msg in msgs.values():
            self._log('\n' + msg)

    #
    # Utils
    #

    def _log(self, msg, server_msg=False):
        if server_msg:
            sys.stderr.write('\n')
        sys.stderr.write('{}\n'.format(msg))
        if server_msg:
            sys.stderr.write('{}'.format(self.prompt))

    def _parse_command(self, params):
        """Parses a command with a first string param and a second
        json-encoded param"""
        name, args = (params + ' ').split(' ', 1)
        return name, args and json.loads(args) or []

    def _remove_sub_by_id(self, sub_id):
        for name, cur_sub_id in self.subscriptions.items():
            if cur_sub_id == sub_id:
                del self.subscriptions[name]
Example #19
0
class MeteorClient(EventEmitter):
    def __init__(self,
                 url,
                 auto_reconnect=True,
                 auto_reconnect_timeout=0.5,
                 debug=False):
        EventEmitter.__init__(self)
        self.collection_data = CollectionData()
        self.ddp_client = DDPClient(
            url,
            auto_reconnect=auto_reconnect,
            auto_reconnect_timeout=auto_reconnect_timeout,
            debug=debug)
        self.ddp_client.on('connected', self.connected)
        self.ddp_client.on('socket_closed', self.closed)
        self.ddp_client.on('failed', self.failed)
        self.ddp_client.on('added', self.added)
        self.ddp_client.on('changed', self.changed)
        self.ddp_client.on('removed', self.removed)
        self.ddp_client.on('reconnected', self._reconnected)
        self.connected = False
        self.subscriptions = {}
        self._login_data = None
        self._login_token = None

    def connect(self):
        """Connect to the meteor server"""
        self.ddp_client.connect()

    def close(self):
        """Close connection with meteor server"""
        self.ddp_client.close()

    def _reconnected(self):
        """Reconnect

        Currently we get a new session every time so we have
        to clear all the data an resubscribe"""

        if self._login_data or self._login_token:

            def reconnect_login_callback(error, result):
                if error:
                    if self._login_token:
                        self._login_token = None
                        self._login(self._login_data,
                                    callback=reconnect_login_callback)
                        return
                    else:
                        raise MeteorClientException(
                            'Failed to re-authenticate during reconnect')

                self.connected = True
                self._resubscribe()

            if self._login_token:
                self._resume(self._login_token,
                             callback=reconnect_login_callback)
            else:
                self._login(self._login_data,
                            callback=reconnect_login_callback)
        else:
            self._resubscribe()

    def _resubscribe(self):
        self.collection_data.data = {}
        cur_subs = self.subscriptions.items()
        self.subscriptions = {}
        for name, value in cur_subs:
            self.subscribe(name, value['params'])
        self.emit('reconnected')

    #
    # Account Management
    #

    def login(self, user, password, token=None, callback=None):
        """Login with a username and password

        Arguments:
        user - username or email address
        password - the password for the account

        Keyword Arguments:
        token - meteor resume token
        callback - callback function containing error as first argument and login data"""
        # TODO: keep the tokenExpires around so we know the next time
        #       we need to authenticate

        # hash the password
        hashed = hashlib.sha256(password).hexdigest()
        # handle username or email address
        if '@' in user:
            user_object = {'email': user}
        else:
            user_object = {'username': user}
        password_object = {'algorithm': 'sha-256', 'digest': hashed}

        self._login_token = token
        self._login_data = {'user': user_object, 'password': password_object}

        if token:
            self._resume(token, callback=callback)
        else:
            self._login(self._login_data, callback=callback)

    def _resume(self, token, callback=None):
        login_data = {'resume': token}
        self._login(login_data, callback=callback)

    def _login(self, login_data, callback=None):
        self.emit('logging_in')

        def logged_in(error, data):
            if error:
                if self._login_token:
                    self._login_token = None
                    self._login(self._login_data, callback=callback)
                    return
                if callback:
                    callback(error, None)
                return

            self._login_token = data['token']

            if callback:
                callback(None, data)
            self.emit('logged_in', data)

        self.ddp_client.call('login', [login_data], callback=logged_in)

    def logout(self, callback=None):
        """Logout a user

        Keyword Arguments:
        callback - callback function called when the user has been logged out"""
        self.ddp_client.call('logout', [], callback=callback)
        self.emit('logged_out')

    #
    # Meteor Method Call
    #

    def call(self, method, params, callback=None):
        """Call a remote method

        Arguments:
        method - remote method name
        params - remote method parameters

        Keyword Arguments:
        callback - callback function containing return data"""
        self._wait_for_connect()
        self.ddp_client.call(method, params, callback=callback)

    #
    # Subscription Management
    #

    def subscribe(self, name, params=[], callback=None):
        """Subscribe to a collection
        Arguments:
        name - the name of the publication
        params - the subscription parameters

        Keyword Arguments:
        callback - a function callback that returns an error (if exists)"""
        self._wait_for_connect()

        def subscribed(error, sub_id):
            if error:
                self._remove_sub_by_id(sub_id)
                if callback:
                    callback(error.get('reason'))
                return
            if callback:
                callback(None)
            self.emit('subscribed', name)

        if name in self.subscriptions:
            raise MeteorClientException('Already subcribed to {}'.format(name))

        sub_id = self.ddp_client.subscribe(name, params, subscribed)
        self.subscriptions[name] = {'id': sub_id, 'params': params}

    def unsubscribe(self, name):
        """Unsubscribe from a collection

        Arguments:
        name - the name of the publication"""
        self._wait_for_connect()
        if name not in self.subscriptions:
            raise MeteorClientException('No subscription for {}'.format(name))
        self.ddp_client.unsubscribe(self.subscriptions[name]['id'])
        del self.subscriptions[name]
        self.emit('unsubscribed', name)

    #
    # Collection Management
    #

    def find(self, collection, selector={}):
        """Find data in a collection

        Arguments:
        collection - collection to search

        Keyword Arguments:
        selector - the query (default returns all items in a collection)"""
        results = []
        for _id, doc in self.collection_data.data.get(collection, {}).items():
            doc.update({'_id': _id})
            if selector == {}:
                results.append(doc)
            for key, value in selector.items():
                if key in doc and doc[key] == value:
                    results.append(doc)
        return results

    def find_one(self, collection, selector={}):
        """Return one item from a collection

        Arguments:
        collection - collection to search

        Keyword Arguments:
        selector - the query (default returns first item found)"""
        for _id, doc in self.collection_data.data.get(collection, {}).items():
            doc.update({'_id': _id})
            if selector == {}:
                return doc
            for key, value in selector.items():
                if key in doc and doc[key] == value:
                    return doc
        return None

    def insert(self, collection, doc, callback=None):
        """Insert an item into a collection

        Arguments:
        collection - the collection to be modified
        doc - The document to insert. May not yet have an _id attribute,
        in which case Meteor will generate one for you.

        Keyword Arguments:
        callback - Optional. If present, called with an error object as the first argument and,
        if no error, the _id as the second."""
        self.call("/" + collection + "/insert", [doc], callback=callback)

    def update(self, collection, selector, modifier, callback=None):
        """Insert an item into a collection

        Arguments:
        collection - the collection to be modified
        selector - specifies which documents to modify
        modifier - Specifies how to modify the documents

        Keyword Arguments:
        callback - Optional. If present, called with an error object as the first argument and,
        if no error, the number of affected documents as the second."""
        self.call("/" + collection + "/update", [selector, modifier],
                  callback=callback)

    def remove(self, collection, selector, callback=None):
        """Remove an item from a collection

        Arguments:
        collection - the collection to be modified
        selector - Specifies which documents to remove

        Keyword Arguments:
        callback - Optional. If present, called with an error object as its argument."""
        self.call("/" + collection + "/remove", [selector], callback=callback)

    #
    # Event Handlers
    #

    def connected(self):
        self.connected = True
        self.emit('connected')

    def closed(self, code, reason):
        self.connected = False
        self.emit('closed', code, reason)

    def failed(self, data):
        self.emit('failed', str(data))

    def added(self, collection, id, fields):
        self.collection_data.add_data(collection, id, fields)
        self.emit('added', collection, id, fields)

    def changed(self, collection, id, fields, cleared):
        self.collection_data.change_data(collection, id, fields, cleared)
        self.emit('changed', collection, id, fields, cleared)

    def removed(self, collection, id):
        self.collection_data.remove_data(collection, id)
        self.emit('removed', collection, id)

    #
    # Helper functions
    #

    def _time_from_start(self, start):
        now = datetime.datetime.now()
        return now - start

    def _remove_sub_by_id(self, sub_id):
        for name, cur_sub_id in self.subscriptions.items():
            if cur_sub_id == sub_id:
                del self.subscriptions[name]

    def _wait_for_connect(self):
        start = datetime.datetime.now()
        while not self.connected and self._time_from_start(start).seconds < 5:
            time.sleep(0.1)

        if not self.connected:
            raise MeteorClientException(
                'Could not subscribe because a connection has not been established'
            )
Example #20
0
class Hoduino():
    """
        A listener for the arduino
    """
    latest_hash = None

    def __init__(self, ddp_server_url, debug=False):
        self.shutting_down = False
        self.server_url = ddp_server_url
        self.debug = debug

        self.board_interface = HoduinoBoardInterface(port=ARDUINO_PORT)

        # Setup connection to the DDP server
        self.ddp_connect()

    def ddp_connect(self):
        print '++ DDP connection attempt...'
        self.ddp_client = DDPClient(self.server_url)
        self.ddp_client.debug = self.debug

        # Some events to handle from DDP
        self.ddp_client.on('added', self.added)
        self.ddp_client.on('connected', self.connected)
        self.ddp_client.on('socket_closed', self.closed)
        self.ddp_client.on('failed', self.failed)

        # Connect to DDP server
        self.ddp_client.connect()

    def exit(self):
        self.shutting_down = True

        # Close connect to Arduino
        if self.board_interface:
            self.board_interface.close_arduino()

        # Close connection to 
        self.ddp_client.close()

    def connected(self):
        print '++ DDP Connected'
        self.ddp_connected = True

        # Subscribe to messages stream. Limit messages to 1 as we 
        # only want the latest message.
        sub_id = self.ddp_client.subscribe('messages', [{'limit': 1}])

    def closed(self, code, reason):
        print '++ DDP connection closed {0} {1}'.format(code, reason)
        self.ddp_connected = False

        if not self.shutting_down:
            while not self.ddp_connected:
                self.ddp_connect()
                time.sleep(30)

    def failed(self, data):
        print '++ DDP failed - data: {0}'.format(str(data))

    def added(self, collection, id, fields):
        print '++ DDP added {0} {1}'.format(len(collection), id)
        if id <> self.latest_hash:
            print '++ New messages!'
            self.board_interface.donation_reaction()
            self.latest_hash = id
        else:
            print '++ No new messages'