diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 04b4cb0999..1d2a88f9ee 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -15,9 +15,9 @@ from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, get_stream_cache_key, to_dict_cache_key_id, is_super_user, \ UserActivityInterval, get_active_user_dicts_in_realm, get_active_streams, \ realm_filters_for_domain, RealmFilter, receives_offline_notifications, \ - ScheduledJob, realm_filters_for_domain, RealmFilter + ScheduledJob, realm_filters_for_domain, RealmFilter, get_active_bot_dicts_in_realm -from zerver.lib.avatar import get_avatar_url +from zerver.lib.avatar import get_avatar_url, avatar_url from guardian.shortcuts import assign_perm, remove_perm from django.db import transaction, IntegrityError @@ -96,6 +96,15 @@ def stream_user_ids(stream): return [sub['user_profile_id'] for sub in subscriptions.values('user_profile_id')] +def bot_owner_userids(user_profile): + is_private_bot = ( + user_profile.default_sending_stream and user_profile.default_sending_stream.invite_only or + user_profile.default_events_register_stream and user_profile.default_events_register_stream.invite_only) + if is_private_bot: + return (user_profile.bot_owner_id,) + else: + return active_user_ids(user_profile.realm) + def notify_created_user(user_profile): event = dict(type="realm_user", op="add", person=dict(email=user_profile.email, @@ -104,18 +113,39 @@ def notify_created_user(user_profile): is_bot=user_profile.is_bot)) send_event(event, active_user_ids(user_profile.realm)) +def notify_created_bot(user_profile): + + def stream_name(stream): + if not stream: + return None + return stream.name + + default_sending_stream_name = stream_name(user_profile.default_sending_stream) + default_events_register_stream_name = stream_name(user_profile.default_events_register_stream) + + event = dict(type="realm_bot", op="add", + bot=dict(email=user_profile.email, + full_name=user_profile.full_name, + api_key=user_profile.api_key, + default_sending_stream=default_sending_stream_name, + default_events_register_stream=default_events_register_stream_name, + default_all_public_streams=user_profile.default_all_public_streams, + avatar_url=avatar_url(user_profile), + )) + send_event(event, bot_owner_userids(user_profile)) + def do_create_user(email, password, realm, full_name, short_name, active=True, bot=False, bot_owner=None, avatar_source=UserProfile.AVATAR_FROM_GRAVATAR, default_sending_stream=None, default_events_register_stream=None, default_all_public_streams=None): event = {'type': 'user_created', - 'timestamp': time.time(), - 'full_name': full_name, - 'short_name': short_name, - 'user': email, - 'domain': realm.domain, - 'bot': bot} + 'timestamp': time.time(), + 'full_name': full_name, + 'short_name': short_name, + 'user': email, + 'domain': realm.domain, + 'bot': bot} if bot: event['bot_owner'] = bot_owner.email log_event(event) @@ -129,6 +159,8 @@ def do_create_user(email, password, realm, full_name, short_name, default_all_public_streams=default_all_public_streams) notify_created_user(user_profile) + if bot: + notify_created_bot(user_profile) return user_profile def user_sessions(user_profile): @@ -2131,6 +2163,16 @@ def get_realm_user_dicts(user_profile): 'full_name' : userdict['full_name']} for userdict in get_active_user_dicts_in_realm(user_profile.realm)] +def get_realm_bot_dicts(user_profile): + return [{'email' : botdict['email'], + 'full_name' : botdict['full_name'], + 'api_key' : botdict['api_key'], + 'default_sending_stream': botdict['default_sending_stream__name'], + 'default_events_register_stream': botdict['default_events_register_stream__name'], + 'default_all_public_streams': botdict['default_all_public_streams'], + 'avatar_url': get_avatar_url(botdict['avatar_source'], botdict['email']), + } + for botdict in get_active_bot_dicts_in_realm(user_profile.realm)] # Fetch initial data. When event_types is not specified, clients want # all event types. Whenever you add new code to this function, you @@ -2181,6 +2223,9 @@ def fetch_initial_state_data(user_profile, event_types, queue_id): if want('realm_user'): state['realm_users'] = get_realm_user_dicts(user_profile) + if want('realm_bot'): + state['realm_bots'] = get_realm_bot_dicts(user_profile) + if want('referral'): state['referrals'] = {'granted': user_profile.invites_granted, 'used': user_profile.invites_used} @@ -2221,6 +2266,11 @@ def apply_events(state, events, user_profile): for p in state['realm_users']: if our_person(p): p.update(person) + + elif event['type'] == 'realm_bot': + if event['op'] == 'add': + state['realm_bots'].append(event['bot']) + elif event['type'] == 'stream': if event['op'] == 'update': # For legacy reasons, we call stream data 'subscriptions' in diff --git a/zerver/lib/cache.py b/zerver/lib/cache.py index 496e7c67be..6a5fe25f9e 100644 --- a/zerver/lib/cache.py +++ b/zerver/lib/cache.py @@ -5,6 +5,7 @@ from functools import wraps from django.core.cache import cache as djcache from django.core.cache import get_cache from django.conf import settings +from django.db.models import Q from zerver.lib.utils import statsd, statsd_key, make_safe_digest import time @@ -239,6 +240,9 @@ def cache_save_user_profile(user_profile): def active_user_dicts_in_realm_cache_key(realm): return "active_user_dicts_in_realm:%s" % (realm.id,) +def active_bot_dicts_in_realm_cache_key(realm): + return "active_bot_dicts_in_realm:%s" % (realm.id,) + def get_stream_cache_key(stream_name, realm): from zerver.models import Realm if isinstance(realm, Realm): @@ -267,6 +271,13 @@ def flush_user_profile(sender, **kwargs): len(set(['full_name', 'short_name', 'email', 'is_active']) & set(kwargs['update_fields'])) > 0: cache_delete(active_user_dicts_in_realm_cache_key(user_profile.realm)) + # Invalidate our active_bots_in_realm info dict if any bot has changed + bot_fields = {'full_name', 'api_key', 'avatar_source', + 'default_all_public_streams', 'is_active', + 'default_sending_stream', 'default_events_register_stream'} + if user_profile.is_bot and (kwargs['update_fields'] is None or bot_fields & set(kwargs['update_fields'])): + cache_delete(active_bot_dicts_in_realm_cache_key(user_profile.realm)) + # Invalidate realm-wide alert words cache if any user in the realm has changed # alert words if kwargs['update_fields'] is None or "alert_words" in kwargs['update_fields']: @@ -282,6 +293,7 @@ def flush_realm(sender, **kwargs): if realm.deactivated: cache_delete(active_user_dicts_in_realm_cache_key(realm)) + cache_delete(active_bot_dicts_in_realm_cache_key(realm)) cache_delete(realm_alert_words_cache_key(realm)) def realm_alert_words_cache_key(realm): @@ -290,7 +302,15 @@ def realm_alert_words_cache_key(realm): # Called by models.py to flush the stream cache whenever we save a stream # object. def flush_stream(sender, **kwargs): + from zerver.models import UserProfile stream = kwargs['instance'] items_for_memcached = {} items_for_memcached[get_stream_cache_key(stream.name, stream.realm)] = (stream,) cache_set_many(items_for_memcached) + + if kwargs['update_fields'] is None or 'name' in kwargs['update_fields'] and \ + UserProfile.objects.filter( + Q(default_sending_stream=stream) | + Q(default_events_register_stream=stream) + ).exists(): + cache_delete(active_bot_dicts_in_realm_cache_key(stream.realm)) diff --git a/zerver/lib/validator.py b/zerver/lib/validator.py index a51a6d5600..22462e1f77 100644 --- a/zerver/lib/validator.py +++ b/zerver/lib/validator.py @@ -41,6 +41,14 @@ def check_bool(var_name, val): return '%s is not a boolean' % (var_name,) return None +def check_none_or(sub_validator): + def f(var_name, val): + if val is None: + return + else: + return sub_validator(var_name, val) + return f + def check_list(sub_validator, length=None): def f(var_name, val): if not isinstance(val, list): diff --git a/zerver/models.py b/zerver/models.py index 3495a38b4e..6522eb585b 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -9,7 +9,8 @@ from zerver.lib.cache import cache_with_key, flush_user_profile, flush_realm, \ user_profile_by_id_cache_key, user_profile_by_email_cache_key, \ generic_bulk_cached_fetch, cache_set, flush_stream, \ display_recipient_cache_key, cache_delete, \ - get_stream_cache_key, active_user_dicts_in_realm_cache_key + get_stream_cache_key, active_user_dicts_in_realm_cache_key, \ + active_bot_dicts_in_realm_cache_key from zerver.lib.utils import make_safe_digest, generate_random_token from django.db import transaction, IntegrityError from zerver.lib.avatar import gravatar_hash, get_avatar_url @@ -1034,6 +1035,17 @@ def get_active_user_dicts_in_realm(realm): return UserProfile.objects.filter(realm=realm, is_active=True) \ .values('id', 'full_name', 'short_name', 'email', 'is_bot') +@cache_with_key(active_bot_dicts_in_realm_cache_key, timeout=3600*24*7) +def get_active_bot_dicts_in_realm(realm): + return UserProfile.objects.filter(realm=realm, is_active=True, is_bot=True) \ + .values('id', 'full_name', 'short_name', + 'email', 'default_sending_stream__name', + 'default_events_register_stream__name', + 'default_all_public_streams', 'api_key', + 'avatar_source') \ + .select_related('default_sending_stream', + 'default_events_register_stream') + def get_prereg_user_by_email(email): # A user can be invited many times, so only return the result of the latest # invite. diff --git a/zerver/test_events.py b/zerver/test_events.py index 20d00d5fa7..c4c45967fb 100644 --- a/zerver/test_events.py +++ b/zerver/test_events.py @@ -23,6 +23,7 @@ from zerver.lib.actions import ( do_set_muted_topics, do_set_realm_name, do_update_pointer, + do_create_user, fetch_initial_state_data, ) @@ -30,7 +31,7 @@ from zerver.lib.event_queue import allocate_client_descriptor from zerver.lib.test_helpers import AuthedTestCase, POSTRequestMock from zerver.lib.validator import ( check_bool, check_dict, check_int, check_list, check_string, - equals, + equals, check_none_or ) from zerver.views import _default_all_public_streams, _default_narrow @@ -202,6 +203,8 @@ class EventsRegisterTest(AuthedTestCase): state['realm_users'] = {u['email']: u for u in state['realm_users']} state['subscriptions'] = {u['name']: u for u in state['subscriptions']} state['unsubscribed'] = {u['name']: u for u in state['unsubscribed']} + if 'realm_bots' in state: + state['realm_bots'] = {u['email']: u for u in state['realm_bots']} normalize(state1) normalize(state2) self.assertEqual(state1, state2) @@ -243,6 +246,27 @@ class EventsRegisterTest(AuthedTestCase): "https://realm.com/my_realm_filter/%(id)s")) self.do_test(lambda: do_remove_realm_filter(get_realm("zulip.com"), "#[123]")) + def test_create_bot(self): + bot_created_checker = check_dict([ + ('type', equals('realm_bot')), + ('op', equals('add')), + ('bot', check_dict([ + ('email', check_string), + ('full_name', check_string), + ('api_key', check_string), + ('default_sending_stream', check_none_or(check_string)), + ('default_events_register_stream', check_none_or(check_string)), + ('default_all_public_streams', check_bool), + ('avatar_url', check_string), + ])), + ]) + + action = lambda: do_create_user('test-bot@zulip.com', '123', get_realm('zulip.com'), + 'Test Bot', 'test', bot=True, bot_owner=self.user_profile) + events = self.do_test(action) + error = bot_created_checker('events[1]', events[1]) + self.assert_on_error(error) + def test_rename_stream(self): realm = get_realm('zulip.com') stream, _ = create_stream_if_needed(realm, 'old_name') @@ -354,7 +378,6 @@ class EventsRegisterTest(AuthedTestCase): error = stream_update_schema_checker('events[0]', events[0]) self.assert_on_error(error) - from zerver.lib.event_queue import EventQueue class EventQueueTest(TestCase): def test_one_event(self): diff --git a/zerver/tests.py b/zerver/tests.py index c995a39180..bb2bcf214a 100644 --- a/zerver/tests.py +++ b/zerver/tests.py @@ -361,9 +361,28 @@ class BotTest(AuthedTestCase): def test_add_bot(self): self.login("hamlet@zulip.com") self.assert_num_bots_equal(0) - self.create_bot() + events = [] + with tornado_redirected_to_list(events): + result = self.create_bot() self.assert_num_bots_equal(1) + event = [e for e in events if e['event']['type'] == 'realm_bot'][0] + self.assertEqual( + dict( + type='realm_bot', + op='add', + bot=dict(email='hambot-bot@zulip.com', + full_name='The Bot of Hamlet', + api_key=result['api_key'], + avatar_url=result['avatar_url'], + default_sending_stream=None, + default_events_register_stream=None, + default_all_public_streams=False, + ) + ), + event['event'] + ) + def test_add_bot_with_default_sending_stream(self): self.login("hamlet@zulip.com") self.assert_num_bots_equal(0) @@ -392,13 +411,33 @@ class BotTest(AuthedTestCase): do_make_stream_private(user_profile.realm, "Denmark") self.assert_num_bots_equal(0) - result = self.create_bot(default_sending_stream='Denmark') + events = [] + with tornado_redirected_to_list(events): + result = self.create_bot(default_sending_stream='Denmark') self.assert_num_bots_equal(1) self.assertEqual(result['default_sending_stream'], 'Denmark') profile = get_user_profile_by_email('hambot-bot@zulip.com') self.assertEqual(profile.default_sending_stream.name, 'Denmark') + event = [e for e in events if e['event']['type'] == 'realm_bot'][0] + self.assertEqual( + dict( + type='realm_bot', + op='add', + bot=dict(email='hambot-bot@zulip.com', + full_name='The Bot of Hamlet', + api_key=result['api_key'], + avatar_url=result['avatar_url'], + default_sending_stream='Denmark', + default_events_register_stream=None, + default_all_public_streams=False, + ) + ), + event['event'] + ) + self.assertEqual(event['users'], (user_profile.id,)) + def test_add_bot_with_default_sending_stream_private_denied(self): self.login("hamlet@zulip.com") user_profile = get_user_profile_by_email("hamlet@zulip.com") @@ -432,13 +471,33 @@ class BotTest(AuthedTestCase): do_make_stream_private(user_profile.realm, "Denmark") self.assert_num_bots_equal(0) - result = self.create_bot(default_events_register_stream='Denmark') + events = [] + with tornado_redirected_to_list(events): + result = self.create_bot(default_events_register_stream='Denmark') self.assert_num_bots_equal(1) self.assertEqual(result['default_events_register_stream'], 'Denmark') profile = get_user_profile_by_email('hambot-bot@zulip.com') self.assertEqual(profile.default_events_register_stream.name, 'Denmark') + event = [e for e in events if e['event']['type'] == 'realm_bot'][0] + self.assertEqual( + dict( + type='realm_bot', + op='add', + bot=dict(email='hambot-bot@zulip.com', + full_name='The Bot of Hamlet', + api_key=result['api_key'], + avatar_url=result['avatar_url'], + default_sending_stream=None, + default_events_register_stream='Denmark', + default_all_public_streams=False, + ) + ), + event['event'] + ) + self.assertEqual(event['users'], (user_profile.id,)) + def test_add_bot_with_default_events_register_stream_private_denied(self): self.login("hamlet@zulip.com") user_profile = get_user_profile_by_email("hamlet@zulip.com") diff --git a/zerver/views/__init__.py b/zerver/views/__init__.py index cd53ba8805..3e3530ed29 100644 --- a/zerver/views/__init__.py +++ b/zerver/views/__init__.py @@ -870,6 +870,7 @@ def home(request): unsubbed_info = register_ret['unsubscribed'], email_dict = register_ret['email_dict'], people_list = register_ret['realm_users'], + bot_list = register_ret['realm_bots'], initial_pointer = register_ret['pointer'], initial_presences = register_ret['presences'], initial_servertime = time.time(), # Used for calculating relative presence age