2023-11-19 19:45:19 +01:00
|
|
|
from datetime import timedelta
|
2019-01-21 19:06:03 +01:00
|
|
|
from typing import Any, Dict, Optional
|
2016-07-26 23:26:39 +02:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.conf import settings
|
2016-07-26 23:26:39 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2017-04-16 21:32:57 +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 _
|
2023-08-17 01:51:21 +02:00
|
|
|
from pydantic import Json, StringConstraints
|
|
|
|
from typing_extensions import Annotated
|
2016-07-26 23:26:39 +02:00
|
|
|
|
2022-09-16 18:05:17 +02:00
|
|
|
from zerver.actions.presence import update_user_presence
|
|
|
|
from zerver.actions.user_status import do_update_user_status
|
2017-11-04 00:59:22 +01:00
|
|
|
from zerver.decorator import human_users_only
|
2023-07-14 14:25:57 +02:00
|
|
|
from zerver.lib.emoji import check_emoji_request, get_emoji_data
|
2021-07-16 22:11:10 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.presence import get_presence_for_user, get_presence_response
|
2023-08-17 01:51:21 +02:00
|
|
|
from zerver.lib.request import RequestNotes
|
2021-06-30 18:35:50 +02:00
|
|
|
from zerver.lib.response import json_success
|
2017-03-06 12:25:37 +01:00
|
|
|
from zerver.lib.timestamp import datetime_to_timestamp
|
2023-08-17 01:51:21 +02:00
|
|
|
from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint
|
2023-10-17 12:56:39 +02:00
|
|
|
from zerver.lib.users import check_can_access_user
|
2023-12-15 01:16:00 +01:00
|
|
|
from zerver.models import UserActivity, UserPresence, UserProfile, UserStatus
|
|
|
|
from zerver.models.users import get_active_user, get_active_user_profile_by_id_in_realm
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2016-07-26 23:26:39 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def get_presence_backend(
|
2021-01-04 19:36:00 +01:00
|
|
|
request: HttpRequest, user_profile: UserProfile, user_id_or_email: str
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2021-05-14 00:16:30 +02:00
|
|
|
# This isn't used by the web app; it's available for API use by
|
2020-02-02 17:29:05 +01:00
|
|
|
# bots and other clients. We may want to add slim_presence
|
|
|
|
# support for it (or just migrate its API wholesale) later.
|
2021-01-04 19:36:00 +01:00
|
|
|
|
2017-02-11 08:38:16 +01:00
|
|
|
try:
|
2021-01-04 19:36:00 +01:00
|
|
|
try:
|
|
|
|
user_id = int(user_id_or_email)
|
|
|
|
target = get_active_user_profile_by_id_in_realm(user_id, user_profile.realm)
|
|
|
|
except ValueError:
|
|
|
|
email = user_id_or_email
|
|
|
|
target = get_active_user(email, user_profile.realm)
|
2017-02-11 08:38:16 +01:00
|
|
|
except UserProfile.DoesNotExist:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("No such user"))
|
2021-01-04 19:36:00 +01:00
|
|
|
|
2017-02-11 08:38:16 +01:00
|
|
|
if target.is_bot:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("Presence is not supported for bot users."))
|
2017-02-11 08:38:16 +01:00
|
|
|
|
2023-10-17 12:56:39 +02:00
|
|
|
if settings.CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE and not check_can_access_user(
|
|
|
|
target, user_profile
|
|
|
|
):
|
|
|
|
raise JsonableError(_("Insufficient permission"))
|
|
|
|
|
2020-02-06 17:52:12 +01:00
|
|
|
presence_dict = get_presence_for_user(target.id)
|
2017-02-11 08:38:16 +01:00
|
|
|
if len(presence_dict) == 0:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(
|
2021-01-04 19:36:00 +01:00
|
|
|
_("No presence data for {user_id_or_email}").format(user_id_or_email=user_id_or_email)
|
|
|
|
)
|
2017-02-11 08:38:16 +01:00
|
|
|
|
|
|
|
# For initial version, we just include the status and timestamp keys
|
|
|
|
result = dict(presence=presence_dict[target.email])
|
2021-02-12 08:20:45 +01:00
|
|
|
aggregated_info = result["presence"]["aggregated"]
|
|
|
|
aggr_status_duration = datetime_to_timestamp(timezone_now()) - aggregated_info["timestamp"]
|
2017-03-06 12:25:37 +01:00
|
|
|
if aggr_status_duration > settings.OFFLINE_THRESHOLD_SECS:
|
2021-02-12 08:20:45 +01:00
|
|
|
aggregated_info["status"] = "offline"
|
|
|
|
for val in result["presence"].values():
|
|
|
|
val.pop("client", None)
|
|
|
|
val.pop("pushable", None)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=result)
|
2017-02-11 08:38:16 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-12-17 22:04:07 +01:00
|
|
|
@human_users_only
|
2023-08-17 01:51:21 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def update_user_status_backend(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2023-08-17 01:51:21 +02:00
|
|
|
*,
|
|
|
|
away: Optional[Json[bool]] = None,
|
|
|
|
status_text: Annotated[
|
|
|
|
Optional[str], StringConstraints(strip_whitespace=True, max_length=60)
|
|
|
|
] = None,
|
|
|
|
emoji_name: Optional[str] = None,
|
|
|
|
emoji_code: Optional[str] = None,
|
2021-06-22 18:42:31 +02:00
|
|
|
# TODO: emoji_type is the more appropriate name for this parameter, but changing
|
|
|
|
# that requires nontrivial work on the API documentation, since it's not clear
|
|
|
|
# that the reactions endpoint would prefer such a change.
|
2023-08-17 01:51:21 +02:00
|
|
|
emoji_type: Annotated[Optional[str], ApiParamConfig("reaction_type")] = None,
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2019-01-21 19:06:03 +01:00
|
|
|
if status_text is not None:
|
|
|
|
status_text = status_text.strip()
|
|
|
|
|
2021-06-22 18:42:31 +02:00
|
|
|
if (away is None) and (status_text is None) and (emoji_name is None):
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("Client did not pass any new values."))
|
2019-01-21 19:06:03 +01:00
|
|
|
|
2021-06-22 18:42:31 +02:00
|
|
|
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:
|
2023-07-14 14:25:57 +02:00
|
|
|
if emoji_code is None or emoji_type is None:
|
|
|
|
emoji_data = get_emoji_data(user_profile.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
|
2021-06-22 18:42:31 +02:00
|
|
|
|
|
|
|
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(user_profile.realm, emoji_name, emoji_code, emoji_type)
|
|
|
|
|
2021-08-21 19:24:20 +02:00
|
|
|
client = RequestNotes.get_notes(request).client
|
2021-07-09 18:10:51 +02:00
|
|
|
assert client is not None
|
2019-01-21 18:19:59 +01:00
|
|
|
do_update_user_status(
|
|
|
|
user_profile=user_profile,
|
|
|
|
away=away,
|
2019-01-21 19:06:03 +01:00
|
|
|
status_text=status_text,
|
2021-07-09 18:10:51 +02:00
|
|
|
client_id=client.id,
|
2021-06-22 18:42:31 +02:00
|
|
|
emoji_name=emoji_name,
|
|
|
|
emoji_code=emoji_code,
|
|
|
|
reaction_type=emoji_type,
|
2019-01-21 18:19:59 +01:00
|
|
|
)
|
2018-12-17 22:04:07 +01:00
|
|
|
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|
2018-12-17 22:04:07 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-04-15 20:51:51 +02:00
|
|
|
@human_users_only
|
2023-08-17 01:51:21 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def update_active_status_backend(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2023-08-17 01:51:21 +02:00
|
|
|
*,
|
|
|
|
status: str,
|
|
|
|
ping_only: Json[bool] = False,
|
|
|
|
new_user_input: Json[bool] = False,
|
|
|
|
slim_presence: Json[bool] = False,
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2016-07-26 23:26:39 +02:00
|
|
|
status_val = UserPresence.status_from_string(status)
|
|
|
|
if status_val is None:
|
2023-07-17 22:40:33 +02:00
|
|
|
raise JsonableError(_("Invalid status: {status}").format(status=status))
|
2020-05-01 20:39:26 +02:00
|
|
|
elif user_profile.presence_enabled:
|
2021-08-21 19:24:20 +02:00
|
|
|
client = RequestNotes.get_notes(request).client
|
2021-07-09 18:10:51 +02:00
|
|
|
assert client is not None
|
|
|
|
update_user_presence(user_profile, client, timezone_now(), status_val, new_user_input)
|
2016-07-26 23:26:39 +02:00
|
|
|
|
2017-03-31 01:46:45 +02:00
|
|
|
if ping_only:
|
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
|
|
|
ret: Dict[str, Any] = {}
|
2017-03-31 01:46:45 +02:00
|
|
|
else:
|
2020-02-06 15:39:58 +01:00
|
|
|
ret = get_presence_response(user_profile, slim_presence)
|
2017-03-31 01:46:45 +02:00
|
|
|
|
2016-07-27 01:45:29 +02:00
|
|
|
if user_profile.realm.is_zephyr_mirror_realm:
|
2016-09-13 23:45:27 +02:00
|
|
|
# In zephyr mirroring realms, users can't see the presence of other
|
|
|
|
# users, but each user **is** interested in whether their mirror bot
|
|
|
|
# (running as their user) has been active.
|
2016-07-26 23:26:39 +02:00
|
|
|
try:
|
2021-02-12 08:19:30 +01:00
|
|
|
activity = UserActivity.objects.get(
|
|
|
|
user_profile=user_profile, query="get_events", client__name="zephyr_mirror"
|
|
|
|
)
|
2016-07-26 23:26:39 +02:00
|
|
|
|
2023-11-19 19:45:19 +01:00
|
|
|
ret["zephyr_mirror_active"] = activity.last_visit > timezone_now() - timedelta(
|
2021-02-12 08:19:30 +01:00
|
|
|
minutes=5
|
|
|
|
)
|
2016-07-26 23:26:39 +02:00
|
|
|
except UserActivity.DoesNotExist:
|
2021-02-12 08:20:45 +01:00
|
|
|
ret["zephyr_mirror_active"] = False
|
2016-07-26 23:26:39 +02:00
|
|
|
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=ret)
|
2018-10-14 19:22:04 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-10-14 19:22:04 +02:00
|
|
|
def get_statuses_for_realm(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
2021-05-14 00:16:30 +02:00
|
|
|
# This isn't used by the web app; it's available for API use by
|
2020-02-02 17:29:05 +01:00
|
|
|
# bots and other clients. We may want to add slim_presence
|
|
|
|
# support for it (or just migrate its API wholesale) later.
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=get_presence_response(user_profile, slim_presence=False))
|