portico: Extend the script to generate thread screenshots.

This commit extends the script used to generate thread
screenshots to be able to create invite_only streams,
add starred messages, create user groups, add edited
notice to messages, and send messages from notification
bots.

The window width is updated, and background for the
screenshots is changed to white.

It is also updated so that user-avatars mapping
occurs within the script itself.

This is a prep commit to #30128
This commit is contained in:
roanster007 2024-05-21 21:38:50 +05:30 committed by Tim Abbott
parent 3aa8b2da40
commit 00d127965e
2 changed files with 86 additions and 13 deletions

View File

@ -8,6 +8,7 @@ import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Dict, List, Optional from typing import Dict, List, Optional
from django.conf import settings
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
SCREENSHOTS_DIR = os.path.abspath(os.path.dirname(__file__)) SCREENSHOTS_DIR = os.path.abspath(os.path.dirname(__file__))
@ -33,9 +34,12 @@ import json
from tools.lib.test_script import prepare_puppeteer_run from tools.lib.test_script import prepare_puppeteer_run
from zerver.actions.create_user import do_create_user from zerver.actions.create_user import do_create_user
from zerver.actions.message_edit import check_update_message
from zerver.actions.message_flags import do_update_message_flags
from zerver.actions.message_send import do_send_messages, internal_prep_stream_message from zerver.actions.message_send import do_send_messages, internal_prep_stream_message
from zerver.actions.reactions import do_add_reaction from zerver.actions.reactions import do_add_reaction
from zerver.actions.streams import bulk_add_subscriptions, do_change_subscription_property from zerver.actions.streams import bulk_add_subscriptions, do_change_subscription_property
from zerver.actions.user_groups import check_add_user_group
from zerver.actions.user_settings import do_change_avatar_fields from zerver.actions.user_settings import do_change_avatar_fields
from zerver.lib.emoji import get_emoji_data from zerver.lib.emoji import get_emoji_data
from zerver.lib.message import SendMessageRequest, access_message from zerver.lib.message import SendMessageRequest, access_message
@ -45,31 +49,51 @@ from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.upload import upload_avatar_image from zerver.lib.upload import upload_avatar_image
from zerver.lib.url_encoding import topic_narrow_url from zerver.lib.url_encoding import topic_narrow_url
from zerver.models import Message, UserProfile from zerver.models import Message, UserProfile
from zerver.models.groups import NamedUserGroup
from zerver.models.realms import get_realm from zerver.models.realms import get_realm
from zerver.models.users import get_user_by_delivery_email from zerver.models.users import get_system_bot, get_user_by_delivery_email
realm = get_realm("zulip") realm = get_realm("zulip")
realm.message_content_edit_limit_seconds = None
realm.save()
DEFAULT_USER = get_user_by_delivery_email("iago@zulip.com", realm) DEFAULT_USER = get_user_by_delivery_email("iago@zulip.com", realm)
NOTIFICATION_BOT = get_system_bot(settings.NOTIFICATION_BOT, realm.id)
message_thread_ids: List[int] = [] message_thread_ids: List[int] = []
USER_AVATARS_MAP = {
"Ariella Drake": "tools/screenshots/user_avatars/AriellaDrake.png",
"Elena Garcia": "tools/screenshots/user_avatars/ElenaGarcia.jpg",
"Kevin Lin": "tools/screenshots/user_avatars/KevinLin.png",
"Zoe Davis": "tools/screenshots/user_avatars/ZoeDavis.png",
"Bo Williams": "tools/screenshots/user_avatars/BoWilliams.png",
"James Williams": "tools/screenshots/user_avatars/JamesWilliams.png",
"Manvir Singh": "tools/screenshots/user_avatars/ManvirSingh.png",
"Dal Kim": "tools/screenshots/user_avatars/DalKim.jpg",
"John Lin": "tools/screenshots/user_avatars/JohnLin.png",
"Maxy Stert": "tools/screenshots/user_avatars/MaxyStert.jpg",
}
class MessageThread(BaseModel): class MessageThread(BaseModel):
model_config = ConfigDict(frozen=True) model_config = ConfigDict(frozen=True)
sender: str sender: str
content: str content: str
starred: bool
edited: bool
reactions: Dict[str, List[str]] reactions: Dict[str, List[str]]
date: Dict[str, int] date: Dict[str, int]
def create_user(full_name: str, avatar_filename: str) -> None: def create_user(full_name: str, avatar_filename: Optional[str]) -> None:
email = f'{full_name.replace(" ", "")}@zulip.com' email = f'{full_name.replace(" ", "")}@zulip.com'
try: try:
user = UserProfile.objects.get(realm=realm, full_name=full_name) user = UserProfile.objects.get(realm=realm, full_name=full_name)
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
user = do_create_user(email, "password", realm, full_name, acting_user=DEFAULT_USER) user = do_create_user(email, "password", realm, full_name, acting_user=DEFAULT_USER)
if avatar_filename != "": if avatar_filename is not None:
set_avatar(user, avatar_filename) set_avatar(user, avatar_filename)
@ -80,9 +104,9 @@ def set_avatar(user: UserProfile, filename: str) -> None:
def create_and_subscribe_stream( def create_and_subscribe_stream(
stream_name: str, users: List[str], color: Optional[str] = None stream_name: str, users: List[str], color: Optional[str] = None, invite_only: bool = False
) -> None: ) -> None:
stream = ensure_stream(realm, stream_name, acting_user=DEFAULT_USER) stream = ensure_stream(realm, stream_name, invite_only=invite_only, acting_user=DEFAULT_USER)
bulk_add_subscriptions( bulk_add_subscriptions(
realm, realm,
[stream], [stream],
@ -112,6 +136,8 @@ def send_stream_messages(
subscriber = UserProfile.objects.get(realm=realm, id=subscriber_id) subscriber = UserProfile.objects.get(realm=realm, id=subscriber_id)
subscribers[subscriber.full_name] = subscriber subscribers[subscriber.full_name] = subscriber
subscribers["Notification Bot"] = NOTIFICATION_BOT
messages: List[Optional[SendMessageRequest]] = [] messages: List[Optional[SendMessageRequest]] = []
for message in staged_messages: for message in staged_messages:
@ -149,6 +175,14 @@ def send_stream_messages(
users = [subscribers[user_name] for user_name in user_names] users = [subscribers[user_name] for user_name in user_names]
add_message_reactions(message_id, reaction, users) add_message_reactions(message_id, reaction, users)
if message.get("starred"):
do_update_message_flags(DEFAULT_USER, "add", "starred", [message_id])
if message.get("edited"):
sender = UserProfile.objects.get(realm=realm, full_name=message["sender"])
updated_content = message["content"] + " "
check_update_message(sender, message_id, content=updated_content)
return message_ids return message_ids
@ -161,6 +195,14 @@ def add_message_reactions(message_id: int, emoji: str, users: List[UserProfile])
) )
def create_user_group(group_name: str, members: List[str]) -> None:
member_profiles = [
UserProfile.objects.get(realm=realm, full_name=member_name) for member_name in members
]
member_profiles.append(DEFAULT_USER)
check_add_user_group(realm, group_name, member_profiles, acting_user=DEFAULT_USER)
def capture_streams_narrow_screenshot( def capture_streams_narrow_screenshot(
image_path: str, stream_name: str, topic: str, unread_msg_id: int image_path: str, stream_name: str, topic: str, unread_msg_id: int
) -> None: ) -> None:
@ -193,13 +235,28 @@ try:
with open(options.thread[0]) as f: with open(options.thread[0]) as f:
threads = json.load(f) threads = json.load(f)
for thread in threads: for thread in threads:
for user, avatar in thread["users"].items(): for user in thread["users"]:
create_user(user, avatar) user_avatar = USER_AVATARS_MAP.get(user)
create_user(user, user_avatar)
if thread["recipient_type"] == "channel": if thread["recipient_type"] == "channel":
users = list(thread["users"].keys()) users = list(thread["users"].keys())
users.append("Iago") users.append("Iago")
create_and_subscribe_stream(thread["channel"], users, thread["color"])
if thread.get("user_groups") is not None:
for user_group in thread.get("user_groups"):
user_grp = NamedUserGroup.objects.filter(
name=user_group["group_name"], realm=realm
).first()
if user_grp is not None:
user_grp.delete()
create_user_group(user_group["group_name"], user_group["members"])
invite_only = (
False if thread.get("invite_only") is None else thread.get("invite_only")
)
create_and_subscribe_stream(thread["channel"], users, thread["color"], invite_only)
message_ids = send_stream_messages( message_ids = send_stream_messages(
thread["channel"], thread["topic"], thread["messages"] thread["channel"], thread["topic"], thread["messages"]
) )

View File

@ -32,7 +32,7 @@ if (options.imagePath === undefined) {
async function run() { async function run() {
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
args: [ args: [
"--window-size=1400,1024", "--window-size=500,1024",
"--no-sandbox", "--no-sandbox",
"--disable-setuid-sandbox", "--disable-setuid-sandbox",
// Helps render fonts correctly on Ubuntu: https://github.com/puppeteer/puppeteer/issues/661 // Helps render fonts correctly on Ubuntu: https://github.com/puppeteer/puppeteer/issues/661
@ -44,7 +44,7 @@ async function run() {
try { try {
const page = await browser.newPage(); const page = await browser.newPage();
// deviceScaleFactor:2 gives better quality screenshots (higher pixel density) // deviceScaleFactor:2 gives better quality screenshots (higher pixel density)
await page.setViewport({width: 1280, height: 1024, deviceScaleFactor: 2}); await page.setViewport({width: 530, height: 1024, deviceScaleFactor: 2});
await page.goto(`${options.realmUrl}/devlogin`); await page.goto(`${options.realmUrl}/devlogin`);
// wait for Iago devlogin button and click on it. // wait for Iago devlogin button and click on it.
await page.waitForSelector('[value="iago@zulip.com"]'); await page.waitForSelector('[value="iago@zulip.com"]');
@ -64,17 +64,33 @@ async function run() {
const messageListSelector = "#message-lists-container"; const messageListSelector = "#message-lists-container";
await page.waitForSelector(messageListSelector); await page.waitForSelector(messageListSelector);
// remove unread marker and don't select message
const marker = `.message-list[data-message-list-id="${CSS.escape(
message_list_id,
)}"] .unread_marker`;
await page.evaluate((sel) => {
$(sel).remove();
}, marker);
const messageSelector = `#message-row-${message_list_id}-${CSS.escape(options.messageId)}`; const messageSelector = `#message-row-${message_list_id}-${CSS.escape(options.messageId)}`;
await page.waitForSelector(messageSelector); await page.waitForSelector(messageSelector);
const messageListBox = await page.$(messageListSelector); const messageListBox = await page.$(messageListSelector);
await page.evaluate((msg) => $(msg).removeClass("selected_message"), messageSelector); await page.evaluate((msg) => $(msg).removeClass("selected_message"), messageSelector);
// This is done so as to get white background while capturing screenshots.
const background_selectors = [".app-main", ".message-feed", ".message_header"];
await page.evaluate((selectors) => {
for (const selector of selectors) {
$(selector).css("background-color", "white");
}
}, background_selectors);
// Compute screenshot area, with some padding around the message group // Compute screenshot area, with some padding around the message group
const clip = {...(await messageListBox.boundingBox())}; const clip = {...(await messageListBox.boundingBox())};
clip.x -= 5; clip.width -= 64;
clip.width += 10; clip.y += 10;
clip.y += 5; clip.height -= 8;
const imagePath = options.imagePath; const imagePath = options.imagePath;
const imageDir = path.dirname(imagePath); const imageDir = path.dirname(imagePath);
mkdirp.sync(imageDir); mkdirp.sync(imageDir);