2017-10-20 17:24:09 +02:00
|
|
|
import json
|
2017-06-21 20:43:26 +02:00
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import signal
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
import re
|
2017-07-21 17:54:34 +02:00
|
|
|
import importlib
|
2017-06-21 20:43:26 +02:00
|
|
|
from zerver.lib.actions import internal_send_message
|
2017-10-27 17:25:51 +02:00
|
|
|
from zerver.models import UserProfile
|
|
|
|
from zerver.lib.bot_storage import get_bot_state, set_bot_state, \
|
|
|
|
is_key_in_bot_state, get_bot_state_size, remove_bot_state
|
2017-07-21 17:54:34 +02:00
|
|
|
from zerver.lib.integrations import EMBEDDED_BOTS
|
2017-06-21 20:43:26 +02:00
|
|
|
|
|
|
|
from six.moves import configparser
|
|
|
|
|
|
|
|
if False:
|
|
|
|
from mypy_extensions import NoReturn
|
2017-10-12 16:34:05 +02:00
|
|
|
from typing import Any, Optional, List, Dict, Text
|
2017-06-21 20:43:26 +02:00
|
|
|
from types import ModuleType
|
|
|
|
|
|
|
|
our_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
2017-07-17 19:30:48 +02:00
|
|
|
from zulip_bots.lib import RateLimit
|
2017-06-21 20:43:26 +02:00
|
|
|
|
2017-07-21 17:54:34 +02:00
|
|
|
def get_bot_handler(service_name):
|
|
|
|
# type: (str) -> Any
|
|
|
|
|
2017-07-25 19:03:09 +02:00
|
|
|
# Check that this service is present in EMBEDDED_BOTS, add exception handling.
|
|
|
|
is_present_in_registry = any(service_name == embedded_bot_service.name for embedded_bot_service in EMBEDDED_BOTS)
|
|
|
|
if not is_present_in_registry:
|
|
|
|
return None
|
2017-07-21 17:54:34 +02:00
|
|
|
bot_module_name = 'zulip_bots.bots.%s.%s' % (service_name, service_name)
|
|
|
|
bot_module = importlib.import_module(bot_module_name) # type: Any
|
|
|
|
return bot_module.handler_class()
|
|
|
|
|
2017-10-12 16:34:05 +02:00
|
|
|
class StateHandlerError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class StateHandler(object):
|
|
|
|
state_size_limit = 10000000 # type: int # TODO: Store this in the server configuration model.
|
|
|
|
|
|
|
|
def __init__(self, user_profile):
|
|
|
|
# type: (UserProfile) -> None
|
|
|
|
self.user_profile = user_profile
|
2017-10-20 17:24:09 +02:00
|
|
|
self.marshal = lambda obj: json.dumps(obj)
|
|
|
|
self.demarshal = lambda obj: json.loads(obj)
|
2017-10-12 16:34:05 +02:00
|
|
|
|
2017-10-20 18:47:06 +02:00
|
|
|
def get(self, key):
|
2017-10-12 16:34:05 +02:00
|
|
|
# type: (Text) -> Text
|
2017-10-20 17:24:09 +02:00
|
|
|
return self.demarshal(get_bot_state(self.user_profile, key))
|
2017-10-12 16:34:05 +02:00
|
|
|
|
2017-10-20 18:47:06 +02:00
|
|
|
def put(self, key, value):
|
2017-10-12 16:34:05 +02:00
|
|
|
# type: (Text, Text) -> None
|
|
|
|
old_entry_size = get_bot_state_size(self.user_profile, key)
|
|
|
|
new_entry_size = len(key) + len(value)
|
|
|
|
old_state_size = get_bot_state_size(self.user_profile)
|
|
|
|
new_state_size = old_state_size + (new_entry_size - old_entry_size)
|
|
|
|
if new_state_size > self.state_size_limit:
|
|
|
|
raise StateHandlerError("Cannot set state. Request would require {} bytes storage. "
|
|
|
|
"The current storage limit is {}.".format(new_state_size, self.state_size_limit))
|
2017-10-20 17:24:09 +02:00
|
|
|
elif type(key) is not str:
|
|
|
|
raise StateHandlerError("Cannot set state. The key type is {}, but it should be str.".format(type(key)))
|
2017-10-12 16:34:05 +02:00
|
|
|
else:
|
2017-10-20 17:24:09 +02:00
|
|
|
marshaled_value = self.marshal(value)
|
|
|
|
if type(marshaled_value) is not str:
|
|
|
|
raise StateHandlerError("Cannot set state. The value type is {}, but it "
|
|
|
|
"should be str.".format(type(marshaled_value)))
|
|
|
|
set_bot_state(self.user_profile, key, marshaled_value)
|
2017-10-12 16:34:05 +02:00
|
|
|
|
2017-10-26 16:02:35 +02:00
|
|
|
def remove(self, key):
|
|
|
|
# type: (Text) -> None
|
|
|
|
remove_bot_state(self.user_profile, key)
|
|
|
|
|
2017-10-20 18:47:06 +02:00
|
|
|
def contains(self, key):
|
2017-10-12 16:34:05 +02:00
|
|
|
# type: (Text) -> bool
|
|
|
|
return is_key_in_bot_state(self.user_profile, key)
|
|
|
|
|
2017-06-21 20:43:26 +02:00
|
|
|
class EmbeddedBotHandler(object):
|
|
|
|
def __init__(self, user_profile):
|
|
|
|
# type: (UserProfile) -> None
|
|
|
|
# Only expose a subset of our UserProfile's functionality
|
|
|
|
self.user_profile = user_profile
|
|
|
|
self._rate_limit = RateLimit(20, 5)
|
2017-07-17 19:30:48 +02:00
|
|
|
self.full_name = user_profile.full_name
|
|
|
|
self.email = user_profile.email
|
2017-10-20 17:42:57 +02:00
|
|
|
self.storage = StateHandler(user_profile)
|
2017-06-21 20:43:26 +02:00
|
|
|
|
|
|
|
def send_message(self, message):
|
|
|
|
# type: (Dict[str, Any]) -> None
|
|
|
|
if self._rate_limit.is_legal():
|
2017-10-10 14:29:59 +02:00
|
|
|
recipients = message['to'] if message['type'] == 'stream' else ','.join(message['to'])
|
|
|
|
internal_send_message(realm=self.user_profile.realm, sender_email=self.user_profile.email,
|
|
|
|
recipient_type_name=message['type'], recipients=recipients,
|
2017-10-27 14:36:15 +02:00
|
|
|
topic_name=message.get('subject', None), content=message['content'])
|
2017-06-21 20:43:26 +02:00
|
|
|
else:
|
|
|
|
self._rate_limit.show_error_and_exit()
|
|
|
|
|
|
|
|
def send_reply(self, message, response):
|
|
|
|
# type: (Dict[str, Any], str) -> None
|
2017-07-17 19:30:48 +02:00
|
|
|
if message['type'] == 'private':
|
|
|
|
self.send_message(dict(
|
|
|
|
type='private',
|
2017-10-10 14:29:59 +02:00
|
|
|
to=[x['email'] for x in message['display_recipient']],
|
2017-07-17 19:30:48 +02:00
|
|
|
content=response,
|
|
|
|
sender_email=message['sender_email'],
|
|
|
|
))
|
|
|
|
else:
|
|
|
|
self.send_message(dict(
|
|
|
|
type='stream',
|
|
|
|
to=message['display_recipient'],
|
|
|
|
subject=message['subject'],
|
|
|
|
content=response,
|
|
|
|
sender_email=message['sender_email'],
|
|
|
|
))
|
2017-06-21 20:43:26 +02:00
|
|
|
|
|
|
|
def get_config_info(self, bot_name, section=None):
|
|
|
|
# type: (str, Optional[str]) -> Dict[str, Any]
|
|
|
|
conf_file_path = os.path.realpath(os.path.join(
|
|
|
|
our_dir, '..', 'bots', bot_name, bot_name + '.conf'))
|
|
|
|
section = section or bot_name
|
|
|
|
config = configparser.ConfigParser()
|
2017-07-28 00:59:36 +02:00
|
|
|
config.readfp(open(conf_file_path)) # type: ignore # likely typeshed issue
|
2017-06-21 20:43:26 +02:00
|
|
|
return dict(config.items(section))
|