actions: Split out zerver.actions.user_settings.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2022-04-14 14:49:26 -07:00
parent d7981dad62
commit ec6355389a
30 changed files with 519 additions and 501 deletions

View File

@ -33,12 +33,8 @@ import zulip
from scripts.lib.zulip_tools import BOLDRED, ENDC
from tools.lib.test_script import prepare_puppeteer_run
from zerver.lib.actions import (
bulk_add_subscriptions,
do_change_avatar_fields,
do_create_user,
notify_created_bot,
)
from zerver.actions.user_settings import do_change_avatar_fields
from zerver.lib.actions import bulk_add_subscriptions, do_create_user, notify_created_bot
from zerver.lib.integrations import (
DOC_SCREENSHOT_CONFIG,
INTEGRATIONS,

View File

@ -0,0 +1,449 @@
import datetime
from typing import Optional, Union
import orjson
from django.db import transaction
from django.db.models import F
from django.utils.timezone import now as timezone_now
from confirmation.models import Confirmation, create_confirmation_link
from zerver.lib.avatar import avatar_url
from zerver.lib.cache import (
cache_delete,
delete_user_profile_caches,
user_profile_by_api_key_cache_key,
)
from zerver.lib.i18n import get_language_name
from zerver.lib.queue import queue_json_publish
from zerver.lib.send_email import FromAddress, clear_scheduled_emails, send_email
from zerver.lib.timezone import canonicalize_timezone
from zerver.lib.upload import delete_avatar_image
from zerver.lib.users import check_bot_name_available, check_full_name
from zerver.lib.utils import generate_api_key
from zerver.models import (
Draft,
EmailChangeStatus,
RealmAuditLog,
ScheduledEmail,
ScheduledMessageNotificationEmail,
UserProfile,
active_user_ids,
bot_owner_user_ids,
)
from zerver.tornado.django_api import send_event
def send_user_email_update_event(user_profile: UserProfile) -> None:
payload = dict(user_id=user_profile.id, new_email=user_profile.email)
event = dict(type="realm_user", op="update", person=payload)
transaction.on_commit(
lambda: send_event(
user_profile.realm,
event,
active_user_ids(user_profile.realm_id),
)
)
@transaction.atomic(savepoint=False)
def do_change_user_delivery_email(user_profile: UserProfile, new_email: str) -> None:
delete_user_profile_caches([user_profile])
user_profile.delivery_email = new_email
if user_profile.email_address_is_realm_public():
user_profile.email = new_email
user_profile.save(update_fields=["email", "delivery_email"])
else:
user_profile.save(update_fields=["delivery_email"])
# We notify just the target user (and eventually org admins, only
# when email_address_visibility=EMAIL_ADDRESS_VISIBILITY_ADMINS)
# about their new delivery email, since that field is private.
payload = dict(user_id=user_profile.id, delivery_email=new_email)
event = dict(type="realm_user", op="update", person=payload)
transaction.on_commit(lambda: send_event(user_profile.realm, event, [user_profile.id]))
if user_profile.avatar_source == UserProfile.AVATAR_FROM_GRAVATAR:
# If the user is using Gravatar to manage their email address,
# their Gravatar just changed, and we need to notify other
# clients.
notify_avatar_url_change(user_profile)
if user_profile.email_address_is_realm_public():
# Additionally, if we're also changing the publicly visible
# email, we send a new_email event as well.
send_user_email_update_event(user_profile)
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=user_profile,
modified_user=user_profile,
event_type=RealmAuditLog.USER_EMAIL_CHANGED,
event_time=event_time,
)
def do_start_email_change_process(user_profile: UserProfile, new_email: str) -> None:
old_email = user_profile.delivery_email
obj = EmailChangeStatus.objects.create(
new_email=new_email,
old_email=old_email,
user_profile=user_profile,
realm=user_profile.realm,
)
activation_url = create_confirmation_link(obj, Confirmation.EMAIL_CHANGE)
from zerver.context_processors import common_context
context = common_context(user_profile)
context.update(
old_email=old_email,
new_email=new_email,
activate_url=activation_url,
)
language = user_profile.default_language
send_email(
"zerver/emails/confirm_new_email",
to_emails=[new_email],
from_name=FromAddress.security_email_from_name(language=language),
from_address=FromAddress.tokenized_no_reply_address(),
language=language,
context=context,
realm=user_profile.realm,
)
def do_change_password(user_profile: UserProfile, password: str, commit: bool = True) -> None:
user_profile.set_password(password)
if commit:
user_profile.save(update_fields=["password"])
# Imported here to prevent import cycles
from zproject.backends import RateLimitedAuthenticationByUsername
RateLimitedAuthenticationByUsername(user_profile.delivery_email).clear_history()
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=user_profile,
modified_user=user_profile,
event_type=RealmAuditLog.USER_PASSWORD_CHANGED,
event_time=event_time,
)
def do_change_full_name(
user_profile: UserProfile, full_name: str, acting_user: Optional[UserProfile]
) -> None:
old_name = user_profile.full_name
user_profile.full_name = full_name
user_profile.save(update_fields=["full_name"])
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=acting_user,
modified_user=user_profile,
event_type=RealmAuditLog.USER_FULL_NAME_CHANGED,
event_time=event_time,
extra_data=old_name,
)
payload = dict(user_id=user_profile.id, full_name=user_profile.full_name)
send_event(
user_profile.realm,
dict(type="realm_user", op="update", person=payload),
active_user_ids(user_profile.realm_id),
)
if user_profile.is_bot:
send_event(
user_profile.realm,
dict(type="realm_bot", op="update", bot=payload),
bot_owner_user_ids(user_profile),
)
def check_change_full_name(
user_profile: UserProfile, full_name_raw: str, acting_user: Optional[UserProfile]
) -> str:
"""Verifies that the user's proposed full name is valid. The caller
is responsible for checking check permissions. Returns the new
full name, which may differ from what was passed in (because this
function strips whitespace)."""
new_full_name = check_full_name(full_name_raw)
do_change_full_name(user_profile, new_full_name, acting_user)
return new_full_name
def check_change_bot_full_name(
user_profile: UserProfile, full_name_raw: str, acting_user: UserProfile
) -> None:
new_full_name = check_full_name(full_name_raw)
if new_full_name == user_profile.full_name:
# Our web app will try to patch full_name even if the user didn't
# modify the name in the form. We just silently ignore those
# situations.
return
check_bot_name_available(
realm_id=user_profile.realm_id,
full_name=new_full_name,
)
do_change_full_name(user_profile, new_full_name, acting_user)
@transaction.atomic(durable=True)
def do_change_tos_version(user_profile: UserProfile, tos_version: str) -> None:
user_profile.tos_version = tos_version
user_profile.save(update_fields=["tos_version"])
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=user_profile,
modified_user=user_profile,
event_type=RealmAuditLog.USER_TERMS_OF_SERVICE_VERSION_CHANGED,
event_time=event_time,
)
def do_regenerate_api_key(user_profile: UserProfile, acting_user: UserProfile) -> str:
old_api_key = user_profile.api_key
new_api_key = generate_api_key()
user_profile.api_key = new_api_key
user_profile.save(update_fields=["api_key"])
# We need to explicitly delete the old API key from our caches,
# because the on-save handler for flushing the UserProfile object
# in zerver/lib/cache.py only has access to the new API key.
cache_delete(user_profile_by_api_key_cache_key(old_api_key))
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=acting_user,
modified_user=user_profile,
event_type=RealmAuditLog.USER_API_KEY_CHANGED,
event_time=event_time,
)
if user_profile.is_bot:
send_event(
user_profile.realm,
dict(
type="realm_bot",
op="update",
bot=dict(
user_id=user_profile.id,
api_key=new_api_key,
),
),
bot_owner_user_ids(user_profile),
)
event = {"type": "clear_push_device_tokens", "user_profile_id": user_profile.id}
queue_json_publish("deferred_work", event)
return new_api_key
def notify_avatar_url_change(user_profile: UserProfile) -> None:
if user_profile.is_bot:
bot_event = dict(
type="realm_bot",
op="update",
bot=dict(
user_id=user_profile.id,
avatar_url=avatar_url(user_profile),
),
)
transaction.on_commit(
lambda: send_event(
user_profile.realm,
bot_event,
bot_owner_user_ids(user_profile),
)
)
payload = dict(
avatar_source=user_profile.avatar_source,
avatar_url=avatar_url(user_profile),
avatar_url_medium=avatar_url(user_profile, medium=True),
avatar_version=user_profile.avatar_version,
# Even clients using client_gravatar don't need the email,
# since we're sending the URL anyway.
user_id=user_profile.id,
)
event = dict(type="realm_user", op="update", person=payload)
transaction.on_commit(
lambda: send_event(
user_profile.realm,
event,
active_user_ids(user_profile.realm_id),
)
)
@transaction.atomic(savepoint=False)
def do_change_avatar_fields(
user_profile: UserProfile,
avatar_source: str,
skip_notify: bool = False,
*,
acting_user: Optional[UserProfile],
) -> None:
user_profile.avatar_source = avatar_source
user_profile.avatar_version += 1
user_profile.save(update_fields=["avatar_source", "avatar_version"])
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
modified_user=user_profile,
event_type=RealmAuditLog.USER_AVATAR_SOURCE_CHANGED,
extra_data={"avatar_source": avatar_source},
event_time=event_time,
acting_user=acting_user,
)
if not skip_notify:
notify_avatar_url_change(user_profile)
def do_delete_avatar_image(user: UserProfile, *, acting_user: Optional[UserProfile]) -> None:
do_change_avatar_fields(user, UserProfile.AVATAR_FROM_GRAVATAR, acting_user=acting_user)
delete_avatar_image(user)
def update_scheduled_email_notifications_time(
user_profile: UserProfile, old_batching_period: int, new_batching_period: int
) -> None:
existing_scheduled_emails = ScheduledMessageNotificationEmail.objects.filter(
user_profile=user_profile
)
scheduled_timestamp_change = datetime.timedelta(
seconds=new_batching_period
) - datetime.timedelta(seconds=old_batching_period)
existing_scheduled_emails.update(
scheduled_timestamp=F("scheduled_timestamp") + scheduled_timestamp_change
)
@transaction.atomic(durable=True)
def do_change_user_setting(
user_profile: UserProfile,
setting_name: str,
setting_value: Union[bool, str, int],
*,
acting_user: Optional[UserProfile],
) -> None:
old_value = getattr(user_profile, setting_name)
event_time = timezone_now()
if setting_name == "timezone":
assert isinstance(setting_value, str)
setting_value = canonicalize_timezone(setting_value)
else:
property_type = UserProfile.property_types[setting_name]
assert isinstance(setting_value, property_type)
setattr(user_profile, setting_name, setting_value)
# TODO: Move these database actions into a transaction.atomic block.
user_profile.save(update_fields=[setting_name])
if setting_name in UserProfile.notification_setting_types:
# Prior to all personal settings being managed by property_types,
# these were only created for notification settings.
#
# TODO: Start creating these for all settings, and do a
# backfilled=True migration.
RealmAuditLog.objects.create(
realm=user_profile.realm,
event_type=RealmAuditLog.USER_SETTING_CHANGED,
event_time=event_time,
acting_user=acting_user,
modified_user=user_profile,
extra_data=orjson.dumps(
{
RealmAuditLog.OLD_VALUE: old_value,
RealmAuditLog.NEW_VALUE: setting_value,
"property": setting_name,
}
).decode(),
)
# Disabling digest emails should clear a user's email queue
if setting_name == "enable_digest_emails" and not setting_value:
clear_scheduled_emails(user_profile.id, ScheduledEmail.DIGEST)
if setting_name == "email_notifications_batching_period_seconds":
assert isinstance(old_value, int)
assert isinstance(setting_value, int)
update_scheduled_email_notifications_time(user_profile, old_value, setting_value)
event = {
"type": "user_settings",
"op": "update",
"property": setting_name,
"value": setting_value,
}
if setting_name == "default_language":
assert isinstance(setting_value, str)
event["language_name"] = get_language_name(setting_value)
transaction.on_commit(lambda: send_event(user_profile.realm, event, [user_profile.id]))
if setting_name in UserProfile.notification_settings_legacy:
# This legacy event format is for backwards-compatibility with
# clients that don't support the new user_settings event type.
# We only send this for settings added before Feature level 89.
legacy_event = {
"type": "update_global_notifications",
"user": user_profile.email,
"notification_name": setting_name,
"setting": setting_value,
}
transaction.on_commit(
lambda: send_event(user_profile.realm, legacy_event, [user_profile.id])
)
if setting_name in UserProfile.display_settings_legacy or setting_name == "timezone":
# This legacy event format is for backwards-compatibility with
# clients that don't support the new user_settings event type.
# We only send this for settings added before Feature level 89.
legacy_event = {
"type": "update_display_settings",
"user": user_profile.email,
"setting_name": setting_name,
"setting": setting_value,
}
if setting_name == "default_language":
assert isinstance(setting_value, str)
legacy_event["language_name"] = get_language_name(setting_value)
transaction.on_commit(
lambda: send_event(user_profile.realm, legacy_event, [user_profile.id])
)
# Updates to the time zone display setting are sent to all users
if setting_name == "timezone":
payload = dict(
email=user_profile.email,
user_id=user_profile.id,
timezone=canonicalize_timezone(user_profile.timezone),
)
timezone_event = dict(type="realm_user", op="update", person=payload)
transaction.on_commit(
lambda: send_event(
user_profile.realm,
timezone_event,
active_user_ids(user_profile.realm_id),
)
)
if setting_name == "enable_drafts_synchronization" and setting_value is False:
# Delete all of the drafts from the backend but don't send delete events
# for them since all that's happened is that we stopped syncing changes,
# not deleted every previously synced draft - to do that use the DELETE
# endpoint.
Draft.objects.filter(user_profile=user_profile).delete()

View File

@ -19,8 +19,9 @@ from markupsafe import Markup as mark_safe
from two_factor.forms import AuthenticationTokenForm as TwoFactorAuthenticationTokenForm
from two_factor.utils import totp_digits
from zerver.actions.user_settings import do_change_password
from zerver.decorator import rate_limit_request_by_ip
from zerver.lib.actions import do_change_password, email_not_system_bot
from zerver.lib.actions import email_not_system_bot
from zerver.lib.email_validation import email_allowed_for_realm
from zerver.lib.exceptions import JsonableError, RateLimited
from zerver.lib.name_restrictions import is_disposable_domain, is_reserved_subdomain

View File

@ -46,6 +46,7 @@ from zerver.actions.user_groups import (
do_send_user_group_members_update_event,
update_users_in_full_members_system_group,
)
from zerver.actions.user_settings import do_delete_avatar_image, send_user_email_update_event
from zerver.actions.user_topics import do_mute_topic, do_unmute_topic
from zerver.actions.users import change_user_is_active, get_service_dicts_for_bot
from zerver.lib import retention as retention
@ -58,12 +59,10 @@ from zerver.lib.cache import (
cache_delete_many,
cache_set,
cache_with_key,
delete_user_profile_caches,
display_recipient_cache_key,
flush_user_profile,
get_stream_cache_key,
to_dict_cache_key_id,
user_profile_by_api_key_cache_key,
user_profile_delivery_email_cache_key,
)
from zerver.lib.create_user import create_user, get_display_email_address
@ -78,7 +77,6 @@ from zerver.lib.exceptions import (
StreamWithIDDoesNotExistError,
ZephyrMessageAlreadySentException,
)
from zerver.lib.i18n import get_language_name
from zerver.lib.markdown import MessageRenderingResult, topic_links
from zerver.lib.markdown import version as markdown_version
from zerver.lib.mention import MentionBackend, MentionData, silent_mention_syntax_for_user
@ -103,9 +101,7 @@ from zerver.lib.recipient_users import recipient_for_user_profiles
from zerver.lib.retention import move_messages_to_archive
from zerver.lib.send_email import (
FromAddress,
clear_scheduled_emails,
clear_scheduled_invitation_emails,
send_email,
send_email_to_admins,
)
from zerver.lib.server_initialization import create_internal_realm, server_initialized
@ -140,7 +136,6 @@ from zerver.lib.streams import (
from zerver.lib.string_validation import check_stream_name, check_stream_topic
from zerver.lib.subscription_info import get_subscribers_query
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
from zerver.lib.timezone import canonicalize_timezone
from zerver.lib.topic import (
ORIG_TOPIC,
RESOLVED_TOPIC_PREFIX,
@ -154,7 +149,6 @@ from zerver.lib.topic import (
update_messages_for_topic_edit,
)
from zerver.lib.types import EditHistoryEvent
from zerver.lib.upload import delete_avatar_image
from zerver.lib.user_counts import realm_user_count, realm_user_count_by_role
from zerver.lib.user_groups import (
create_system_user_groups_for_realm,
@ -163,14 +157,8 @@ from zerver.lib.user_groups import (
from zerver.lib.user_message import UserMessageLite, bulk_insert_ums
from zerver.lib.user_mutes import add_user_mute, get_muting_users, get_user_mutes
from zerver.lib.user_topics import get_users_muting_topic, remove_topic_mute
from zerver.lib.users import (
check_bot_name_available,
check_full_name,
format_user_row,
get_api_key,
user_profile_to_user_row,
)
from zerver.lib.utils import generate_api_key, log_statsd_event
from zerver.lib.users import format_user_row, get_api_key, user_profile_to_user_row
from zerver.lib.utils import log_statsd_event
from zerver.lib.validator import check_widget_content
from zerver.lib.widget import do_widget_post_save_actions, is_widget_message
from zerver.models import (
@ -179,8 +167,6 @@ from zerver.models import (
Client,
DefaultStream,
DefaultStreamGroup,
Draft,
EmailChangeStatus,
Message,
MutedUser,
PreregistrationUser,
@ -192,7 +178,6 @@ from zerver.models import (
Recipient,
ScheduledEmail,
ScheduledMessage,
ScheduledMessageNotificationEmail,
Stream,
Subscription,
UserGroup,
@ -1198,87 +1183,6 @@ def do_deactivate_stream(
)
def send_user_email_update_event(user_profile: UserProfile) -> None:
payload = dict(user_id=user_profile.id, new_email=user_profile.email)
event = dict(type="realm_user", op="update", person=payload)
transaction.on_commit(
lambda: send_event(
user_profile.realm,
event,
active_user_ids(user_profile.realm_id),
)
)
@transaction.atomic(savepoint=False)
def do_change_user_delivery_email(user_profile: UserProfile, new_email: str) -> None:
delete_user_profile_caches([user_profile])
user_profile.delivery_email = new_email
if user_profile.email_address_is_realm_public():
user_profile.email = new_email
user_profile.save(update_fields=["email", "delivery_email"])
else:
user_profile.save(update_fields=["delivery_email"])
# We notify just the target user (and eventually org admins, only
# when email_address_visibility=EMAIL_ADDRESS_VISIBILITY_ADMINS)
# about their new delivery email, since that field is private.
payload = dict(user_id=user_profile.id, delivery_email=new_email)
event = dict(type="realm_user", op="update", person=payload)
transaction.on_commit(lambda: send_event(user_profile.realm, event, [user_profile.id]))
if user_profile.avatar_source == UserProfile.AVATAR_FROM_GRAVATAR:
# If the user is using Gravatar to manage their email address,
# their Gravatar just changed, and we need to notify other
# clients.
notify_avatar_url_change(user_profile)
if user_profile.email_address_is_realm_public():
# Additionally, if we're also changing the publicly visible
# email, we send a new_email event as well.
send_user_email_update_event(user_profile)
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=user_profile,
modified_user=user_profile,
event_type=RealmAuditLog.USER_EMAIL_CHANGED,
event_time=event_time,
)
def do_start_email_change_process(user_profile: UserProfile, new_email: str) -> None:
old_email = user_profile.delivery_email
obj = EmailChangeStatus.objects.create(
new_email=new_email,
old_email=old_email,
user_profile=user_profile,
realm=user_profile.realm,
)
activation_url = create_confirmation_link(obj, Confirmation.EMAIL_CHANGE)
from zerver.context_processors import common_context
context = common_context(user_profile)
context.update(
old_email=old_email,
new_email=new_email,
activate_url=activation_url,
)
language = user_profile.default_language
send_email(
"zerver/emails/confirm_new_email",
to_emails=[new_email],
from_name=FromAddress.security_email_from_name(language=language),
from_address=FromAddress.tokenized_no_reply_address(),
language=language,
context=context,
realm=user_profile.realm,
)
def compute_irc_user_fullname(email: str) -> str:
return email.split("@")[0] + " (IRC)"
@ -3708,84 +3612,6 @@ def do_change_subscription_property(
send_event(user_profile.realm, event, [user_profile.id])
def do_change_password(user_profile: UserProfile, password: str, commit: bool = True) -> None:
user_profile.set_password(password)
if commit:
user_profile.save(update_fields=["password"])
# Imported here to prevent import cycles
from zproject.backends import RateLimitedAuthenticationByUsername
RateLimitedAuthenticationByUsername(user_profile.delivery_email).clear_history()
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=user_profile,
modified_user=user_profile,
event_type=RealmAuditLog.USER_PASSWORD_CHANGED,
event_time=event_time,
)
def do_change_full_name(
user_profile: UserProfile, full_name: str, acting_user: Optional[UserProfile]
) -> None:
old_name = user_profile.full_name
user_profile.full_name = full_name
user_profile.save(update_fields=["full_name"])
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=acting_user,
modified_user=user_profile,
event_type=RealmAuditLog.USER_FULL_NAME_CHANGED,
event_time=event_time,
extra_data=old_name,
)
payload = dict(user_id=user_profile.id, full_name=user_profile.full_name)
send_event(
user_profile.realm,
dict(type="realm_user", op="update", person=payload),
active_user_ids(user_profile.realm_id),
)
if user_profile.is_bot:
send_event(
user_profile.realm,
dict(type="realm_bot", op="update", bot=payload),
bot_owner_user_ids(user_profile),
)
def check_change_full_name(
user_profile: UserProfile, full_name_raw: str, acting_user: Optional[UserProfile]
) -> str:
"""Verifies that the user's proposed full name is valid. The caller
is responsible for checking check permissions. Returns the new
full name, which may differ from what was passed in (because this
function strips whitespace)."""
new_full_name = check_full_name(full_name_raw)
do_change_full_name(user_profile, new_full_name, acting_user)
return new_full_name
def check_change_bot_full_name(
user_profile: UserProfile, full_name_raw: str, acting_user: UserProfile
) -> None:
new_full_name = check_full_name(full_name_raw)
if new_full_name == user_profile.full_name:
# Our web app will try to patch full_name even if the user didn't
# modify the name in the form. We just silently ignore those
# situations.
return
check_bot_name_available(
realm_id=user_profile.realm_id,
full_name=new_full_name,
)
do_change_full_name(user_profile, new_full_name, acting_user)
@transaction.atomic(durable=True)
def do_change_bot_owner(
user_profile: UserProfile, bot_owner: UserProfile, acting_user: UserProfile
@ -3865,128 +3691,6 @@ def do_change_bot_owner(
)
@transaction.atomic(durable=True)
def do_change_tos_version(user_profile: UserProfile, tos_version: str) -> None:
user_profile.tos_version = tos_version
user_profile.save(update_fields=["tos_version"])
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=user_profile,
modified_user=user_profile,
event_type=RealmAuditLog.USER_TERMS_OF_SERVICE_VERSION_CHANGED,
event_time=event_time,
)
def do_regenerate_api_key(user_profile: UserProfile, acting_user: UserProfile) -> str:
old_api_key = user_profile.api_key
new_api_key = generate_api_key()
user_profile.api_key = new_api_key
user_profile.save(update_fields=["api_key"])
# We need to explicitly delete the old API key from our caches,
# because the on-save handler for flushing the UserProfile object
# in zerver/lib/cache.py only has access to the new API key.
cache_delete(user_profile_by_api_key_cache_key(old_api_key))
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=acting_user,
modified_user=user_profile,
event_type=RealmAuditLog.USER_API_KEY_CHANGED,
event_time=event_time,
)
if user_profile.is_bot:
send_event(
user_profile.realm,
dict(
type="realm_bot",
op="update",
bot=dict(
user_id=user_profile.id,
api_key=new_api_key,
),
),
bot_owner_user_ids(user_profile),
)
event = {"type": "clear_push_device_tokens", "user_profile_id": user_profile.id}
queue_json_publish("deferred_work", event)
return new_api_key
def notify_avatar_url_change(user_profile: UserProfile) -> None:
if user_profile.is_bot:
bot_event = dict(
type="realm_bot",
op="update",
bot=dict(
user_id=user_profile.id,
avatar_url=avatar_url(user_profile),
),
)
transaction.on_commit(
lambda: send_event(
user_profile.realm,
bot_event,
bot_owner_user_ids(user_profile),
)
)
payload = dict(
avatar_source=user_profile.avatar_source,
avatar_url=avatar_url(user_profile),
avatar_url_medium=avatar_url(user_profile, medium=True),
avatar_version=user_profile.avatar_version,
# Even clients using client_gravatar don't need the email,
# since we're sending the URL anyway.
user_id=user_profile.id,
)
event = dict(type="realm_user", op="update", person=payload)
transaction.on_commit(
lambda: send_event(
user_profile.realm,
event,
active_user_ids(user_profile.realm_id),
)
)
@transaction.atomic(savepoint=False)
def do_change_avatar_fields(
user_profile: UserProfile,
avatar_source: str,
skip_notify: bool = False,
*,
acting_user: Optional[UserProfile],
) -> None:
user_profile.avatar_source = avatar_source
user_profile.avatar_version += 1
user_profile.save(update_fields=["avatar_source", "avatar_version"])
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=user_profile.realm,
modified_user=user_profile,
event_type=RealmAuditLog.USER_AVATAR_SOURCE_CHANGED,
extra_data={"avatar_source": avatar_source},
event_time=event_time,
acting_user=acting_user,
)
if not skip_notify:
notify_avatar_url_change(user_profile)
def do_delete_avatar_image(user: UserProfile, *, acting_user: Optional[UserProfile]) -> None:
do_change_avatar_fields(user, UserProfile.AVATAR_FROM_GRAVATAR, acting_user=acting_user)
delete_avatar_image(user)
@transaction.atomic(durable=True)
def do_change_realm_org_type(
realm: Realm,
@ -4828,141 +4532,6 @@ def do_create_realm(
return realm
def update_scheduled_email_notifications_time(
user_profile: UserProfile, old_batching_period: int, new_batching_period: int
) -> None:
existing_scheduled_emails = ScheduledMessageNotificationEmail.objects.filter(
user_profile=user_profile
)
scheduled_timestamp_change = datetime.timedelta(
seconds=new_batching_period
) - datetime.timedelta(seconds=old_batching_period)
existing_scheduled_emails.update(
scheduled_timestamp=F("scheduled_timestamp") + scheduled_timestamp_change
)
@transaction.atomic(durable=True)
def do_change_user_setting(
user_profile: UserProfile,
setting_name: str,
setting_value: Union[bool, str, int],
*,
acting_user: Optional[UserProfile],
) -> None:
old_value = getattr(user_profile, setting_name)
event_time = timezone_now()
if setting_name == "timezone":
assert isinstance(setting_value, str)
setting_value = canonicalize_timezone(setting_value)
else:
property_type = UserProfile.property_types[setting_name]
assert isinstance(setting_value, property_type)
setattr(user_profile, setting_name, setting_value)
# TODO: Move these database actions into a transaction.atomic block.
user_profile.save(update_fields=[setting_name])
if setting_name in UserProfile.notification_setting_types:
# Prior to all personal settings being managed by property_types,
# these were only created for notification settings.
#
# TODO: Start creating these for all settings, and do a
# backfilled=True migration.
RealmAuditLog.objects.create(
realm=user_profile.realm,
event_type=RealmAuditLog.USER_SETTING_CHANGED,
event_time=event_time,
acting_user=acting_user,
modified_user=user_profile,
extra_data=orjson.dumps(
{
RealmAuditLog.OLD_VALUE: old_value,
RealmAuditLog.NEW_VALUE: setting_value,
"property": setting_name,
}
).decode(),
)
# Disabling digest emails should clear a user's email queue
if setting_name == "enable_digest_emails" and not setting_value:
clear_scheduled_emails(user_profile.id, ScheduledEmail.DIGEST)
if setting_name == "email_notifications_batching_period_seconds":
assert isinstance(old_value, int)
assert isinstance(setting_value, int)
update_scheduled_email_notifications_time(user_profile, old_value, setting_value)
event = {
"type": "user_settings",
"op": "update",
"property": setting_name,
"value": setting_value,
}
if setting_name == "default_language":
assert isinstance(setting_value, str)
event["language_name"] = get_language_name(setting_value)
transaction.on_commit(lambda: send_event(user_profile.realm, event, [user_profile.id]))
if setting_name in UserProfile.notification_settings_legacy:
# This legacy event format is for backwards-compatibility with
# clients that don't support the new user_settings event type.
# We only send this for settings added before Feature level 89.
legacy_event = {
"type": "update_global_notifications",
"user": user_profile.email,
"notification_name": setting_name,
"setting": setting_value,
}
transaction.on_commit(
lambda: send_event(user_profile.realm, legacy_event, [user_profile.id])
)
if setting_name in UserProfile.display_settings_legacy or setting_name == "timezone":
# This legacy event format is for backwards-compatibility with
# clients that don't support the new user_settings event type.
# We only send this for settings added before Feature level 89.
legacy_event = {
"type": "update_display_settings",
"user": user_profile.email,
"setting_name": setting_name,
"setting": setting_value,
}
if setting_name == "default_language":
assert isinstance(setting_value, str)
legacy_event["language_name"] = get_language_name(setting_value)
transaction.on_commit(
lambda: send_event(user_profile.realm, legacy_event, [user_profile.id])
)
# Updates to the time zone display setting are sent to all users
if setting_name == "timezone":
payload = dict(
email=user_profile.email,
user_id=user_profile.id,
timezone=canonicalize_timezone(user_profile.timezone),
)
timezone_event = dict(type="realm_user", op="update", person=payload)
transaction.on_commit(
lambda: send_event(
user_profile.realm,
timezone_event,
active_user_ids(user_profile.realm_id),
)
)
if setting_name == "enable_drafts_synchronization" and setting_value is False:
# Delete all of the drafts from the backend but don't send delete events
# for them since all that's happened is that we stopped syncing changes,
# not deleted every previously synced draft - to do that use the DELETE
# endpoint.
Draft.objects.filter(user_profile=user_profile).delete()
def get_default_subs(user_profile: UserProfile) -> List[Stream]:
# Right now default streams are realm-wide. This wrapper gives us flexibility
# to some day further customize how we set up default streams for new users.

View File

@ -47,7 +47,7 @@ def copy_default_settings(
target_profile.save()
if settings_source.avatar_source == UserProfile.AVATAR_FROM_USER:
from zerver.lib.actions import do_change_avatar_fields
from zerver.actions.user_settings import do_change_avatar_fields
do_change_avatar_fields(
target_profile,

View File

@ -16,7 +16,8 @@ from psycopg2.extras import execute_values
from psycopg2.sql import SQL, Identifier
from analytics.models import RealmCount, StreamCount, UserCount
from zerver.lib.actions import do_change_avatar_fields, do_change_realm_plan_type
from zerver.actions.user_settings import do_change_avatar_fields
from zerver.lib.actions import do_change_realm_plan_type
from zerver.lib.avatar_hash import user_avatar_path_from_ids
from zerver.lib.bulk_create import bulk_create_users, bulk_set_users_or_streams_recipient_fields
from zerver.lib.export import DATE_FIELDS, Field, Path, Record, TableData, TableName

View File

@ -9,13 +9,9 @@ from django.http import HttpRequest
from django_scim.adapters import SCIMUser
from scim2_filter_parser.attr_paths import AttrPath
from zerver.actions.user_settings import check_change_full_name, do_change_user_delivery_email
from zerver.actions.users import do_deactivate_user
from zerver.lib.actions import (
check_change_full_name,
do_change_user_delivery_email,
do_create_user,
do_reactivate_user,
)
from zerver.lib.actions import do_create_user, do_reactivate_user
from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm
from zerver.lib.request import RequestNotes
from zerver.lib.subdomains import get_subdomain

View File

@ -2,7 +2,7 @@ from typing import Any, Dict
from django.utils.translation import gettext as _
from zerver.lib.actions import do_change_user_setting
from zerver.actions.user_settings import do_change_user_setting
from zerver.lib.exceptions import JsonableError
from zerver.models import UserProfile

View File

@ -3,7 +3,7 @@ from typing import Any
from django.core.management.base import CommandError
from zerver.lib.actions import do_change_full_name
from zerver.actions.user_settings import do_change_full_name
from zerver.lib.management import ZulipBaseCommand

View File

@ -1,7 +1,7 @@
from argparse import ArgumentParser
from typing import Any
from zerver.lib.actions import do_change_user_delivery_email
from zerver.actions.user_settings import do_change_user_delivery_email
from zerver.lib.management import ZulipBaseCommand

View File

@ -8,29 +8,31 @@ from django.utils.timezone import now as timezone_now
from analytics.models import StreamCount
from zerver.actions.realm_icon import do_change_icon_source
from zerver.actions.realm_playgrounds import do_add_realm_playground, do_remove_realm_playground
from zerver.actions.user_settings import (
do_change_avatar_fields,
do_change_password,
do_change_tos_version,
do_change_user_delivery_email,
do_change_user_setting,
do_regenerate_api_key,
)
from zerver.actions.users import do_change_user_role, do_deactivate_user
from zerver.lib.actions import (
bulk_add_subscriptions,
bulk_remove_subscriptions,
do_activate_mirror_dummy_user,
do_add_realm_domain,
do_change_avatar_fields,
do_change_bot_owner,
do_change_default_all_public_streams,
do_change_default_events_register_stream,
do_change_default_sending_stream,
do_change_password,
do_change_realm_domain,
do_change_subscription_property,
do_change_tos_version,
do_change_user_delivery_email,
do_change_user_setting,
do_create_user,
do_deactivate_realm,
do_deactivate_stream,
do_reactivate_realm,
do_reactivate_user,
do_regenerate_api_key,
do_remove_realm_domain,
do_rename_stream,
do_set_realm_authentication_methods,

View File

@ -41,9 +41,9 @@ from social_django.strategy import DjangoStrategy
from confirmation.models import Confirmation, create_confirmation_link
from zerver.actions.invites import do_invite_users
from zerver.actions.user_settings import do_change_password
from zerver.actions.users import change_user_is_active, do_deactivate_user
from zerver.lib.actions import (
do_change_password,
do_create_realm,
do_create_user,
do_deactivate_realm,
@ -6122,7 +6122,7 @@ class TestZulipLDAPUserPopulator(ZulipLDAPTestCase):
self.assertEqual(hamlet.full_name, "Full Name")
def test_same_full_name(self) -> None:
with mock.patch("zerver.lib.actions.do_change_full_name") as fn:
with mock.patch("zerver.actions.user_settings.do_change_full_name") as fn:
self.perform_ldap_sync(self.example_user("hamlet"))
fn.assert_not_called()
@ -6239,7 +6239,7 @@ class TestZulipLDAPUserPopulator(ZulipLDAPTestCase):
self.change_ldap_user_attr("hamlet", "cn", "Second Hamlet")
expected_call_args = [hamlet2, "Second Hamlet", None]
with self.settings(AUTH_LDAP_USER_ATTR_MAP={"full_name": "cn"}):
with mock.patch("zerver.lib.actions.do_change_full_name") as f:
with mock.patch("zerver.actions.user_settings.do_change_full_name") as f:
self.perform_ldap_sync(hamlet2)
f.assert_called_once_with(*expected_call_args)

View File

@ -11,12 +11,9 @@ from confirmation.models import (
create_confirmation_link,
generate_key,
)
from zerver.actions.user_settings import do_start_email_change_process
from zerver.actions.users import do_deactivate_user
from zerver.lib.actions import (
do_deactivate_realm,
do_set_realm_property,
do_start_email_change_process,
)
from zerver.lib.actions import do_deactivate_realm, do_set_realm_property
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import (
EmailChangeStatus,

View File

@ -15,8 +15,8 @@ from django.test import override_settings
from django.utils.timezone import now as timezone_now
from django_auth_ldap.config import LDAPSearch
from zerver.actions.user_settings import do_change_user_setting
from zerver.actions.users import do_change_user_role
from zerver.lib.actions import do_change_user_setting
from zerver.lib.email_notifications import (
enqueue_welcome_emails,
fix_emojis,

View File

@ -58,6 +58,13 @@ from zerver.actions.user_groups import (
do_update_user_group_name,
remove_members_from_user_group,
)
from zerver.actions.user_settings import (
do_change_avatar_fields,
do_change_full_name,
do_change_user_delivery_email,
do_change_user_setting,
do_regenerate_api_key,
)
from zerver.actions.user_topics import do_mute_topic, do_unmute_topic
from zerver.actions.users import (
do_change_user_role,
@ -71,12 +78,10 @@ from zerver.lib.actions import (
bulk_remove_subscriptions,
do_add_reaction,
do_add_realm_domain,
do_change_avatar_fields,
do_change_bot_owner,
do_change_default_all_public_streams,
do_change_default_events_register_stream,
do_change_default_sending_stream,
do_change_full_name,
do_change_realm_domain,
do_change_realm_plan_type,
do_change_stream_description,
@ -84,15 +89,12 @@ from zerver.lib.actions import (
do_change_stream_permission,
do_change_stream_post_policy,
do_change_subscription_property,
do_change_user_delivery_email,
do_change_user_setting,
do_create_user,
do_deactivate_realm,
do_deactivate_stream,
do_delete_messages,
do_mute_user,
do_reactivate_user,
do_regenerate_api_key,
do_remove_reaction,
do_remove_realm_domain,
do_rename_stream,

View File

@ -12,8 +12,9 @@ from markdown import Markdown
from zerver.actions.alert_words import do_add_alert_words
from zerver.actions.realm_emoji import do_remove_realm_emoji
from zerver.actions.user_settings import do_change_user_setting
from zerver.actions.users import change_user_is_active
from zerver.lib.actions import do_change_user_setting, do_create_realm, do_set_realm_property
from zerver.lib.actions import do_create_realm, do_set_realm_property
from zerver.lib.alert_words import get_alert_word_automaton
from zerver.lib.camo import get_camo_url
from zerver.lib.create_user import create_user

View File

@ -8,7 +8,8 @@ from django.core import mail
from django.test import override_settings
from corporate.lib.stripe import get_latest_seat_count
from zerver.lib.actions import do_change_user_setting, notify_new_user
from zerver.actions.user_settings import do_change_user_setting
from zerver.lib.actions import notify_new_user
from zerver.lib.initial_password import initial_password
from zerver.lib.streams import create_stream_if_needed
from zerver.lib.test_classes import ZulipTestCase

View File

@ -23,10 +23,10 @@ from requests.models import PreparedRequest
from analytics.lib.counts import CountStat, LoggingCountStat
from analytics.models import InstallationCount, RealmCount
from zerver.actions.user_settings import do_regenerate_api_key
from zerver.lib.actions import (
do_delete_messages,
do_mark_stream_messages_as_read,
do_regenerate_api_key,
do_update_message_flags,
)
from zerver.lib.avatar import absolute_avatar_url

View File

@ -639,7 +639,7 @@ class WorkerTest(ZulipTestCase):
with simulated_queue_client(fake_client):
worker = queue_processors.ConfirmationEmailWorker()
worker.setup()
with patch("zerver.lib.actions.send_email"), patch(
with patch("zerver.actions.user_settings.send_email"), patch(
"zerver.worker.queue_processors.send_future_email"
) as send_mock:
worker.start()

View File

@ -7,7 +7,7 @@ import orjson
from django.conf import settings
from django.http import HttpResponse
from zerver.lib.actions import do_change_full_name
from zerver.actions.user_settings import do_change_full_name
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import SCIMClient, UserProfile, get_realm

View File

@ -35,13 +35,13 @@ from zerver.actions.invites import (
do_get_invites_controlled_by_user,
do_invite_users,
)
from zerver.actions.user_settings import do_change_full_name
from zerver.actions.users import change_user_is_active, do_change_user_role, do_deactivate_user
from zerver.context_processors import common_context
from zerver.decorator import do_two_factor_login
from zerver.forms import HomepageForm, check_subdomain_available
from zerver.lib.actions import (
add_new_user_history,
do_change_full_name,
do_change_realm_subdomain,
do_create_realm,
do_create_user,

View File

@ -22,6 +22,7 @@ import zerver.lib.upload
from zerver.actions.realm_icon import do_change_icon_source
from zerver.actions.realm_logo import do_change_logo_source
from zerver.actions.uploads import do_delete_old_unclaimed_attachments
from zerver.actions.user_settings import do_delete_avatar_image
from zerver.lib.actions import (
do_change_realm_plan_type,
do_create_realm,
@ -1287,7 +1288,7 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
self.assertTrue(os.path.isfile(avatar_original_path_id))
self.assertTrue(os.path.isfile(avatar_medium_path_id))
zerver.lib.actions.do_delete_avatar_image(user, acting_user=user)
do_delete_avatar_image(user, acting_user=user)
self.assertEqual(user.avatar_source, UserProfile.AVATAR_FROM_GRAVATAR)
self.assertFalse(os.path.isfile(avatar_path_id))
@ -2124,7 +2125,7 @@ class S3Test(ZulipTestCase):
self.assertIsNotNone(bucket.Object(avatar_original_image_path_id))
self.assertIsNotNone(bucket.Object(avatar_medium_path_id))
zerver.lib.actions.do_delete_avatar_image(user, acting_user=user)
do_delete_avatar_image(user, acting_user=user)
self.assertEqual(user.avatar_source, UserProfile.AVATAR_FROM_GRAVATAR)

View File

@ -9,8 +9,9 @@ from django.shortcuts import redirect, render
from django.views.decorators.http import require_safe
from confirmation.models import Confirmation, confirmation_url
from zerver.actions.user_settings import do_change_user_delivery_email
from zerver.actions.users import change_user_is_active
from zerver.lib.actions import do_change_user_delivery_email, do_send_realm_reactivation_email
from zerver.lib.actions import do_send_realm_reactivation_email
from zerver.lib.email_notifications import enqueue_welcome_emails
from zerver.lib.response import json_success
from zerver.models import Realm, get_realm, get_realm_stream, get_user_by_delivery_email

View File

@ -7,10 +7,10 @@ from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.utils.cache import patch_cache_control
from zerver.actions.user_settings import do_change_tos_version
from zerver.context_processors import get_valid_realm_from_request
from zerver.decorator import web_public_view, zulip_login_required
from zerver.forms import ToSForm
from zerver.lib.actions import do_change_tos_version
from zerver.lib.compatibility import is_outdated_desktop_app, is_unsupported_browser
from zerver.lib.home import build_page_params_for_home_page_load, get_user_permission_info
from zerver.lib.request import RequestNotes

View File

@ -26,6 +26,11 @@ from confirmation.models import (
validate_key,
)
from zerver.actions.default_streams import lookup_default_stream_groups
from zerver.actions.user_settings import (
do_change_full_name,
do_change_password,
do_change_user_setting,
)
from zerver.context_processors import get_realm_from_request, login_context
from zerver.decorator import do_login, rate_limit_request_by_ip, require_post
from zerver.forms import (
@ -35,14 +40,7 @@ from zerver.forms import (
RealmRedirectForm,
RegistrationForm,
)
from zerver.lib.actions import (
do_activate_mirror_dummy_user,
do_change_full_name,
do_change_password,
do_change_user_setting,
do_create_realm,
do_create_user,
)
from zerver.lib.actions import do_activate_mirror_dummy_user, do_create_realm, do_create_user
from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm
from zerver.lib.exceptions import RateLimited
from zerver.lib.pysa import mark_sanitized

View File

@ -5,8 +5,8 @@ from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from confirmation.models import Confirmation, ConfirmationKeyException, get_object_from_key
from zerver.actions.user_settings import do_change_user_setting
from zerver.context_processors import common_context
from zerver.lib.actions import do_change_user_setting
from zerver.lib.send_email import clear_scheduled_emails
from zerver.models import ScheduledEmail, UserProfile

View File

@ -17,8 +17,7 @@ from confirmation.models import (
get_object_from_key,
render_confirmation_key_error,
)
from zerver.decorator import human_users_only
from zerver.lib.actions import (
from zerver.actions.user_settings import (
check_change_full_name,
do_change_avatar_fields,
do_change_password,
@ -27,6 +26,7 @@ from zerver.lib.actions import (
do_regenerate_api_key,
do_start_email_change_process,
)
from zerver.decorator import human_users_only
from zerver.lib.avatar import avatar_url
from zerver.lib.email_validation import (
get_realm_email_validator,

View File

@ -10,6 +10,12 @@ from zerver.actions.custom_profile_fields import (
check_remove_custom_profile_field_value,
do_update_user_custom_profile_data_if_changed,
)
from zerver.actions.user_settings import (
check_change_bot_full_name,
check_change_full_name,
do_change_avatar_fields,
do_regenerate_api_key,
)
from zerver.actions.users import (
do_change_user_role,
do_deactivate_user,
@ -20,16 +26,12 @@ from zerver.context_processors import get_valid_realm_from_request
from zerver.decorator import require_member_or_admin, require_realm_admin
from zerver.forms import PASSWORD_TOO_WEAK_ERROR, CreateUserForm
from zerver.lib.actions import (
check_change_bot_full_name,
check_change_full_name,
do_change_avatar_fields,
do_change_bot_owner,
do_change_default_all_public_streams,
do_change_default_events_register_stream,
do_change_default_sending_stream,
do_create_user,
do_reactivate_user,
do_regenerate_api_key,
notify_created_bot,
)
from zerver.lib.avatar import avatar_url, get_gravatar_url

View File

@ -2,10 +2,10 @@ from typing import Any, Dict, List
from django.core.management.base import BaseCommand
from zerver.actions.user_settings import do_change_avatar_fields
from zerver.lib.actions import (
bulk_add_subscriptions,
do_add_reaction,
do_change_avatar_fields,
do_create_user,
do_send_messages,
internal_prep_stream_message,

View File

@ -59,9 +59,10 @@ from typing_extensions import TypedDict
from zxcvbn import zxcvbn
from zerver.actions.custom_profile_fields import do_update_user_custom_profile_data_if_changed
from zerver.actions.user_settings import do_regenerate_api_key
from zerver.actions.users import do_deactivate_user
from zerver.decorator import client_is_exempt_from_rate_limiting
from zerver.lib.actions import do_create_user, do_reactivate_user, do_regenerate_api_key
from zerver.lib.actions import do_create_user, do_reactivate_user
from zerver.lib.avatar import avatar_url, is_avatar_new
from zerver.lib.avatar_hash import user_avatar_content_hash
from zerver.lib.dev_ldap_directory import init_fakeldap
@ -704,7 +705,7 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend):
# We do local imports here to avoid import loops
from io import BytesIO
from zerver.lib.actions import do_change_avatar_fields
from zerver.actions.user_settings import do_change_avatar_fields
from zerver.lib.upload import upload_avatar_image
avatar_attr_name = settings.AUTH_LDAP_USER_ATTR_MAP["avatar"]
@ -818,7 +819,7 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend):
return full_name
def sync_full_name_from_ldap(self, user_profile: UserProfile, ldap_user: _LDAPUser) -> None:
from zerver.lib.actions import do_change_full_name
from zerver.actions.user_settings import do_change_full_name
full_name = self.get_mapped_name(ldap_user)
if full_name != user_profile.full_name: