# -*- 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.
from bottle import request
from functools import wraps
from io import BytesIO
import itertools
import logging
from multiprocessing import Process, Queue
import os
import re
import requests
import shutil
from six import string_types
import tempfile
import time
from shellbot.channel import Channel
from shellbot.events import Event, Message, Join, Leave
from .base import Space
[docs]def retry(give_up="Unable to request Cisco Spark API",
silent=False,
delays=(0.1, 1, 5),
skipped=(401, 403, 404, 409)):
"""
Improves a call to Cisco Spark API
:param give_up: message to log on final failure
:type give_up: str
:param silent: if exceptions should be masked as much as possible
:type silent: bool
:param delays: time to wait between repetitions
:type delays: a list of positive numbers
:param skipped: do not retry for these status codes
:type skipped: a list of web status codes
This decorator compensates for common transient communication issues
with the Cisco Spark platform in the cloud.
Example::
@retry(give_up="Unable to get information on this bot")
def api_call():
return self.api.people.me()
me = api_call()
credit: http://code.activestate.com/recipes/580745-retry-decorator-in-python/
"""
def wrapper(function):
def wrapped(*args, **kwargs):
from ciscosparkapi import SparkApiError
for delay in itertools.chain(delays, [ None ]):
try:
return function(*args, **kwargs)
except Exception as feedback:
if isinstance(feedback, SparkApiError) and feedback.response_code in skipped:
delay = None
if str(feedback).startswith("TEST"): # horrible hack, right?
delay = None
if delay is None:
logging.warning(give_up)
if silent:
logging.debug(feedback)
return
else:
raise
else:
logging.debug(feedback)
logging.warning(u"Retrying the API request...")
time.sleep(delay)
return wrapped
return wrapper
[docs]def no_exception(function, return_value=None):
"""
Stops the propagation of exceptions
:param return_value: Returned by the decorated function on exception
This decorator is a convenient approach for silently discarding
exceptions.
#wip -- this should be moved in a general-purpose module of shellbot
Example::
@no_exception(return_value=[])
def list_items():
... # if an exception is raised here, an empty list is returned
"""
def wrapper(*args, **kwargs):
wraps(function)
try:
return function(*args, **kwargs)
except Exception as feedback:
logging.debug(feedback)
return return_value
return wrapper
[docs]class SparkSpace(Space):
"""
Handles a Cisco Spark room
This is a representation of a chat space hosted at Cisco Spark.
"""
DEFAULT_SETTINGS = {
'space': {
'room': '$CHAT_ROOM_TITLE',
},
'server': {
'url': '$SERVER_URL',
'hook': '/hook',
'binding': '0.0.0.0',
'port': 8080,
},
}
[docs] def on_init(self,
token=None,
**kwargs):
"""
Handles extended initialisation parameters
:param token: bot authentication token for the Cisco Spark API
:type token: str
Example::
space = SparkSpace(context=context)
"""
if token:
self.context.set('space.token', token)
self.api = None
self.audit_api = None
self._last_message_id = 0
[docs] def check(self):
"""
Checks settings of the space
This function reads key ``space`` and below, and update
the context accordingly::
space.configure({'space': {
'type': 'spark',
'room': 'My preferred room',
'participants':
['alan.droit@azerty.org', 'bob.nard@support.tv'],
'team': 'Anchor team',
'token': '$MY_BOT_TOKEN',
}})
This can also be written in a more compact form::
space.configure({'space.room': 'My preferred room',
'space.token': '$MY_BOT_TOKEN',
})
This function handles following parameters:
* ``space.room`` - title of the associated Cisco Spark room.
This can refer to an environment variable if it starts
with ``$``, e.g., ``$ROOM_TITLE``.
* ``space.participants`` - list of initial participants. This can
be taken from ``$CHANNEL_DEFAULT_PARTICIPANTS`` from the
environment.
* ``space.team`` - title of a team associated with this room
* ``space.token`` - private token of the bot, given by Cisco Spark.
Instead of putting the real value of the token you are encouraged
to use an environment variable instead,
e.g., ``$MY_BOT_TOKEN``.
If ``space.token`` is not provided, then the function looks for an
environment variable ``CISCO_SPARK_BOT_TOKEN``.
* ``space.audit_token`` - token to be used for the audit of chat events.
It is recommended that a token of a person is used, so that the
visibility is maximised for the proper audit of events.
Instead of putting the real value of the token you are encouraged
to use an environment variable instead,
e.g., ``$MY_AUDIT_TOKEN``.
If ``space.audit_token`` is not provided, then the function looks
for an environment variable ``CISCO_SPARK_AUDIT_TOKEN``.
If a single value is provided for ``participants`` then it is turned
automatically to a list.
Example::
>>>space.configure({'space.participants': 'bobby@jah.com'})
>>>space.context.get('space.participants')
['bobby@jah.com']
"""
self.context.check('space.room',
is_mandatory=True, filter=True)
self.context.check('space.participants',
'$CHANNEL_DEFAULT_PARTICIPANTS', filter=True)
self.context.check('space.team')
self.context.check('space.token',
'$CISCO_SPARK_BOT_TOKEN', filter=True)
self.context.check('space.audit_token',
'$CISCO_SPARK_AUDIT_TOKEN', filter=True)
values = self.context.get('space.participants')
if isinstance(values, string_types):
self.context.set('space.participants', [values])
[docs] def connect(self, factory=None, **kwargs):
"""
Connects to the back-end API
:parameter factory: an API factory, for test purpose
:type: object
If a factory is provided, it is used to get API instances. Else
the regular CiscoSparkAPI is invoked instead.
This function loads two instances of Cisco Spark API, one using
the bot token, and one using the audit token, if this is available.
"""
if not factory:
from ciscosparkapi import CiscoSparkAPI
factory = CiscoSparkAPI
logging.debug(u"Loading Cisco Spark API")
bot_token = self.context.get('space.token')
assert bot_token # some token is needed
self.api = None
try:
logging.debug(u"- token: {}".format(bot_token))
self.api = factory(access_token=bot_token)
except Exception as feedback:
logging.error(u"Unable to load Cisco Spark API")
logging.exception(feedback)
audit_token = self.context.get('space.audit_token')
self.audit_api = None
if audit_token:
try:
logging.debug(u"- audit token: {}".format(audit_token))
self.audit_api = factory(access_token=audit_token)
except Exception as feedback:
logging.warning(feedback)
self.on_connect()
[docs] def on_connect(self):
"""
Retrieves attributes of this bot
This function queries the Cisco Spark API to remember the id of this
bot. This is used afterwards to filter inbound messages to the shell.
"""
assert self.api is not None # connect() is prerequisite
@retry(u"Unable to retrieve bot information")
def bot_identity():
return self.api.people.me()
logging.debug(u"Retrieving bot information")
me = bot_identity()
# logging.debug(u"- {}".format(str(me)))
self.context.set('bot.address', str(me.emails[0]))
logging.debug(u"- bot email: {}".format(
self.context.get('bot.address')))
self.context.set('bot.name',
str(me.displayName))
logging.debug(u"- bot name: {}".format(
self.context.get('bot.name')))
self.context.set('bot.id', me.id)
logging.debug(u"- bot id: {}".format(
self.context.get('bot.id')))
[docs] def list_group_channels(self, quantity=10, **kwargs):
"""
Lists available channels
:param quantity: maximum quantity of channels to return
:type quantity: positive integer
:return: list of Channel
"""
assert quantity >= 0
if not quantity:
return []
logging.info(u"Listing {} recent rooms".format(quantity))
@retry(u"Unable to list rooms", silent=True)
def list_rooms():
return [self._to_channel(x) \
for x in self.api.rooms.list(type='group',
sortBy='lastactivity',
max=quantity)]
return list_rooms()[:quantity] # enforce the maximum results
[docs] def create(self, title, ex_team=None, **kwargs):
"""
Creates a room
:param title: title of a new channel
:type title: str
:param ex_team: the team attached to this room (optional)
:type ex_team: str or object
If the parameter ``ex_team`` is provided, then it can be either a
simple name, or a team object featuring an id.
:return: Channel or None
This function returns a representation of the local channel.
"""
assert title
assert self.api is not None # connect() is prerequisite
teamId = None
if ex_team:
try:
teamId = ex_team.id
except:
team = self.get_team(ex_team)
if team and team.id:
teamId = team.id
logging.info(u"Creating Cisco Spark room '{}'".format(title))
@retry(u"Unable to create room", silent=True)
def do_it():
room = self.api.rooms.create(title=title,
teamId=teamId)
return self._to_channel(room)
return do_it()
[docs] def get_by_title(self, title, **kwargs):
"""
Looks for an existing room by name
:param title: title of the target room
:type title: str
:return: Channel instance or None
Note: This function looks only into group rooms. To get a direct room
use ``get_by_person()`` instead.
"""
assert title
assert self.api is not None # connect() is prerequisite
logging.info(u"Looking for Cisco Spark room '{}'".format(title))
@retry(u"Unable to list rooms", silent=True)
def do_it():
for room in self.api.rooms.list(type='group'):
if title == room.title:
logging.info(u"- found it")
return self._to_channel(room)
logging.info(u"- not found")
return do_it()
[docs] def get_by_id(self, id, **kwargs):
"""
Looks for an existing room by id
:param id: identifier of the target room
:type id: str
:return: Channel instance or None
"""
assert id
assert self.api is not None # connect() is prerequisite
logging.info(u"Using Cisco Spark room '{}'".format(id))
@retry(u"Unable to list rooms", silent=True)
def do_it():
room = self.api.rooms.get(id)
if room:
logging.info(u"- found it")
return self._to_channel(room)
logging.info(u"- not found")
return do_it()
[docs] def get_by_person(self, label, **kwargs):
"""
Looks for an existing private room with a person
:param label: the display name of the person's account
:type label: str
:return: Channel instance or None
If a channel already exists for this person, a representation of it is
returned. Else the value ``None``is returned.
"""
assert label
assert self.api is not None # connect() is prerequisite
logging.info(
u"Looking for Cisco Spark private room with '{}'".format(label))
@retry(u"Unable to list rooms", silent=True)
def do_it():
for room in self.api.rooms.list(type='direct'):
if room.title.startswith(label):
logging.info(u"- found it")
return self._to_channel(room)
logging.info(u"- not found")
return do_it()
[docs] def update(self, channel, **kwargs):
"""
Updates an existing room
:param channel: a representation of the updated room
:type channel: Channel
This function can change the title of a room.
For example, change the title from a bot instance::
bot.channel.title = "A new title"
bot.space.update(bot.channel)
"""
assert channel is not None
assert self.api is not None # connect() is prerequisite
@retry(u"Unable to update room", silent=True)
def do_it():
self.api.rooms.update(channel.id, channel.title)
do_it()
[docs] def delete(self, id, **kwargs):
"""
Deletes a room
:param id: the unique id of an existing room
:type id: str
"""
assert id
assert self.api is not None # connect() is prerequisite
logging.info(u"Deleting Cisco Spark room '{}'".format(id))
@retry(u"Unable to delete room", silent=True)
def do_it():
self.api.rooms.delete(roomId=id)
do_it()
[docs] def get_team(self, name):
"""
Gets a team by name
:param name: name of the target team
:type name: str
:return: attributes of the team
:rtype: Team or None
>>>print(space.get_team("Hello World"))
Team({
"id" : "Y2lzY29zcGFyazovL3VzL1RFQU0Yy0xMWU2LWE5ZDgtMjExYTBkYzc5NzY5",
"name" : "Hello World",
"created" : "2015-10-18T14:26:16+00:00"
})
"""
assert name
assert self.api is not None # connect() is prerequisite
logging.info(u"Looking for Cisco Spark team '{}'".format(name))
@retry(u"Unable to list teams", silent=True)
def do_it():
for team in self.api.teams.list():
if name == team.name:
logging.info(u"- found team")
return team
logging.info(u"- team not found")
return do_it()
[docs] def list_participants(self, id):
"""
Lists participants to a channel
:param id: the unique id of an existing channel
:type id: str
:return: a list of persons
:rtype: list of str
Note: this function returns all participants, except the bot itself.
"""
assert id # target channel is required
logging.debug(
u"Looking for Cisco Spark room participants")
@retry(u"Unable to list memberships", silent=True)
def do_it():
participants = set()
avoided = set()
avoided.add(self.context.get('bot.address'))
for item in self.api.memberships.list(roomId=id):
person = item.personEmail
if person in avoided:
continue
logging.debug(u"- {}".format(item.personEmail))
participants.add(person)
return participants
return do_it()
@no_exception
[docs] def add_participant(self, id, person, is_moderator=False):
"""
Adds one participant
:param id: the unique id of an existing room
:type id: str
:param person: e-mail address of the person to add
:type person: str
:param is_moderator: if this person has special powers on this channel
:type is_moderator: True or False
"""
assert id # target channel is required
assert person
assert is_moderator in (True, False)
assert self.api is not None # connect() is prerequisite
@retry(u"Unable to add participant '{}'".format(person), silent=True)
def do_it():
self.api.memberships.create(roomId=id,
personEmail=person,
isModerator=is_moderator)
do_it()
@no_exception
[docs] def remove_participant(self, id, person):
"""
Removes a participant
:param id: the unique id of an existing room
:type id: str
:param person: e-mail address of the person to remove
:type person: str
"""
assert id # target channel is required
assert person # target person
assert self.api is not None # connect() is prerequisite
@retry(u"Unable to remove participant '{}'".format(person), silent=True)
def do_it():
self.api.memberships.delete(roomId=id,
personEmail=person)
do_it()
[docs] def walk_messages(self,
id=None,
**kwargs):
"""
Walk messages from a Cisco Spark room
:param id: the unique id of an existing room
:type id: str
:return: an iterator of Message objects
"""
assert self.api is not None # connect() is prerequisite
for item in self.api.messages.list(roomId=id):
item._json['hook'] = 'shellbot-messages'
yield self.on_message(item._json)
[docs] def post_message(self,
id=None,
text=None,
content=None,
file=None,
person=None,
**kwargs):
"""
Posts a message to a Cisco Spark room
:param id: the unique id of an existing room
:type id: str
:param person: address for a direct message
:type person: str
:param text: message in plain text
:type text: str
:param content: rich format, such as Markdown or HTML
:type content: str
:param file: URL or local path for an attachment
:type file: str
Example message out of plain text::
space.post_message(id=id, text='hello world')
Example message with Markdown::
space.post_message(id, content='this is a **bold** statement')
Example file upload::
space.post_message(id, file='./my_file.pdf')
Of course, you can combine text with the upload of a file::
text = 'This is the presentation that was used for our meeting'
space.post_message(id=id,
text=text,
file='./my_file.pdf')
For direct messages, provide who you want to reach instead of
a channel id, like this::
space.post_message(person='foo.bar@acme.com', text='hello guy')
"""
assert id or person # need a recipient
assert id is None or person is None # only one recipient
assert self.api is not None # connect() is prerequisite
logging.info(u"Posting message")
if text:
logging.debug(u"- text: {}".format(
text[:50] + (text[50:] and '...')))
if content:
logging.debug(u"- content: {}".format(
content[:50] + (content[50:] and '...')))
if file:
logging.debug(u"- file: {}".format(
file[:50] + (file[50:] and '...')))
@retry(u"Unable to post message", silent=True)
def do_it():
files = [file] if file else None
self.api.messages.create(roomId=id,
toPersonEmail=person,
text=text,
markdown=content,
files=files)
do_it()
[docs] def register(self, hook_url):
"""
Connects in the background to Cisco Spark inbound events
:param webhook: web address to be used by Cisco Spark service
:type webhook: str
This function registers the provided hook multiple times, so as to
receive mutiple kind of updates:
- The bot is invited to a room, or kicked out of it. People are
joining or leaving:
webhook name = shellbot-memberships
resource = memberships,
event = all,
registered with bot token
- Messages are sent, maybe with some files:
webhook name = shellbot-messages
resource = messages,
event = created,
registered with bot token
- Messages sent, maybe with some files, for audit purpose:
webhook name = shellbot-audit
resource = messages,
event = created,
registered with audit token
Previous webhooks registered with the bot token are all removed before
registration. This means that only the most recent instance of the bot
will be notified of new invitations.
"""
assert hook_url
assert self.api is not None # connect() is prerequisite
self.deregister()
@retry(u"Unable to create webhook", silent=True)
def create_webhook(api, name, resource, event, filter):
api.webhooks.create(name=name,
targetUrl=hook_url,
resource=resource,
event=event,
filter=filter)
logging.info(u"Registering webhook to Cisco Spark")
logging.debug(u"- url: {}".format(hook_url))
logging.debug(u"- registering 'shellbot-memberships'")
create_webhook(api=self.api,
name='shellbot-memberships',
resource='memberships',
event='all',
filter=None)
logging.debug(u"- registering 'shellbot-messages'")
create_webhook(api=self.api,
name='shellbot-messages',
resource='messages',
event='created',
filter=None)
if self.audit_api and self.fan:
self.context.set('audit.has_been_armed', True)
logging.debug(u"- registering 'shellbot-audit'")
create_webhook(api=self.audit_api,
name='shellbot-audit',
resource='messages',
event='created',
filter=None)
[docs] def deregister(self):
"""
Stops inbound flow from Cisco Spark
This function deregisters hooks that it may have created.
Previous webhooks registered with the bot token are all removed before
registration. This means that only the most recent instance of the bot
will be notified of new invitations.
This function also removes webhooks created with the audit token, if
any. So after deregister the audit of individual rooms just stops.
"""
assert self.api is not None # connect() is prerequisite
@retry(u"Unable to list webhooks", silent=True)
def list_webhooks(api):
return [x for x in api.webhooks.list()]
@retry(u"Unable to delete webhook", silent=True)
def delete_webhook(api, id):
api.webhooks.delete(webhookId=id)
logging.info(u"Purging webhooks")
for webhook in list_webhooks(self.api):
logging.debug(u"- deleting '{}'".format(webhook.name))
delete_webhook(self.api, webhook.id)
if self.audit_api:
for webhook in list_webhooks(self.audit_api):
logging.debug(u"- deleting '{}'".format(webhook.name))
delete_webhook(self.audit_api, webhook.id)
[docs] def webhook(self, item=None):
"""
Processes the flow of events from Cisco Spark
:param item: if provided, do not invoke the ``request`` object
:type item: dict
This function is called from far far away, over the Internet,
most of the time. Or it is called locally, from test environment,
when an item is provided.
The structure of the provided item should be identical to those of
updates sent by Cisco Spark.
Example event on message creation::
{
"resource": "messages",
"event": "created",
"data": { "id": "...." },
"name": "shellbot-audit"
}
"""
logging.debug(u'Receiving data from webhook')
if not item:
item = request.json
assert isinstance(item, dict)
# logging.debug(u"- {}".format(item))
resource = item['resource']
event = item['event']
data = item['data']
hook = item['name']
if hook == 'shellbot-audit':
logging.debug(u"- for audit")
api = self.audit_api
queue = self.fan
else:
api = self.api
queue = self.ears
if resource == 'messages' and event == 'created':
if hook != 'shellbot-audit':
filter_id = self.context.get('bot.id')
if filter_id and data.get('personId') == filter_id:
logging.debug(u"- sent by me, thrown away")
return 'OK'
logging.debug(u"- handling '{}:{}'".format(resource, event))
@retry(u"Unable to retrieve new message")
def fetch_message():
item = api.messages.get(messageId=data['id'])
item._json['hook'] = hook
return item._json
self.on_message(fetch_message(), queue)
elif resource == 'memberships' and event == 'created':
logging.debug(u"- handling '{}:{}'".format(resource, event))
self.on_join(data, queue)
elif resource == 'memberships' and event == 'deleted':
logging.debug(u"- handling '{}:{}'".format(resource, event))
self.on_leave(data, queue)
else:
logging.debug(u"- throwing away {}:{}".format(resource, event))
logging.debug(u"- {}".format(data))
return "OK"
[docs] def pull(self):
"""
Fetches events from Cisco Spark
This function senses most recent items, and pushes them
to a processing queue.
"""
assert self.api is not None # connect() is prerequisite
logging.info(u'Pulling messages')
self.context.increment(u'puller.counter')
@retry(u"Unable to pull messages", silent=True)
def call_api():
return self.api.messages.list(mentionedPeople=['me'],
max=10)
new_items = []
items = call_api()
for item in items:
if item.id == self._last_message_id:
break
new_items.append(item)
if len(new_items):
logging.info(u"Pulling {} new messages".format(len(new_items)))
while len(new_items):
item = new_items.pop()
self._last_message_id = item.id
item._json['hook'] = 'pull'
self.on_message(item._json, self.ears)
[docs] def on_message(self, item, queue=None):
"""
Normalizes message for the listener
:param item: attributes of the inbound message
:type item: dict
:param queue: the processing queue (optional)
:type queue: Queue
:return: a Message
This function prepares a Message and push it to the provided queue.
This function adds following keys to messages so that a neutral format
can be used with the listener:
* ``type`` is set to ``message``
* ``content`` is a copy of ``html``
* ``from_id`` is a copy of ``personId``
* ``from_label`` is a copy of ``personEmail``
* ``is_direct`` if the message is coming from 1:1 room
* ``mentioned_ids`` is a copy of ``mentionedPeople``
* ``channel_id`` is a copy of ``roomId``
* ``stamp`` is a copy of ``created``
"""
message = Message(item.copy())
message.content = message.get('html', message.text)
message.from_id = message.get('personId')
message.from_label = message.get('personEmail')
message.is_direct = True if message.get('roomType') == 'direct' else False
message.mentioned_ids = message.get('mentionedPeople', [])
message.channel_id = message.get('roomId')
message.stamp = message.get('created')
files = item.get('files', [])
if files:
url = files[0]
message.url = url
if message.hook == 'shellbot-audit':
token = self.context.get('space.audit_token', '*no*token')
else:
token = self.context.get('space.token', '*no*token')
message.attachment = self.name_attachment(url, token=token)
if queue:
logging.debug(u"- putting message to queue")
queue.put(str(message))
return message
[docs] def download_attachment(self, url, token=None):
"""
Copies a shared document locally
"""
path = os.path.join(tempfile.gettempdir(),
self.name_attachment(url, token=token))
logging.debug(u"- writing to {}".format(path))
with open(path, "wb") as handle:
shutil.copyfileobj(self.get_attachment(url, token=token),
handle)
return path
[docs] def name_attachment(self, url, token=None, response=None):
"""
Retrieves a document attached to a room
"""
logging.debug(u"- sensing {}".format(url))
if not token:
token = self.context.get('space.token', '*no*token')
headers = {}
headers['Authorization'] = 'Bearer '+token
if not response:
response = requests.head(url=url, headers=headers)
# logging.debug(u"- status: {}".format(response.status_code))
if response.status_code != 200:
raise Exception(u"Unable to download attachment")
# logging.debug(u"- headers: {}".format(response.headers))
line = response.headers['Content-Disposition']
match = re.search('filename=(.+)', line)
if match:
name = match.group(1).strip()
if name.startswith('"') and name.endswith('"'):
name = name[1:-1]
return name
return 'downloadable'
[docs] def get_attachment(self, url, token=None, response=None):
"""
Retrieves a document attached to a room
:return: a stream of BytesIO
:rtype: BytesIO
#TODO: stream from requests
response = requests.get(url, stream=True)
response.decode_content = True
shutil.copyfileobj(response.raw, target)
"""
logging.debug(u"- fetching {}".format(url))
if not token:
token = self.context.get('space.token', '*no*token')
headers = {}
headers['Authorization'] = 'Bearer '+token
if not response:
response = requests.get(url=url, headers=headers)
# logging.debug(u"- status: {}".format(response.status_code))
if response.status_code != 200:
raise Exception(u"Unable to download attachment")
# logging.debug(u"- headers: {}".format(response.headers))
# logging.debug(u"- encoding: {}".format(response.encoding))
logging.debug(u"- length: {}".format(len(response.content)))
return BytesIO(response.content)
[docs] def on_join(self, item, queue=None):
"""
Normalizes message for the listener
:param item: attributes of the inbound message
:type item: dict
:param queue: the processing queue (optional)
:type queue: Queue
Example item received on memberships:create::
{
'isMonitor': False,
'created': '2017-05-31T21:25:30.424Z',
'personId': 'Y2lzY29zcGFyazovL3VRiMTAtODZkYy02YzU0Yjg5ODA5N2U',
'isModerator': False,
'personOrgId': 'Y2lzY29zcGFyazovL3V0FOSVpBVElPTi9jb25zdW1lcg',
'personDisplayName': 'foo.bar@acme.com',
'personEmail': 'foo.bar@acme.com',
'roomId': 'Y2lzY29zcGFyazovL3VzL1JP3LTk5MDAtMDU5MDI2YjBiNDUz',
'id': 'Y2lzY29zcGFyazovL3VzDctMTFlNy05OTAwLTA1OTAyNmIwYjQ1Mw'
}
This function prepares a Join and push it to the provided queue.
* ``type`` is set to ``join``
* ``actor_id`` is a copy of ``personId``
* ``actor_address`` is a copy of ``personEmail``
* ``actor_label`` is a copy of ``personDisplayName``
* ``stamp`` is a copy of ``created``
"""
join = Join(item.copy())
join.actor_id = join.get('personId')
join.actor_address = join.get('personEmail')
join.actor_label = join.get('personDisplayName')
join.channel_id = join.get('roomId')
join.stamp = join.get('created')
if queue:
logging.debug(u"- putting join to queue")
queue.put(str(join))
return join
[docs] def on_leave(self, item, queue=None):
"""
Normalizes message for the listener
:param item: attributes of the inbound message
:type item: dict
:param queue: the processing queue (optional)
:type queue: Queue
Example item received on memberships:delete::
{
'isMonitor': False,
'created': '2017-05-31T21:25:30.424Z',
'personId': 'Y2lzY29zcGFyazovL3VRiMTAtODZkYy02YzU0Yjg5ODA5N2U',
'isModerator': False,
'personOrgId': 'Y2lzY29zcGFyazovL3V0FOSVpBVElPTi9jb25zdW1lcg',
'personDisplayName': 'foo.bar@acme.com',
'personEmail': 'foo.bar@acme.com',
'roomId': 'Y2lzY29zcGFyazovL3VzL1JP3LTk5MDAtMDU5MDI2YjBiNDUz',
'id': 'Y2lzY29zcGFyazovL3VzDctMTFlNy05OTAwLTA1OTAyNmIwYjQ1Mw'
}
This function prepares a Leave and push it to the provided queue.
* ``type`` is set to ``leave``
* ``actor_id`` is a copy of ``personId``
* ``actor_address`` is a copy of ``personEmail``
* ``actor_label`` is a copy of ``personDisplayName``
* ``stamp`` is a copy of ``created``
"""
leave = Leave(item.copy())
leave.actor_id = leave.get('personId')
leave.actor_address = leave.get('personEmail')
leave.actor_label = leave.get('personDisplayName')
leave.channel_id = leave.get('roomId')
leave.stamp = leave.get('created')
if queue:
logging.debug(u"- putting leave to queue")
queue.put(str(leave))
return leave
def _to_channel(self, room):
"""
Turns a Cisco Spark room to a shellbot channel
:param room: the representation to use
:return: Channel
"""
channel = Channel()
channel.title = room.title
channel.id = room.id
channel.type = room.type
channel.is_group = True if room.type in ("group", "team") else False
channel.is_team = True if room.type == "team" else False
channel.is_direct = True if room.type == "direct" else False
channel.is_moderated = True if room.isLocked else False
try:
channel.team_id = room.teamId
except:
channel.team_id = None
return channel