From 65e28907cbf3fb34d2551c0b13d487384c495b45 Mon Sep 17 00:00:00 2001 From: Priyansh Garg Date: Mon, 2 Aug 2021 22:41:32 +0530 Subject: [PATCH] data_import: Import custom emoji from Rocket.Chat. --- .../zerver/help/import-from-rocketchat.md | 1 - zerver/data_import/rocketchat.py | 117 +++++++++++++++++- .../custom_emoji.chunks.bson | Bin 0 -> 38683 bytes .../custom_emoji.files.bson | Bin 0 -> 501 bytes .../rocketchat_custom_emoji.bson | Bin 0 -> 347 bytes .../rocketchat_message.bson | Bin 14766 -> 17755 bytes zerver/tests/test_rocketchat_importer.py | 113 ++++++++++++++--- 7 files changed, 209 insertions(+), 22 deletions(-) create mode 100644 zerver/tests/fixtures/rocketchat_fixtures/custom_emoji.chunks.bson create mode 100644 zerver/tests/fixtures/rocketchat_fixtures/custom_emoji.files.bson create mode 100644 zerver/tests/fixtures/rocketchat_fixtures/rocketchat_custom_emoji.bson diff --git a/templates/zerver/help/import-from-rocketchat.md b/templates/zerver/help/import-from-rocketchat.md index b67af6fcd4..421b6d28d8 100644 --- a/templates/zerver/help/import-from-rocketchat.md +++ b/templates/zerver/help/import-from-rocketchat.md @@ -83,7 +83,6 @@ root domain. Replace the last line above with the following, after replacing This import tool is currently beta and does not support importing the following data: -- Custom emoji - User avatars - Uploaded files - Default channels for new users diff --git a/zerver/data_import/rocketchat.py b/zerver/data_import/rocketchat.py index ad39d09bed..5897f694b7 100644 --- a/zerver/data_import/rocketchat.py +++ b/zerver/data_import/rocketchat.py @@ -15,6 +15,7 @@ from zerver.data_import.import_util import ( build_message, build_personal_subscriptions, build_realm, + build_realm_emoji, build_recipients, build_stream, build_stream_subscriptions, @@ -28,7 +29,7 @@ from zerver.data_import.sequencer import NEXT_ID, IdMapper from zerver.data_import.user_handler import UserHandler from zerver.lib.emoji import name_to_codepoint from zerver.lib.utils import process_list_in_batches -from zerver.models import Reaction, Recipient, UserProfile +from zerver.models import Reaction, RealmEmoji, Recipient, UserProfile def make_realm( @@ -238,18 +239,92 @@ def convert_huddle_data( return zerver_huddle +def build_custom_emoji( + realm_id: int, custom_emoji_data: Dict[str, List[Dict[str, Any]]], output_dir: str +) -> List[ZerverFieldsT]: + logging.info("Starting to process custom emoji") + + emoji_folder = os.path.join(output_dir, "emoji") + os.makedirs(emoji_folder, exist_ok=True) + + zerver_realmemoji: List[ZerverFieldsT] = [] + emoji_records: List[ZerverFieldsT] = [] + + # Map emoji file_id to emoji file data + emoji_file_data = {} + for emoji_file in custom_emoji_data["file"]: + emoji_file_data[emoji_file["_id"]] = {"filename": emoji_file["filename"], "chunks": []} + for emoji_chunk in custom_emoji_data["chunk"]: + emoji_file_data[emoji_chunk["files_id"]]["chunks"].append(emoji_chunk["data"]) + + # Build custom emoji + for rc_emoji in custom_emoji_data["emoji"]: + # Subject to change with changes in database + emoji_file_id = ".".join([rc_emoji["name"], rc_emoji["extension"]]) + + emoji_file_info = emoji_file_data[emoji_file_id] + + emoji_filename = emoji_file_info["filename"] + emoji_data = b"".join(emoji_file_info["chunks"]) + + target_sub_path = RealmEmoji.PATH_ID_TEMPLATE.format( + realm_id=realm_id, + emoji_file_name=emoji_filename, + ) + target_path = os.path.join(emoji_folder, target_sub_path) + + os.makedirs(os.path.dirname(target_path), exist_ok=True) + with open(target_path, "wb") as e_file: + e_file.write(emoji_data) + + emoji_aliases = [rc_emoji["name"]] + emoji_aliases.extend(rc_emoji["aliases"]) + + for alias in emoji_aliases: + emoji_record = dict( + path=target_path, + s3_path=target_path, + file_name=emoji_filename, + realm_id=realm_id, + name=alias, + ) + emoji_records.append(emoji_record) + + realmemoji = build_realm_emoji( + realm_id=realm_id, + name=alias, + id=NEXT_ID("realmemoji"), + file_name=emoji_filename, + ) + zerver_realmemoji.append(realmemoji) + + create_converted_data_files(emoji_records, output_dir, "/emoji/records.json") + logging.info("Done processing emoji") + + return zerver_realmemoji + + def build_reactions( total_reactions: List[ZerverFieldsT], reactions: List[Dict[str, Any]], message_id: int, + zerver_realmemoji: List[ZerverFieldsT], ) -> None: + realmemoji = {} + for emoji in zerver_realmemoji: + realmemoji[emoji["name"]] = emoji["id"] + # For the Unicode emoji codes, we use equivalent of # function 'emoji_name_to_emoji_code' in 'zerver/lib/emoji' here for reaction in reactions: emoji_name = reaction["name"] user_id = reaction["user_id"] + # Check in realm emoji + if emoji_name in realmemoji: + emoji_code = realmemoji[emoji_name] + reaction_type = Reaction.REALM_EMOJI # Check in Unicode emoji - if emoji_name in name_to_codepoint: + elif emoji_name in name_to_codepoint: emoji_code = name_to_codepoint[emoji_name] reaction_type = Reaction.UNICODE_EMOJI else: # nocoverage @@ -276,6 +351,7 @@ def process_raw_message_batch( user_handler: UserHandler, is_pm_data: bool, output_dir: str, + zerver_realmemoji: List[ZerverFieldsT], total_reactions: List[ZerverFieldsT], ) -> None: def fix_mentions(content: str, mention_user_ids: Set[int]) -> str: @@ -331,6 +407,7 @@ def process_raw_message_batch( total_reactions=total_reactions, reactions=raw_message["reactions"], message_id=message_id, + zerver_realmemoji=zerver_realmemoji, ) zerver_usermessage = make_user_messages( @@ -366,6 +443,7 @@ def process_messages( dsc_id_to_dsc_map: Dict[str, Dict[str, Any]], direct_id_to_direct_map: Dict[str, Dict[str, Any]], huddle_id_to_huddle_map: Dict[str, Dict[str, Any]], + zerver_realmemoji: List[ZerverFieldsT], total_reactions: List[ZerverFieldsT], output_dir: str, ) -> None: @@ -458,6 +536,7 @@ def process_messages( user_handler=user_handler, is_pm_data=is_pm_data, output_dir=output_dir, + zerver_realmemoji=zerver_realmemoji, total_reactions=total_reactions, ) @@ -550,6 +629,7 @@ def rocketchat_data_to_dict(rocketchat_data_dir: str) -> Dict[str, Any]: rocketchat_data["avatar"] = {"avatar": [], "file": [], "chunk": []} rocketchat_data["room"] = [] rocketchat_data["message"] = [] + rocketchat_data["custom_emoji"] = {"emoji": [], "file": [], "chunk": []} # Get instance with open(os.path.join(rocketchat_data_dir, "instances.bson"), "rb") as fcache: @@ -563,11 +643,16 @@ def rocketchat_data_to_dict(rocketchat_data_dir: str) -> Dict[str, Any]: with open(os.path.join(rocketchat_data_dir, "rocketchat_avatars.bson"), "rb") as fcache: rocketchat_data["avatar"]["avatar"] = bson.decode_all(fcache.read()) - with open(os.path.join(rocketchat_data_dir, "rocketchat_avatars.chunks.bson"), "rb") as fcache: - rocketchat_data["avatar"]["chunk"] = bson.decode_all(fcache.read()) + if rocketchat_data["avatar"]["avatar"]: + with open( + os.path.join(rocketchat_data_dir, "rocketchat_avatars.files.bson"), "rb" + ) as fcache: + rocketchat_data["avatar"]["file"] = bson.decode_all(fcache.read()) - with open(os.path.join(rocketchat_data_dir, "rocketchat_avatars.files.bson"), "rb") as fcache: - rocketchat_data["avatar"]["file"] = bson.decode_all(fcache.read()) + with open( + os.path.join(rocketchat_data_dir, "rocketchat_avatars.chunks.bson"), "rb" + ) as fcache: + rocketchat_data["avatar"]["chunk"] = bson.decode_all(fcache.read()) # Get room with open(os.path.join(rocketchat_data_dir, "rocketchat_room.bson"), "rb") as fcache: @@ -577,6 +662,17 @@ def rocketchat_data_to_dict(rocketchat_data_dir: str) -> Dict[str, Any]: with open(os.path.join(rocketchat_data_dir, "rocketchat_message.bson"), "rb") as fcache: rocketchat_data["message"] = bson.decode_all(fcache.read()) + # Get custom emoji + with open(os.path.join(rocketchat_data_dir, "rocketchat_custom_emoji.bson"), "rb") as fcache: + rocketchat_data["custom_emoji"]["emoji"] = bson.decode_all(fcache.read()) + + if rocketchat_data["custom_emoji"]["emoji"]: + with open(os.path.join(rocketchat_data_dir, "custom_emoji.files.bson"), "rb") as fcache: + rocketchat_data["custom_emoji"]["file"] = bson.decode_all(fcache.read()) + + with open(os.path.join(rocketchat_data_dir, "custom_emoji.chunks.bson"), "rb") as fcache: + rocketchat_data["custom_emoji"]["chunk"] = bson.decode_all(fcache.read()) + return rocketchat_data @@ -677,6 +773,13 @@ def do_convert_data(rocketchat_data_dir: str, output_dir: str) -> None: zerver_subscription = personal_subscriptions + stream_subscriptions + huddle_subscriptions realm["zerver_subscription"] = zerver_subscription + zerver_realmemoji = build_custom_emoji( + realm_id=realm_id, + custom_emoji_data=rocketchat_data["custom_emoji"], + output_dir=output_dir, + ) + realm["zerver_realmemoji"] = zerver_realmemoji + subscriber_map = make_subscriber_map( zerver_subscription=zerver_subscription, ) @@ -722,6 +825,7 @@ def do_convert_data(rocketchat_data_dir: str, output_dir: str) -> None: dsc_id_to_dsc_map=dsc_id_to_dsc_map, direct_id_to_direct_map=direct_id_to_direct_map, huddle_id_to_huddle_map=huddle_id_to_huddle_map, + zerver_realmemoji=zerver_realmemoji, total_reactions=total_reactions, output_dir=output_dir, ) @@ -742,6 +846,7 @@ def do_convert_data(rocketchat_data_dir: str, output_dir: str) -> None: dsc_id_to_dsc_map=dsc_id_to_dsc_map, direct_id_to_direct_map=direct_id_to_direct_map, huddle_id_to_huddle_map=huddle_id_to_huddle_map, + zerver_realmemoji=zerver_realmemoji, total_reactions=total_reactions, output_dir=output_dir, ) diff --git a/zerver/tests/fixtures/rocketchat_fixtures/custom_emoji.chunks.bson b/zerver/tests/fixtures/rocketchat_fixtures/custom_emoji.chunks.bson new file mode 100644 index 0000000000000000000000000000000000000000..46bee4dbbfa899ceae436e32668a5bfd5959c2d9 GIT binary patch literal 38683 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 b+%bvfobmKwcp`T5*1W*(O#JofLI2qRTFFpV literal 0 HcmV?d00001 diff --git a/zerver/tests/fixtures/rocketchat_fixtures/custom_emoji.files.bson b/zerver/tests/fixtures/rocketchat_fixtures/custom_emoji.files.bson new file mode 100644 index 0000000000000000000000000000000000000000..ac9826021c342fe6747d808d1ab614fbabf5ec75 GIT binary patch literal 501 zcmajbJx;?g6bJA}8Bj|l$`LpKB_EFMYz&NSup@swC#H!LMaj^N%$%ZP=O8@^cK`_& z6se6+*`A*Lw}1L?0ATWDHB9}`TD4l%E`y1&1v*jUXss&OqH7=5HNwmXY$-Cl*(DbK ze%9u2e+IBndFNISwnu=^QRlwqya>@m!QWzRX z(kKkVND0k2z}q0)eb{$}tw+Ss{(s|ke$;3-^Fw3E(u5MFIEf@>aYAAyRm3FmSe6C^ z;C(P&_#qcfv%YRBw|uT~zstW4_qFFf{M+bHaH#b5&|v literal 0 HcmV?d00001 diff --git a/zerver/tests/fixtures/rocketchat_fixtures/rocketchat_custom_emoji.bson b/zerver/tests/fixtures/rocketchat_fixtures/rocketchat_custom_emoji.bson new file mode 100644 index 0000000000000000000000000000000000000000..7f417206d8238a1e945fbe86628e07c675acc77e GIT binary patch literal 347 zcmXS9U|?X1&rD$u0y2UHQyEx+awVC`*$ga+Ihl#Y zsl{N`Oa=^WK#AmxRGejWo0P_!T~oq;pHv>+w1BsImcgkjSQuD?}` zK#5c!hFh5Ip5|^@^Vt#1R0e*;(ZP literal 0 HcmV?d00001 diff --git a/zerver/tests/fixtures/rocketchat_fixtures/rocketchat_message.bson b/zerver/tests/fixtures/rocketchat_fixtures/rocketchat_message.bson index 97739d28a00404291110009c813698b60617bed7..efdd9761b22e1a2038b83cd1e8762ecff4b0ff95 100644 GIT binary patch delta 1848 zcmb7F&2QsG6rW96Hl-5VF1vh43$zQ;?ozsT(l&`JAu&!I$4#BaNwbL~q2R`j?R?ly zY{y9u%4vmw14zDbKwOYk;<8!ax4)}Uia~9uO!b5L5a|h z(9b+gTU5&9D^8)5!kx}8PA^0|*bJlnG|xh`@2~f7d=W?xD7pufXB>IAGIBCvsZn%m ziGG2V{NJD7n0;Fx{1IRyeOpC=4T`q)o`U#A_k3Ti*MyVoj#d{ZY(>&h#E|+5dImt6 zG#pO0ho*76W2q3<#8S{I==Hv&Dn!RpRD@#c&~z>71bESsnjFq&;1#EC)ka9 zFhpSfA{fT06uC{t;W;MB{rPj7B*_KP1*E}zzSW6&f{;&Is|I#reULNz`W+^vZJBjU|-iXMOQTkJ&J%n5~8i4 zGSU?fQJ^EJ4w3NaEXE>33u2DOA`4@l_CzkD;X+339%QtcaFq2tAvYFawN=e&qLi;x z+pK1H^0ZjvGM>s!M59nKEw-F?_zY?CnW|Z+{7o*jfY1a&z5v~1!5)~+|J-Nif;)tV zaXEvrfHOFYD;&leL*p?+6KMgn7@Eol?|(GNj;3*&7-QUFFj%Tgmc~520HVPTb_26a znbY_xlb>0VusEFhI^_8I92h%7jP*XIvQq#U(F$xF>3v zLAOwsJET@l9Sxkp5uvw?d`_-X%wE1?QPNB-x~00E>W!)L-km#%UOr#yl#RX7q<6@k zbce!meCwDU9f)n$s?|*{d56_)Ba^BcQ})PCa7OK@R!WY$Tg5}uJSM$#u{%iXyij91 z?QzP@DDhmQrj#YBCbW-7JA|tycI_VKokG7z7iDEOWwvT1JIOYt<~Uh$%V~q4x^JEy zD0kTVzb6{*V0akMyNPSzlC4h_^pK69NMeo(D$o4ZM`Xor4Ry None: fixture_dir_name = self.fixture_file_name("", "rocketchat_fixtures") rocketchat_data = rocketchat_data_to_dict(fixture_dir_name) - self.assert_length(rocketchat_data, 5) + self.assert_length(rocketchat_data, 6) self.assert_length(rocketchat_data["user"], 6) self.assertEqual(rocketchat_data["user"][2]["username"], "harry.potter") @@ -41,11 +42,14 @@ class RocketChatImporter(ZulipTestCase): self.assertEqual(rocketchat_data["room"][0]["_id"], "GENERAL") self.assertEqual(rocketchat_data["room"][0]["name"], "general") - self.assert_length(rocketchat_data["message"], 52) + self.assert_length(rocketchat_data["message"], 58) self.assertEqual(rocketchat_data["message"][1]["msg"], "Hey everyone, how's it going??") self.assertEqual(rocketchat_data["message"][1]["rid"], "GENERAL") self.assertEqual(rocketchat_data["message"][1]["u"]["username"], "priyansh3133") + self.assert_length(rocketchat_data["custom_emoji"]["emoji"], 3) + self.assertEqual(rocketchat_data["custom_emoji"]["emoji"][0]["name"], "tick") + def test_map_user_id_to_user(self) -> None: fixture_dir_name = self.fixture_file_name("", "rocketchat_fixtures") rocketchat_data = rocketchat_data_to_dict(fixture_dir_name) @@ -438,6 +442,51 @@ class RocketChatImporter(ZulipTestCase): huddle_id = huddle_id_mapper.get(rc_huddle_id) self.assertEqual(subscriber_handler.get_users(huddle_id=huddle_id), {3, 4, 5}) + def test_write_emoticon_data(self) -> None: + fixture_dir_name = self.fixture_file_name("", "rocketchat_fixtures") + rocketchat_data = rocketchat_data_to_dict(fixture_dir_name) + output_dir = self.make_import_output_dir("rocketchat") + + with self.assertLogs(level="INFO"): + zerver_realmemoji = build_custom_emoji( + realm_id=3, + custom_emoji_data=rocketchat_data["custom_emoji"], + output_dir=output_dir, + ) + + self.assert_length(zerver_realmemoji, 5) + self.assertEqual(zerver_realmemoji[0]["name"], "tick") + self.assertEqual(zerver_realmemoji[0]["file_name"], "tick.png") + self.assertEqual(zerver_realmemoji[0]["realm"], 3) + self.assertEqual(zerver_realmemoji[0]["deactivated"], False) + + self.assertEqual(zerver_realmemoji[1]["name"], "check") + self.assertEqual(zerver_realmemoji[1]["file_name"], "tick.png") + self.assertEqual(zerver_realmemoji[1]["realm"], 3) + self.assertEqual(zerver_realmemoji[1]["deactivated"], False) + + self.assertEqual(zerver_realmemoji[2]["name"], "zulip") + self.assertEqual(zerver_realmemoji[2]["file_name"], "zulip.png") + self.assertEqual(zerver_realmemoji[2]["realm"], 3) + self.assertEqual(zerver_realmemoji[2]["deactivated"], False) + + records_file = os.path.join(output_dir, "emoji", "records.json") + with open(records_file, "rb") as f: + records_json = orjson.loads(f.read()) + + self.assertEqual(records_json[0]["name"], "tick") + self.assertEqual(records_json[0]["file_name"], "tick.png") + self.assertEqual(records_json[0]["realm_id"], 3) + self.assertEqual(records_json[1]["name"], "check") + self.assertEqual(records_json[1]["file_name"], "tick.png") + self.assertEqual(records_json[1]["realm_id"], 3) + self.assertTrue(os.path.isfile(records_json[0]["path"])) + + self.assertEqual(records_json[2]["name"], "zulip") + self.assertEqual(records_json[2]["file_name"], "zulip.png") + self.assertEqual(records_json[2]["realm_id"], 3) + self.assertTrue(os.path.isfile(records_json[2]["path"])) + def test_map_receiver_id_to_recipient_id(self) -> None: fixture_dir_name = self.fixture_file_name("", "rocketchat_fixtures") rocketchat_data = rocketchat_data_to_dict(fixture_dir_name) @@ -564,8 +613,8 @@ class RocketChatImporter(ZulipTestCase): private_messages=private_messages, ) - self.assert_length(rocketchat_data["message"], 52) - self.assert_length(channel_messages, 47) + self.assert_length(rocketchat_data["message"], 58) + self.assert_length(channel_messages, 53) self.assert_length(private_messages, 5) self.assertIn(rocketchat_data["message"][0], channel_messages) @@ -612,10 +661,21 @@ class RocketChatImporter(ZulipTestCase): ) # No new message added to channel or private messages - self.assert_length(channel_messages, 47) + self.assert_length(channel_messages, 53) self.assert_length(private_messages, 5) def test_build_reactions(self) -> None: + fixture_dir_name = self.fixture_file_name("", "rocketchat_fixtures") + rocketchat_data = rocketchat_data_to_dict(fixture_dir_name) + output_dir = self.make_import_output_dir("rocketchat") + + with self.assertLogs(level="INFO"): + zerver_realmemoji = build_custom_emoji( + realm_id=3, + custom_emoji_data=rocketchat_data["custom_emoji"], + output_dir=output_dir, + ) + total_reactions: List[ZerverFieldsT] = [] reactions = [ @@ -625,32 +685,53 @@ class RocketChatImporter(ZulipTestCase): {"name": "star_struck", "user_id": 4}, {"name": "heart", "user_id": 3}, {"name": "rocket", "user_id": 4}, + {"name": "check", "user_id": 2}, + {"name": "zulip", "user_id": 3}, + {"name": "harry-ron", "user_id": 4}, ] - build_reactions(total_reactions=total_reactions, reactions=reactions, message_id=3) + build_reactions( + total_reactions=total_reactions, + reactions=reactions, + message_id=3, + zerver_realmemoji=zerver_realmemoji, + ) # :grin: and :star_struck: are not present in Zulip's default # emoji set, or in Reaction.UNICODE_EMOJI reaction type. - self.assert_length(total_reactions, 4) + self.assert_length(total_reactions, 7) grinning_emoji_code = name_to_codepoint["grinning"] innocent_emoji_code = name_to_codepoint["innocent"] heart_emoji_code = name_to_codepoint["heart"] rocket_emoji_code = name_to_codepoint["rocket"] + realmemoji_code = {} + for emoji in zerver_realmemoji: + realmemoji_code[emoji["name"]] = emoji["id"] + self.assertEqual( self.get_set(total_reactions, "reaction_type"), - {Reaction.UNICODE_EMOJI}, + {Reaction.UNICODE_EMOJI, Reaction.REALM_EMOJI}, ) self.assertEqual( - self.get_set(total_reactions, "emoji_name"), {"grinning", "innocent", "heart", "rocket"} + self.get_set(total_reactions, "emoji_name"), + {"grinning", "innocent", "heart", "rocket", "check", "zulip", "harry-ron"}, ) self.assertEqual( self.get_set(total_reactions, "emoji_code"), - {grinning_emoji_code, innocent_emoji_code, heart_emoji_code, rocket_emoji_code}, + { + grinning_emoji_code, + innocent_emoji_code, + heart_emoji_code, + rocket_emoji_code, + realmemoji_code["check"], + realmemoji_code["zulip"], + realmemoji_code["harry-ron"], + }, ) self.assertEqual(self.get_set(total_reactions, "user_profile"), {2, 3, 4}) - self.assert_length(self.get_set(total_reactions, "id"), 4) + self.assert_length(self.get_set(total_reactions, "id"), 7) self.assert_length(self.get_set(total_reactions, "message"), 1) def read_file(self, team_output_dir: str, output_file: str) -> Any: @@ -670,13 +751,15 @@ class RocketChatImporter(ZulipTestCase): self.assertEqual( info_log.output, [ + "INFO:root:Starting to process custom emoji", + "INFO:root:Done processing emoji", "INFO:root:Start making tarball", "INFO:root:Done making tarball", ], ) self.assertEqual(os.path.exists(os.path.join(output_dir, "avatars")), True) - self.assertEqual(os.path.exists(os.path.join(output_dir, "emoji")), False) + self.assertEqual(os.path.exists(os.path.join(output_dir, "emoji")), True) self.assertEqual(os.path.exists(os.path.join(output_dir, "attachment.json")), True) realm = self.read_file(output_dir, "realm.json") @@ -784,12 +867,12 @@ class RocketChatImporter(ZulipTestCase): for message in messages: self.assertIsNotNone(message.rendered_content) # After removing user_joined, added_user, discussion_created, etc. - # messages. (Total messages were 44.) - self.assert_length(messages, 27) + # messages. (Total messages were 58.) + self.assert_length(messages, 31) stream_messages = messages.filter(recipient__type=Recipient.STREAM).order_by("date_sent") stream_recipients = stream_messages.values_list("recipient", flat=True) - self.assert_length(stream_messages, 22) + self.assert_length(stream_messages, 26) self.assert_length(set(stream_recipients), 5) self.assertEqual(stream_messages[0].sender.email, "priyansh3133@email.com") self.assertEqual(stream_messages[0].content, "Hey everyone, how's it going??")