Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow rich formatted message for each bot implementation. #7

Merged
merged 2 commits into from
Sep 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions sarah/bot/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from apscheduler.schedulers.background import BackgroundScheduler
from typing import Sequence, Optional, Callable, Union
from sarah.bot.types import PluginConfig, AnyFunction, CommandFunction
from sarah.bot.values import Command, CommandMessage, UserContext
from sarah.bot.values import Command, CommandMessage, UserContext, RichMessage
from sarah.thread import ThreadExecutor


Expand Down Expand Up @@ -122,7 +122,7 @@ def load_plugin(module_name: str) -> None:
else:
logging.info('Loaded plugin. %s' % module_name)

def respond(self, user_key, user_input) -> Optional[str]:
def respond(self, user_key, user_input) -> Union[RichMessage, str]:
user_context = self.user_context_map.get(user_key, None)

ret = None
Expand Down Expand Up @@ -189,7 +189,7 @@ def respond(self, user_key, user_input) -> Optional[str]:
return ret.message

else:
# String
# String or RichMessage
return ret

def find_command(self, text: str) -> Optional[Command]:
Expand Down
89 changes: 60 additions & 29 deletions sarah/bot/plugins/bmw_quotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,84 @@
from typing import Dict
from sarah.bot.values import CommandMessage
from sarah.bot.hipchat import HipChat
from sarah.bot.slack import Slack
from sarah.bot.slack import Slack, SlackMessage, MessageAttachment


# http://www.imdb.com/title/tt0105958/quotes
quotes = (("Eric: So I said to myself, 'Kyle,'\n"
"Alan: Kyle?\n"
"Eric: That's what I call myself."),
("Cory: It's hard to imagine you as a boy. \n"
"Did your parents call you Mr. Feeny?"),
("[Jack and Eric are dressed up as girls to avoid bullies]\n"
"Mr. George Feeny: Hmm, double d's, just like your grades."),
("Morgan Matthews: Mommy, if my dolly's cold, "
"can I put her in the toaster oven?\n"
"Amy Matthews: No, honey. That would be a mistake.\n"
"Morgan Matthews: Mommy?\n"
"Amy Matthews: Yes?\n"
"Morgan Matthews: I made a mistake."),
("Amy Matthews: Apparently, Cory would rather listen to the game "
"then try and understand the emotional content of Romeo & Juliet.\n"
"Cory: Mom, I'm a kid. I don't understand the emotional content "
"of Full House.\n"
"Morgan Matthews: I do."),
("Topanga: Cory, the worst thing that ever happened "
"when we were kids was that your Pop-Tart fell on the ground.\n"
"Cory: Yeah, and *you* convinced me to eat it. You said, "
"\"God made dirt, dirt won't hurt.\""))
quotes = ([('Eric', "So i said to myself, 'Kyle'"),
('Alan', "Kyle?"),
('Eric', "That's what I call myself.")],
[('Cory', "It's hard to imagine you as a boy.\n"
"Did your parents call you Mr. Feeny?")],
["[Jack and Eric are dressed up as girls to avoid bullies]",
('Feeny', "Hmm, double d's, just like your grades.")],
[('Morgan', "Mommy, if my dolly's cold, "
"Can I put her in the toaster oven?"),
('Amy', "No, honey. That would be a mistake."),
('Morgan', "Mommy?"),
('Amy', "Yes?"),
('Morgan', "I made a mistake.")],
[('Amy', "Apparently, Cory would rather listen to the "
"game than try and understand the emotional "
"content of Romeo & Juliet."),
('Cory', "Mom, I'm a kid. I don't understand the emotional content"
"of Full House."),
('Morgan', "I do.")],
[('Topanga', "Cory, the worst thing that ever happened when we were "
"kids was that your Pop-Tart fell on the ground."),
('Cory', "Yeah, and *you* convinced me to eat it. You said, "
"\"God made dirt, dirt won't hurt.\"")])


def _hipchat_message():
return "\n".join([q if isinstance(q, str) else
"%s: %s" % (q[0], q[1]) for q in random.choice(quotes)])


# noinspection PyUnusedLocal
@HipChat.command('.bmw')
def hipchat_quote(msg: CommandMessage, config: Dict) -> str:
return random.choice(quotes)
return _hipchat_message()


# noinspection PyUnusedLocal
@HipChat.schedule('bmw_quotes')
def hipchat_scheduled_quote(config: Dict) -> str:
return random.choice(quotes)
return _hipchat_message()


def _slack_message():
quote = random.choice(quotes)
if isinstance(quote[0], str):
title = quote.pop(0)
else:
title = None

color_map = {'Eric': "danger",
'Amy': "warning",
'Alan': "green",
'Cory': "green",
'Topanga': "warning",
'Morgan': "danger",
'Feeny': "danger"}
return SlackMessage(
text=title,
attachments=[
MessageAttachment(
fallback="%s : %s" % (q[0], q[1]),
pretext=q[0],
title=q[1],
color=color_map.get(q[0], "green")
) for q in quote])


# noinspection PyUnusedLocal
@Slack.command('.bmw')
def slack_quote(msg: CommandMessage, config: Dict) -> str:
return random.choice(quotes)
def slack_quote(msg: CommandMessage, config: Dict) -> SlackMessage:
return _slack_message()


# noinspection PyUnusedLocal
@Slack.schedule('bmw_quotes')
def slack_scheduled_quote(config: Dict) -> str:
return random.choice(quotes)
def slack_scheduled_quote(config: Dict) -> SlackMessage:
return _slack_message()
110 changes: 104 additions & 6 deletions sarah/bot/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from typing import Optional, Dict, Sequence
import requests
from websocket import WebSocketApp
from sarah import ValueObject

from sarah.exceptions import SarahException
from sarah.bot import Base, concurrent
from sarah.bot.values import Command
from sarah.bot.values import Command, RichMessage
from sarah.bot.types import PluginConfig


Expand Down Expand Up @@ -59,6 +60,91 @@ def request(self,
return json.loads(response.content.decode())


class AttachmentField(ValueObject):
def __init__(self, title: str, value: str, short: bool=None):
pass

def to_dict(self):
# Exclude empty fields
params = dict()
for param in self.keys():
if self[param] is None:
continue

params[param] = self[param]

return params


# https://api.slack.com/docs/attachments
class MessageAttachment(ValueObject):
def __init__(self,
fallback: str,
title: str,
title_link: str=None,
author_name: str=None,
author_link: str=None,
author_icon: str=None,
fields: Sequence[AttachmentField]=None,
image_url: str=None,
thumb_url: str=None,
pretext: str=None,
color: str=None):
pass

def to_dict(self):
# Exclude empty fields
params = dict()
for param in self.keys():
if self[param] is None:
continue

params[param] = self[param]

if 'fields' in params:
params['fields'] = [f.to_dict() for f in params['fields']]

return params


class SlackMessage(RichMessage):
def __init__(self,
text: str=None,
as_user: bool=True,
username: str=None,
parse: str="full",
link_names: int=1,
unfurl_links: bool=True,
unfurl_media: bool=False,
icon_url: str=None,
icon_emoji: str=None,
attachments: Sequence[MessageAttachment]=None):
pass

def __str__(self) -> str:
return self['text']

def to_dict(self):
# Exclude empty fields
params = dict()
for param in self.keys():
if self[param] is None:
continue

params[param] = self[param]

return params

def to_request_params(self):
params = self.to_dict()

if 'attachments' in params:
params['attachments'] = json.dumps(
[a.to_dict() for a in params['attachments']])

return params


class Slack(Base):
def __init__(self,
token: str='',
Expand Down Expand Up @@ -102,10 +188,17 @@ def add_schedule_job(self, command: Command) -> None:

def job_function() -> None:
ret = command.execute()
for channel in command.config['channels']:
self.enqueue_sending_message(self.send_message,
channel,
str(ret))
if isinstance(ret, SlackMessage):
for channel in command.config['channels']:
# TODO Error handling
data = dict({'channel': channel})
data.update(ret.to_request_params())
self.client.post('chat.postMessage', data=data)
else:
for channel in command.config['channels']:
self.enqueue_sending_message(self.send_message,
channel,
str(ret))

job_id = '%s.%s' % (command.module_name, command.name)
logging.info("Add schedule %s" % id)
Expand Down Expand Up @@ -189,7 +282,12 @@ def handle_message(self, content: Dict) -> Optional[Future]:
return

ret = self.respond(content['user'], content['text'])
if ret:
if isinstance(ret, SlackMessage):
# TODO Error handling
data = dict({'channel': content["channel"]})
data.update(ret.to_request_params())
self.client.post('chat.postMessage', data=data)
elif isinstance(ret, str):
return self.enqueue_sending_message(self.send_message,
content['channel'],
ret)
Expand Down
13 changes: 10 additions & 3 deletions sarah/bot/values/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# -*- coding: utf-8 -*-
import abc
import re
from typing import Union, Pattern, AnyStr, Callable, Sequence
from sarah import ValueObject
from sarah.bot.types import CommandFunction, CommandConfig


class RichMessage(ValueObject, metaclass=abc.ABCMeta):
@abc.abstractmethod
def __str__(self) -> str:
pass


class InputOption(ValueObject):
def __init__(self,
pattern: Union[Pattern, AnyStr],
Expand All @@ -27,16 +34,16 @@ def match(self, msg: str) -> bool:

class UserContext(ValueObject):
def __init__(self,
message: str,
message: Union[str, RichMessage],
help_message: str,
input_options: Sequence[InputOption]) -> None:
pass

def __str__(self):
return self.message
return str(self.message)

@property
def message(self) -> str:
def message(self) -> Union[str, RichMessage]:
return self['message']

@property
Expand Down
3 changes: 3 additions & 0 deletions sarah/value_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ def __eq__(self, other: Any) -> bool:

def __ne__(self, other) -> bool:
return not self.__eq__(other)

def keys(self):
return self.__stash.keys()
9 changes: 2 additions & 7 deletions tests/test_hipchat_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from sarah.bot.plugins.simple_counter import hipchat_count, \
hipchat_reset_count, reset_count
from sarah.bot.plugins.bmw_quotes import hipchat_quote, hipchat_scheduled_quote
import sarah.bot.plugins.bmw_quotes


class TestEcho(object):
Expand Down Expand Up @@ -87,14 +86,10 @@ def test_valid(self):
msg = CommandMessage(original_text='.bmw',
text='',
sender='123_homer@localhost/Oklahomer')
assert_that(sarah.bot.plugins.bmw_quotes.quotes) \
.described_as("Returned text is part of stored sample") \
.contains(hipchat_quote(msg, {}))
assert_that(hipchat_quote(msg, {})).is_type_of(str)

def test_schedule(self):
assert_that(sarah.bot.plugins.bmw_quotes.quotes) \
.described_as("Scheduled message is part of stored sample") \
.contains(hipchat_scheduled_quote({}))
assert_that(hipchat_scheduled_quote({})).is_type_of(str)


class TestHello(object):
Expand Down
7 changes: 3 additions & 4 deletions tests/test_slack_plugin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
from assertpy import assert_that
from sarah.bot.slack import SlackMessage
from sarah.bot.values import CommandMessage
from sarah.bot.plugins.bmw_quotes import slack_quote
from sarah.bot.plugins.echo import slack_echo
from sarah.bot.plugins.simple_counter import reset_count, slack_count, \
slack_reset_count
import sarah.bot.plugins.bmw_quotes


class TestEcho(object):
Expand Down Expand Up @@ -84,6 +84,5 @@ def test_valid(self):
msg = CommandMessage(original_text='.bmw',
text='',
sender='U06TXXXXX')
assert_that(sarah.bot.plugins.bmw_quotes.quotes) \
.described_as("Returned text is part of stored sample") \
.contains(slack_quote(msg, {}))
# assert_that(slack_quote(msg, {})).is_instance_of(SlackMessage)
assert_that(isinstance(slack_quote(msg, {}), SlackMessage)).is_true()