def test_emoji_name_to_emoji_code(self): # type: () -> None """ An emoji name is mapped canonically to emoji code. """ realm = get_realm('zulip') # Test active realm emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'green_tick') self.assertEqual(emoji_code, 'green_tick') self.assertEqual(reaction_type, 'realm_emoji') # Test deactivated realm emoji. emoji = RealmEmoji.objects.get(name="green_tick") emoji.deactivated = True emoji.save(update_fields=['deactivated']) with self.assertRaises(JsonableError) as exc: emoji_name_to_emoji_code(realm, 'green_tick') self.assertEqual(str(exc.exception), "Emoji 'green_tick' does not exist") # Test ':zulip:' emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'zulip') self.assertEqual(emoji_code, 'zulip') self.assertEqual(reaction_type, 'zulip_extra_emoji') # Test unicode emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'astonished') self.assertEqual(emoji_code, '1f632') self.assertEqual(reaction_type, 'unicode_emoji') # Test override unicode emoji. overriding_emoji = RealmEmoji.objects.create( name='astonished', realm=realm, file_name='astonished') emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'astonished') self.assertEqual(emoji_code, 'astonished') self.assertEqual(reaction_type, 'realm_emoji') # Test deactivate over-ridding realm emoji. overriding_emoji.deactivated = True overriding_emoji.save(update_fields=['deactivated']) emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'astonished') self.assertEqual(emoji_code, '1f632') self.assertEqual(reaction_type, 'unicode_emoji') # Test override `:zulip:` emoji. overriding_emoji = RealmEmoji.objects.create( name='zulip', realm=realm, file_name='zulip') emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'zulip') self.assertEqual(emoji_code, 'zulip') self.assertEqual(reaction_type, 'realm_emoji') # Test non-existent emoji. with self.assertRaises(JsonableError) as exc: emoji_name_to_emoji_code(realm, 'invalid_emoji') self.assertEqual(str(exc.exception), "Emoji 'invalid_emoji' does not exist")
def test_emoji_name_to_emoji_code(self) -> None: """ An emoji name is mapped canonically to emoji code. """ realm = get_realm('zulip') # Test active realm emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'green_tick') self.assertEqual(emoji_code, 'green_tick') self.assertEqual(reaction_type, 'realm_emoji') # Test deactivated realm emoji. emoji = RealmEmoji.objects.get(name="green_tick") emoji.deactivated = True emoji.save(update_fields=['deactivated']) with self.assertRaises(JsonableError) as exc: emoji_name_to_emoji_code(realm, 'green_tick') self.assertEqual(str(exc.exception), "Emoji 'green_tick' does not exist") # Test ':zulip:' emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'zulip') self.assertEqual(emoji_code, 'zulip') self.assertEqual(reaction_type, 'zulip_extra_emoji') # Test unicode emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'astonished') self.assertEqual(emoji_code, '1f632') self.assertEqual(reaction_type, 'unicode_emoji') # Test override unicode emoji. overriding_emoji = RealmEmoji.objects.create( name='astonished', realm=realm, file_name='astonished') emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'astonished') self.assertEqual(emoji_code, 'astonished') self.assertEqual(reaction_type, 'realm_emoji') # Test deactivate over-ridding realm emoji. overriding_emoji.deactivated = True overriding_emoji.save(update_fields=['deactivated']) emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'astonished') self.assertEqual(emoji_code, '1f632') self.assertEqual(reaction_type, 'unicode_emoji') # Test override `:zulip:` emoji. overriding_emoji = RealmEmoji.objects.create( name='zulip', realm=realm, file_name='zulip') emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'zulip') self.assertEqual(emoji_code, 'zulip') self.assertEqual(reaction_type, 'realm_emoji') # Test non-existent emoji. with self.assertRaises(JsonableError) as exc: emoji_name_to_emoji_code(realm, 'invalid_emoji') self.assertEqual(str(exc.exception), "Emoji 'invalid_emoji' does not exist")
def test_emoji_name_to_emoji_code(self) -> None: """ An emoji name is mapped canonically to emoji code. """ realm = get_realm("zulip") realm_emoji = RealmEmoji.objects.get(name="green_tick") # Test active realm emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "green_tick") self.assertEqual(emoji_code, str(realm_emoji.id)) self.assertEqual(reaction_type, "realm_emoji") # Test deactivated realm emoji. realm_emoji.deactivated = True realm_emoji.save(update_fields=["deactivated"]) with self.assertRaises(JsonableError) as exc: emoji_name_to_emoji_code(realm, "green_tick") self.assertEqual(str(exc.exception), "Emoji 'green_tick' does not exist") # Test ':zulip:' emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "zulip") self.assertEqual(emoji_code, "zulip") self.assertEqual(reaction_type, "zulip_extra_emoji") # Test Unicode emoji. emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "astonished") self.assertEqual(emoji_code, "1f632") self.assertEqual(reaction_type, "unicode_emoji") # Test override Unicode emoji. overriding_emoji = RealmEmoji.objects.create( name="astonished", realm=realm, file_name="astonished" ) emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "astonished") self.assertEqual(emoji_code, str(overriding_emoji.id)) self.assertEqual(reaction_type, "realm_emoji") # Test deactivate over-ridding realm emoji. overriding_emoji.deactivated = True overriding_emoji.save(update_fields=["deactivated"]) emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "astonished") self.assertEqual(emoji_code, "1f632") self.assertEqual(reaction_type, "unicode_emoji") # Test override `:zulip:` emoji. overriding_emoji = RealmEmoji.objects.create(name="zulip", realm=realm, file_name="zulip") emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "zulip") self.assertEqual(emoji_code, str(overriding_emoji.id)) self.assertEqual(reaction_type, "realm_emoji") # Test non-existent emoji. with self.assertRaises(JsonableError) as exc: emoji_name_to_emoji_code(realm, "invalid_emoji") self.assertEqual(str(exc.exception), "Emoji 'invalid_emoji' does not exist")
def add_reaction( request: HttpRequest, user_profile: UserProfile, message_id: int, emoji_name: str = REQ(), emoji_code: Optional[str] = REQ(default=None), reaction_type: str = REQ(default="unicode_emoji") ) -> HttpResponse: message, user_message = access_message(user_profile, message_id) if emoji_code is None: # The emoji_code argument is only required for rare corner # cases discussed in the long block comment below. For simple # API clients, we allow specifying just the name, and just # look up the code using the current name->code mapping. emoji_code = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[0] if Reaction.objects.filter(user_profile=user_profile, message=message, emoji_code=emoji_code, reaction_type=reaction_type).exists(): raise JsonableError(_("Reaction already exists.")) query = Reaction.objects.filter(message=message, emoji_code=emoji_code, reaction_type=reaction_type) if query.exists(): # If another user has already reacted to this message with # same emoji code, we treat the new reaction as a vote for the # existing reaction. So the emoji name used by that earlier # reaction takes precendence over whatever was passed in this # request. This is necessary to avoid a message having 2 # "different" emoji reactions with the same emoji code (and # thus same image) on the same message, which looks ugly. # # In this "voting for an existing reaction" case, we shouldn't # check whether the emoji code and emoji name match, since # it's possible that the (emoji_type, emoji_name, emoji_code) # triple for this existing rection xmay not pass validation # now (e.g. because it is for a realm emoji that has been # since deactivated). We still want to allow users to add a # vote any old reaction they see in the UI even if that is a # deactivated custom emoji, so we just use the emoji name from # the existing reaction with no further validation. emoji_name = query.first().emoji_name else: # Otherwise, use the name provided in this request, but verify # it is valid in the user's realm (e.g. not a deactivated # realm emoji). check_emoji_request(message.sender.realm, emoji_name, emoji_code, reaction_type) if user_message is None: create_historical_message(user_profile, message) do_add_reaction(user_profile, message, emoji_name, emoji_code, reaction_type) return json_success()
def add_reaction(request: HttpRequest, user_profile: UserProfile, message_id: int, emoji_name: str=REQ(), emoji_code: Optional[str]=REQ(default=None), reaction_type: str=REQ(default="unicode_emoji")) -> HttpResponse: message, user_message = access_message(user_profile, message_id) if emoji_code is None: # The emoji_code argument is only required for rare corner # cases discussed in the long block comment below. For simple # API clients, we allow specifying just the name, and just # look up the code using the current name->code mapping. emoji_code = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[0] if Reaction.objects.filter(user_profile=user_profile, message=message, emoji_code=emoji_code, reaction_type=reaction_type).exists(): raise JsonableError(_("Reaction already exists.")) query = Reaction.objects.filter(message=message, emoji_code=emoji_code, reaction_type=reaction_type) if query.exists(): # If another user has already reacted to this message with # same emoji code, we treat the new reaction as a vote for the # existing reaction. So the emoji name used by that earlier # reaction takes precendence over whatever was passed in this # request. This is necessary to avoid a message having 2 # "different" emoji reactions with the same emoji code (and # thus same image) on the same message, which looks ugly. # # In this "voting for an existing reaction" case, we shouldn't # check whether the emoji code and emoji name match, since # it's possible that the (emoji_type, emoji_name, emoji_code) # triple for this existing rection xmay not pass validation # now (e.g. because it is for a realm emoji that has been # since deactivated). We still want to allow users to add a # vote any old reaction they see in the UI even if that is a # deactivated custom emoji, so we just use the emoji name from # the existing reaction with no further validation. emoji_name = query.first().emoji_name else: # Otherwise, use the name provided in this request, but verify # it is valid in the user's realm (e.g. not a deactivated # realm emoji). check_emoji_request(message.sender.realm, emoji_name, emoji_code, reaction_type) if user_message is None: create_historical_message(user_profile, message) do_add_reaction(user_profile, message, emoji_name, emoji_code, reaction_type) return json_success()
def send_initial_realm_messages(realm: Realm) -> None: welcome_bot = get_system_bot(settings.WELCOME_BOT) # Make sure each stream created in the realm creation process has at least one message below # Order corresponds to the ordering of the streams on the left sidebar, to make the initial Home # view slightly less overwhelming welcome_messages = [ {'stream': Realm.INITIAL_PRIVATE_STREAM_NAME, 'topic': "private streams", 'content': "This is a private stream, as indicated by the " "lock icon next to the stream name. Private streams are only visible to stream members. " "\n\nTo manage this stream, go to [Stream settings](#streams/subscribed) and click on " "`%(initial_private_stream_name)s`."}, {'stream': Realm.DEFAULT_NOTIFICATION_STREAM_NAME, 'topic': "topic demonstration", 'content': "This is a message on stream #**%(default_notification_stream_name)s** with the " "topic `topic demonstration`."}, {'stream': Realm.DEFAULT_NOTIFICATION_STREAM_NAME, 'topic': "topic demonstration", 'content': "Topics are a lightweight tool to keep conversations organized. " "You can learn more about topics at [Streams and topics](/help/about-streams-and-topics). "}, {'stream': realm.DEFAULT_NOTIFICATION_STREAM_NAME, 'topic': "swimming turtles", 'content': "This is a message on stream #**%(default_notification_stream_name)s** with the " "topic `swimming turtles`. Why turtles? Why not. Who cares anyway? Excercise your right to Free Speech and don't worry about turtles. \n\n" #'content': "Why turtles? Why not. Who cares anyway? Excercise your right to Free Speech and don't worry about turtles." "\n\n[](/static/images/cute/turtle.png)" "\n\n[Start a new topic](/help/start-a-new-topic) any time you're not replying to a " "previous message."}, ] # type: List[Dict[str, str]] messages = [internal_prep_stream_message_by_name( realm, welcome_bot, message['stream'], message['topic'], message['content'] % { 'initial_private_stream_name': Realm.INITIAL_PRIVATE_STREAM_NAME, 'default_notification_stream_name': Realm.DEFAULT_NOTIFICATION_STREAM_NAME, } ) for message in welcome_messages] message_ids = do_send_messages(messages) # We find the one of our just-sent messages with turtle.png in it, # and react to it. This is a bit hacky, but works and is kinda a # 1-off thing. turtle_message = Message.objects.get( id__in=message_ids, content__icontains='cute/turtle.png') (emoji_code, reaction_type) = emoji_name_to_emoji_code(realm, 'turtle') do_add_reaction(welcome_bot, turtle_message, 'turtle', emoji_code, reaction_type)
def remove_reaction( request: HttpRequest, user_profile: UserProfile, message_id: int, emoji_name: Optional[str] = REQ(default=None), emoji_code: Optional[str] = REQ(default=None), reaction_type: str = REQ(default="unicode_emoji"), ) -> HttpResponse: message, user_message = access_message(user_profile, message_id) if emoji_code is None: if emoji_name is None: raise JsonableError( _( "At least one of the following arguments " "must be present: emoji_name, emoji_code" ) ) # A correct full Zulip client implementation should always # pass an emoji_code, because of the corner cases discussed in # the long block comments elsewhere in this file. However, to # make it easy for simple API clients to use the reactions API # without needing the mapping between emoji names and codes, # we allow instead passing the emoji_name and looking up the # corresponding code using the current data. emoji_code = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[0] if not Reaction.objects.filter( user_profile=user_profile, message=message, emoji_code=emoji_code, reaction_type=reaction_type, ).exists(): raise JsonableError(_("Reaction doesn't exist.")) # Unlike adding reactions, while deleting a reaction, we don't # check whether the provided (emoji_type, emoji_code) pair is # valid in this realm. Since there's a row in the database, we # know it was valid when the user added their reaction in the # first place, so it is safe to just remove the reaction if it # exists. And the (reaction_type, emoji_code) pair may no longer be # valid in legitimate situations (e.g. if a realm emoji was # deactivated by an administrator in the meantime). do_remove_reaction(user_profile, message, emoji_code, reaction_type) return json_success()
def test_remove_existing_reaction_with_renamed_emoji(self) -> None: """ Removes an old existing reaction but the name of emoji got changed during various emoji infra changes. """ realm = get_realm("zulip") sender = self.example_user("hamlet") emoji_code, reaction_type = emoji_name_to_emoji_code(realm, "smile") reaction_info = { "emoji_name": "smile", "emoji_code": emoji_code, "reaction_type": reaction_type, } result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info) self.assert_json_success(result) with mock.patch("zerver.lib.emoji.name_to_codepoint", name_to_codepoint={}): result = self.api_delete(sender, "/api/v1/messages/1/reactions", reaction_info) self.assert_json_success(result)
def test_remove_existing_reaction_with_renamed_emoji(self) -> None: """ Removes an old existing reaction but the name of emoji got changed during various emoji infra changes. """ realm = get_realm('zulip') sender = self.example_user("hamlet") emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'smile') reaction_info = { 'emoji_name': 'smile', 'emoji_code': emoji_code, 'reaction_type': reaction_type } result = self.api_post(sender, '/api/v1/messages/1/reactions', reaction_info) self.assert_json_success(result) with mock.patch('zerver.lib.emoji.name_to_codepoint', name_to_codepoint={}): result = self.api_delete(sender, '/api/v1/messages/1/reactions', reaction_info) self.assert_json_success(result)
def remove_reaction(request: HttpRequest, user_profile: UserProfile, message_id: int, emoji_name: Optional[str]=REQ(default=None), emoji_code: Optional[str]=REQ(default=None), reaction_type: str=REQ(default="unicode_emoji")) -> HttpResponse: message, user_message = access_message(user_profile, message_id) if emoji_code is None: if emoji_name is None: raise JsonableError(_('At least one of the following arguments ' 'must be present: emoji_name, emoji_code')) # A correct full Zulip client implementation should always # pass an emoji_code, because of the corner cases discussed in # the long block comments elsewhere in this file. However, to # make it easy for simple API clients to use the reactions API # without needing the mapping between emoji names and codes, # we allow instead passing the emoji_name and looking up the # corresponding code using the current data. emoji_code = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[0] if not Reaction.objects.filter(user_profile=user_profile, message=message, emoji_code=emoji_code, reaction_type=reaction_type).exists(): raise JsonableError(_("Reaction doesn't exist.")) # Unlike adding reactions, while deleting a reaction, we don't # check whether the provided (emoji_type, emoji_code) pair is # valid in this realm. Since there's a row in the database, we # know it was valid when the user added their reaction in the # first place, so it is safe to just remove the reaction if it # exists. And the (reaction_type, emoji_code) pair may no longer be # valid in legitimate situations (e.g. if a realm emoji was # deactivated by an administrator in the meantime). do_remove_reaction(user_profile, message, emoji_code, reaction_type) return json_success()
def update_user_status_backend( request: HttpRequest, user_profile: UserProfile, away: Optional[bool] = REQ(json_validator=check_bool, default=None), status_text: Optional[str] = REQ(str_validator=check_capped_string(60), default=None), emoji_name: Optional[str] = REQ(default=None), emoji_code: Optional[str] = REQ(default=None), # TODO: emoji_type is the more appropriate name for this parameter, but changing # that requires nontrivial work on the API documentation, since it's not clear # that the reactions endpoint would prefer such a change. emoji_type: Optional[str] = REQ("reaction_type", default=None), ) -> HttpResponse: if status_text is not None: status_text = status_text.strip() if (away is None) and (status_text is None) and (emoji_name is None): raise JsonableError(_("Client did not pass any new values.")) if emoji_name == "": # Reset the emoji_code and reaction_type if emoji_name is empty. # This should clear the user's configured emoji. emoji_code = "" emoji_type = UserStatus.UNICODE_EMOJI elif emoji_name is not None: if emoji_code is None: # The emoji_code argument is only required for rare corner # cases discussed in the long block comment below. For simple # API clients, we allow specifying just the name, and just # look up the code using the current name->code mapping. emoji_code = emoji_name_to_emoji_code(user_profile.realm, emoji_name)[0] if emoji_type is None: emoji_type = emoji_name_to_emoji_code(user_profile.realm, emoji_name)[1] elif emoji_type or emoji_code: raise JsonableError( _("Client must pass emoji_name if they pass either emoji_code or reaction_type.") ) # If we're asking to set an emoji (not clear it ("") or not adjust # it (None)), we need to verify the emoji is valid. if emoji_name not in ["", None]: assert emoji_name is not None assert emoji_code is not None assert emoji_type is not None check_emoji_request(user_profile.realm, emoji_name, emoji_code, emoji_type) client = RequestNotes.get_notes(request).client assert client is not None do_update_user_status( user_profile=user_profile, away=away, status_text=status_text, client_id=client.id, emoji_name=emoji_name, emoji_code=emoji_code, reaction_type=emoji_type, ) return json_success(request)
def send_initial_realm_messages(realm: Realm) -> None: welcome_bot = get_system_bot(settings.WELCOME_BOT, realm.id) # Make sure each stream created in the realm creation process has at least one message below # Order corresponds to the ordering of the streams on the left sidebar, to make the initial Home # view slightly less overwhelming content_of_private_streams_topic = ( _("This is a private stream, as indicated by the lock icon next to the stream name." ) + " " + _("Private streams are only visible to stream members.") + "\n" "\n" + _("To manage this stream, go to [Stream settings]({stream_settings_url}) " "and click on `{initial_private_stream_name}`.")).format( stream_settings_url="#streams/subscribed", initial_private_stream_name=Realm.INITIAL_PRIVATE_STREAM_NAME, ) content1_of_topic_demonstration_topic = (_( "This is a message on stream #**{default_notification_stream_name}** with the " "topic `topic demonstration`.")).format( default_notification_stream_name=Realm. DEFAULT_NOTIFICATION_STREAM_NAME) content2_of_topic_demonstration_topic = (_( "Topics are a lightweight tool to keep conversations organized." ) + " " + _( "You can learn more about topics at [Streams and topics]({about_topics_help_url})." )).format(about_topics_help_url="/help/about-streams-and-topics") content_of_swimming_turtles_topic = ( _("This is a message on stream #**{default_notification_stream_name}** with the " "topic `swimming turtles`.") + "\n" "\n" "[](/static/images/cute/turtle.png)" "\n" "\n" + _("[Start a new topic]({start_topic_help_url}) any time you're not replying to a \ previous message.")).format( default_notification_stream_name=Realm. DEFAULT_NOTIFICATION_STREAM_NAME, start_topic_help_url="/help/start-a-new-topic", ) welcome_messages: List[Dict[str, str]] = [ { "stream": Realm.INITIAL_PRIVATE_STREAM_NAME, "topic": "private streams", "content": content_of_private_streams_topic, }, { "stream": Realm.DEFAULT_NOTIFICATION_STREAM_NAME, "topic": "topic demonstration", "content": content1_of_topic_demonstration_topic, }, { "stream": Realm.DEFAULT_NOTIFICATION_STREAM_NAME, "topic": "topic demonstration", "content": content2_of_topic_demonstration_topic, }, { "stream": realm.DEFAULT_NOTIFICATION_STREAM_NAME, "topic": "swimming turtles", "content": content_of_swimming_turtles_topic, }, ] messages = [ internal_prep_stream_message_by_name( realm, welcome_bot, message["stream"], message["topic"], message["content"], ) for message in welcome_messages ] message_ids = do_send_messages(messages) # We find the one of our just-sent messages with turtle.png in it, # and react to it. This is a bit hacky, but works and is kinda a # 1-off thing. turtle_message = Message.objects.select_for_update().get( id__in=message_ids, content__icontains="cute/turtle.png") (emoji_code, reaction_type) = emoji_name_to_emoji_code(realm, "turtle") do_add_reaction(welcome_bot, turtle_message, "turtle", emoji_code, reaction_type)
def add_message_formatting_conversation(self) -> None: realm = get_realm("zulip") stream = ensure_stream(realm, "zulip features", acting_user=None) UserProfile.objects.filter(email__contains="stage").delete() starr = do_create_user("*****@*****.**", "password", realm, "Ada Starr", acting_user=None) self.set_avatar(starr, "static/images/characters/starr.png") fisher = do_create_user("*****@*****.**", "password", realm, "Bel Fisher", acting_user=None) self.set_avatar(fisher, "static/images/characters/fisher.png") twitter_bot = do_create_user( "*****@*****.**", "password", realm, "Twitter Bot", bot_type=UserProfile.DEFAULT_BOT, acting_user=None, ) self.set_avatar(twitter_bot, "static/images/features/twitter.png") bulk_add_subscriptions(realm, [stream], list(UserProfile.objects.filter(realm=realm)), acting_user=None) staged_messages: List[Dict[str, Any]] = [ { "sender": starr, "content": "Hey @**Bel Fisher**, check out Zulip's Markdown formatting! " "You can have:\n* bulleted lists\n * with sub-bullets too\n" "* **bold**, *italic*, and ~~strikethrough~~ text\n" "* LaTeX for mathematical formulas, both inline -- $$O(n^2)$$ -- and displayed:\n" "```math\n\\int_a^b f(t)\\, dt=F(b)-F(a)\n```", }, { "sender": fisher, "content": "My favorite is the syntax highlighting for code blocks\n" "```python\ndef fib(n: int) -> int:\n # returns the n-th Fibonacci number\n" " return fib(n-1) + fib(n-2)\n```", }, { "sender": starr, "content": "I think you forgot your base case there, Bel :laughing:\n" "```quote\n```python\ndef fib(n: int) -> int:\n # returns the n-th Fibonacci number\n" " return fib(n-1) + fib(n-2)\n```\n```", }, { "sender": fisher, "content": "I'm also a big fan of inline link, tweet, video, and image previews. " "Check out this picture of Çet Whalin[](/static/images/features/whale.png)!", }, { "sender": starr, "content": "I just set up a custom linkifier, " "so `#1234` becomes [#1234](github.com/zulip/zulip/1234), " "a link to the corresponding GitHub issue.", }, { "sender": twitter_bot, "content": "https://twitter.com/gvanrossum/status/786661035637772288", }, { "sender": fisher, "content": "Oops, the Twitter bot I set up shouldn't be posting here. Let me go fix that.", }, ] messages = [ internal_prep_stream_message( message["sender"], stream, "message formatting", message["content"], ) for message in staged_messages ] message_ids = do_send_messages(messages) preview_message = Message.objects.get( id__in=message_ids, content__icontains="image previews") (emoji_code, reaction_type) = emoji_name_to_emoji_code(realm, "whale") do_add_reaction(starr, preview_message, "whale", emoji_code, reaction_type) twitter_message = Message.objects.get(id__in=message_ids, content__icontains="gvanrossum") # Setting up a twitter integration in dev is a decent amount of work. If you need # to update this tweet, either copy the format below, or send the link to the tweet # to chat.zulip.org and ask an admin of that server to get you the rendered_content. twitter_message.rendered_content = ( "<p><a>https://twitter.com/gvanrossum/status/786661035637772288</a></p>\n" '<div class="inline-preview-twitter"><div class="twitter-tweet">' '<a><img class="twitter-avatar" ' 'src="https://pbs.twimg.com/profile_images/424495004/GuidoAvatar_bigger.jpg"></a>' "<p>Great blog post about Zulip's use of mypy: " "<a>http://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/</a></p>" "<span>- Guido van Rossum (@gvanrossum)</span></div></div>") twitter_message.save(update_fields=["rendered_content"]) # Put a short pause between the whale reaction and this, so that the # thumbs_up shows up second (emoji_code, reaction_type) = emoji_name_to_emoji_code(realm, "thumbs_up") do_add_reaction(starr, preview_message, "thumbs_up", emoji_code, reaction_type)