mirror of https://github.com/zulip/zulip.git
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:
parent
3aa8b2da40
commit
00d127965e
|
@ -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"]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue