actions: Split out zerver.actions.invites.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2022-04-14 14:36:07 -07:00
parent 241463e215
commit ca8d374e21
10 changed files with 446 additions and 428 deletions

View File

@ -32,17 +32,19 @@ from analytics.models import (
UserCount,
installation_epoch,
)
from zerver.actions.invites import (
do_invite_users,
do_resend_user_invite_email,
do_revoke_user_invite,
)
from zerver.lib.actions import (
do_activate_mirror_dummy_user,
do_create_realm,
do_create_user,
do_deactivate_user,
do_invite_users,
do_mark_all_as_read,
do_mark_stream_messages_as_read,
do_reactivate_user,
do_resend_user_invite_email,
do_revoke_user_invite,
do_update_message_flags,
update_user_activity_interval,
)

View File

@ -7,11 +7,8 @@ from django.utils.timezone import now as timezone_now
from corporate.lib.stripe import add_months, update_sponsorship_status
from corporate.models import Customer, CustomerPlan, LicenseLedger, get_customer_by_realm
from zerver.lib.actions import (
do_create_multiuse_invite_link,
do_send_realm_reactivation_email,
do_set_realm_property,
)
from zerver.actions.invites import do_create_multiuse_invite_link
from zerver.lib.actions import do_send_realm_reactivation_email, do_set_realm_property
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import reset_emails_in_zulip_realm
from zerver.models import (

419
zerver/actions/invites.py Normal file
View File

@ -0,0 +1,419 @@
import datetime
from typing import Any, Collection, Dict, List, Optional, Sequence, Set, Tuple, Union
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, Sum
from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
from analytics.lib.counts import COUNT_STATS, do_increment_logging_stat
from analytics.models import RealmCount
from confirmation.models import Confirmation, confirmation_url, create_confirmation_link
from zerver.lib.email_validation import (
get_existing_user_errors,
get_realm_email_validator,
validate_email_is_valid,
)
from zerver.lib.exceptions import InvitationError
from zerver.lib.queue import queue_json_publish
from zerver.lib.send_email import FromAddress, clear_scheduled_invitation_emails, send_email
from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.types import UnspecifiedValue
from zerver.models import (
MultiuseInvite,
PreregistrationUser,
Realm,
Stream,
UserProfile,
filter_to_valid_prereg_users,
)
from zerver.tornado.django_api import send_event
def notify_invites_changed(realm: Realm) -> None:
event = dict(type="invites_changed")
admin_ids = [user.id for user in realm.get_admin_users_and_bots()]
send_event(realm, event, admin_ids)
def do_send_confirmation_email(
invitee: PreregistrationUser,
referrer: UserProfile,
email_language: str,
invite_expires_in_days: Union[Optional[int], UnspecifiedValue] = UnspecifiedValue(),
) -> str:
"""
Send the confirmation/welcome e-mail to an invited user.
"""
activation_url = create_confirmation_link(
invitee, Confirmation.INVITATION, validity_in_days=invite_expires_in_days
)
context = {
"referrer_full_name": referrer.full_name,
"referrer_email": referrer.delivery_email,
"activate_url": activation_url,
"referrer_realm_name": referrer.realm.name,
}
send_email(
"zerver/emails/invitation",
to_emails=[invitee.email],
from_address=FromAddress.tokenized_no_reply_address(),
language=email_language,
context=context,
realm=referrer.realm,
)
return activation_url
def estimate_recent_invites(realms: Collection[Realm], *, days: int) -> int:
"""An upper bound on the number of invites sent in the last `days` days"""
recent_invites = RealmCount.objects.filter(
realm__in=realms,
property="invites_sent::day",
end_time__gte=timezone_now() - datetime.timedelta(days=days),
).aggregate(Sum("value"))["value__sum"]
if recent_invites is None:
return 0
return recent_invites
def check_invite_limit(realm: Realm, num_invitees: int) -> None:
"""Discourage using invitation emails as a vector for carrying spam."""
msg = _(
"To protect users, Zulip limits the number of invitations you can send in one day. Because you have reached the limit, no invitations were sent."
)
if not settings.OPEN_REALM_CREATION:
return
recent_invites = estimate_recent_invites([realm], days=1)
if num_invitees + recent_invites > realm.max_invites:
raise InvitationError(
msg,
[],
sent_invitations=False,
daily_limit_reached=True,
)
default_max = settings.INVITES_DEFAULT_REALM_DAILY_MAX
newrealm_age = datetime.timedelta(days=settings.INVITES_NEW_REALM_DAYS)
if realm.date_created <= timezone_now() - newrealm_age:
# If this isn't a "newly-created" realm, we're done. The
# remaining code applies an aggregate limit across all
# "new" realms, to address sudden bursts of spam realms.
return
if realm.max_invites > default_max:
# If a user is on a realm where we've bumped up
# max_invites, then we exempt them from invite limits.
return
new_realms = Realm.objects.filter(
date_created__gte=timezone_now() - newrealm_age,
_max_invites__lte=default_max,
).all()
for days, count in settings.INVITES_NEW_REALM_LIMIT_DAYS:
recent_invites = estimate_recent_invites(new_realms, days=days)
if num_invitees + recent_invites > count:
raise InvitationError(
msg,
[],
sent_invitations=False,
daily_limit_reached=True,
)
def do_invite_users(
user_profile: UserProfile,
invitee_emails: Collection[str],
streams: Collection[Stream],
*,
invite_expires_in_days: Optional[int],
invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
) -> None:
num_invites = len(invitee_emails)
check_invite_limit(user_profile.realm, num_invites)
if settings.BILLING_ENABLED:
from corporate.lib.registration import check_spare_licenses_available_for_inviting_new_users
check_spare_licenses_available_for_inviting_new_users(user_profile.realm, num_invites)
realm = user_profile.realm
if not realm.invite_required:
# Inhibit joining an open realm to send spam invitations.
min_age = datetime.timedelta(days=settings.INVITES_MIN_USER_AGE_DAYS)
if user_profile.date_joined > timezone_now() - min_age and not user_profile.is_realm_admin:
raise InvitationError(
_(
"Your account is too new to send invites for this organization. "
"Ask an organization admin, or a more experienced user."
),
[],
sent_invitations=False,
)
good_emails: Set[str] = set()
errors: List[Tuple[str, str, bool]] = []
validate_email_allowed_in_realm = get_realm_email_validator(user_profile.realm)
for email in invitee_emails:
if email == "":
continue
email_error = validate_email_is_valid(
email,
validate_email_allowed_in_realm,
)
if email_error:
errors.append((email, email_error, False))
else:
good_emails.add(email)
"""
good_emails are emails that look ok so far,
but we still need to make sure they're not
gonna conflict with existing users
"""
error_dict = get_existing_user_errors(user_profile.realm, good_emails)
skipped: List[Tuple[str, str, bool]] = []
for email in error_dict:
msg, deactivated = error_dict[email]
skipped.append((email, msg, deactivated))
good_emails.remove(email)
validated_emails = list(good_emails)
if errors:
raise InvitationError(
_("Some emails did not validate, so we didn't send any invitations."),
errors + skipped,
sent_invitations=False,
)
if skipped and len(skipped) == len(invitee_emails):
# All e-mails were skipped, so we didn't actually invite anyone.
raise InvitationError(
_("We weren't able to invite anyone."), skipped, sent_invitations=False
)
# We do this here rather than in the invite queue processor since this
# is used for rate limiting invitations, rather than keeping track of
# when exactly invitations were sent
do_increment_logging_stat(
user_profile.realm,
COUNT_STATS["invites_sent::day"],
None,
timezone_now(),
increment=len(validated_emails),
)
# Now that we are past all the possible errors, we actually create
# the PreregistrationUser objects and trigger the email invitations.
for email in validated_emails:
# The logged in user is the referrer.
prereg_user = PreregistrationUser(
email=email, referred_by=user_profile, invited_as=invite_as, realm=user_profile.realm
)
prereg_user.save()
stream_ids = [stream.id for stream in streams]
prereg_user.streams.set(stream_ids)
event = {
"prereg_id": prereg_user.id,
"referrer_id": user_profile.id,
"email_language": user_profile.realm.default_language,
"invite_expires_in_days": invite_expires_in_days,
}
queue_json_publish("invites", event)
if skipped:
raise InvitationError(
_(
"Some of those addresses are already using Zulip, "
"so we didn't send them an invitation. We did send "
"invitations to everyone else!"
),
skipped,
sent_invitations=True,
)
notify_invites_changed(user_profile.realm)
def get_invitation_expiry_date(confirmation_obj: Confirmation) -> Optional[int]:
expiry_date = confirmation_obj.expiry_date
if expiry_date is None:
return expiry_date
return datetime_to_timestamp(expiry_date)
def do_get_invites_controlled_by_user(user_profile: UserProfile) -> List[Dict[str, Any]]:
"""
Returns a list of dicts representing invitations that can be controlled by user_profile.
This isn't necessarily the same as all the invitations generated by the user, as administrators
can control also invitations that they did not themselves create.
"""
if user_profile.is_realm_admin:
prereg_users = filter_to_valid_prereg_users(
PreregistrationUser.objects.filter(referred_by__realm=user_profile.realm)
)
else:
prereg_users = filter_to_valid_prereg_users(
PreregistrationUser.objects.filter(referred_by=user_profile)
)
invites = []
for invitee in prereg_users:
invites.append(
dict(
email=invitee.email,
invited_by_user_id=invitee.referred_by.id,
invited=datetime_to_timestamp(invitee.invited_at),
expiry_date=get_invitation_expiry_date(invitee.confirmation.get()),
id=invitee.id,
invited_as=invitee.invited_as,
is_multiuse=False,
)
)
if not user_profile.is_realm_admin:
# We do not return multiuse invites to non-admin users.
return invites
multiuse_confirmation_objs = Confirmation.objects.filter(
realm=user_profile.realm, type=Confirmation.MULTIUSE_INVITE
).filter(Q(expiry_date__gte=timezone_now()) | Q(expiry_date=None))
for confirmation_obj in multiuse_confirmation_objs:
invite = confirmation_obj.content_object
assert invite is not None
invites.append(
dict(
invited_by_user_id=invite.referred_by.id,
invited=datetime_to_timestamp(confirmation_obj.date_sent),
expiry_date=get_invitation_expiry_date(confirmation_obj),
id=invite.id,
link_url=confirmation_url(
confirmation_obj.confirmation_key,
user_profile.realm,
Confirmation.MULTIUSE_INVITE,
),
invited_as=invite.invited_as,
is_multiuse=True,
)
)
return invites
def get_valid_invite_confirmations_generated_by_user(
user_profile: UserProfile,
) -> List[Confirmation]:
prereg_user_ids = filter_to_valid_prereg_users(
PreregistrationUser.objects.filter(referred_by=user_profile)
).values_list("id", flat=True)
confirmations = list(
Confirmation.objects.filter(type=Confirmation.INVITATION, object_id__in=prereg_user_ids)
)
multiuse_invite_ids = MultiuseInvite.objects.filter(referred_by=user_profile).values_list(
"id", flat=True
)
confirmations += list(
Confirmation.objects.filter(
type=Confirmation.MULTIUSE_INVITE,
object_id__in=multiuse_invite_ids,
).filter(Q(expiry_date__gte=timezone_now()) | Q(expiry_date=None))
)
return confirmations
def revoke_invites_generated_by_user(user_profile: UserProfile) -> None:
confirmations_to_revoke = get_valid_invite_confirmations_generated_by_user(user_profile)
now = timezone_now()
for confirmation in confirmations_to_revoke:
confirmation.expiry_date = now
Confirmation.objects.bulk_update(confirmations_to_revoke, ["expiry_date"])
if len(confirmations_to_revoke):
notify_invites_changed(realm=user_profile.realm)
def do_create_multiuse_invite_link(
referred_by: UserProfile,
invited_as: int,
invite_expires_in_days: Optional[int],
streams: Sequence[Stream] = [],
) -> str:
realm = referred_by.realm
invite = MultiuseInvite.objects.create(realm=realm, referred_by=referred_by)
if streams:
invite.streams.set(streams)
invite.invited_as = invited_as
invite.save()
notify_invites_changed(referred_by.realm)
return create_confirmation_link(
invite, Confirmation.MULTIUSE_INVITE, validity_in_days=invite_expires_in_days
)
def do_revoke_user_invite(prereg_user: PreregistrationUser) -> None:
email = prereg_user.email
realm = prereg_user.realm
assert realm is not None
# Delete both the confirmation objects and the prereg_user object.
# TODO: Probably we actually want to set the confirmation objects
# to a "revoked" status so that we can give the invited user a better
# error message.
content_type = ContentType.objects.get_for_model(PreregistrationUser)
Confirmation.objects.filter(content_type=content_type, object_id=prereg_user.id).delete()
prereg_user.delete()
clear_scheduled_invitation_emails(email)
notify_invites_changed(realm)
def do_revoke_multi_use_invite(multiuse_invite: MultiuseInvite) -> None:
realm = multiuse_invite.referred_by.realm
content_type = ContentType.objects.get_for_model(MultiuseInvite)
Confirmation.objects.filter(content_type=content_type, object_id=multiuse_invite.id).delete()
multiuse_invite.delete()
notify_invites_changed(realm)
def do_resend_user_invite_email(prereg_user: PreregistrationUser) -> int:
# These are two structurally for the caller's code path.
assert prereg_user.referred_by is not None
assert prereg_user.realm is not None
check_invite_limit(prereg_user.referred_by.realm, 1)
prereg_user.invited_at = timezone_now()
prereg_user.save()
expiry_date = prereg_user.confirmation.get().expiry_date
if expiry_date is None:
invite_expires_in_days = None
else:
# The resent invitation is reset to expire as long after the
# reminder is sent as it lasted originally.
invite_expires_in_days = (expiry_date - prereg_user.invited_at).days
prereg_user.confirmation.clear()
do_increment_logging_stat(
prereg_user.realm, COUNT_STATS["invites_sent::day"], None, prereg_user.invited_at
)
clear_scheduled_invitation_emails(prereg_user.email)
# We don't store the custom email body, so just set it to None
event = {
"prereg_id": prereg_user.id,
"referrer_id": prereg_user.referred_by.id,
"email_language": prereg_user.referred_by.realm.default_language,
"invite_expires_in_days": invite_expires_in_days,
}
queue_json_publish("invites", event)
return datetime_to_timestamp(prereg_user.invited_at)

View File

@ -26,10 +26,9 @@ from typing import (
import django.db.utils
import orjson
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection, transaction
from django.db.models import Exists, F, OuterRef, Q, Sum
from django.db.models import Exists, F, OuterRef, Q
from django.db.models.query import QuerySet
from django.utils.html import escape
from django.utils.timezone import now as timezone_now
@ -40,19 +39,14 @@ from psycopg2.sql import SQL
from typing_extensions import TypedDict
from analytics.lib.counts import COUNT_STATS, do_increment_logging_stat
from analytics.models import RealmCount
from confirmation import settings as confirmation_settings
from confirmation.models import (
Confirmation,
confirmation_url,
create_confirmation_link,
generate_key,
)
from confirmation.models import Confirmation, create_confirmation_link, generate_key
from zerver.actions.default_streams import (
do_remove_default_stream,
do_remove_streams_from_default_stream_group,
get_default_streams_for_realm,
)
from zerver.actions.invites import notify_invites_changed, revoke_invites_generated_by_user
from zerver.actions.user_groups import (
do_send_user_group_members_update_event,
update_users_in_full_members_system_group,
@ -82,15 +76,9 @@ from zerver.lib.cache import (
from zerver.lib.create_user import create_user, get_display_email_address
from zerver.lib.email_mirror_helpers import encode_email_address
from zerver.lib.email_notifications import enqueue_welcome_emails
from zerver.lib.email_validation import (
email_reserved_for_system_bots_error,
get_existing_user_errors,
get_realm_email_validator,
validate_email_is_valid,
)
from zerver.lib.email_validation import email_reserved_for_system_bots_error
from zerver.lib.emoji import check_emoji_request, emoji_name_to_emoji_code, get_emoji_file_name
from zerver.lib.exceptions import (
InvitationError,
JsonableError,
MarkdownRenderingException,
StreamDoesNotExistError,
@ -187,7 +175,6 @@ from zerver.lib.types import (
RawSubscriptionDict,
SubscriptionInfo,
SubscriptionStreamDict,
UnspecifiedValue,
)
from zerver.lib.upload import (
claim_attachment,
@ -231,7 +218,6 @@ from zerver.models import (
Draft,
EmailChangeStatus,
Message,
MultiuseInvite,
MutedUser,
PreregistrationUser,
Reaction,
@ -260,7 +246,6 @@ from zerver.models import (
active_user_ids,
bot_owner_user_ids,
custom_profile_fields_for_realm,
filter_to_valid_prereg_users,
get_active_streams,
get_bot_dicts_in_realm,
get_bot_services,
@ -372,12 +357,6 @@ def notify_new_user(user_profile: UserProfile) -> None:
pass
def notify_invites_changed(realm: Realm) -> None:
event = dict(type="invites_changed")
admin_ids = [user.id for user in realm.get_admin_users_and_bots()]
send_event(realm, event, admin_ids)
def add_new_user_history(user_profile: UserProfile, streams: Iterable[Stream]) -> None:
"""Give you the last ONBOARDING_TOTAL_MESSAGES messages on your public
streams, so you have something to look at in your home view once
@ -7013,35 +6992,6 @@ def filter_presence_idle_user_ids(user_ids: Set[int]) -> List[int]:
return sorted(idle_user_ids)
def do_send_confirmation_email(
invitee: PreregistrationUser,
referrer: UserProfile,
email_language: str,
invite_expires_in_days: Union[Optional[int], UnspecifiedValue] = UnspecifiedValue(),
) -> str:
"""
Send the confirmation/welcome e-mail to an invited user.
"""
activation_url = create_confirmation_link(
invitee, Confirmation.INVITATION, validity_in_days=invite_expires_in_days
)
context = {
"referrer_full_name": referrer.full_name,
"referrer_email": referrer.delivery_email,
"activate_url": activation_url,
"referrer_realm_name": referrer.realm.name,
}
send_email(
"zerver/emails/invitation",
to_emails=[invitee.email],
from_address=FromAddress.tokenized_no_reply_address(),
language=email_language,
context=context,
realm=referrer.realm,
)
return activation_url
def email_not_system_bot(email: str) -> None:
if is_cross_realm_bot_email(email):
msg = email_reserved_for_system_bots_error(email)
@ -7053,359 +7003,6 @@ def email_not_system_bot(email: str) -> None:
)
def estimate_recent_invites(realms: Collection[Realm], *, days: int) -> int:
"""An upper bound on the number of invites sent in the last `days` days"""
recent_invites = RealmCount.objects.filter(
realm__in=realms,
property="invites_sent::day",
end_time__gte=timezone_now() - datetime.timedelta(days=days),
).aggregate(Sum("value"))["value__sum"]
if recent_invites is None:
return 0
return recent_invites
def check_invite_limit(realm: Realm, num_invitees: int) -> None:
"""Discourage using invitation emails as a vector for carrying spam."""
msg = _(
"To protect users, Zulip limits the number of invitations you can send in one day. Because you have reached the limit, no invitations were sent."
)
if not settings.OPEN_REALM_CREATION:
return
recent_invites = estimate_recent_invites([realm], days=1)
if num_invitees + recent_invites > realm.max_invites:
raise InvitationError(
msg,
[],
sent_invitations=False,
daily_limit_reached=True,
)
default_max = settings.INVITES_DEFAULT_REALM_DAILY_MAX
newrealm_age = datetime.timedelta(days=settings.INVITES_NEW_REALM_DAYS)
if realm.date_created <= timezone_now() - newrealm_age:
# If this isn't a "newly-created" realm, we're done. The
# remaining code applies an aggregate limit across all
# "new" realms, to address sudden bursts of spam realms.
return
if realm.max_invites > default_max:
# If a user is on a realm where we've bumped up
# max_invites, then we exempt them from invite limits.
return
new_realms = Realm.objects.filter(
date_created__gte=timezone_now() - newrealm_age,
_max_invites__lte=default_max,
).all()
for days, count in settings.INVITES_NEW_REALM_LIMIT_DAYS:
recent_invites = estimate_recent_invites(new_realms, days=days)
if num_invitees + recent_invites > count:
raise InvitationError(
msg,
[],
sent_invitations=False,
daily_limit_reached=True,
)
def do_invite_users(
user_profile: UserProfile,
invitee_emails: Collection[str],
streams: Collection[Stream],
*,
invite_expires_in_days: Optional[int],
invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
) -> None:
num_invites = len(invitee_emails)
check_invite_limit(user_profile.realm, num_invites)
if settings.BILLING_ENABLED:
from corporate.lib.registration import check_spare_licenses_available_for_inviting_new_users
check_spare_licenses_available_for_inviting_new_users(user_profile.realm, num_invites)
realm = user_profile.realm
if not realm.invite_required:
# Inhibit joining an open realm to send spam invitations.
min_age = datetime.timedelta(days=settings.INVITES_MIN_USER_AGE_DAYS)
if user_profile.date_joined > timezone_now() - min_age and not user_profile.is_realm_admin:
raise InvitationError(
_(
"Your account is too new to send invites for this organization. "
"Ask an organization admin, or a more experienced user."
),
[],
sent_invitations=False,
)
good_emails: Set[str] = set()
errors: List[Tuple[str, str, bool]] = []
validate_email_allowed_in_realm = get_realm_email_validator(user_profile.realm)
for email in invitee_emails:
if email == "":
continue
email_error = validate_email_is_valid(
email,
validate_email_allowed_in_realm,
)
if email_error:
errors.append((email, email_error, False))
else:
good_emails.add(email)
"""
good_emails are emails that look ok so far,
but we still need to make sure they're not
gonna conflict with existing users
"""
error_dict = get_existing_user_errors(user_profile.realm, good_emails)
skipped: List[Tuple[str, str, bool]] = []
for email in error_dict:
msg, deactivated = error_dict[email]
skipped.append((email, msg, deactivated))
good_emails.remove(email)
validated_emails = list(good_emails)
if errors:
raise InvitationError(
_("Some emails did not validate, so we didn't send any invitations."),
errors + skipped,
sent_invitations=False,
)
if skipped and len(skipped) == len(invitee_emails):
# All e-mails were skipped, so we didn't actually invite anyone.
raise InvitationError(
_("We weren't able to invite anyone."), skipped, sent_invitations=False
)
# We do this here rather than in the invite queue processor since this
# is used for rate limiting invitations, rather than keeping track of
# when exactly invitations were sent
do_increment_logging_stat(
user_profile.realm,
COUNT_STATS["invites_sent::day"],
None,
timezone_now(),
increment=len(validated_emails),
)
# Now that we are past all the possible errors, we actually create
# the PreregistrationUser objects and trigger the email invitations.
for email in validated_emails:
# The logged in user is the referrer.
prereg_user = PreregistrationUser(
email=email, referred_by=user_profile, invited_as=invite_as, realm=user_profile.realm
)
prereg_user.save()
stream_ids = [stream.id for stream in streams]
prereg_user.streams.set(stream_ids)
event = {
"prereg_id": prereg_user.id,
"referrer_id": user_profile.id,
"email_language": user_profile.realm.default_language,
"invite_expires_in_days": invite_expires_in_days,
}
queue_json_publish("invites", event)
if skipped:
raise InvitationError(
_(
"Some of those addresses are already using Zulip, "
"so we didn't send them an invitation. We did send "
"invitations to everyone else!"
),
skipped,
sent_invitations=True,
)
notify_invites_changed(user_profile.realm)
def get_invitation_expiry_date(confirmation_obj: Confirmation) -> Optional[int]:
expiry_date = confirmation_obj.expiry_date
if expiry_date is None:
return expiry_date
return datetime_to_timestamp(expiry_date)
def do_get_invites_controlled_by_user(user_profile: UserProfile) -> List[Dict[str, Any]]:
"""
Returns a list of dicts representing invitations that can be controlled by user_profile.
This isn't necessarily the same as all the invitations generated by the user, as administrators
can control also invitations that they did not themselves create.
"""
if user_profile.is_realm_admin:
prereg_users = filter_to_valid_prereg_users(
PreregistrationUser.objects.filter(referred_by__realm=user_profile.realm)
)
else:
prereg_users = filter_to_valid_prereg_users(
PreregistrationUser.objects.filter(referred_by=user_profile)
)
invites = []
for invitee in prereg_users:
invites.append(
dict(
email=invitee.email,
invited_by_user_id=invitee.referred_by.id,
invited=datetime_to_timestamp(invitee.invited_at),
expiry_date=get_invitation_expiry_date(invitee.confirmation.get()),
id=invitee.id,
invited_as=invitee.invited_as,
is_multiuse=False,
)
)
if not user_profile.is_realm_admin:
# We do not return multiuse invites to non-admin users.
return invites
multiuse_confirmation_objs = Confirmation.objects.filter(
realm=user_profile.realm, type=Confirmation.MULTIUSE_INVITE
).filter(Q(expiry_date__gte=timezone_now()) | Q(expiry_date=None))
for confirmation_obj in multiuse_confirmation_objs:
invite = confirmation_obj.content_object
assert invite is not None
invites.append(
dict(
invited_by_user_id=invite.referred_by.id,
invited=datetime_to_timestamp(confirmation_obj.date_sent),
expiry_date=get_invitation_expiry_date(confirmation_obj),
id=invite.id,
link_url=confirmation_url(
confirmation_obj.confirmation_key,
user_profile.realm,
Confirmation.MULTIUSE_INVITE,
),
invited_as=invite.invited_as,
is_multiuse=True,
)
)
return invites
def get_valid_invite_confirmations_generated_by_user(
user_profile: UserProfile,
) -> List[Confirmation]:
prereg_user_ids = filter_to_valid_prereg_users(
PreregistrationUser.objects.filter(referred_by=user_profile)
).values_list("id", flat=True)
confirmations = list(
Confirmation.objects.filter(type=Confirmation.INVITATION, object_id__in=prereg_user_ids)
)
multiuse_invite_ids = MultiuseInvite.objects.filter(referred_by=user_profile).values_list(
"id", flat=True
)
confirmations += list(
Confirmation.objects.filter(
type=Confirmation.MULTIUSE_INVITE,
object_id__in=multiuse_invite_ids,
).filter(Q(expiry_date__gte=timezone_now()) | Q(expiry_date=None))
)
return confirmations
def revoke_invites_generated_by_user(user_profile: UserProfile) -> None:
confirmations_to_revoke = get_valid_invite_confirmations_generated_by_user(user_profile)
now = timezone_now()
for confirmation in confirmations_to_revoke:
confirmation.expiry_date = now
Confirmation.objects.bulk_update(confirmations_to_revoke, ["expiry_date"])
if len(confirmations_to_revoke):
notify_invites_changed(realm=user_profile.realm)
def do_create_multiuse_invite_link(
referred_by: UserProfile,
invited_as: int,
invite_expires_in_days: Optional[int],
streams: Sequence[Stream] = [],
) -> str:
realm = referred_by.realm
invite = MultiuseInvite.objects.create(realm=realm, referred_by=referred_by)
if streams:
invite.streams.set(streams)
invite.invited_as = invited_as
invite.save()
notify_invites_changed(referred_by.realm)
return create_confirmation_link(
invite, Confirmation.MULTIUSE_INVITE, validity_in_days=invite_expires_in_days
)
def do_revoke_user_invite(prereg_user: PreregistrationUser) -> None:
email = prereg_user.email
realm = prereg_user.realm
assert realm is not None
# Delete both the confirmation objects and the prereg_user object.
# TODO: Probably we actually want to set the confirmation objects
# to a "revoked" status so that we can give the invited user a better
# error message.
content_type = ContentType.objects.get_for_model(PreregistrationUser)
Confirmation.objects.filter(content_type=content_type, object_id=prereg_user.id).delete()
prereg_user.delete()
clear_scheduled_invitation_emails(email)
notify_invites_changed(realm)
def do_revoke_multi_use_invite(multiuse_invite: MultiuseInvite) -> None:
realm = multiuse_invite.referred_by.realm
content_type = ContentType.objects.get_for_model(MultiuseInvite)
Confirmation.objects.filter(content_type=content_type, object_id=multiuse_invite.id).delete()
multiuse_invite.delete()
notify_invites_changed(realm)
def do_resend_user_invite_email(prereg_user: PreregistrationUser) -> int:
# These are two structurally for the caller's code path.
assert prereg_user.referred_by is not None
assert prereg_user.realm is not None
check_invite_limit(prereg_user.referred_by.realm, 1)
prereg_user.invited_at = timezone_now()
prereg_user.save()
expiry_date = prereg_user.confirmation.get().expiry_date
if expiry_date is None:
invite_expires_in_days = None
else:
# The resent invitation is reset to expire as long after the
# reminder is sent as it lasted originally.
invite_expires_in_days = (expiry_date - prereg_user.invited_at).days
prereg_user.confirmation.clear()
do_increment_logging_stat(
prereg_user.realm, COUNT_STATS["invites_sent::day"], None, prereg_user.invited_at
)
clear_scheduled_invitation_emails(prereg_user.email)
# We don't store the custom email body, so just set it to None
event = {
"prereg_id": prereg_user.id,
"referrer_id": prereg_user.referred_by.id,
"email_language": prereg_user.referred_by.realm.default_language,
"invite_expires_in_days": invite_expires_in_days,
}
queue_json_publish("invites", event)
return datetime_to_timestamp(prereg_user.invited_at)
def notify_realm_emoji(realm: Realm) -> None:
event = dict(type="realm_emoji", op="update", realm_emoji=realm.get_emoji())
send_event(realm, event, active_user_ids(realm.id))

View File

@ -40,6 +40,7 @@ from social_django.storage import BaseDjangoStorage
from social_django.strategy import DjangoStrategy
from confirmation.models import Confirmation, create_confirmation_link
from zerver.actions.invites import do_invite_users
from zerver.lib.actions import (
change_user_is_active,
do_change_password,
@ -47,7 +48,6 @@ from zerver.lib.actions import (
do_create_user,
do_deactivate_realm,
do_deactivate_user,
do_invite_users,
do_reactivate_realm,
do_reactivate_user,
do_set_realm_property,

View File

@ -26,6 +26,12 @@ from zerver.actions.default_streams import (
lookup_default_stream_groups,
)
from zerver.actions.hotspots import do_mark_hotspot_as_read
from zerver.actions.invites import (
do_create_multiuse_invite_link,
do_invite_users,
do_revoke_multi_use_invite,
do_revoke_user_invite,
)
from zerver.actions.realm_linkifiers import (
do_add_linkifier,
do_remove_linkifier,
@ -67,13 +73,11 @@ from zerver.lib.actions import (
do_change_user_delivery_email,
do_change_user_role,
do_change_user_setting,
do_create_multiuse_invite_link,
do_create_user,
do_deactivate_realm,
do_deactivate_stream,
do_deactivate_user,
do_delete_messages,
do_invite_users,
do_make_user_billing_admin,
do_mute_topic,
do_mute_user,
@ -84,8 +88,6 @@ from zerver.lib.actions import (
do_remove_realm_domain,
do_remove_realm_emoji,
do_rename_stream,
do_revoke_multi_use_invite,
do_revoke_user_invite,
do_set_realm_authentication_methods,
do_set_realm_message_editing,
do_set_realm_notifications_stream,

View File

@ -30,6 +30,11 @@ from zerver.actions.default_streams import (
do_create_default_stream_group,
get_default_streams_for_realm,
)
from zerver.actions.invites import (
do_create_multiuse_invite_link,
do_get_invites_controlled_by_user,
do_invite_users,
)
from zerver.context_processors import common_context
from zerver.decorator import do_two_factor_login
from zerver.forms import HomepageForm, check_subdomain_available
@ -39,13 +44,10 @@ from zerver.lib.actions import (
do_change_full_name,
do_change_realm_subdomain,
do_change_user_role,
do_create_multiuse_invite_link,
do_create_realm,
do_create_user,
do_deactivate_realm,
do_deactivate_user,
do_get_invites_controlled_by_user,
do_invite_users,
do_set_realm_property,
do_set_realm_user_default_setting,
process_new_human_user,

View File

@ -12,16 +12,15 @@ from django.test import override_settings
from django.utils.timezone import now as timezone_now
from confirmation.models import Confirmation
from zerver.actions.invites import do_create_multiuse_invite_link, do_invite_users
from zerver.lib.actions import (
change_user_is_active,
create_users,
do_change_can_create_users,
do_change_user_role,
do_create_multiuse_invite_link,
do_create_user,
do_deactivate_user,
do_delete_user,
do_invite_users,
do_mute_user,
do_reactivate_user,
do_set_realm_property,

View File

@ -5,8 +5,7 @@ from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from zerver.decorator import require_member_or_admin, require_realm_admin
from zerver.lib.actions import (
from zerver.actions.invites import (
do_create_multiuse_invite_link,
do_get_invites_controlled_by_user,
do_invite_users,
@ -14,6 +13,7 @@ from zerver.lib.actions import (
do_revoke_multi_use_invite,
do_revoke_user_invite,
)
from zerver.decorator import require_member_or_admin, require_realm_admin
from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequired
from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success

View File

@ -46,10 +46,10 @@ from django.utils.translation import override as override_language
from sentry_sdk import add_breadcrumb, configure_scope
from zulip_bots.lib import extract_query_without_mention
from zerver.actions.invites import do_send_confirmation_email
from zerver.context_processors import common_context
from zerver.lib.actions import (
do_mark_stream_messages_as_read,
do_send_confirmation_email,
do_update_embedded_data,
do_update_user_activity,
do_update_user_activity_interval,