2024-03-23 04:29:56 +01:00
|
|
|
import re
|
2024-07-12 02:30:25 +02:00
|
|
|
from collections.abc import Callable, Collection, Mapping, Sequence
|
2020-11-24 12:31:28 +01:00
|
|
|
from dataclasses import dataclass, field
|
2023-11-19 19:45:19 +01:00
|
|
|
from datetime import datetime, timedelta
|
2024-07-12 02:30:25 +02:00
|
|
|
from typing import Any, TypedDict
|
2016-10-04 15:52:26 +02:00
|
|
|
|
2021-06-03 15:04:22 +02:00
|
|
|
from django.conf import settings
|
2019-03-20 04:15:58 +01:00
|
|
|
from django.db import connection
|
2023-09-26 17:34:55 +02:00
|
|
|
from django.db.models import Exists, Max, OuterRef, QuerySet, Sum
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2021-04-16 00:57:30 +02:00
|
|
|
from django.utils.translation import gettext as _
|
2020-06-09 11:57:28 +02:00
|
|
|
from psycopg2.sql import SQL
|
2018-01-22 21:50:22 +01:00
|
|
|
|
2021-07-16 22:11:10 +02:00
|
|
|
from analytics.lib.counts import COUNT_STATS
|
|
|
|
from analytics.models import RealmCount
|
2023-10-03 03:25:57 +02:00
|
|
|
from zerver.lib.cache import generic_bulk_cached_fetch, to_dict_cache_key_id
|
|
|
|
from zerver.lib.display_recipient import get_display_recipient_by_id
|
2021-09-20 10:34:10 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError, MissingAuthenticationError
|
2023-10-03 03:25:57 +02:00
|
|
|
from zerver.lib.markdown import MessageRenderingResult
|
2021-06-13 00:51:30 +02:00
|
|
|
from zerver.lib.mention import MentionData
|
2023-10-03 03:25:57 +02:00
|
|
|
from zerver.lib.message_cache import MessageDict, extract_message_dict, stringify_message_dict
|
2024-04-29 23:20:36 +02:00
|
|
|
from zerver.lib.partial import partial
|
2021-06-14 18:49:28 +02:00
|
|
|
from zerver.lib.request import RequestVariableConversionError
|
2020-09-11 16:11:06 +02:00
|
|
|
from zerver.lib.stream_subscription import (
|
2024-09-26 15:14:27 +02:00
|
|
|
get_active_subscriptions_for_stream_id,
|
2020-09-11 16:11:06 +02:00
|
|
|
get_stream_subscriptions_for_user,
|
2021-05-12 23:40:58 +02:00
|
|
|
get_subscribed_stream_recipient_ids_for_user,
|
2020-09-11 16:11:06 +02:00
|
|
|
num_subscribers_for_stream_id,
|
|
|
|
)
|
2023-06-17 17:37:04 +02:00
|
|
|
from zerver.lib.streams import can_access_stream_history, get_web_public_streams_queryset
|
2023-10-03 03:25:57 +02:00
|
|
|
from zerver.lib.topic import MESSAGE__TOPIC, TOPIC_NAME, messages_for_topic
|
|
|
|
from zerver.lib.types import UserDisplayRecipient
|
2024-09-06 16:41:41 +02:00
|
|
|
from zerver.lib.user_groups import user_has_permission_for_group_setting
|
2023-09-25 11:27:15 +02:00
|
|
|
from zerver.lib.user_topics import build_get_topic_visibility_policy, get_topic_visibility_policy
|
2023-11-08 04:53:05 +01:00
|
|
|
from zerver.lib.users import get_inaccessible_user_ids
|
2016-10-04 15:52:26 +02:00
|
|
|
from zerver.models import (
|
|
|
|
Message,
|
2024-04-18 09:52:37 +02:00
|
|
|
NamedUserGroup,
|
2017-01-18 23:19:18 +01:00
|
|
|
Realm,
|
2016-10-04 15:52:26 +02:00
|
|
|
Recipient,
|
2016-10-12 02:14:08 +02:00
|
|
|
Stream,
|
2017-08-09 02:22:00 +02:00
|
|
|
Subscription,
|
2016-10-12 02:14:08 +02:00
|
|
|
UserMessage,
|
2020-06-11 00:54:34 +02:00
|
|
|
UserProfile,
|
2023-03-26 15:36:01 +02:00
|
|
|
UserTopic,
|
2016-10-04 15:52:26 +02:00
|
|
|
)
|
2023-12-05 19:32:36 +01:00
|
|
|
from zerver.models.constants import MAX_TOPIC_NAME_LENGTH
|
2024-01-10 07:58:52 +01:00
|
|
|
from zerver.models.groups import SystemGroups
|
2023-12-15 19:52:47 +01:00
|
|
|
from zerver.models.messages import get_usermessage_by_message_id
|
2024-05-22 11:43:10 +02:00
|
|
|
from zerver.models.realms import WildcardMentionPolicyEnum
|
2024-01-10 07:58:52 +01:00
|
|
|
from zerver.models.users import is_cross_realm_bot_email
|
2016-10-04 15:52:26 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-06-09 13:31:39 +02:00
|
|
|
class MessageDetailsDict(TypedDict, total=False):
|
|
|
|
type: str
|
|
|
|
mentioned: bool
|
2024-07-12 02:30:17 +02:00
|
|
|
user_ids: list[int]
|
2021-06-09 13:31:39 +02:00
|
|
|
stream_id: int
|
|
|
|
topic: str
|
|
|
|
unmuted_stream_msg: bool
|
|
|
|
|
|
|
|
|
2021-07-09 20:34:02 +02:00
|
|
|
class RawUnreadStreamDict(TypedDict):
|
|
|
|
stream_id: int
|
|
|
|
topic: str
|
|
|
|
|
|
|
|
|
2023-06-19 17:05:53 +02:00
|
|
|
class RawUnreadDirectMessageDict(TypedDict):
|
2022-03-07 16:47:49 +01:00
|
|
|
other_user_id: int
|
2021-07-09 20:34:02 +02:00
|
|
|
|
|
|
|
|
2024-07-04 14:05:48 +02:00
|
|
|
class RawUnreadDirectMessageGroupDict(TypedDict):
|
2021-07-09 20:34:02 +02:00
|
|
|
user_ids_string: str
|
|
|
|
|
|
|
|
|
2020-05-02 06:24:43 +02:00
|
|
|
class RawUnreadMessagesResult(TypedDict):
|
2024-07-12 02:30:17 +02:00
|
|
|
pm_dict: dict[int, RawUnreadDirectMessageDict]
|
|
|
|
stream_dict: dict[int, RawUnreadStreamDict]
|
|
|
|
huddle_dict: dict[int, RawUnreadDirectMessageGroupDict]
|
|
|
|
mentions: set[int]
|
|
|
|
muted_stream_ids: set[int]
|
|
|
|
unmuted_stream_msgs: set[int]
|
2021-03-18 22:33:52 +01:00
|
|
|
old_unreads_missing: bool
|
2017-11-10 15:57:43 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2022-03-07 15:12:24 +01:00
|
|
|
class UnreadStreamInfo(TypedDict):
|
|
|
|
stream_id: int
|
|
|
|
topic: str
|
2024-07-12 02:30:17 +02:00
|
|
|
unread_message_ids: list[int]
|
2022-03-07 15:12:24 +01:00
|
|
|
|
|
|
|
|
2023-06-19 17:05:53 +02:00
|
|
|
class UnreadDirectMessageInfo(TypedDict):
|
2022-03-07 16:47:49 +01:00
|
|
|
other_user_id: int
|
|
|
|
# Deprecated and misleading synonym for other_user_id
|
2022-03-07 15:12:24 +01:00
|
|
|
sender_id: int
|
2024-07-12 02:30:17 +02:00
|
|
|
unread_message_ids: list[int]
|
2022-03-07 15:12:24 +01:00
|
|
|
|
|
|
|
|
2024-07-04 14:05:48 +02:00
|
|
|
class UnreadDirectMessageGroupInfo(TypedDict):
|
2022-03-07 15:12:24 +01:00
|
|
|
user_ids_string: str
|
2024-07-12 02:30:17 +02:00
|
|
|
unread_message_ids: list[int]
|
2022-03-07 15:12:24 +01:00
|
|
|
|
|
|
|
|
2020-05-02 06:24:43 +02:00
|
|
|
class UnreadMessagesResult(TypedDict):
|
2024-07-12 02:30:17 +02:00
|
|
|
pms: list[UnreadDirectMessageInfo]
|
|
|
|
streams: list[UnreadStreamInfo]
|
|
|
|
huddles: list[UnreadDirectMessageGroupInfo]
|
|
|
|
mentions: list[int]
|
2020-05-02 06:24:43 +02:00
|
|
|
count: int
|
2021-03-18 22:33:52 +01:00
|
|
|
old_unreads_missing: bool
|
2017-08-09 04:01:00 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-11-24 12:31:28 +01:00
|
|
|
@dataclass
|
|
|
|
class SendMessageRequest:
|
|
|
|
message: Message
|
2021-06-17 12:20:40 +02:00
|
|
|
rendering_result: MessageRenderingResult
|
2024-07-12 02:30:23 +02:00
|
|
|
stream: Stream | None
|
|
|
|
sender_muted_stream: bool | None
|
|
|
|
local_id: str | None
|
|
|
|
sender_queue_id: str | None
|
2020-11-24 12:31:28 +01:00
|
|
|
realm: Realm
|
|
|
|
mention_data: MentionData
|
2024-07-12 02:30:17 +02:00
|
|
|
mentioned_user_groups_map: dict[int, int]
|
|
|
|
active_user_ids: set[int]
|
|
|
|
online_push_user_ids: set[int]
|
|
|
|
dm_mention_push_disabled_user_ids: set[int]
|
|
|
|
dm_mention_email_disabled_user_ids: set[int]
|
|
|
|
stream_push_user_ids: set[int]
|
|
|
|
stream_email_user_ids: set[int]
|
2023-06-07 19:19:33 +02:00
|
|
|
# IDs of users who have followed the topic the message is being sent to,
|
|
|
|
# and have the followed topic push notifications setting ON.
|
2024-07-12 02:30:17 +02:00
|
|
|
followed_topic_push_user_ids: set[int]
|
2023-06-07 19:19:33 +02:00
|
|
|
# IDs of users who have followed the topic the message is being sent to,
|
|
|
|
# and have the followed topic email notifications setting ON.
|
2024-07-12 02:30:17 +02:00
|
|
|
followed_topic_email_user_ids: set[int]
|
|
|
|
muted_sender_user_ids: set[int]
|
|
|
|
um_eligible_user_ids: set[int]
|
|
|
|
long_term_idle_user_ids: set[int]
|
|
|
|
default_bot_user_ids: set[int]
|
|
|
|
service_bot_tuples: list[tuple[int, int]]
|
|
|
|
all_bot_user_ids: set[int]
|
2023-06-07 19:19:33 +02:00
|
|
|
# IDs of topic participants who should be notified of topic wildcard mention.
|
|
|
|
# The 'user_allows_notifications_in_StreamTopic' with 'wildcard_mentions_notify'
|
|
|
|
# setting ON should return True.
|
|
|
|
# A user_id can exist in either or both of the 'topic_wildcard_mention_user_ids'
|
|
|
|
# and 'topic_wildcard_mention_in_followed_topic_user_ids' sets.
|
2024-07-12 02:30:17 +02:00
|
|
|
topic_wildcard_mention_user_ids: set[int]
|
2023-06-07 19:19:33 +02:00
|
|
|
# IDs of users subscribed to the stream who should be notified of
|
|
|
|
# stream wildcard mention.
|
|
|
|
# The 'user_allows_notifications_in_StreamTopic' with 'wildcard_mentions_notify'
|
|
|
|
# setting ON should return True.
|
|
|
|
# A user_id can exist in either or both of the 'stream_wildcard_mention_user_ids'
|
|
|
|
# and 'stream_wildcard_mention_in_followed_topic_user_ids' sets.
|
2024-07-12 02:30:17 +02:00
|
|
|
stream_wildcard_mention_user_ids: set[int]
|
2023-06-07 19:19:33 +02:00
|
|
|
# IDs of topic participants who have followed the topic the message
|
|
|
|
# (having topic wildcard) is being sent to, and have the
|
|
|
|
# 'followed_topic_wildcard_mentions_notify' setting ON.
|
2024-07-12 02:30:17 +02:00
|
|
|
topic_wildcard_mention_in_followed_topic_user_ids: set[int]
|
2023-06-07 19:19:33 +02:00
|
|
|
# IDs of users who have followed the topic the message
|
|
|
|
# (having stream wildcard) is being sent to, and have the
|
|
|
|
# 'followed_topic_wildcard_mentions_notify' setting ON.
|
2024-07-12 02:30:17 +02:00
|
|
|
stream_wildcard_mention_in_followed_topic_user_ids: set[int]
|
2023-08-16 05:45:05 +02:00
|
|
|
# A topic participant is anyone who either sent or reacted to messages in the topic.
|
2024-07-12 02:30:17 +02:00
|
|
|
topic_participant_user_ids: set[int]
|
|
|
|
links_for_embed: set[str]
|
2024-07-12 02:30:23 +02:00
|
|
|
widget_content: dict[str, Any] | None
|
2024-07-12 02:30:17 +02:00
|
|
|
submessages: list[dict[str, Any]] = field(default_factory=list)
|
2024-07-12 02:30:23 +02:00
|
|
|
deliver_at: datetime | None = None
|
|
|
|
delivery_type: str | None = None
|
|
|
|
limit_unread_user_ids: set[int] | None = None
|
|
|
|
service_queue_events: dict[str, list[dict[str, Any]]] | None = None
|
2022-10-22 13:25:06 +02:00
|
|
|
disable_external_notifications: bool = False
|
2024-07-12 02:30:23 +02:00
|
|
|
automatic_new_visibility_policy: int | None = None
|
|
|
|
recipients_for_user_creation_events: dict[UserProfile, set[int]] | None = None
|
2020-11-24 12:31:28 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-08-01 20:56:40 +02:00
|
|
|
# We won't try to fetch more unread message IDs from the database than
|
|
|
|
# this limit. The limit is super high, in large part because it means
|
|
|
|
# client-side code mostly doesn't need to think about the case that a
|
|
|
|
# user has more older unread messages that were cut off.
|
|
|
|
MAX_UNREAD_MESSAGES = 50000
|
2017-08-01 18:28:56 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-03-28 07:46:04 +01:00
|
|
|
def truncate_content(content: str, max_length: int, truncation_message: str) -> str:
|
|
|
|
if len(content) > max_length:
|
2021-02-12 08:19:30 +01:00
|
|
|
content = content[: max_length - len(truncation_message)] + truncation_message
|
2020-03-28 07:46:04 +01:00
|
|
|
return content
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-12-19 02:36:50 +01:00
|
|
|
def normalize_body(body: str) -> str:
|
2021-07-05 17:55:02 +02:00
|
|
|
body = body.rstrip().lstrip("\n")
|
2020-12-19 02:36:50 +01:00
|
|
|
if len(body) == 0:
|
|
|
|
raise JsonableError(_("Message must not be empty"))
|
2021-02-12 08:20:45 +01:00
|
|
|
if "\x00" in body:
|
2020-12-19 02:36:50 +01:00
|
|
|
raise JsonableError(_("Message must not contain null bytes"))
|
2021-06-03 15:04:22 +02:00
|
|
|
return truncate_content(body, settings.MAX_MESSAGE_LENGTH, "\n[message truncated]")
|
2020-03-28 07:46:04 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-10-21 16:09:02 +02:00
|
|
|
def normalize_body_for_import(body: str) -> str:
|
|
|
|
if "\x00" in body:
|
|
|
|
body = re.sub(r"\x00", "", body)
|
|
|
|
return truncate_content(body, settings.MAX_MESSAGE_LENGTH, "\n[message truncated]")
|
|
|
|
|
|
|
|
|
2024-01-14 14:38:50 +01:00
|
|
|
def truncate_topic(topic_name: str) -> str:
|
|
|
|
return truncate_content(topic_name, MAX_TOPIC_NAME_LENGTH, "...")
|
2020-03-28 07:46:04 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def messages_for_ids(
|
2024-07-12 02:30:17 +02:00
|
|
|
message_ids: list[int],
|
|
|
|
user_message_flags: dict[int, list[str]],
|
|
|
|
search_fields: dict[int, dict[str, str]],
|
2021-02-12 08:19:30 +01:00
|
|
|
apply_markdown: bool,
|
|
|
|
client_gravatar: bool,
|
|
|
|
allow_edit_history: bool,
|
2024-07-12 02:30:23 +02:00
|
|
|
user_profile: UserProfile | None,
|
2023-11-08 04:53:05 +01:00
|
|
|
realm: Realm,
|
2024-07-12 02:30:17 +02:00
|
|
|
) -> list[dict[str, Any]]:
|
2021-02-12 08:20:45 +01:00
|
|
|
id_fetcher = lambda row: row["id"]
|
2017-11-07 17:36:29 +01:00
|
|
|
|
2019-08-08 21:34:06 +02:00
|
|
|
message_dicts = generic_bulk_cached_fetch(
|
|
|
|
to_dict_cache_key_id,
|
2023-10-13 03:53:42 +02:00
|
|
|
MessageDict.ids_to_dict,
|
2019-08-08 21:34:06 +02:00
|
|
|
message_ids,
|
|
|
|
id_fetcher=id_fetcher,
|
2023-10-13 03:53:42 +02:00
|
|
|
cache_transformer=lambda obj: obj,
|
2019-08-08 21:34:06 +02:00
|
|
|
extractor=extract_message_dict,
|
2021-02-12 08:19:30 +01:00
|
|
|
setter=stringify_message_dict,
|
|
|
|
)
|
2017-11-07 17:36:29 +01:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
message_list: list[dict[str, Any]] = []
|
2017-11-07 17:36:29 +01:00
|
|
|
|
2023-11-08 04:53:05 +01:00
|
|
|
sender_ids = [message_dicts[message_id]["sender_id"] for message_id in message_ids]
|
|
|
|
inaccessible_sender_ids = get_inaccessible_user_ids(sender_ids, user_profile)
|
|
|
|
|
2017-11-07 17:36:29 +01:00
|
|
|
for message_id in message_ids:
|
|
|
|
msg_dict = message_dicts[message_id]
|
2023-11-03 15:20:44 +01:00
|
|
|
flags = user_message_flags[message_id]
|
|
|
|
# TODO/compatibility: The `wildcard_mentioned` flag was deprecated in favor of
|
|
|
|
# the `stream_wildcard_mentioned` and `topic_wildcard_mentioned` flags. The
|
|
|
|
# `wildcard_mentioned` flag exists for backwards-compatibility with older
|
|
|
|
# clients. Remove this when we no longer support legacy clients that have not
|
|
|
|
# been updated to access `stream_wildcard_mentioned`.
|
|
|
|
if "stream_wildcard_mentioned" in flags or "topic_wildcard_mentioned" in flags:
|
|
|
|
flags.append("wildcard_mentioned")
|
|
|
|
msg_dict.update(flags=flags)
|
2017-11-07 17:36:29 +01:00
|
|
|
if message_id in search_fields:
|
|
|
|
msg_dict.update(search_fields[message_id])
|
|
|
|
# Make sure that we never send message edit history to clients
|
|
|
|
# in realms with allow_edit_history disabled.
|
|
|
|
if "edit_history" in msg_dict and not allow_edit_history:
|
|
|
|
del msg_dict["edit_history"]
|
2023-11-08 04:53:05 +01:00
|
|
|
msg_dict["can_access_sender"] = msg_dict["sender_id"] not in inaccessible_sender_ids
|
2017-11-07 17:36:29 +01:00
|
|
|
message_list.append(msg_dict)
|
|
|
|
|
2023-11-08 04:53:05 +01:00
|
|
|
MessageDict.post_process_dicts(message_list, apply_markdown, client_gravatar, realm)
|
2017-11-07 17:36:29 +01:00
|
|
|
|
|
|
|
return message_list
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def access_message(
|
2021-06-03 07:04:25 +02:00
|
|
|
user_profile: UserProfile,
|
|
|
|
message_id: int,
|
|
|
|
lock_message: bool = False,
|
2024-04-11 22:48:10 +02:00
|
|
|
) -> Message:
|
2016-10-12 02:14:08 +02:00
|
|
|
"""You can access a message by ID in our APIs that either:
|
|
|
|
(1) You received or have previously accessed via starring
|
|
|
|
(aka have a UserMessage row for).
|
|
|
|
(2) Was sent to a public stream in your realm.
|
|
|
|
|
|
|
|
We produce consistent, boring error messages to avoid leaking any
|
|
|
|
information from a security perspective.
|
2021-06-03 07:04:25 +02:00
|
|
|
|
|
|
|
The lock_message parameter should be passed by callers that are
|
|
|
|
planning to modify the Message object. This will use the SQL
|
|
|
|
`SELECT FOR UPDATE` feature to ensure that other processes cannot
|
|
|
|
delete the message during the current transaction, which is
|
|
|
|
important to prevent rare race conditions. Callers must only
|
|
|
|
pass lock_message when inside a @transaction.atomic block.
|
2016-10-12 02:14:08 +02:00
|
|
|
"""
|
|
|
|
try:
|
2023-08-01 16:12:18 +02:00
|
|
|
base_query = Message.objects.select_related(*Message.DEFAULT_SELECT_RELATED)
|
2021-06-03 15:46:13 +02:00
|
|
|
if lock_message:
|
2021-06-03 07:04:25 +02:00
|
|
|
# We want to lock only the `Message` row, and not the related fields
|
|
|
|
# because the `Message` row only has a possibility of races.
|
|
|
|
base_query = base_query.select_for_update(of=("self",))
|
|
|
|
message = base_query.get(id=message_id)
|
2016-10-12 02:14:08 +02:00
|
|
|
except Message.DoesNotExist:
|
|
|
|
raise JsonableError(_("Invalid message(s)"))
|
|
|
|
|
2024-04-11 22:48:10 +02:00
|
|
|
has_user_message = lambda: UserMessage.objects.filter(
|
|
|
|
user_profile=user_profile, message_id=message_id
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
if has_message_access(user_profile, message, has_user_message=has_user_message):
|
|
|
|
return message
|
|
|
|
raise JsonableError(_("Invalid message(s)"))
|
|
|
|
|
|
|
|
|
|
|
|
def access_message_and_usermessage(
|
|
|
|
user_profile: UserProfile,
|
|
|
|
message_id: int,
|
|
|
|
lock_message: bool = False,
|
2024-07-12 02:30:23 +02:00
|
|
|
) -> tuple[Message, UserMessage | None]:
|
2024-04-11 22:48:10 +02:00
|
|
|
"""As access_message, but also returns the usermessage, if any."""
|
|
|
|
try:
|
|
|
|
base_query = Message.objects.select_related(*Message.DEFAULT_SELECT_RELATED)
|
|
|
|
if lock_message:
|
|
|
|
# We want to lock only the `Message` row, and not the related fields
|
|
|
|
# because the `Message` row only has a possibility of races.
|
|
|
|
base_query = base_query.select_for_update(of=("self",))
|
|
|
|
message = base_query.get(id=message_id)
|
|
|
|
except Message.DoesNotExist:
|
|
|
|
raise JsonableError(_("Invalid message(s)"))
|
|
|
|
|
|
|
|
user_message = get_usermessage_by_message_id(user_profile, message_id)
|
|
|
|
has_user_message = lambda: user_message is not None
|
2024-03-22 06:45:17 +01:00
|
|
|
|
|
|
|
if has_message_access(user_profile, message, has_user_message=has_user_message):
|
2024-04-11 22:48:10 +02:00
|
|
|
return (message, user_message)
|
2018-07-27 12:28:42 +02:00
|
|
|
raise JsonableError(_("Invalid message(s)"))
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-09-20 10:34:10 +02:00
|
|
|
def access_web_public_message(
|
|
|
|
realm: Realm,
|
|
|
|
message_id: int,
|
|
|
|
) -> Message:
|
|
|
|
"""Access control method for unauthenticated requests interacting
|
2022-01-29 00:54:13 +01:00
|
|
|
with a message in web-public streams.
|
2021-09-20 10:34:10 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
# We throw a MissingAuthenticationError for all errors in this
|
|
|
|
# code path, to avoid potentially leaking information on whether a
|
|
|
|
# message with the provided ID exists on the server if the client
|
|
|
|
# shouldn't have access to it.
|
|
|
|
if not realm.web_public_streams_enabled():
|
2023-02-04 02:07:20 +01:00
|
|
|
raise MissingAuthenticationError
|
2021-09-20 10:34:10 +02:00
|
|
|
|
|
|
|
try:
|
2023-08-01 16:12:18 +02:00
|
|
|
message = Message.objects.select_related(*Message.DEFAULT_SELECT_RELATED).get(id=message_id)
|
2021-09-20 10:34:10 +02:00
|
|
|
except Message.DoesNotExist:
|
2023-02-04 02:07:20 +01:00
|
|
|
raise MissingAuthenticationError
|
2021-09-20 10:34:10 +02:00
|
|
|
|
|
|
|
if not message.is_stream_message():
|
2023-02-04 02:07:20 +01:00
|
|
|
raise MissingAuthenticationError
|
2021-09-20 10:34:10 +02:00
|
|
|
|
|
|
|
queryset = get_web_public_streams_queryset(realm)
|
|
|
|
try:
|
|
|
|
stream = queryset.get(id=message.recipient.type_id)
|
|
|
|
except Stream.DoesNotExist:
|
2023-02-04 02:07:20 +01:00
|
|
|
raise MissingAuthenticationError
|
2021-09-20 10:34:10 +02:00
|
|
|
|
|
|
|
# These should all have been enforced by the code in
|
|
|
|
# get_web_public_streams_queryset
|
|
|
|
assert stream.is_web_public
|
|
|
|
assert not stream.deactivated
|
|
|
|
assert not stream.invite_only
|
|
|
|
assert stream.history_public_to_subscribers
|
|
|
|
|
|
|
|
# Now that we've confirmed this message was sent to the target
|
|
|
|
# web-public stream, we can return it as having been successfully
|
|
|
|
# accessed.
|
|
|
|
return message
|
|
|
|
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def has_message_access(
|
2021-05-12 00:31:03 +02:00
|
|
|
user_profile: UserProfile,
|
|
|
|
message: Message,
|
|
|
|
*,
|
2024-03-22 07:04:07 +01:00
|
|
|
has_user_message: Callable[[], bool],
|
2024-07-12 02:30:23 +02:00
|
|
|
stream: Stream | None = None,
|
|
|
|
is_subscribed: bool | None = None,
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> bool:
|
2021-05-12 00:31:03 +02:00
|
|
|
"""
|
|
|
|
Returns whether a user has access to a given message.
|
|
|
|
|
2021-05-18 14:44:05 +02:00
|
|
|
* The user_message parameter must be provided if the user has a UserMessage
|
2021-05-12 00:31:03 +02:00
|
|
|
row for the target message.
|
|
|
|
* The optional stream parameter is validated; is_subscribed is not.
|
|
|
|
"""
|
|
|
|
|
2021-05-12 00:21:24 +02:00
|
|
|
if message.recipient.type != Recipient.STREAM:
|
2023-06-15 00:39:53 +02:00
|
|
|
# You can only access direct messages you received
|
2024-03-22 07:04:07 +01:00
|
|
|
return has_user_message()
|
2021-05-12 00:21:24 +02:00
|
|
|
|
2021-05-12 00:31:03 +02:00
|
|
|
if stream is None:
|
|
|
|
stream = Stream.objects.get(id=message.recipient.type_id)
|
|
|
|
else:
|
|
|
|
assert stream.recipient_id == message.recipient_id
|
|
|
|
|
2023-01-10 22:41:07 +01:00
|
|
|
if stream.realm_id != user_profile.realm_id:
|
2021-05-12 00:21:24 +02:00
|
|
|
# You can't access public stream messages in other realms
|
|
|
|
return False
|
|
|
|
|
2024-06-05 16:45:52 +02:00
|
|
|
if stream.deactivated:
|
|
|
|
# You can't access messages in deactivated streams
|
|
|
|
return False
|
|
|
|
|
2023-06-15 00:39:53 +02:00
|
|
|
def is_subscribed_helper() -> bool:
|
|
|
|
if is_subscribed is not None:
|
|
|
|
return is_subscribed
|
|
|
|
|
|
|
|
return Subscription.objects.filter(
|
|
|
|
user_profile=user_profile, active=True, recipient=message.recipient
|
|
|
|
).exists()
|
2021-05-12 00:21:24 +02:00
|
|
|
|
|
|
|
if stream.is_public() and user_profile.can_access_public_streams():
|
|
|
|
return True
|
|
|
|
|
2023-06-15 00:39:53 +02:00
|
|
|
if not stream.is_history_public_to_subscribers():
|
|
|
|
# Unless history is public to subscribers, you need to both:
|
|
|
|
# (1) Have directly received the message.
|
|
|
|
# AND
|
|
|
|
# (2) Be subscribed to the stream.
|
2024-03-22 07:04:07 +01:00
|
|
|
return has_user_message() and is_subscribed_helper()
|
2021-05-12 00:31:03 +02:00
|
|
|
|
2023-06-15 00:39:53 +02:00
|
|
|
# is_history_public_to_subscribers, so check if you're subscribed
|
|
|
|
return is_subscribed_helper()
|
2016-10-12 02:14:08 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-09-26 15:14:27 +02:00
|
|
|
def event_recipient_ids_for_action_on_messages(
|
|
|
|
messages: list[Message],
|
|
|
|
*,
|
|
|
|
channel: Stream | None = None,
|
|
|
|
exclude_long_term_idle_users: bool = True,
|
|
|
|
) -> set[int]:
|
|
|
|
"""Returns IDs of users who should receive events when an action
|
|
|
|
(delete, react, etc) is performed on given set of messages, which
|
|
|
|
are expected to all be in a single conversation.
|
|
|
|
|
|
|
|
This function aligns with the 'has_message_access' above to ensure
|
|
|
|
that events reach only those users who have access to the messages.
|
|
|
|
|
|
|
|
Notably, for performance reasons, we do not send live-update
|
|
|
|
events to everyone who could potentially have a cached copy of a
|
|
|
|
message because they fetched messages in a public channel to which
|
|
|
|
they are not subscribed. Such events are limited to those messages
|
|
|
|
where the user has a UserMessage row (including `historical` rows).
|
|
|
|
"""
|
|
|
|
assert len(messages) > 0
|
|
|
|
message_ids = [message.id for message in messages]
|
|
|
|
|
|
|
|
def get_user_ids_having_usermessage_row_for_messages(message_ids: list[int]) -> set[int]:
|
|
|
|
"""Returns the IDs of users who actually received the messages."""
|
|
|
|
usermessages = UserMessage.objects.filter(message_id__in=message_ids)
|
|
|
|
if exclude_long_term_idle_users:
|
|
|
|
usermessages = usermessages.exclude(user_profile__long_term_idle=True)
|
|
|
|
return set(usermessages.values_list("user_profile_id", flat=True))
|
|
|
|
|
|
|
|
sample_message = messages[0]
|
|
|
|
if not sample_message.is_stream_message():
|
|
|
|
# For DM, event is sent to users who actually received the message.
|
|
|
|
return get_user_ids_having_usermessage_row_for_messages(message_ids)
|
|
|
|
|
|
|
|
channel_id = sample_message.recipient.type_id
|
|
|
|
if channel is None:
|
|
|
|
channel = Stream.objects.get(id=channel_id)
|
|
|
|
|
|
|
|
subscriptions = get_active_subscriptions_for_stream_id(
|
|
|
|
channel_id, include_deactivated_users=False
|
|
|
|
)
|
|
|
|
if exclude_long_term_idle_users:
|
|
|
|
subscriptions = subscriptions.exclude(user_profile__long_term_idle=True)
|
|
|
|
subscriber_ids = set(subscriptions.values_list("user_profile_id", flat=True))
|
|
|
|
|
|
|
|
if not channel.is_history_public_to_subscribers():
|
|
|
|
# For protected history, only users who are subscribed and
|
|
|
|
# received the original message are notified.
|
|
|
|
assert not channel.is_public()
|
|
|
|
user_ids_with_usermessage_row = get_user_ids_having_usermessage_row_for_messages(
|
|
|
|
message_ids
|
|
|
|
)
|
|
|
|
return user_ids_with_usermessage_row & subscriber_ids
|
|
|
|
|
|
|
|
if not channel.is_public():
|
|
|
|
# For private channel with shared history, the set of
|
|
|
|
# users with access is exactly the subscribers.
|
|
|
|
return subscriber_ids
|
|
|
|
|
|
|
|
# The remaining case is public channels with public history. Events are sent to:
|
|
|
|
# 1. Current channel subscribers
|
|
|
|
# 2. Unsubscribed users having usermessage row & channel access.
|
|
|
|
# * Users who never subscribed but starred or reacted on messages
|
|
|
|
# (usermessages with historical flag exists for such cases).
|
|
|
|
# * Users who were initially subscribed and later unsubscribed
|
|
|
|
# (usermessages exist for messages they received while subscribed).
|
|
|
|
usermessage_rows = UserMessage.objects.filter(message_id__in=message_ids).exclude(
|
|
|
|
# Excluding guests here implements can_access_public_channels,
|
|
|
|
# since we already know realm.is_zephyr_mirror_realm is false,
|
|
|
|
# based on the value of is_history_public_to_subscribers.
|
|
|
|
user_profile__role=UserProfile.ROLE_GUEST
|
|
|
|
)
|
|
|
|
if exclude_long_term_idle_users:
|
|
|
|
usermessage_rows = usermessage_rows.exclude(user_profile__long_term_idle=True)
|
|
|
|
user_ids_with_usermessage_row_and_channel_access = set(
|
|
|
|
usermessage_rows.values_list("user_profile_id", flat=True)
|
|
|
|
)
|
|
|
|
return user_ids_with_usermessage_row_and_channel_access | subscriber_ids
|
|
|
|
|
|
|
|
|
2021-05-12 23:21:39 +02:00
|
|
|
def bulk_access_messages(
|
2024-04-17 05:28:33 +02:00
|
|
|
user_profile: UserProfile,
|
|
|
|
messages: Collection[Message] | QuerySet[Message],
|
|
|
|
*,
|
2024-07-12 02:30:23 +02:00
|
|
|
stream: Stream | None = None,
|
2024-07-12 02:30:17 +02:00
|
|
|
) -> list[Message]:
|
2021-05-12 23:40:58 +02:00
|
|
|
"""This function does the full has_message_access check for each
|
|
|
|
message. If stream is provided, it is used to avoid unnecessary
|
|
|
|
database queries, and will use exactly 2 bulk queries instead.
|
|
|
|
|
|
|
|
Throws AssertionError if stream is passed and any of the messages
|
|
|
|
were not sent to that stream.
|
|
|
|
|
|
|
|
"""
|
2018-07-27 16:23:17 +02:00
|
|
|
filtered_messages = []
|
|
|
|
|
2021-05-12 23:13:54 +02:00
|
|
|
user_message_set = set(
|
2023-06-17 00:10:28 +02:00
|
|
|
get_messages_with_usermessage_rows_for_user(
|
2021-05-12 23:13:54 +02:00
|
|
|
user_profile.id, [message.id for message in messages]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2022-11-17 03:45:05 +01:00
|
|
|
if stream is None:
|
|
|
|
streams = {
|
|
|
|
stream.recipient_id: stream
|
|
|
|
for stream in Stream.objects.filter(
|
|
|
|
id__in={
|
|
|
|
message.recipient.type_id
|
|
|
|
for message in messages
|
|
|
|
if message.recipient.type == Recipient.STREAM
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
2021-05-12 23:40:58 +02:00
|
|
|
|
|
|
|
subscribed_recipient_ids = set(get_subscribed_stream_recipient_ids_for_user(user_profile))
|
|
|
|
|
2018-07-27 16:23:17 +02:00
|
|
|
for message in messages:
|
2021-05-12 23:40:58 +02:00
|
|
|
is_subscribed = message.recipient_id in subscribed_recipient_ids
|
2021-05-12 23:21:39 +02:00
|
|
|
if has_message_access(
|
2021-05-12 23:40:58 +02:00
|
|
|
user_profile,
|
|
|
|
message,
|
2024-03-22 07:04:07 +01:00
|
|
|
has_user_message=partial(lambda m: m.id in user_message_set, message),
|
2022-11-17 03:45:05 +01:00
|
|
|
stream=streams.get(message.recipient_id) if stream is None else stream,
|
2021-05-12 23:40:58 +02:00
|
|
|
is_subscribed=is_subscribed,
|
2021-05-12 23:21:39 +02:00
|
|
|
):
|
2018-07-27 16:23:17 +02:00
|
|
|
filtered_messages.append(message)
|
|
|
|
return filtered_messages
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-09-26 17:34:55 +02:00
|
|
|
def bulk_access_stream_messages_query(
|
|
|
|
user_profile: UserProfile, messages: QuerySet[Message], stream: Stream
|
|
|
|
) -> QuerySet[Message]:
|
|
|
|
"""This function mirrors bulk_access_messages, above, but applies the
|
|
|
|
limits to a QuerySet and returns a new QuerySet which only
|
|
|
|
contains messages in the given stream which the user can access.
|
|
|
|
Note that this only works with streams. It may return an empty
|
|
|
|
QuerySet if the user has access to no messages (for instance, for
|
|
|
|
a private stream which the user is not subscribed to).
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2024-10-21 01:20:42 +02:00
|
|
|
assert stream.recipient_id is not None
|
2023-09-26 17:34:55 +02:00
|
|
|
messages = messages.filter(realm_id=user_profile.realm_id, recipient_id=stream.recipient_id)
|
|
|
|
|
|
|
|
if stream.is_public() and user_profile.can_access_public_streams():
|
|
|
|
return messages
|
|
|
|
|
|
|
|
if not Subscription.objects.filter(
|
|
|
|
user_profile=user_profile, active=True, recipient=stream.recipient
|
|
|
|
).exists():
|
|
|
|
return Message.objects.none()
|
|
|
|
if not stream.is_history_public_to_subscribers():
|
2024-07-11 17:04:53 +02:00
|
|
|
messages = messages.alias(
|
2023-09-26 17:34:55 +02:00
|
|
|
has_usermessage=Exists(
|
|
|
|
UserMessage.objects.filter(
|
|
|
|
user_profile_id=user_profile.id, message_id=OuterRef("id")
|
|
|
|
)
|
|
|
|
)
|
2024-07-11 17:04:53 +02:00
|
|
|
).filter(has_usermessage=True)
|
2023-09-26 17:34:55 +02:00
|
|
|
return messages
|
|
|
|
|
|
|
|
|
2023-06-17 00:10:28 +02:00
|
|
|
def get_messages_with_usermessage_rows_for_user(
|
2021-02-12 08:19:30 +01:00
|
|
|
user_profile_id: int, message_ids: Sequence[int]
|
2024-08-25 02:30:41 +02:00
|
|
|
) -> QuerySet[UserMessage, int]:
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2019-02-14 02:01:42 +01:00
|
|
|
Returns a subset of `message_ids` containing only messages the
|
2023-06-17 00:10:28 +02:00
|
|
|
user has a UserMessage for. Makes O(1) database queries.
|
|
|
|
Note that this is not sufficient for access verification for
|
|
|
|
stream messages.
|
2019-02-14 02:01:42 +01:00
|
|
|
|
2023-06-17 00:10:28 +02:00
|
|
|
See `access_message`, `bulk_access_messages` for proper message access
|
|
|
|
checks that follow our security model.
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2019-02-14 02:01:42 +01:00
|
|
|
return UserMessage.objects.filter(
|
|
|
|
user_profile_id=user_profile_id,
|
|
|
|
message_id__in=message_ids,
|
2021-02-12 08:20:45 +01:00
|
|
|
).values_list("message_id", flat=True)
|
2019-02-14 02:01:42 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-07-04 14:05:48 +02:00
|
|
|
def direct_message_group_users(recipient_id: int) -> str:
|
2024-07-12 02:30:17 +02:00
|
|
|
display_recipient: list[UserDisplayRecipient] = get_display_recipient_by_id(
|
2021-02-12 08:19:30 +01:00
|
|
|
recipient_id,
|
2024-03-22 00:39:33 +01:00
|
|
|
Recipient.DIRECT_MESSAGE_GROUP,
|
2021-02-12 08:19:30 +01:00
|
|
|
None,
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
)
|
2017-05-23 03:02:01 +02:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
user_ids: list[int] = [obj["id"] for obj in display_recipient]
|
2017-05-23 03:02:01 +02:00
|
|
|
user_ids = sorted(user_ids)
|
2021-02-12 08:20:45 +01:00
|
|
|
return ",".join(str(uid) for uid in user_ids)
|
2017-05-23 03:02:01 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def get_inactive_recipient_ids(user_profile: UserProfile) -> list[int]:
|
2021-02-12 08:19:30 +01:00
|
|
|
rows = (
|
|
|
|
get_stream_subscriptions_for_user(user_profile)
|
|
|
|
.filter(
|
|
|
|
active=False,
|
|
|
|
)
|
|
|
|
.values(
|
2021-02-12 08:20:45 +01:00
|
|
|
"recipient_id",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2017-08-09 02:22:00 +02:00
|
|
|
)
|
2021-02-12 08:20:45 +01:00
|
|
|
inactive_recipient_ids = [row["recipient_id"] for row in rows]
|
2017-08-09 02:22:00 +02:00
|
|
|
return inactive_recipient_ids
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def get_muted_stream_ids(user_profile: UserProfile) -> set[int]:
|
2021-02-12 08:19:30 +01:00
|
|
|
rows = (
|
|
|
|
get_stream_subscriptions_for_user(user_profile)
|
|
|
|
.filter(
|
|
|
|
active=True,
|
|
|
|
is_muted=True,
|
|
|
|
)
|
|
|
|
.values(
|
2021-02-12 08:20:45 +01:00
|
|
|
"recipient__type_id",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2017-08-23 22:45:50 +02:00
|
|
|
)
|
2023-09-26 11:48:34 +02:00
|
|
|
muted_stream_ids = {row["recipient__type_id"] for row in rows}
|
2017-10-05 16:18:13 +02:00
|
|
|
return muted_stream_ids
|
2017-08-23 22:45:50 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def get_starred_message_ids(user_profile: UserProfile) -> list[int]:
|
2021-02-12 08:19:30 +01:00
|
|
|
return list(
|
|
|
|
UserMessage.objects.filter(
|
|
|
|
user_profile=user_profile,
|
|
|
|
)
|
2024-06-27 20:17:17 +02:00
|
|
|
.extra( # noqa: S610
|
2021-02-12 08:19:30 +01:00
|
|
|
where=[UserMessage.where_starred()],
|
|
|
|
)
|
|
|
|
.order_by(
|
2021-02-12 08:20:45 +01:00
|
|
|
"message_id",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-02-12 08:20:45 +01:00
|
|
|
.values_list("message_id", flat=True)[0:10000]
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
|
2018-08-14 23:57:20 +02:00
|
|
|
|
2021-06-09 13:31:39 +02:00
|
|
|
def get_raw_unread_data(
|
2024-07-12 02:30:23 +02:00
|
|
|
user_profile: UserProfile, message_ids: list[int] | None = None
|
2021-06-09 13:31:39 +02:00
|
|
|
) -> RawUnreadMessagesResult:
|
2017-08-09 02:22:00 +02:00
|
|
|
excluded_recipient_ids = get_inactive_recipient_ids(user_profile)
|
2024-05-23 19:25:30 +02:00
|
|
|
first_visible_message_id = get_first_visible_message_id(user_profile.realm)
|
2021-02-12 08:19:30 +01:00
|
|
|
user_msgs = (
|
|
|
|
UserMessage.objects.filter(
|
|
|
|
user_profile=user_profile,
|
2024-05-23 19:25:30 +02:00
|
|
|
message_id__gte=first_visible_message_id,
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
.exclude(
|
|
|
|
message__recipient_id__in=excluded_recipient_ids,
|
|
|
|
)
|
|
|
|
.values(
|
2021-02-12 08:20:45 +01:00
|
|
|
"message_id",
|
|
|
|
"message__sender_id",
|
2021-02-12 08:19:30 +01:00
|
|
|
MESSAGE__TOPIC,
|
2021-02-12 08:20:45 +01:00
|
|
|
"message__recipient_id",
|
|
|
|
"message__recipient__type",
|
|
|
|
"message__recipient__type_id",
|
|
|
|
"flags",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
.order_by("-message_id")
|
|
|
|
)
|
2017-08-01 18:28:56 +02:00
|
|
|
|
2021-06-09 13:31:39 +02:00
|
|
|
if message_ids is not None:
|
|
|
|
# When users are marking just a few messages as unread, we just need
|
|
|
|
# those ids, and we know they're unread.
|
|
|
|
user_msgs = user_msgs.filter(message_id__in=message_ids)
|
|
|
|
else:
|
|
|
|
# At page load we need all unread messages.
|
2024-06-27 20:17:17 +02:00
|
|
|
user_msgs = user_msgs.extra( # noqa: S610
|
2021-06-09 13:31:39 +02:00
|
|
|
where=[UserMessage.where_unread()],
|
|
|
|
)
|
|
|
|
|
2017-08-01 18:28:56 +02:00
|
|
|
# Limit unread messages for performance reasons.
|
|
|
|
user_msgs = list(user_msgs[:MAX_UNREAD_MESSAGES])
|
2017-05-23 03:02:01 +02:00
|
|
|
|
2017-08-01 18:28:56 +02:00
|
|
|
rows = list(reversed(user_msgs))
|
2020-09-27 19:12:24 +02:00
|
|
|
return extract_unread_data_from_um_rows(rows, user_profile)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-09-27 19:12:24 +02:00
|
|
|
def extract_unread_data_from_um_rows(
|
2024-07-12 02:30:23 +02:00
|
|
|
rows: list[dict[str, Any]], user_profile: UserProfile | None
|
2020-09-27 19:12:24 +02:00
|
|
|
) -> RawUnreadMessagesResult:
|
2024-07-12 02:30:17 +02:00
|
|
|
pm_dict: dict[int, RawUnreadDirectMessageDict] = {}
|
|
|
|
stream_dict: dict[int, RawUnreadStreamDict] = {}
|
|
|
|
muted_stream_ids: set[int] = set()
|
|
|
|
unmuted_stream_msgs: set[int] = set()
|
|
|
|
direct_message_group_dict: dict[int, RawUnreadDirectMessageGroupDict] = {}
|
|
|
|
mentions: set[int] = set()
|
2021-03-18 22:33:52 +01:00
|
|
|
total_unreads = 0
|
2020-09-27 19:12:52 +02:00
|
|
|
|
|
|
|
raw_unread_messages: RawUnreadMessagesResult = dict(
|
|
|
|
pm_dict=pm_dict,
|
|
|
|
stream_dict=stream_dict,
|
2023-09-26 11:48:34 +02:00
|
|
|
muted_stream_ids=muted_stream_ids,
|
2020-09-27 19:12:52 +02:00
|
|
|
unmuted_stream_msgs=unmuted_stream_msgs,
|
2024-07-04 14:05:48 +02:00
|
|
|
huddle_dict=direct_message_group_dict,
|
2020-09-27 19:12:52 +02:00
|
|
|
mentions=mentions,
|
2021-03-18 22:33:52 +01:00
|
|
|
old_unreads_missing=False,
|
2020-09-27 19:12:52 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if user_profile is None:
|
2020-09-27 06:49:16 +02:00
|
|
|
return raw_unread_messages
|
2020-09-27 19:12:52 +02:00
|
|
|
|
2017-10-05 16:18:13 +02:00
|
|
|
muted_stream_ids = get_muted_stream_ids(user_profile)
|
2023-09-29 10:32:44 +02:00
|
|
|
raw_unread_messages["muted_stream_ids"] = muted_stream_ids
|
2017-08-31 23:19:05 +02:00
|
|
|
|
2023-09-25 09:09:53 +02:00
|
|
|
get_topic_visibility_policy = build_get_topic_visibility_policy(user_profile)
|
2017-08-31 23:19:05 +02:00
|
|
|
|
2024-01-14 14:38:50 +01:00
|
|
|
def is_row_muted(stream_id: int, recipient_id: int, topic_name: str) -> bool:
|
2023-09-25 11:27:15 +02:00
|
|
|
stream_muted = stream_id in muted_stream_ids
|
2024-01-14 14:38:50 +01:00
|
|
|
visibility_policy = get_topic_visibility_policy(recipient_id, topic_name)
|
2023-09-25 11:27:15 +02:00
|
|
|
|
|
|
|
if stream_muted and visibility_policy in [
|
|
|
|
UserTopic.VisibilityPolicy.UNMUTED,
|
|
|
|
UserTopic.VisibilityPolicy.FOLLOWED,
|
|
|
|
]:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if stream_muted:
|
2017-08-31 23:19:05 +02:00
|
|
|
return True
|
|
|
|
|
2023-09-25 11:27:15 +02:00
|
|
|
# muted topic in unmuted stream
|
2023-09-25 09:09:53 +02:00
|
|
|
if visibility_policy == UserTopic.VisibilityPolicy.MUTED:
|
2017-08-31 23:19:05 +02:00
|
|
|
return True
|
|
|
|
|
2021-02-05 05:58:44 +01:00
|
|
|
# Messages sent by muted users are never unread, so we don't
|
|
|
|
# need any logic related to muted users here.
|
|
|
|
|
2017-08-31 23:19:05 +02:00
|
|
|
return False
|
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
direct_message_group_cache: dict[int, str] = {}
|
2017-10-04 18:13:04 +02:00
|
|
|
|
2024-07-04 14:05:48 +02:00
|
|
|
def get_direct_message_group_users(recipient_id: int) -> str:
|
|
|
|
if recipient_id in direct_message_group_cache:
|
|
|
|
return direct_message_group_cache[recipient_id]
|
2017-08-23 22:45:50 +02:00
|
|
|
|
2024-07-04 14:05:48 +02:00
|
|
|
user_ids_string = direct_message_group_users(recipient_id)
|
|
|
|
direct_message_group_cache[recipient_id] = user_ids_string
|
2017-10-04 18:13:04 +02:00
|
|
|
return user_ids_string
|
2017-05-23 03:02:01 +02:00
|
|
|
|
2017-10-04 18:13:04 +02:00
|
|
|
for row in rows:
|
2021-03-18 22:33:52 +01:00
|
|
|
total_unreads += 1
|
2021-02-12 08:20:45 +01:00
|
|
|
message_id = row["message_id"]
|
|
|
|
msg_type = row["message__recipient__type"]
|
|
|
|
recipient_id = row["message__recipient_id"]
|
|
|
|
sender_id = row["message__sender_id"]
|
2017-10-04 18:13:04 +02:00
|
|
|
|
|
|
|
if msg_type == Recipient.STREAM:
|
2021-02-12 08:20:45 +01:00
|
|
|
stream_id = row["message__recipient__type_id"]
|
2024-01-14 14:38:50 +01:00
|
|
|
topic_name = row[MESSAGE__TOPIC]
|
2017-10-04 18:13:04 +02:00
|
|
|
stream_dict[message_id] = dict(
|
|
|
|
stream_id=stream_id,
|
2024-01-14 14:38:50 +01:00
|
|
|
topic=topic_name,
|
2017-10-04 18:13:04 +02:00
|
|
|
)
|
2024-01-14 14:38:50 +01:00
|
|
|
if not is_row_muted(stream_id, recipient_id, topic_name):
|
2017-10-04 18:13:04 +02:00
|
|
|
unmuted_stream_msgs.add(message_id)
|
|
|
|
|
|
|
|
elif msg_type == Recipient.PERSONAL:
|
2020-03-17 23:17:12 +01:00
|
|
|
if sender_id == user_profile.id:
|
2021-02-12 08:20:45 +01:00
|
|
|
other_user_id = row["message__recipient__type_id"]
|
2020-03-17 23:17:12 +01:00
|
|
|
else:
|
|
|
|
other_user_id = sender_id
|
|
|
|
|
2017-10-04 18:13:04 +02:00
|
|
|
pm_dict[message_id] = dict(
|
2022-03-07 16:47:49 +01:00
|
|
|
other_user_id=other_user_id,
|
2017-10-04 18:13:04 +02:00
|
|
|
)
|
|
|
|
|
2024-03-22 00:39:33 +01:00
|
|
|
elif msg_type == Recipient.DIRECT_MESSAGE_GROUP:
|
2024-07-04 14:05:48 +02:00
|
|
|
user_ids_string = get_direct_message_group_users(recipient_id)
|
|
|
|
direct_message_group_dict[message_id] = dict(
|
2017-10-04 18:13:04 +02:00
|
|
|
user_ids_string=user_ids_string,
|
|
|
|
)
|
|
|
|
|
2019-08-26 05:11:18 +02:00
|
|
|
# TODO: Add support for alert words here as well.
|
2021-02-12 08:20:45 +01:00
|
|
|
is_mentioned = (row["flags"] & UserMessage.flags.mentioned) != 0
|
2023-11-03 15:20:44 +01:00
|
|
|
is_stream_wildcard_mentioned = (
|
|
|
|
row["flags"] & UserMessage.flags.stream_wildcard_mentioned
|
|
|
|
) != 0
|
2023-10-19 18:11:53 +02:00
|
|
|
is_topic_wildcard_mentioned = (
|
|
|
|
row["flags"] & UserMessage.flags.topic_wildcard_mentioned
|
|
|
|
) != 0
|
2017-10-04 18:13:04 +02:00
|
|
|
if is_mentioned:
|
|
|
|
mentions.add(message_id)
|
2023-11-03 15:20:44 +01:00
|
|
|
if is_stream_wildcard_mentioned or is_topic_wildcard_mentioned:
|
2019-08-26 05:11:18 +02:00
|
|
|
if msg_type == Recipient.STREAM:
|
2021-02-12 08:20:45 +01:00
|
|
|
stream_id = row["message__recipient__type_id"]
|
2024-01-14 14:38:50 +01:00
|
|
|
topic_name = row[MESSAGE__TOPIC]
|
|
|
|
if not is_row_muted(stream_id, recipient_id, topic_name):
|
2019-08-26 05:11:18 +02:00
|
|
|
mentions.add(message_id)
|
2023-06-19 16:42:11 +02:00
|
|
|
else: # nocoverage # TODO: Test wildcard mentions in direct messages.
|
2019-08-26 05:11:18 +02:00
|
|
|
mentions.add(message_id)
|
2017-10-04 18:13:04 +02:00
|
|
|
|
2021-03-18 22:33:52 +01:00
|
|
|
# Record whether the user had more than MAX_UNREAD_MESSAGES total
|
|
|
|
# unreads -- that's a state where Zulip's behavior will start to
|
|
|
|
# be erroneous, and clients should display a warning.
|
|
|
|
raw_unread_messages["old_unreads_missing"] = total_unreads == MAX_UNREAD_MESSAGES
|
|
|
|
|
2020-09-27 19:12:52 +02:00
|
|
|
return raw_unread_messages
|
2017-10-04 18:13:04 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def aggregate_streams(*, input_dict: dict[int, RawUnreadStreamDict]) -> list[UnreadStreamInfo]:
|
|
|
|
lookup_dict: dict[tuple[int, str], UnreadStreamInfo] = {}
|
2022-03-07 15:12:24 +01:00
|
|
|
for message_id, attribute_dict in input_dict.items():
|
|
|
|
stream_id = attribute_dict["stream_id"]
|
2024-01-14 14:38:50 +01:00
|
|
|
topic_name = attribute_dict["topic"]
|
2024-10-03 23:31:49 +02:00
|
|
|
lookup_key = (stream_id, topic_name.lower())
|
2022-03-07 15:12:24 +01:00
|
|
|
if lookup_key not in lookup_dict:
|
|
|
|
obj = UnreadStreamInfo(
|
|
|
|
stream_id=stream_id,
|
2024-01-14 14:38:50 +01:00
|
|
|
topic=topic_name,
|
2022-03-07 15:12:24 +01:00
|
|
|
unread_message_ids=[],
|
|
|
|
)
|
|
|
|
lookup_dict[lookup_key] = obj
|
|
|
|
|
|
|
|
bucket = lookup_dict[lookup_key]
|
|
|
|
bucket["unread_message_ids"].append(message_id)
|
|
|
|
|
|
|
|
for dct in lookup_dict.values():
|
|
|
|
dct["unread_message_ids"].sort()
|
|
|
|
|
|
|
|
sorted_keys = sorted(lookup_dict.keys())
|
|
|
|
|
|
|
|
return [lookup_dict[k] for k in sorted_keys]
|
|
|
|
|
|
|
|
|
|
|
|
def aggregate_pms(
|
2024-07-12 02:30:17 +02:00
|
|
|
*, input_dict: dict[int, RawUnreadDirectMessageDict]
|
|
|
|
) -> list[UnreadDirectMessageInfo]:
|
|
|
|
lookup_dict: dict[int, UnreadDirectMessageInfo] = {}
|
2022-03-07 15:12:24 +01:00
|
|
|
for message_id, attribute_dict in input_dict.items():
|
2022-03-07 16:47:49 +01:00
|
|
|
other_user_id = attribute_dict["other_user_id"]
|
2022-03-07 15:12:24 +01:00
|
|
|
if other_user_id not in lookup_dict:
|
2022-03-07 16:47:49 +01:00
|
|
|
# The `sender_id` field here is only supported for
|
|
|
|
# legacy mobile clients. Its actual semantics are the same
|
|
|
|
# as `other_user_id`.
|
2023-06-19 17:05:53 +02:00
|
|
|
obj = UnreadDirectMessageInfo(
|
2022-03-07 16:47:49 +01:00
|
|
|
other_user_id=other_user_id,
|
2022-03-07 15:12:24 +01:00
|
|
|
sender_id=other_user_id,
|
|
|
|
unread_message_ids=[],
|
|
|
|
)
|
|
|
|
lookup_dict[other_user_id] = obj
|
|
|
|
|
|
|
|
bucket = lookup_dict[other_user_id]
|
|
|
|
bucket["unread_message_ids"].append(message_id)
|
|
|
|
|
|
|
|
for dct in lookup_dict.values():
|
|
|
|
dct["unread_message_ids"].sort()
|
|
|
|
|
|
|
|
sorted_keys = sorted(lookup_dict.keys())
|
|
|
|
|
|
|
|
return [lookup_dict[k] for k in sorted_keys]
|
|
|
|
|
|
|
|
|
2024-07-04 14:05:48 +02:00
|
|
|
def aggregate_direct_message_groups(
|
2024-07-12 02:30:17 +02:00
|
|
|
*, input_dict: dict[int, RawUnreadDirectMessageGroupDict]
|
|
|
|
) -> list[UnreadDirectMessageGroupInfo]:
|
|
|
|
lookup_dict: dict[str, UnreadDirectMessageGroupInfo] = {}
|
2022-03-07 15:12:24 +01:00
|
|
|
for message_id, attribute_dict in input_dict.items():
|
|
|
|
user_ids_string = attribute_dict["user_ids_string"]
|
|
|
|
if user_ids_string not in lookup_dict:
|
2024-07-04 14:05:48 +02:00
|
|
|
obj = UnreadDirectMessageGroupInfo(
|
2022-03-07 15:12:24 +01:00
|
|
|
user_ids_string=user_ids_string,
|
|
|
|
unread_message_ids=[],
|
|
|
|
)
|
|
|
|
lookup_dict[user_ids_string] = obj
|
|
|
|
|
|
|
|
bucket = lookup_dict[user_ids_string]
|
|
|
|
bucket["unread_message_ids"].append(message_id)
|
|
|
|
|
|
|
|
for dct in lookup_dict.values():
|
|
|
|
dct["unread_message_ids"].sort()
|
|
|
|
|
|
|
|
sorted_keys = sorted(lookup_dict.keys())
|
|
|
|
|
|
|
|
return [lookup_dict[k] for k in sorted_keys]
|
|
|
|
|
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def aggregate_unread_data(raw_data: RawUnreadMessagesResult) -> UnreadMessagesResult:
|
2021-02-12 08:20:45 +01:00
|
|
|
pm_dict = raw_data["pm_dict"]
|
|
|
|
stream_dict = raw_data["stream_dict"]
|
|
|
|
unmuted_stream_msgs = raw_data["unmuted_stream_msgs"]
|
2024-07-04 14:05:48 +02:00
|
|
|
direct_message_group_dict = raw_data["huddle_dict"]
|
2021-02-12 08:20:45 +01:00
|
|
|
mentions = list(raw_data["mentions"])
|
2017-10-04 18:13:04 +02:00
|
|
|
|
2024-07-04 14:05:48 +02:00
|
|
|
count = len(pm_dict) + len(unmuted_stream_msgs) + len(direct_message_group_dict)
|
2017-05-23 03:02:01 +02:00
|
|
|
|
2022-03-07 15:12:24 +01:00
|
|
|
pm_objects = aggregate_pms(input_dict=pm_dict)
|
|
|
|
stream_objects = aggregate_streams(input_dict=stream_dict)
|
2024-07-04 14:05:48 +02:00
|
|
|
direct_message_groups = aggregate_direct_message_groups(input_dict=direct_message_group_dict)
|
2017-05-23 03:02:01 +02:00
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
result: UnreadMessagesResult = dict(
|
2017-05-23 03:02:01 +02:00
|
|
|
pms=pm_objects,
|
|
|
|
streams=stream_objects,
|
2024-07-04 14:05:48 +02:00
|
|
|
huddles=direct_message_groups,
|
2017-10-04 18:13:04 +02:00
|
|
|
mentions=mentions,
|
2021-02-12 08:19:30 +01:00
|
|
|
count=count,
|
2021-03-18 22:33:52 +01:00
|
|
|
old_unreads_missing=raw_data["old_unreads_missing"],
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2017-05-23 03:02:01 +02:00
|
|
|
|
|
|
|
return result
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def apply_unread_message_event(
|
|
|
|
user_profile: UserProfile,
|
|
|
|
state: RawUnreadMessagesResult,
|
2024-07-12 02:30:17 +02:00
|
|
|
message: dict[str, Any],
|
|
|
|
flags: list[str],
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> None:
|
2021-02-12 08:20:45 +01:00
|
|
|
message_id = message["id"]
|
|
|
|
if message["type"] == "stream":
|
2023-04-18 17:43:35 +02:00
|
|
|
recipient_type = "stream"
|
2021-02-12 08:20:45 +01:00
|
|
|
elif message["type"] == "private":
|
|
|
|
others = [recip for recip in message["display_recipient"] if recip["id"] != user_profile.id]
|
2017-05-23 03:02:01 +02:00
|
|
|
if len(others) <= 1:
|
2023-04-18 17:43:35 +02:00
|
|
|
recipient_type = "private"
|
2017-05-23 03:02:01 +02:00
|
|
|
else:
|
2023-04-18 17:43:35 +02:00
|
|
|
recipient_type = "huddle"
|
2017-08-25 09:39:36 +02:00
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
raise AssertionError("Invalid message type {}".format(message["type"]))
|
2017-05-23 03:02:01 +02:00
|
|
|
|
2023-04-18 17:43:35 +02:00
|
|
|
if recipient_type == "stream":
|
2021-02-12 08:20:45 +01:00
|
|
|
stream_id = message["stream_id"]
|
2024-01-14 14:38:50 +01:00
|
|
|
topic_name = message[TOPIC_NAME]
|
2021-07-09 20:34:02 +02:00
|
|
|
state["stream_dict"][message_id] = RawUnreadStreamDict(
|
2017-05-23 03:02:01 +02:00
|
|
|
stream_id=stream_id,
|
2024-01-14 14:38:50 +01:00
|
|
|
topic=topic_name,
|
2017-05-23 03:02:01 +02:00
|
|
|
)
|
Simplify how we apply events for unread messages.
The logic to apply events to page_params['unread_msgs'] was
complicated due to the aggregated data structures that we pass
down to the client.
Now we defer the aggregation logic until after we apply the
events. This leads to some simplifications in that codepath,
as well as some performance enhancements.
The intermediate data structure has sets and dictionaries that
generally are keyed by message_id, so most message-related
updates are O(1) in nature.
Also, by waiting to compute the counts until the end, it's a
bit less messy to try to keep track of increments/decrements.
Instead, we just update the dictionaries and sets during the
event-apply phase.
This change also fixes some corner cases:
* We now respect mutes when updating counts.
* For message updates, instead of bluntly updating
the whole topic bucket, we update individual
message ids.
Unfortunately, this change doesn't seem to address the pesky
test that fails sporadically on Travis, related to mention
updates. It will change the symptom, slightly, though.
2017-10-05 00:34:19 +02:00
|
|
|
|
2023-09-25 11:27:15 +02:00
|
|
|
stream_muted = stream_id in state["muted_stream_ids"]
|
2024-01-14 14:38:50 +01:00
|
|
|
visibility_policy = get_topic_visibility_policy(user_profile, stream_id, topic_name)
|
2023-09-25 11:27:15 +02:00
|
|
|
# A stream message is unmuted if it belongs to:
|
|
|
|
# * a not muted topic in a normal stream
|
|
|
|
# * an unmuted or followed topic in a muted stream
|
|
|
|
if (not stream_muted and visibility_policy != UserTopic.VisibilityPolicy.MUTED) or (
|
|
|
|
stream_muted
|
|
|
|
and visibility_policy
|
|
|
|
in [UserTopic.VisibilityPolicy.UNMUTED, UserTopic.VisibilityPolicy.FOLLOWED]
|
2023-01-18 02:59:37 +01:00
|
|
|
):
|
|
|
|
state["unmuted_stream_msgs"].add(message_id)
|
Simplify how we apply events for unread messages.
The logic to apply events to page_params['unread_msgs'] was
complicated due to the aggregated data structures that we pass
down to the client.
Now we defer the aggregation logic until after we apply the
events. This leads to some simplifications in that codepath,
as well as some performance enhancements.
The intermediate data structure has sets and dictionaries that
generally are keyed by message_id, so most message-related
updates are O(1) in nature.
Also, by waiting to compute the counts until the end, it's a
bit less messy to try to keep track of increments/decrements.
Instead, we just update the dictionaries and sets during the
event-apply phase.
This change also fixes some corner cases:
* We now respect mutes when updating counts.
* For message updates, instead of bluntly updating
the whole topic bucket, we update individual
message ids.
Unfortunately, this change doesn't seem to address the pesky
test that fails sporadically on Travis, related to mention
updates. It will change the symptom, slightly, though.
2017-10-05 00:34:19 +02:00
|
|
|
|
2023-04-18 17:43:35 +02:00
|
|
|
elif recipient_type == "private":
|
2020-03-17 23:17:12 +01:00
|
|
|
if len(others) == 1:
|
2022-03-07 16:47:49 +01:00
|
|
|
other_user_id = others[0]["id"]
|
2020-03-17 23:17:12 +01:00
|
|
|
else:
|
2022-03-07 16:47:49 +01:00
|
|
|
other_user_id = user_profile.id
|
2020-03-17 23:17:12 +01:00
|
|
|
|
2023-06-19 17:05:53 +02:00
|
|
|
state["pm_dict"][message_id] = RawUnreadDirectMessageDict(
|
2022-03-07 16:47:49 +01:00
|
|
|
other_user_id=other_user_id,
|
2017-05-23 03:02:01 +02:00
|
|
|
)
|
Simplify how we apply events for unread messages.
The logic to apply events to page_params['unread_msgs'] was
complicated due to the aggregated data structures that we pass
down to the client.
Now we defer the aggregation logic until after we apply the
events. This leads to some simplifications in that codepath,
as well as some performance enhancements.
The intermediate data structure has sets and dictionaries that
generally are keyed by message_id, so most message-related
updates are O(1) in nature.
Also, by waiting to compute the counts until the end, it's a
bit less messy to try to keep track of increments/decrements.
Instead, we just update the dictionaries and sets during the
event-apply phase.
This change also fixes some corner cases:
* We now respect mutes when updating counts.
* For message updates, instead of bluntly updating
the whole topic bucket, we update individual
message ids.
Unfortunately, this change doesn't seem to address the pesky
test that fails sporadically on Travis, related to mention
updates. It will change the symptom, slightly, though.
2017-10-05 00:34:19 +02:00
|
|
|
|
2017-05-23 03:02:01 +02:00
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
display_recipient = message["display_recipient"]
|
|
|
|
user_ids = [obj["id"] for obj in display_recipient]
|
2017-05-23 03:02:01 +02:00
|
|
|
user_ids = sorted(user_ids)
|
2021-02-12 08:20:45 +01:00
|
|
|
user_ids_string = ",".join(str(uid) for uid in user_ids)
|
2021-07-09 20:34:02 +02:00
|
|
|
|
2024-07-04 14:05:48 +02:00
|
|
|
state["huddle_dict"][message_id] = RawUnreadDirectMessageGroupDict(
|
Simplify how we apply events for unread messages.
The logic to apply events to page_params['unread_msgs'] was
complicated due to the aggregated data structures that we pass
down to the client.
Now we defer the aggregation logic until after we apply the
events. This leads to some simplifications in that codepath,
as well as some performance enhancements.
The intermediate data structure has sets and dictionaries that
generally are keyed by message_id, so most message-related
updates are O(1) in nature.
Also, by waiting to compute the counts until the end, it's a
bit less messy to try to keep track of increments/decrements.
Instead, we just update the dictionaries and sets during the
event-apply phase.
This change also fixes some corner cases:
* We now respect mutes when updating counts.
* For message updates, instead of bluntly updating
the whole topic bucket, we update individual
message ids.
Unfortunately, this change doesn't seem to address the pesky
test that fails sporadically on Travis, related to mention
updates. It will change the symptom, slightly, though.
2017-10-05 00:34:19 +02:00
|
|
|
user_ids_string=user_ids_string,
|
2017-05-23 03:02:01 +02:00
|
|
|
)
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
if "mentioned" in flags:
|
|
|
|
state["mentions"].add(message_id)
|
2023-10-19 18:11:53 +02:00
|
|
|
if (
|
2023-11-03 15:20:44 +01:00
|
|
|
"stream_wildcard_mentioned" in flags or "topic_wildcard_mentioned" in flags
|
2023-10-19 18:11:53 +02:00
|
|
|
) and message_id in state["unmuted_stream_msgs"]:
|
2023-01-18 02:59:37 +01:00
|
|
|
state["mentions"].add(message_id)
|
2018-01-02 18:33:28 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def remove_message_id_from_unread_mgs(state: RawUnreadMessagesResult, message_id: int) -> None:
|
2019-08-03 02:24:00 +02:00
|
|
|
# The opposite of apply_unread_message_event; removes a read or
|
|
|
|
# deleted message from a raw_unread_msgs data structure.
|
2021-02-12 08:20:45 +01:00
|
|
|
state["pm_dict"].pop(message_id, None)
|
|
|
|
state["stream_dict"].pop(message_id, None)
|
|
|
|
state["huddle_dict"].pop(message_id, None)
|
|
|
|
state["unmuted_stream_msgs"].discard(message_id)
|
|
|
|
state["mentions"].discard(message_id)
|
2019-08-03 02:24:00 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-06-09 13:31:39 +02:00
|
|
|
def format_unread_message_details(
|
|
|
|
my_user_id: int,
|
|
|
|
raw_unread_data: RawUnreadMessagesResult,
|
2024-07-12 02:30:17 +02:00
|
|
|
) -> dict[str, MessageDetailsDict]:
|
2021-06-09 13:31:39 +02:00
|
|
|
unread_data = {}
|
|
|
|
|
|
|
|
for message_id, private_message_details in raw_unread_data["pm_dict"].items():
|
|
|
|
other_user_id = private_message_details["other_user_id"]
|
|
|
|
if other_user_id == my_user_id:
|
|
|
|
user_ids = []
|
|
|
|
else:
|
|
|
|
user_ids = [other_user_id]
|
|
|
|
|
|
|
|
# Note that user_ids excludes ourself, even for the case we send messages
|
|
|
|
# to ourself.
|
|
|
|
message_details = MessageDetailsDict(
|
|
|
|
type="private",
|
|
|
|
user_ids=user_ids,
|
|
|
|
)
|
|
|
|
if message_id in raw_unread_data["mentions"]:
|
|
|
|
message_details["mentioned"] = True
|
|
|
|
unread_data[str(message_id)] = message_details
|
|
|
|
|
|
|
|
for message_id, stream_message_details in raw_unread_data["stream_dict"].items():
|
2023-01-18 03:28:19 +01:00
|
|
|
unmuted_stream_msg = message_id in raw_unread_data["unmuted_stream_msgs"]
|
2021-06-09 13:31:39 +02:00
|
|
|
|
|
|
|
message_details = MessageDetailsDict(
|
|
|
|
type="stream",
|
|
|
|
stream_id=stream_message_details["stream_id"],
|
|
|
|
topic=stream_message_details["topic"],
|
|
|
|
# Clients don't need this detail, but we need it internally for apply_events.
|
|
|
|
unmuted_stream_msg=unmuted_stream_msg,
|
|
|
|
)
|
|
|
|
if message_id in raw_unread_data["mentions"]:
|
|
|
|
message_details["mentioned"] = True
|
|
|
|
unread_data[str(message_id)] = message_details
|
|
|
|
|
|
|
|
for message_id, huddle_message_details in raw_unread_data["huddle_dict"].items():
|
|
|
|
# The client wants a list of user_ids in the conversation, excluding ourself,
|
|
|
|
# that is sorted in numerical order.
|
2023-09-12 23:19:57 +02:00
|
|
|
user_ids = sorted(
|
|
|
|
user_id
|
|
|
|
for s in huddle_message_details["user_ids_string"].split(",")
|
|
|
|
if (user_id := int(s)) != my_user_id
|
|
|
|
)
|
2021-06-09 13:31:39 +02:00
|
|
|
message_details = MessageDetailsDict(
|
|
|
|
type="private",
|
|
|
|
user_ids=user_ids,
|
|
|
|
)
|
|
|
|
if message_id in raw_unread_data["mentions"]:
|
|
|
|
message_details["mentioned"] = True
|
|
|
|
unread_data[str(message_id)] = message_details
|
|
|
|
|
|
|
|
return unread_data
|
|
|
|
|
|
|
|
|
|
|
|
def add_message_to_unread_msgs(
|
|
|
|
my_user_id: int,
|
|
|
|
state: RawUnreadMessagesResult,
|
|
|
|
message_id: int,
|
|
|
|
message_details: MessageDetailsDict,
|
|
|
|
) -> None:
|
|
|
|
if message_details.get("mentioned"):
|
|
|
|
state["mentions"].add(message_id)
|
|
|
|
|
|
|
|
if message_details["type"] == "private":
|
2024-07-12 02:30:17 +02:00
|
|
|
user_ids: list[int] = message_details["user_ids"]
|
2021-06-09 13:31:39 +02:00
|
|
|
user_ids = [user_id for user_id in user_ids if user_id != my_user_id]
|
|
|
|
if user_ids == []:
|
2023-06-19 17:05:53 +02:00
|
|
|
state["pm_dict"][message_id] = RawUnreadDirectMessageDict(
|
2021-06-09 13:31:39 +02:00
|
|
|
other_user_id=my_user_id,
|
|
|
|
)
|
|
|
|
elif len(user_ids) == 1:
|
2023-06-19 17:05:53 +02:00
|
|
|
state["pm_dict"][message_id] = RawUnreadDirectMessageDict(
|
2021-06-09 13:31:39 +02:00
|
|
|
other_user_id=user_ids[0],
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
user_ids.append(my_user_id)
|
|
|
|
user_ids_string = ",".join(str(user_id) for user_id in sorted(user_ids))
|
2024-07-04 14:05:48 +02:00
|
|
|
state["huddle_dict"][message_id] = RawUnreadDirectMessageGroupDict(
|
2021-06-09 13:31:39 +02:00
|
|
|
user_ids_string=user_ids_string,
|
|
|
|
)
|
|
|
|
elif message_details["type"] == "stream":
|
|
|
|
state["stream_dict"][message_id] = RawUnreadStreamDict(
|
|
|
|
stream_id=message_details["stream_id"],
|
|
|
|
topic=message_details["topic"],
|
|
|
|
)
|
|
|
|
if message_details["unmuted_stream_msg"]:
|
|
|
|
state["unmuted_stream_msgs"].add(message_id)
|
|
|
|
|
|
|
|
|
2018-01-22 21:50:22 +01:00
|
|
|
def estimate_recent_messages(realm: Realm, hours: int) -> int:
|
2021-02-12 08:20:45 +01:00
|
|
|
stat = COUNT_STATS["messages_sent:is_bot:hour"]
|
2023-11-19 19:45:19 +01:00
|
|
|
d = timezone_now() - timedelta(hours=hours)
|
2021-02-12 08:19:30 +01:00
|
|
|
return (
|
|
|
|
RealmCount.objects.filter(property=stat.property, end_time__gt=d, realm=realm).aggregate(
|
2021-02-12 08:20:45 +01:00
|
|
|
Sum("value")
|
|
|
|
)["value__sum"]
|
2021-02-12 08:19:30 +01:00
|
|
|
or 0
|
|
|
|
)
|
|
|
|
|
2018-01-04 13:49:39 +01:00
|
|
|
|
2018-01-22 21:50:22 +01:00
|
|
|
def get_first_visible_message_id(realm: Realm) -> int:
|
2018-10-25 07:54:37 +02:00
|
|
|
return realm.first_visible_message_id
|
2018-01-22 21:50:22 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-01-22 21:50:22 +01:00
|
|
|
def maybe_update_first_visible_message_id(realm: Realm, lookback_hours: int) -> None:
|
|
|
|
recent_messages_count = estimate_recent_messages(realm, lookback_hours)
|
2018-10-25 07:54:37 +02:00
|
|
|
if realm.message_visibility_limit is not None and recent_messages_count > 0:
|
2018-01-22 21:50:22 +01:00
|
|
|
update_first_visible_message_id(realm)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-01-22 21:50:22 +01:00
|
|
|
def update_first_visible_message_id(realm: Realm) -> None:
|
2018-10-25 07:54:37 +02:00
|
|
|
if realm.message_visibility_limit is None:
|
|
|
|
realm.first_visible_message_id = 0
|
|
|
|
else:
|
|
|
|
try:
|
2021-02-12 08:19:30 +01:00
|
|
|
first_visible_message_id = (
|
2023-08-30 21:19:37 +02:00
|
|
|
# Uses index: zerver_message_realm_id
|
2022-10-28 21:05:40 +02:00
|
|
|
Message.objects.filter(realm=realm)
|
2021-02-12 08:20:45 +01:00
|
|
|
.values("id")
|
|
|
|
.order_by("-id")[realm.message_visibility_limit - 1]["id"]
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2018-10-25 07:54:37 +02:00
|
|
|
except IndexError:
|
|
|
|
first_visible_message_id = 0
|
|
|
|
realm.first_visible_message_id = first_visible_message_id
|
|
|
|
realm.save(update_fields=["first_visible_message_id"])
|
2019-03-20 04:15:58 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-10-13 15:49:40 +02:00
|
|
|
def get_last_message_id() -> int:
|
|
|
|
# We generally use this function to populate RealmAuditLog, and
|
2022-02-08 00:13:33 +01:00
|
|
|
# the max id here is actually system-wide, not per-realm. I
|
2020-10-13 15:49:40 +02:00
|
|
|
# assume there's some advantage in not filtering by realm.
|
2021-02-12 08:20:45 +01:00
|
|
|
last_id = Message.objects.aggregate(Max("id"))["id__max"]
|
2020-10-13 15:49:40 +02:00
|
|
|
if last_id is None:
|
|
|
|
# During initial realm creation, there might be 0 messages in
|
|
|
|
# the database; in that case, the `aggregate` query returns
|
|
|
|
# None. Since we want an int for "beginning of time", use -1.
|
|
|
|
last_id = -1
|
|
|
|
return last_id
|
2019-03-20 04:15:58 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def get_recent_conversations_recipient_id(
|
|
|
|
user_profile: UserProfile, recipient_id: int, sender_id: int
|
|
|
|
) -> int:
|
2019-03-20 04:15:58 +01:00
|
|
|
"""Helper for doing lookups of the recipient_id that
|
|
|
|
get_recent_private_conversations would have used to record that
|
|
|
|
message in its data structure.
|
|
|
|
"""
|
2021-02-04 18:15:38 +01:00
|
|
|
my_recipient_id = user_profile.recipient_id
|
2019-03-20 04:15:58 +01:00
|
|
|
if recipient_id == my_recipient_id:
|
2021-02-12 08:20:45 +01:00
|
|
|
return UserProfile.objects.values_list("recipient_id", flat=True).get(id=sender_id)
|
2019-03-20 04:15:58 +01:00
|
|
|
return recipient_id
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def get_recent_private_conversations(user_profile: UserProfile) -> dict[int, dict[str, Any]]:
|
2019-03-20 04:15:58 +01:00
|
|
|
"""This function uses some carefully optimized SQL queries, designed
|
|
|
|
to use the UserMessage index on private_messages. It is
|
message: Rewrite personals query to be more performant and accurate.
The previous query suffered from bad corner cases when the user had
received a large number of direct messages but sent very few,
comparatively. This mean that the first half of the UNION would
retrieve a very large number of UserMessage rows, requiring fetching a
large number of Message rows, merely to throw them away upon
determining that the recipient was the current user.
Instead of merging two queries of "last 1k received" + "last 1k sent",
we instead make better use of the UserMessage rows to find "last 1k
sent or received." This may change the list of recipients, as large
disparities in sent/received messages may result in pushing the
most-recently-sent users off of the list. These are likely uncommon
edge cases, however -- and the disparity is the whole reason for the
performance problem.
This also provides more correct answers. In the case where a user's
1001'th message sent was to person A today, but my most recent message
received was from them yesterday, the previous plan would show the
message I received yesterday message-id as the max, and not the more
recent message I sent today.
While we could theoretically raise the `RECENT_CONVERSATIONS_LIMIT` to
more frequently match the same recipient list as previously, this
increases the cost of the most common cases unreasonably. With a
1000-message limit, the common cases are slightly faster, and the tail
latencies are very much improved; raising `RECENT_CONVERSATIONS_LIMIT`
would increase the result similarity to the old algorithm, at the cost
of the p50 and p75.
| | Old | New |
| ------ | ------- | ------- |
| Mean | 0.05287 | 0.02520 |
| p50 | 0.00695 | 0.00556 |
| p75 | 0.05592 | 0.03351 |
| p90 | 0.14645 | 0.08026 |
| p95 | 0.20181 | 0.10906 |
| p99 | 0.30691 | 0.16014 |
| p99.9 | 0.57894 | 0.19521 |
| max | 22.0610 | 0.22184 |
On the whole, however, the much more bounded worst case are worth the
small changes to the resultset.
2023-12-01 17:25:31 +01:00
|
|
|
somewhat complicated by the fact that for 1:1 direct
|
2019-03-20 04:15:58 +01:00
|
|
|
messages, we store the message against a recipient_id of whichever
|
2023-06-19 16:42:11 +02:00
|
|
|
user was the recipient, and thus for 1:1 direct messages sent
|
2019-03-20 04:15:58 +01:00
|
|
|
directly to us, we need to look up the other user from the
|
|
|
|
sender_id on those messages. You'll see that pattern repeated
|
|
|
|
both here and also in zerver/lib/events.py.
|
|
|
|
|
message: Rewrite personals query to be more performant and accurate.
The previous query suffered from bad corner cases when the user had
received a large number of direct messages but sent very few,
comparatively. This mean that the first half of the UNION would
retrieve a very large number of UserMessage rows, requiring fetching a
large number of Message rows, merely to throw them away upon
determining that the recipient was the current user.
Instead of merging two queries of "last 1k received" + "last 1k sent",
we instead make better use of the UserMessage rows to find "last 1k
sent or received." This may change the list of recipients, as large
disparities in sent/received messages may result in pushing the
most-recently-sent users off of the list. These are likely uncommon
edge cases, however -- and the disparity is the whole reason for the
performance problem.
This also provides more correct answers. In the case where a user's
1001'th message sent was to person A today, but my most recent message
received was from them yesterday, the previous plan would show the
message I received yesterday message-id as the max, and not the more
recent message I sent today.
While we could theoretically raise the `RECENT_CONVERSATIONS_LIMIT` to
more frequently match the same recipient list as previously, this
increases the cost of the most common cases unreasonably. With a
1000-message limit, the common cases are slightly faster, and the tail
latencies are very much improved; raising `RECENT_CONVERSATIONS_LIMIT`
would increase the result similarity to the old algorithm, at the cost
of the p50 and p75.
| | Old | New |
| ------ | ------- | ------- |
| Mean | 0.05287 | 0.02520 |
| p50 | 0.00695 | 0.00556 |
| p75 | 0.05592 | 0.03351 |
| p90 | 0.14645 | 0.08026 |
| p95 | 0.20181 | 0.10906 |
| p99 | 0.30691 | 0.16014 |
| p99.9 | 0.57894 | 0.19521 |
| max | 22.0610 | 0.22184 |
On the whole, however, the much more bounded worst case are worth the
small changes to the resultset.
2023-12-01 17:25:31 +01:00
|
|
|
It may be possible to write this query directly in Django, however
|
|
|
|
it is made much easier by using CTEs, which Django does not
|
|
|
|
natively support.
|
2019-03-20 04:15:58 +01:00
|
|
|
|
|
|
|
We return a dictionary structure for convenient modification
|
|
|
|
below; this structure is converted into its final form by
|
|
|
|
post_process.
|
|
|
|
|
|
|
|
"""
|
|
|
|
RECENT_CONVERSATIONS_LIMIT = 1000
|
|
|
|
|
|
|
|
recipient_map = {}
|
2019-11-28 20:31:18 +01:00
|
|
|
my_recipient_id = user_profile.recipient_id
|
2019-03-20 04:15:58 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
query = SQL(
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
message: Rewrite personals query to be more performant and accurate.
The previous query suffered from bad corner cases when the user had
received a large number of direct messages but sent very few,
comparatively. This mean that the first half of the UNION would
retrieve a very large number of UserMessage rows, requiring fetching a
large number of Message rows, merely to throw them away upon
determining that the recipient was the current user.
Instead of merging two queries of "last 1k received" + "last 1k sent",
we instead make better use of the UserMessage rows to find "last 1k
sent or received." This may change the list of recipients, as large
disparities in sent/received messages may result in pushing the
most-recently-sent users off of the list. These are likely uncommon
edge cases, however -- and the disparity is the whole reason for the
performance problem.
This also provides more correct answers. In the case where a user's
1001'th message sent was to person A today, but my most recent message
received was from them yesterday, the previous plan would show the
message I received yesterday message-id as the max, and not the more
recent message I sent today.
While we could theoretically raise the `RECENT_CONVERSATIONS_LIMIT` to
more frequently match the same recipient list as previously, this
increases the cost of the most common cases unreasonably. With a
1000-message limit, the common cases are slightly faster, and the tail
latencies are very much improved; raising `RECENT_CONVERSATIONS_LIMIT`
would increase the result similarity to the old algorithm, at the cost
of the p50 and p75.
| | Old | New |
| ------ | ------- | ------- |
| Mean | 0.05287 | 0.02520 |
| p50 | 0.00695 | 0.00556 |
| p75 | 0.05592 | 0.03351 |
| p90 | 0.14645 | 0.08026 |
| p95 | 0.20181 | 0.10906 |
| p99 | 0.30691 | 0.16014 |
| p99.9 | 0.57894 | 0.19521 |
| max | 22.0610 | 0.22184 |
On the whole, however, the much more bounded worst case are worth the
small changes to the resultset.
2023-12-01 17:25:31 +01:00
|
|
|
WITH personals AS (
|
|
|
|
SELECT um.message_id AS message_id
|
|
|
|
FROM zerver_usermessage um
|
|
|
|
WHERE um.user_profile_id = %(user_profile_id)s
|
|
|
|
AND um.flags & 2048 <> 0
|
|
|
|
ORDER BY message_id DESC limit %(conversation_limit)s
|
|
|
|
),
|
|
|
|
message AS (
|
|
|
|
SELECT message_id,
|
|
|
|
CASE
|
|
|
|
WHEN m.recipient_id = %(my_recipient_id)s
|
|
|
|
THEN m.sender_id
|
|
|
|
ELSE NULL
|
|
|
|
END AS sender_id,
|
|
|
|
CASE
|
|
|
|
WHEN m.recipient_id <> %(my_recipient_id)s
|
|
|
|
THEN m.recipient_id
|
|
|
|
ELSE NULL
|
|
|
|
END AS outgoing_recipient_id
|
|
|
|
FROM personals
|
|
|
|
JOIN zerver_message m
|
|
|
|
ON personals.message_id = m.id
|
|
|
|
),
|
|
|
|
unified AS (
|
|
|
|
SELECT message_id,
|
|
|
|
COALESCE(zerver_userprofile.recipient_id, outgoing_recipient_id) AS other_recipient_id
|
|
|
|
FROM message
|
|
|
|
LEFT JOIN zerver_userprofile
|
|
|
|
ON zerver_userprofile.id = sender_id
|
|
|
|
)
|
|
|
|
SELECT other_recipient_id,
|
|
|
|
MAX(message_id)
|
|
|
|
FROM unified
|
|
|
|
GROUP BY other_recipient_id
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2019-03-20 04:15:58 +01:00
|
|
|
|
2020-06-09 11:57:28 +02:00
|
|
|
with connection.cursor() as cursor:
|
2021-02-12 08:19:30 +01:00
|
|
|
cursor.execute(
|
|
|
|
query,
|
|
|
|
{
|
|
|
|
"user_profile_id": user_profile.id,
|
|
|
|
"conversation_limit": RECENT_CONVERSATIONS_LIMIT,
|
|
|
|
"my_recipient_id": my_recipient_id,
|
|
|
|
},
|
|
|
|
)
|
2020-06-09 11:57:28 +02:00
|
|
|
rows = cursor.fetchall()
|
2019-03-20 04:15:58 +01:00
|
|
|
|
|
|
|
# The resulting rows will be (recipient_id, max_message_id)
|
|
|
|
# objects for all parties we've had recent (group?) private
|
2023-06-19 16:42:11 +02:00
|
|
|
# message conversations with, including direct messages with
|
|
|
|
# yourself (those will generate an empty list of user_ids).
|
2019-03-20 04:15:58 +01:00
|
|
|
for recipient_id, max_message_id in rows:
|
|
|
|
recipient_map[recipient_id] = dict(
|
|
|
|
max_message_id=max_message_id,
|
2020-09-02 08:17:06 +02:00
|
|
|
user_ids=[],
|
2019-03-20 04:15:58 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
# Now we need to map all the recipient_id objects to lists of user IDs
|
2023-02-02 04:35:24 +01:00
|
|
|
for recipient_id, user_profile_id in (
|
2021-02-12 08:19:30 +01:00
|
|
|
Subscription.objects.filter(recipient_id__in=recipient_map.keys())
|
|
|
|
.exclude(user_profile_id=user_profile.id)
|
|
|
|
.values_list("recipient_id", "user_profile_id")
|
|
|
|
):
|
2021-02-12 08:20:45 +01:00
|
|
|
recipient_map[recipient_id]["user_ids"].append(user_profile_id)
|
2020-01-01 16:27:14 +01:00
|
|
|
|
|
|
|
# Sort to prevent test flakes and client bugs.
|
|
|
|
for rec in recipient_map.values():
|
2021-02-12 08:20:45 +01:00
|
|
|
rec["user_ids"].sort()
|
2020-01-01 16:27:14 +01:00
|
|
|
|
2019-03-20 04:15:58 +01:00
|
|
|
return recipient_map
|
2020-09-11 16:11:06 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-11-21 10:39:13 +01:00
|
|
|
def wildcard_mention_policy_authorizes_user(sender: UserProfile, realm: Realm) -> bool:
|
|
|
|
"""Helper function for 'topic_wildcard_mention_allowed' and
|
|
|
|
'stream_wildcard_mention_allowed' to check if the sender is allowed to use
|
|
|
|
wildcard mentions based on the 'wildcard_mention_policy' setting of that realm.
|
|
|
|
This check is used only if the participants count in the topic or the subscribers
|
|
|
|
count in the stream is greater than 'Realm.WILDCARD_MENTION_THRESHOLD'.
|
|
|
|
"""
|
2024-05-22 11:43:10 +02:00
|
|
|
if realm.wildcard_mention_policy == WildcardMentionPolicyEnum.NOBODY:
|
2020-09-11 16:11:06 +02:00
|
|
|
return False
|
|
|
|
|
2024-05-22 11:43:10 +02:00
|
|
|
if realm.wildcard_mention_policy == WildcardMentionPolicyEnum.EVERYONE:
|
2020-09-11 16:11:06 +02:00
|
|
|
return True
|
|
|
|
|
2024-05-22 11:43:10 +02:00
|
|
|
if realm.wildcard_mention_policy == WildcardMentionPolicyEnum.ADMINS:
|
2020-09-11 16:11:06 +02:00
|
|
|
return sender.is_realm_admin
|
|
|
|
|
2024-05-22 11:43:10 +02:00
|
|
|
if realm.wildcard_mention_policy == WildcardMentionPolicyEnum.MODERATORS:
|
2021-05-02 09:56:58 +02:00
|
|
|
return sender.is_realm_admin or sender.is_moderator
|
|
|
|
|
2024-05-22 11:43:10 +02:00
|
|
|
if realm.wildcard_mention_policy == WildcardMentionPolicyEnum.FULL_MEMBERS:
|
2021-02-24 20:39:28 +01:00
|
|
|
return sender.is_realm_admin or (not sender.is_provisional_member and not sender.is_guest)
|
2020-09-11 16:11:06 +02:00
|
|
|
|
2024-05-22 11:43:10 +02:00
|
|
|
if realm.wildcard_mention_policy == WildcardMentionPolicyEnum.MEMBERS:
|
2020-09-11 16:11:06 +02:00
|
|
|
return not sender.is_guest
|
|
|
|
|
|
|
|
raise AssertionError("Invalid wildcard mention policy")
|
2021-06-14 18:49:28 +02:00
|
|
|
|
|
|
|
|
2023-11-21 10:39:13 +01:00
|
|
|
def topic_wildcard_mention_allowed(
|
|
|
|
sender: UserProfile, topic_participant_count: int, realm: Realm
|
|
|
|
) -> bool:
|
|
|
|
if topic_participant_count <= Realm.WILDCARD_MENTION_THRESHOLD:
|
|
|
|
return True
|
|
|
|
return wildcard_mention_policy_authorizes_user(sender, realm)
|
|
|
|
|
|
|
|
|
|
|
|
def stream_wildcard_mention_allowed(sender: UserProfile, stream: Stream, realm: Realm) -> bool:
|
|
|
|
# If there are fewer than Realm.WILDCARD_MENTION_THRESHOLD, we
|
|
|
|
# allow sending. In the future, we may want to make this behavior
|
|
|
|
# a default, and also just allow explicitly setting whether this
|
|
|
|
# applies to a stream as an override.
|
|
|
|
if num_subscribers_for_stream_id(stream.id) <= Realm.WILDCARD_MENTION_THRESHOLD:
|
|
|
|
return True
|
|
|
|
return wildcard_mention_policy_authorizes_user(sender, realm)
|
|
|
|
|
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def check_user_group_mention_allowed(sender: UserProfile, user_group_ids: list[int]) -> None:
|
2024-04-18 09:52:37 +02:00
|
|
|
user_groups = NamedUserGroup.objects.filter(id__in=user_group_ids).select_related(
|
2024-04-19 16:37:29 +02:00
|
|
|
"can_mention_group", "can_mention_group__named_user_group"
|
2023-06-13 05:47:02 +02:00
|
|
|
)
|
2024-01-10 07:58:52 +01:00
|
|
|
sender_is_system_bot = is_cross_realm_bot_email(sender.delivery_email)
|
2023-06-13 05:47:02 +02:00
|
|
|
|
|
|
|
for group in user_groups:
|
|
|
|
can_mention_group = group.can_mention_group
|
2024-05-18 09:52:33 +02:00
|
|
|
if (
|
|
|
|
hasattr(can_mention_group, "named_user_group")
|
|
|
|
and can_mention_group.named_user_group.name == SystemGroups.EVERYONE
|
|
|
|
):
|
|
|
|
continue
|
2024-01-10 07:58:52 +01:00
|
|
|
if sender_is_system_bot:
|
|
|
|
raise JsonableError(
|
2024-05-18 09:48:42 +02:00
|
|
|
_("You are not allowed to mention user group '{user_group_name}'.").format(
|
|
|
|
user_group_name=group.name
|
|
|
|
)
|
2024-01-10 07:58:52 +01:00
|
|
|
)
|
|
|
|
|
2024-09-06 16:41:41 +02:00
|
|
|
if not user_has_permission_for_group_setting(
|
|
|
|
can_mention_group,
|
|
|
|
sender,
|
|
|
|
NamedUserGroup.GROUP_PERMISSION_SETTINGS["can_mention_group"],
|
|
|
|
direct_member_only=False,
|
|
|
|
):
|
2023-06-13 05:47:02 +02:00
|
|
|
raise JsonableError(
|
2024-05-18 09:48:42 +02:00
|
|
|
_("You are not allowed to mention user group '{user_group_name}'.").format(
|
|
|
|
user_group_name=group.name
|
|
|
|
)
|
2023-06-13 05:47:02 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-12-23 14:56:27 +01:00
|
|
|
def parse_message_time_limit_setting(
|
2024-07-12 02:30:23 +02:00
|
|
|
value: int | str,
|
|
|
|
special_values_map: Mapping[str, int | None],
|
2022-04-12 13:13:02 +02:00
|
|
|
*,
|
|
|
|
setting_name: str,
|
2024-07-12 02:30:23 +02:00
|
|
|
) -> int | None:
|
2022-12-12 03:39:16 +01:00
|
|
|
if isinstance(value, str) and value in special_values_map:
|
2021-06-14 18:49:28 +02:00
|
|
|
return special_values_map[value]
|
|
|
|
if isinstance(value, str) or value <= 0:
|
2022-04-12 13:13:02 +02:00
|
|
|
raise RequestVariableConversionError(setting_name, value)
|
2021-06-14 18:49:28 +02:00
|
|
|
assert isinstance(value, int)
|
|
|
|
return value
|
2022-04-14 23:45:46 +02:00
|
|
|
|
|
|
|
|
2023-06-17 17:37:04 +02:00
|
|
|
def visibility_policy_for_participation(
|
|
|
|
sender: UserProfile,
|
2024-07-12 02:30:23 +02:00
|
|
|
is_stream_muted: bool | None,
|
|
|
|
) -> int | None:
|
2023-06-17 17:37:04 +02:00
|
|
|
"""
|
|
|
|
This function determines the visibility policy to set when a user
|
|
|
|
participates in a topic, depending on the 'automatically_follow_topics_policy'
|
|
|
|
and 'automatically_unmute_topics_in_muted_streams_policy' settings.
|
|
|
|
"""
|
|
|
|
if (
|
|
|
|
sender.automatically_follow_topics_policy
|
|
|
|
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION
|
|
|
|
):
|
|
|
|
return UserTopic.VisibilityPolicy.FOLLOWED
|
|
|
|
|
|
|
|
if (
|
|
|
|
is_stream_muted
|
|
|
|
and sender.automatically_unmute_topics_in_muted_streams_policy
|
|
|
|
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION
|
|
|
|
):
|
|
|
|
return UserTopic.VisibilityPolicy.UNMUTED
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def visibility_policy_for_send(
|
|
|
|
sender: UserProfile,
|
2024-07-12 02:30:23 +02:00
|
|
|
is_stream_muted: bool | None,
|
|
|
|
) -> int | None:
|
2023-06-17 17:37:04 +02:00
|
|
|
if (
|
|
|
|
sender.automatically_follow_topics_policy
|
|
|
|
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND
|
|
|
|
):
|
|
|
|
return UserTopic.VisibilityPolicy.FOLLOWED
|
|
|
|
|
|
|
|
if (
|
|
|
|
is_stream_muted
|
|
|
|
and sender.automatically_unmute_topics_in_muted_streams_policy
|
|
|
|
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND
|
|
|
|
):
|
|
|
|
return UserTopic.VisibilityPolicy.UNMUTED
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def visibility_policy_for_send_message(
|
|
|
|
sender: UserProfile,
|
|
|
|
message: Message,
|
|
|
|
stream: Stream,
|
2024-07-12 02:30:23 +02:00
|
|
|
is_stream_muted: bool | None,
|
2023-06-17 17:37:04 +02:00
|
|
|
current_visibility_policy: int,
|
2024-07-12 02:30:23 +02:00
|
|
|
) -> int | None:
|
2023-06-17 17:37:04 +02:00
|
|
|
"""
|
|
|
|
This function determines the visibility policy to set when a message
|
|
|
|
is sent to a topic, depending on the 'automatically_follow_topics_policy'
|
|
|
|
and 'automatically_unmute_topics_in_muted_streams_policy' settings.
|
|
|
|
|
|
|
|
It returns None when the policies can't make it more visible than the
|
|
|
|
current visibility policy.
|
|
|
|
"""
|
|
|
|
# We prioritize 'FOLLOW' over 'UNMUTE' in muted streams.
|
|
|
|
# We need to carefully handle the following two cases:
|
|
|
|
#
|
|
|
|
# 1. When an action qualifies for multiple values. Example:
|
|
|
|
# - starting a topic is INITIATION, PARTICIPATION as well as SEND
|
|
|
|
# - sending a non-first message is PARTICIPATION as well as SEND
|
|
|
|
# action | 'automatically_follow_topics_policy' | 'automatically_unmute_topics_in_muted_streams_policy' | visibility_policy
|
|
|
|
# start | ON_PARTICIPATION / ON_SEND | ON_INITIATION | FOLLOWED
|
|
|
|
# send | ON_SEND / ON_PARTICIPATION | ON_PARTICIPATION / ON_SEND | FOLLOWED
|
|
|
|
#
|
|
|
|
# 2. When both the policies have the same values.
|
|
|
|
# action | 'automatically_follow_topics_policy' | 'automatically_unmute_topics_in_muted_streams_policy' | visibility_policy
|
|
|
|
# start | ON_INITIATION | ON_INITIATION | FOLLOWED
|
|
|
|
# partc | ON_PARTICIPATION | ON_PARTICIPATION | FOLLOWED
|
|
|
|
# send | ON_SEND | ON_SEND | FOLLOWED
|
|
|
|
visibility_policy = None
|
|
|
|
|
|
|
|
if current_visibility_policy == UserTopic.VisibilityPolicy.FOLLOWED:
|
|
|
|
return visibility_policy
|
|
|
|
|
|
|
|
visibility_policy_participation = visibility_policy_for_participation(sender, is_stream_muted)
|
|
|
|
visibility_policy_send = visibility_policy_for_send(sender, is_stream_muted)
|
|
|
|
|
|
|
|
if UserTopic.VisibilityPolicy.FOLLOWED in (
|
|
|
|
visibility_policy_participation,
|
|
|
|
visibility_policy_send,
|
|
|
|
):
|
|
|
|
return UserTopic.VisibilityPolicy.FOLLOWED
|
|
|
|
|
|
|
|
if UserTopic.VisibilityPolicy.UNMUTED in (
|
|
|
|
visibility_policy_participation,
|
|
|
|
visibility_policy_send,
|
|
|
|
):
|
|
|
|
visibility_policy = UserTopic.VisibilityPolicy.UNMUTED
|
|
|
|
|
|
|
|
# If a topic has a visibility policy set, it can't be the case
|
|
|
|
# of initiation. We return early, thus saving a DB query.
|
|
|
|
if current_visibility_policy != UserTopic.VisibilityPolicy.INHERIT:
|
|
|
|
if visibility_policy and current_visibility_policy == visibility_policy:
|
|
|
|
return None
|
|
|
|
return visibility_policy
|
|
|
|
|
|
|
|
# Now we need to check if the user initiated the topic.
|
2024-07-12 02:30:23 +02:00
|
|
|
old_accessible_messages_in_topic: QuerySet[Message] | QuerySet[UserMessage]
|
2023-06-17 17:37:04 +02:00
|
|
|
if can_access_stream_history(sender, stream):
|
|
|
|
old_accessible_messages_in_topic = messages_for_topic(
|
|
|
|
realm_id=sender.realm_id,
|
|
|
|
stream_recipient_id=message.recipient_id,
|
|
|
|
topic_name=message.topic_name(),
|
|
|
|
).exclude(id=message.id)
|
|
|
|
else:
|
|
|
|
# We use the user's own message access to avoid leaking information in
|
|
|
|
# private streams with protected history.
|
|
|
|
old_accessible_messages_in_topic = UserMessage.objects.filter(
|
|
|
|
user_profile=sender,
|
|
|
|
message__recipient_id=message.recipient_id,
|
|
|
|
message__subject__iexact=message.topic_name(),
|
|
|
|
).exclude(message_id=message.id)
|
|
|
|
|
|
|
|
if (
|
|
|
|
sender.automatically_follow_topics_policy
|
|
|
|
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION
|
|
|
|
and not old_accessible_messages_in_topic.exists()
|
|
|
|
):
|
|
|
|
return UserTopic.VisibilityPolicy.FOLLOWED
|
|
|
|
|
|
|
|
if (
|
|
|
|
is_stream_muted
|
|
|
|
and sender.automatically_unmute_topics_in_muted_streams_policy
|
|
|
|
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION
|
|
|
|
and not old_accessible_messages_in_topic.exists()
|
|
|
|
):
|
|
|
|
visibility_policy = UserTopic.VisibilityPolicy.UNMUTED
|
|
|
|
|
|
|
|
return visibility_policy
|
|
|
|
|
|
|
|
|
|
|
|
def should_change_visibility_policy(
|
|
|
|
new_visibility_policy: int,
|
|
|
|
sender: UserProfile,
|
|
|
|
stream_id: int,
|
|
|
|
topic_name: str,
|
|
|
|
) -> bool:
|
|
|
|
try:
|
|
|
|
user_topic = UserTopic.objects.get(
|
|
|
|
user_profile=sender, stream_id=stream_id, topic_name__iexact=topic_name
|
|
|
|
)
|
|
|
|
except UserTopic.DoesNotExist:
|
|
|
|
return True
|
|
|
|
current_visibility_policy = user_topic.visibility_policy
|
|
|
|
|
|
|
|
if new_visibility_policy == current_visibility_policy:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# The intent of these "automatically follow or unmute" policies is that they
|
|
|
|
# can only increase the user's visibility policy for the topic. If a topic is
|
|
|
|
# already FOLLOWED, we don't change the state to UNMUTED due to these policies.
|
|
|
|
if current_visibility_policy == UserTopic.VisibilityPolicy.FOLLOWED:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def set_visibility_policy_possible(user_profile: UserProfile, message: Message) -> bool:
|
|
|
|
"""If the user can set a visibility policy."""
|
|
|
|
if not message.is_stream_message():
|
|
|
|
return False
|
|
|
|
|
|
|
|
if user_profile.is_bot:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if user_profile.realm != message.get_realm():
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
2024-03-23 04:29:56 +01:00
|
|
|
|
|
|
|
|
|
|
|
def remove_single_newlines(content: str) -> str:
|
|
|
|
content = content.strip("\n")
|
2024-05-30 20:32:52 +02:00
|
|
|
return re.sub(r"(?<!\n)\n(?!\n|[-*] |[0-9]+\. )", " ", content)
|