snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
153
zerver/lib/widget.py
Normal file
153
zerver/lib/widget.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import json
|
||||
import re
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
from zerver.lib.message import SendMessageRequest
|
||||
from zerver.models import Message, SubMessage
|
||||
|
||||
|
||||
@dataclass
|
||||
class PollData:
|
||||
question: str
|
||||
options: list[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TodoTaskData:
|
||||
task: str
|
||||
desc: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TodoData:
|
||||
task_list_title: str
|
||||
tasks: list[TodoTaskData]
|
||||
|
||||
|
||||
def get_widget_data(content: str) -> tuple[str | None, Any]:
|
||||
valid_widget_types = ["poll", "todo"]
|
||||
tokens = re.split(r"\s+|\n+", content)
|
||||
|
||||
# tokens[0] will always exist
|
||||
if tokens[0].startswith("/"):
|
||||
widget_type = tokens[0].removeprefix("/")
|
||||
if widget_type in valid_widget_types:
|
||||
remaining_content = content.replace(tokens[0], "", 1)
|
||||
extra_data = get_extra_data_from_widget_type(remaining_content, widget_type)
|
||||
return widget_type, asdict(extra_data)
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def parse_poll_extra_data(content: str) -> PollData:
|
||||
# This is used to extract the question from the poll command.
|
||||
# The command '/poll question' will pre-set the question in the poll
|
||||
lines = content.splitlines()
|
||||
question = ""
|
||||
options = []
|
||||
if lines and lines[0]:
|
||||
question = lines.pop(0).strip()
|
||||
for line in lines:
|
||||
# If someone is using the list syntax, we remove it
|
||||
# before adding an option.
|
||||
option = re.sub(r"(\s*[-*]?\s*)", "", line.strip(), count=1)
|
||||
if len(option) > 0:
|
||||
options.append(option)
|
||||
return PollData(question=question, options=options)
|
||||
|
||||
|
||||
def parse_todo_extra_data(content: str) -> TodoData:
|
||||
# This is used to extract the task list title from the todo command.
|
||||
# The command '/todo Title' will pre-set the task list title
|
||||
lines = content.splitlines()
|
||||
task_list_title = ""
|
||||
if lines and lines[0]:
|
||||
task_list_title = lines.pop(0).strip()
|
||||
tasks = []
|
||||
for line in lines:
|
||||
# If someone is using the list syntax, we remove it
|
||||
# before adding a task.
|
||||
task_data = re.sub(r"(\s*[-*]?\s*)", "", line.strip(), count=1)
|
||||
if len(task_data) > 0:
|
||||
# a task and its description (optional) are separated
|
||||
# by the (first) `: ` substring
|
||||
task_data_array = task_data.split(": ", 1)
|
||||
tasks.append(
|
||||
TodoTaskData(
|
||||
task=task_data_array[0].strip(),
|
||||
desc=task_data_array[1].strip() if len(task_data_array) > 1 else "",
|
||||
)
|
||||
)
|
||||
return TodoData(task_list_title=task_list_title, tasks=tasks)
|
||||
|
||||
|
||||
def get_extra_data_from_widget_type(content: str, widget_type: str | None) -> PollData | TodoData:
|
||||
if widget_type == "poll":
|
||||
return parse_poll_extra_data(content)
|
||||
else:
|
||||
return parse_todo_extra_data(content)
|
||||
|
||||
|
||||
def do_widget_post_save_actions(send_request: SendMessageRequest) -> None:
|
||||
"""
|
||||
This code works with the web app; mobile and other
|
||||
clients should also start supporting this soon.
|
||||
"""
|
||||
message_content = send_request.message.content
|
||||
sender_id = send_request.message.sender_id
|
||||
message_id = send_request.message.id
|
||||
|
||||
widget_type = None
|
||||
extra_data = None
|
||||
|
||||
widget_type, extra_data = get_widget_data(message_content)
|
||||
widget_content = send_request.widget_content
|
||||
if widget_content is not None:
|
||||
# Note that we validate this data in check_message,
|
||||
# so we can trust it here.
|
||||
widget_type = widget_content["widget_type"]
|
||||
extra_data = widget_content["extra_data"]
|
||||
|
||||
if widget_type:
|
||||
content = dict(
|
||||
widget_type=widget_type,
|
||||
extra_data=extra_data,
|
||||
)
|
||||
submessage = SubMessage(
|
||||
sender_id=sender_id,
|
||||
message_id=message_id,
|
||||
msg_type="widget",
|
||||
content=json.dumps(content),
|
||||
)
|
||||
submessage.save()
|
||||
send_request.submessages = SubMessage.get_raw_db_rows([message_id])
|
||||
|
||||
|
||||
def get_widget_type(*, message_id: int) -> str | None:
|
||||
submessage = (
|
||||
SubMessage.objects.filter(
|
||||
message_id=message_id,
|
||||
msg_type="widget",
|
||||
)
|
||||
.only("content")
|
||||
.first()
|
||||
)
|
||||
|
||||
if submessage is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(submessage.content)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
return data["widget_type"]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def is_widget_message(message: Message) -> bool:
|
||||
# Right now all messages that are widgetized use submessage, and vice versa.
|
||||
return message.submessage_set.exists()
|
||||
Reference in New Issue
Block a user