# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
from six import string_types
[docs]class Event(object):
"""
Represents an event received from the chat system
Events, and derivated objects such as instances of Message, abstract
pieces of information received from various chat systems. They are
designed as dictionary wrappers, with minimum exposure to shellbot, while
enabling the transmission of rich information through serialization.
The life cycle of an event starts within a Space instance, most often, in
the webhook triggered by a remote chat system. In order to adapt to
shellbot, code should build the appropriate event instance, and push
it to the queue used by the listener.
Example::
item = self.api.messages.get(messageId=message_id)
my_engine.ears.put(Message(item._json))
"""
type = 'event'
def __init__(self,
attributes=None):
"""
Represents an event received from the chat system
:param attributes: the set of atributes of this event
:type attributes: dict or json-encoded string
This function may raise AttributeError if some mandatory
attribute is missing.
"""
if not attributes:
self.__dict__['attributes'] = {}
elif isinstance(attributes, string_types):
self.__dict__['attributes'] = json.loads(attributes)
else:
self.__dict__['attributes'] = attributes
try:
del self.__dict__['attributes']['type']
except:
pass
def __getattr__(self, key):
"""
Provides access to any native attribute
:param key: name of the attribute
:type key: str
:return: the value of the attribute
This method is called when attempting to access a object attribute that
hasn't been defined for the object. For example trying to access
object.attribute1 when attribute1 hasn't been defined.
Event.__getattr__() checks original attributes to see if the
attribute exists, and returns its value. Else an
AttributeError is raised.
"""
try:
return self.attributes[key]
except KeyError:
raise AttributeError(u"'{}' has no attribute '{}'".format(
self.__class__.__name__, key))
def __setattr__(self, key, value):
"""
Changes an attribute
:param key: name of the attribute
:type key: str
:param value: new value of the attribute
:type value: str or other serializable object
The use case for this function is when you adapt an event that does
not feature an attribute that is expected by shellbot.
For example, Cisco Spark messages do not feature the key ``from_name``
but have ``personEmail`` instead. This can be adapted like this::
message = Message(received_item)
message.from_name = message.personEmail
"""
self.attributes[key] = value
[docs] def get(self, key, default=None):
"""
Returns the value of one attribute
:param key: name of the attribute
:type key: str
:param default: default value of the attribute
:type default: str or other serializable object
:return: value of the attribute
:rtype: str or other serializable object or None
The use case for this function is when you adapt an event that does
not feature an attribute that is expected by shellbot.
More specifically, call this function on optional attributes so as
to avoid AttributeError
For example, some Cisco Spark messages may have ``toPersonId``, but
not all. So you could do::
message = Message(received_item)
to_id = message.get('toPersonId')
if to_id:
...
"""
value = self.attributes.get(key) # do not use default here!
if value is None:
value = default
return value
def __repr__(self):
"""
Returns a string representing this object as valid Python expression.
"""
return u"{}({})".format(
self.__class__.__name__,
json.dumps(self.attributes, sort_keys=True))
def __str__(self):
"""
Returns a human-readable string representation of this object.
"""
with_type = self.attributes.copy()
with_type.update({'type': self.type})
return json.dumps(with_type, sort_keys=True)
def __eq__(self, other):
"""
Compares with another object
"""
try:
if self.type != other.type:
return False
if self.attributes != other.attributes:
return False
return True
except:
return False # not same duck types
[docs]class Message(Event):
"""
Represents a message received from the chat system
"""
type = 'message'
@property
def text(self):
"""
Returns message textual content
:rtype: str
This function returns a bare string that can be handled
directly by the shell. This has no tags nor specific binary format.
"""
return self.__getattr__('text')
@property
def content(self):
"""
Returns message rich content
:rtype: str
This function preserves rich content that was used to create the
message, be it Markdown, HTML, or something else.
If no rich content is provided, than this attribute is equivalent
to ``self.text``
"""
content = self.attributes.get('content')
if content:
return content
return self.__getattr__('text')
@property
def from_id(self):
"""
Returns the id of the message originator
:rtype: str or None
This attribute allows listener to distinguish between messages
from the bot and messages from other chat participants.
"""
return self.attributes.get('from_id')
@property
def from_label(self):
"""
Returns the name or title of the message originator
:rtype: str or None
This attribute is used by updaters that log messages or copy them
for archiving.
"""
return self.attributes.get('from_label')
@property
def is_direct(self):
"""
Determines if this is a direct message
:rtype: True or False
This attribute is set for 1-to-1 channels. It allows the listener to
determine if the input is explicitly for this bot or not.
"""
return self.attributes.get('is_direct', False)
@property
def mentioned_ids(self):
"""
Returns the list of mentioned persons
:rtype: list of str, or []
This attribute allows the listener to determine if the input is
explicitly for this bot or not.
"""
return self.attributes.get('mentioned_ids', [])
@property
def channel_id(self):
"""
Returns the id of the chat space
:rtype: str or None
"""
return self.attributes.get('channel_id')
@property
def attachment(self):
"""
Returns name of uploaded file
:rtype: str
This attribute is set on file upload. It provides with the
external name of the file that has been shared, if any.
For example, to get a local copy of an uploaded file::
if message.attachment:
path = space.download_attachment(message.url)
"""
return self.attributes.get('attachment')
@property
def url(self):
"""
Returns link to uploaded file
:rtype: str
This attribute is set on file upload. It provides with the
address that can be used to fetch the actual content.
There is a need to rely on the underlying space to authenticate and
get the file itself. For example::
if message.url:
content = space.get_attachment(message.url)
"""
return self.attributes.get('url')
@property
def stamp(self):
"""
Returns the date and time of this event in ISO format
:rtype: str or None
This attribute allows listener to limit the horizon of messages
fetched from a space back-end.
"""
return self.attributes.get('stamp')
[docs]class Join(Event):
"""
Represents the addition of someone to a space
"""
type = 'join'
@property
def actor_id(self):
"""
Returns the id of the joining actor
:rtype: str or None
This attribute allows listener to identify who joins a space.
"""
return self.__getattr__('actor_id')
@property
def actor_address(self):
"""
Returns the address of the joining actor
:rtype: str or None
This attribute can be passed to ``add_participant()`` if needed.
"""
return self.__getattr__('actor_address')
@property
def actor_label(self):
"""
Returns the name or title of the joining actor
:rtype: str or None
This attribute allows listener to identify who joins a space.
"""
return self.__getattr__('actor_label')
@property
def channel_id(self):
"""
Returns the id of the joined space
:rtype: str or None
"""
return self.__getattr__('channel_id')
@property
def stamp(self):
"""
Returns the date and time of this event in ISO format
:rtype: str or None
"""
return self.attributes.get('stamp')
[docs]class Leave(Event):
"""
Represents the removal of someone to a space
"""
type = 'leave'
@property
def actor_id(self):
"""
Returns the id of the leaving actor
:rtype: str or None
This attribute allows listener to identify who leaves a space.
"""
return self.__getattr__('actor_id')
@property
def actor_address(self):
"""
Returns the address of the leaving actor
:rtype: str or None
This attribute can be passed to ``add_participant()`` if needed.
"""
return self.__getattr__('actor_address')
@property
def actor_label(self):
"""
Returns the name or title of the leaving actor
:rtype: str or None
This attribute allows listener to identify who leaves a space.
"""
return self.__getattr__('actor_label')
@property
def channel_id(self):
"""
Returns the id of the left space
:rtype: str or None
"""
return self.__getattr__('channel_id')
@property
def stamp(self):
"""
Returns the date and time of this event in ISO format
:rtype: str or None
"""
return self.attributes.get('stamp')
[docs]class EventFactory(object):
"""
Generates events
"""
@classmethod
[docs] def build_event(cls, attributes):
"""
Turns a dictionary to a typed event
:param attributes: the set of attributes to consider
:type attributes: dict
:return: an Event, such as a Message, a Join, a Leave, etc.
"""
assert isinstance(attributes, dict)
flavour= attributes.get('type')
if not flavour:
return Event(attributes)
all_types = {
'message': Message,
'join': Join,
'leave': Leave,
}
loader = all_types.get(flavour)
if loader:
return loader(attributes)
return Event(attributes)