Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
202 lines
5.9 KiB
Python
202 lines
5.9 KiB
Python
from dataclasses import dataclass
|
|
from typing import TypedDict
|
|
|
|
from django.db.models import Q
|
|
from django.utils.timezone import now as timezone_now
|
|
from django.utils.translation import gettext as _
|
|
|
|
from zerver.lib.emoji import check_emoji_request, get_emoji_data
|
|
from zerver.lib.exceptions import JsonableError
|
|
from zerver.lib.users import check_user_can_access_all_users, get_accessible_user_ids
|
|
from zerver.models import Realm, UserProfile, UserStatus
|
|
|
|
|
|
class UserInfoDict(TypedDict, total=False):
|
|
status_text: str
|
|
emoji_name: str
|
|
emoji_code: str
|
|
reaction_type: str
|
|
away: bool
|
|
|
|
|
|
class RawUserInfoDict(TypedDict):
|
|
user_profile_id: int
|
|
user_profile__presence_enabled: bool
|
|
status_text: str
|
|
emoji_name: str
|
|
emoji_code: str
|
|
reaction_type: str
|
|
|
|
|
|
def format_user_status(row: RawUserInfoDict) -> UserInfoDict:
|
|
# Deprecated way for clients to access the user's `presence_enabled`
|
|
# setting, with away != presence_enabled. Can be removed when clients
|
|
# migrate "away" (also referred to as "unavailable") feature to directly
|
|
# use and update the user's presence_enabled setting.
|
|
presence_enabled = row["user_profile__presence_enabled"]
|
|
away = not presence_enabled
|
|
status_text = row["status_text"]
|
|
emoji_name = row["emoji_name"]
|
|
emoji_code = row["emoji_code"]
|
|
reaction_type = row["reaction_type"]
|
|
|
|
dct: UserInfoDict = {}
|
|
if away:
|
|
dct["away"] = away
|
|
if status_text:
|
|
dct["status_text"] = status_text
|
|
if emoji_name:
|
|
dct["emoji_name"] = emoji_name
|
|
dct["emoji_code"] = emoji_code
|
|
dct["reaction_type"] = reaction_type
|
|
|
|
return dct
|
|
|
|
|
|
def get_all_users_status_dict(realm: Realm, user_profile: UserProfile) -> dict[str, UserInfoDict]:
|
|
query = UserStatus.objects.filter(
|
|
user_profile__realm_id=realm.id,
|
|
user_profile__is_active=True,
|
|
).exclude(
|
|
Q(user_profile__presence_enabled=True)
|
|
& Q(status_text="")
|
|
& Q(emoji_name="")
|
|
& Q(emoji_code="")
|
|
& Q(reaction_type=UserStatus.UNICODE_EMOJI),
|
|
)
|
|
|
|
if not check_user_can_access_all_users(user_profile):
|
|
accessible_user_ids = get_accessible_user_ids(user_profile)
|
|
query = query.filter(user_profile_id__in=accessible_user_ids)
|
|
|
|
rows = query.values(
|
|
"user_profile_id",
|
|
"user_profile__presence_enabled",
|
|
"status_text",
|
|
"emoji_name",
|
|
"emoji_code",
|
|
"reaction_type",
|
|
)
|
|
|
|
user_dict: dict[str, UserInfoDict] = {}
|
|
for row in rows:
|
|
user_id = row["user_profile_id"]
|
|
user_dict[str(user_id)] = format_user_status(row)
|
|
|
|
return user_dict
|
|
|
|
|
|
def update_user_status(
|
|
user_profile_id: int,
|
|
status_text: str | None,
|
|
client_id: int,
|
|
emoji_name: str | None,
|
|
emoji_code: str | None,
|
|
reaction_type: str | None,
|
|
) -> None:
|
|
timestamp = timezone_now()
|
|
|
|
defaults = dict(
|
|
client_id=client_id,
|
|
timestamp=timestamp,
|
|
)
|
|
|
|
if status_text is not None:
|
|
defaults["status_text"] = status_text
|
|
|
|
if emoji_name is not None:
|
|
defaults["emoji_name"] = emoji_name
|
|
|
|
if emoji_code is not None:
|
|
defaults["emoji_code"] = emoji_code
|
|
|
|
if reaction_type is not None:
|
|
defaults["reaction_type"] = reaction_type
|
|
|
|
UserStatus.objects.update_or_create(
|
|
user_profile_id=user_profile_id,
|
|
defaults=defaults,
|
|
)
|
|
|
|
|
|
def get_user_status(user_profile: UserProfile) -> UserInfoDict:
|
|
status_set_by_user = (
|
|
UserStatus.objects.filter(user_profile=user_profile)
|
|
.values(
|
|
"user_profile_id",
|
|
"user_profile__presence_enabled",
|
|
"status_text",
|
|
"emoji_name",
|
|
"emoji_code",
|
|
"reaction_type",
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if not status_set_by_user:
|
|
return {}
|
|
return format_user_status(status_set_by_user)
|
|
|
|
|
|
@dataclass
|
|
class NewUserStatus:
|
|
status_text: str | None
|
|
emoji_name: str | None
|
|
emoji_code: str | None
|
|
reaction_type: str | None
|
|
|
|
|
|
def check_update_user_status(
|
|
realm: Realm,
|
|
*,
|
|
away: bool | None = None,
|
|
status_text: str | None = None,
|
|
emoji_name: str | None = None,
|
|
emoji_code: str | None = None,
|
|
emoji_type: str | None = None,
|
|
) -> NewUserStatus:
|
|
if status_text is not None:
|
|
status_text = status_text.strip()
|
|
|
|
if (away is None) and (status_text is None) and (emoji_name is None):
|
|
raise JsonableError(_("Client did not pass any new values."))
|
|
|
|
if emoji_name == "":
|
|
# Reset the emoji_code and reaction_type if emoji_name is empty.
|
|
# This should clear the user's configured emoji.
|
|
emoji_code = ""
|
|
emoji_type = UserStatus.UNICODE_EMOJI
|
|
|
|
elif emoji_name is not None:
|
|
if emoji_code is None or emoji_type is None:
|
|
emoji_data = get_emoji_data(realm.id, emoji_name)
|
|
if emoji_code is None:
|
|
# The emoji_code argument is only required for rare corner
|
|
# cases discussed in the long block comment below. For simple
|
|
# API clients, we allow specifying just the name, and just
|
|
# look up the code using the current name->code mapping.
|
|
emoji_code = emoji_data.emoji_code
|
|
|
|
if emoji_type is None:
|
|
emoji_type = emoji_data.reaction_type
|
|
|
|
elif emoji_type or emoji_code:
|
|
raise JsonableError(
|
|
_("Client must pass emoji_name if they pass either emoji_code or reaction_type.")
|
|
)
|
|
|
|
# If we're asking to set an emoji (not clear it ("") or not adjust
|
|
# it (None)), we need to verify the emoji is valid.
|
|
if emoji_name not in ["", None]:
|
|
assert emoji_name is not None
|
|
assert emoji_code is not None
|
|
assert emoji_type is not None
|
|
check_emoji_request(realm, emoji_name, emoji_code, emoji_type)
|
|
|
|
return NewUserStatus(
|
|
status_text=status_text,
|
|
emoji_name=emoji_name,
|
|
emoji_code=emoji_code,
|
|
reaction_type=emoji_type,
|
|
)
|