2020-06-11 00:54:34 +02:00
|
|
|
import re
|
2021-11-30 13:34:37 +01:00
|
|
|
from typing import List, Optional, Sequence, Set
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2021-04-05 18:42:45 +02:00
|
|
|
from django.conf import settings
|
2016-10-12 05:13:32 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2024-04-30 22:12:34 +02:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2021-04-16 00:57:30 +02:00
|
|
|
from django.utils.translation import gettext as _
|
2016-10-12 05:13:32 +02:00
|
|
|
|
2022-09-12 00:39:43 +02:00
|
|
|
from confirmation import settings as confirmation_settings
|
2022-04-14 23:36:07 +02:00
|
|
|
from zerver.actions.invites import (
|
2020-06-11 00:54:34 +02:00
|
|
|
do_create_multiuse_invite_link,
|
2022-01-12 19:39:57 +01:00
|
|
|
do_get_invites_controlled_by_user,
|
2020-06-11 00:54:34 +02:00
|
|
|
do_invite_users,
|
|
|
|
do_revoke_multi_use_invite,
|
|
|
|
do_revoke_user_invite,
|
2024-04-30 22:12:34 +02:00
|
|
|
do_send_user_invite_email,
|
2020-06-11 00:54:34 +02:00
|
|
|
)
|
2023-08-05 12:41:47 +02:00
|
|
|
from zerver.decorator import require_member_or_admin
|
2024-01-10 22:01:21 +01:00
|
|
|
from zerver.lib.exceptions import InvitationError, JsonableError, OrganizationOwnerRequiredError
|
2021-07-16 22:11:10 +02:00
|
|
|
from zerver.lib.request import REQ, has_request_variables
|
2021-06-30 18:35:50 +02:00
|
|
|
from zerver.lib.response import json_success
|
2019-02-02 23:53:22 +01:00
|
|
|
from zerver.lib.streams import access_stream_by_id
|
2023-05-25 16:06:13 +02:00
|
|
|
from zerver.lib.validator import check_bool, check_int, check_int_in, check_list, check_none_or
|
2021-04-07 21:09:43 +02:00
|
|
|
from zerver.models import MultiuseInvite, PreregistrationUser, Stream, UserProfile
|
2016-10-12 05:13:32 +02:00
|
|
|
|
2022-07-19 22:59:47 +02:00
|
|
|
# Convert INVITATION_LINK_VALIDITY_DAYS into minutes.
|
|
|
|
# Because mypy fails to correctly infer the type of the validator, we want this constant
|
|
|
|
# to be Optional[int] to avoid a mypy error when using it as the default value.
|
|
|
|
# https://github.com/python/mypy/issues/13234
|
|
|
|
INVITATION_LINK_VALIDITY_MINUTES: Optional[int] = 24 * 60 * settings.INVITATION_LINK_VALIDITY_DAYS
|
|
|
|
|
2016-10-12 05:13:32 +02:00
|
|
|
|
2023-06-21 14:33:11 +02:00
|
|
|
def check_role_based_permissions(
|
|
|
|
invited_as: int, user_profile: UserProfile, *, require_admin: bool
|
|
|
|
) -> None:
|
2021-02-12 08:19:30 +01:00
|
|
|
if (
|
2021-02-12 08:20:45 +01:00
|
|
|
invited_as == PreregistrationUser.INVITE_AS["REALM_OWNER"]
|
2021-02-12 08:19:30 +01:00
|
|
|
and not user_profile.is_realm_owner
|
|
|
|
):
|
2023-02-04 02:07:20 +01:00
|
|
|
raise OrganizationOwnerRequiredError
|
2020-06-18 13:03:06 +02:00
|
|
|
|
2023-06-21 14:33:11 +02:00
|
|
|
if require_admin and not user_profile.is_realm_admin:
|
|
|
|
raise JsonableError(_("Must be an organization administrator"))
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2019-06-18 16:43:22 +02:00
|
|
|
@require_member_or_admin
|
2016-10-12 05:13:32 +02:00
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def invite_users_backend(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
|
|
|
invitee_emails_raw: str = REQ("invitee_emails"),
|
2022-02-10 11:52:34 +01:00
|
|
|
invite_expires_in_minutes: Optional[int] = REQ(
|
2022-07-19 22:59:47 +02:00
|
|
|
json_validator=check_none_or(check_int), default=INVITATION_LINK_VALIDITY_MINUTES
|
2021-04-05 18:42:45 +02:00
|
|
|
),
|
2022-12-30 23:04:18 +01:00
|
|
|
invite_as: int = REQ(
|
|
|
|
json_validator=check_int_in(
|
|
|
|
list(PreregistrationUser.INVITE_AS.values()),
|
|
|
|
),
|
|
|
|
default=PreregistrationUser.INVITE_AS["MEMBER"],
|
|
|
|
),
|
2021-04-07 22:00:44 +02:00
|
|
|
stream_ids: List[int] = REQ(json_validator=check_list(check_int)),
|
2023-05-25 16:06:13 +02:00
|
|
|
include_realm_default_subscriptions: bool = REQ(json_validator=check_bool, default=False),
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2023-06-26 23:38:08 +02:00
|
|
|
if not user_profile.can_invite_users_by_email():
|
2021-04-07 21:09:43 +02:00
|
|
|
# Guest users case will not be handled here as it will
|
|
|
|
# be handled by the decorator above.
|
|
|
|
raise JsonableError(_("Insufficient permission"))
|
2023-06-21 14:33:11 +02:00
|
|
|
|
|
|
|
require_admin = invite_as in [
|
2023-09-07 01:19:39 +02:00
|
|
|
# Owners can only be invited by owners, checked by separate
|
|
|
|
# logic in check_role_based_permissions.
|
|
|
|
PreregistrationUser.INVITE_AS["REALM_OWNER"],
|
2023-06-21 14:33:11 +02:00
|
|
|
PreregistrationUser.INVITE_AS["REALM_ADMIN"],
|
|
|
|
PreregistrationUser.INVITE_AS["MODERATOR"],
|
|
|
|
]
|
|
|
|
check_role_based_permissions(invite_as, user_profile, require_admin=require_admin)
|
|
|
|
|
2016-10-12 05:13:32 +02:00
|
|
|
if not invitee_emails_raw:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("You must specify at least one email address."))
|
2016-10-12 05:13:32 +02:00
|
|
|
|
|
|
|
invitee_emails = get_invitee_emails_set(invitee_emails_raw)
|
|
|
|
|
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
|
|
|
streams: List[Stream] = []
|
2018-12-22 05:41:54 +01:00
|
|
|
for stream_id in stream_ids:
|
2017-01-30 03:09:04 +01:00
|
|
|
try:
|
2020-10-16 17:25:48 +02:00
|
|
|
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
2017-01-30 03:09:04 +01:00
|
|
|
except JsonableError:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(
|
2024-04-17 18:18:56 +02:00
|
|
|
_("Invalid channel ID {channel_id}. No invites were sent.").format(
|
2024-04-15 21:24:26 +02:00
|
|
|
channel_id=stream_id
|
2023-07-17 22:40:33 +02:00
|
|
|
)
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2016-10-12 05:13:32 +02:00
|
|
|
streams.append(stream)
|
|
|
|
|
2023-05-25 16:06:13 +02:00
|
|
|
if len(streams) and not user_profile.can_subscribe_other_users():
|
|
|
|
raise JsonableError(_("You do not have permission to subscribe other users to channels."))
|
2023-05-02 11:18:13 +02:00
|
|
|
|
2024-01-10 22:01:21 +01:00
|
|
|
skipped = do_invite_users(
|
2021-08-01 20:02:06 +02:00
|
|
|
user_profile,
|
|
|
|
invitee_emails,
|
|
|
|
streams,
|
2022-02-10 11:52:34 +01:00
|
|
|
invite_expires_in_minutes=invite_expires_in_minutes,
|
2023-05-25 16:06:13 +02:00
|
|
|
include_realm_default_subscriptions=include_realm_default_subscriptions,
|
2021-08-01 20:02:06 +02:00
|
|
|
invite_as=invite_as,
|
|
|
|
)
|
2024-01-10 22:01:21 +01:00
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|
2016-10-12 05:13:32 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-11-27 09:28:57 +01:00
|
|
|
def get_invitee_emails_set(invitee_emails_raw: str) -> Set[str]:
|
2021-02-12 08:20:45 +01:00
|
|
|
invitee_emails_list = set(re.split(r"[,\n]", invitee_emails_raw))
|
2016-10-12 05:13:32 +02:00
|
|
|
invitee_emails = set()
|
|
|
|
for email in invitee_emails_list:
|
2021-02-12 08:20:45 +01:00
|
|
|
is_email_with_name = re.search(r"<(?P<email>.*)>", email)
|
2016-10-12 05:13:32 +02:00
|
|
|
if is_email_with_name:
|
2021-02-12 08:20:45 +01:00
|
|
|
email = is_email_with_name.group("email")
|
2016-10-12 05:13:32 +02:00
|
|
|
invitee_emails.add(email.strip())
|
|
|
|
return invitee_emails
|
2017-10-21 03:15:12 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-04-30 21:41:21 +02:00
|
|
|
@require_member_or_admin
|
2017-11-27 09:28:57 +01:00
|
|
|
def get_user_invites(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
2022-01-12 19:39:57 +01:00
|
|
|
all_users = do_get_invites_controlled_by_user(user_profile)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data={"invites": all_users})
|
2017-10-21 03:15:12 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-04-30 21:41:21 +02:00
|
|
|
@require_member_or_admin
|
2017-10-21 03:15:12 +02:00
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def revoke_user_invite(
|
2024-04-20 08:41:38 +02:00
|
|
|
request: HttpRequest, user_profile: UserProfile, invite_id: int
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2017-12-05 20:01:55 +01:00
|
|
|
try:
|
2024-04-20 08:41:38 +02:00
|
|
|
prereg_user = PreregistrationUser.objects.get(id=invite_id)
|
2017-12-05 20:01:55 +01:00
|
|
|
except PreregistrationUser.DoesNotExist:
|
2017-12-05 20:05:17 +01:00
|
|
|
raise JsonableError(_("No such invitation"))
|
2017-12-05 20:01:55 +01:00
|
|
|
|
2021-07-24 20:11:03 +02:00
|
|
|
if prereg_user.realm != user_profile.realm:
|
2017-12-05 20:05:17 +01:00
|
|
|
raise JsonableError(_("No such invitation"))
|
2017-12-05 20:01:55 +01:00
|
|
|
|
2020-06-18 13:03:06 +02:00
|
|
|
if prereg_user.referred_by_id != user_profile.id:
|
2023-06-21 14:33:11 +02:00
|
|
|
check_role_based_permissions(prereg_user.invited_as, user_profile, require_admin=True)
|
2020-04-30 21:41:21 +02:00
|
|
|
|
2017-12-05 20:01:55 +01:00
|
|
|
do_revoke_user_invite(prereg_user)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|
2017-10-21 03:15:12 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-08-05 12:41:47 +02:00
|
|
|
@require_member_or_admin
|
2019-02-15 19:09:25 +01:00
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def revoke_multiuse_invite(
|
|
|
|
request: HttpRequest, user_profile: UserProfile, invite_id: int
|
|
|
|
) -> HttpResponse:
|
2019-02-15 19:09:25 +01:00
|
|
|
try:
|
|
|
|
invite = MultiuseInvite.objects.get(id=invite_id)
|
|
|
|
except MultiuseInvite.DoesNotExist:
|
|
|
|
raise JsonableError(_("No such invitation"))
|
|
|
|
|
|
|
|
if invite.realm != user_profile.realm:
|
|
|
|
raise JsonableError(_("No such invitation"))
|
|
|
|
|
2023-08-05 12:41:47 +02:00
|
|
|
if invite.referred_by_id != user_profile.id:
|
|
|
|
check_role_based_permissions(invite.invited_as, user_profile, require_admin=True)
|
2020-06-18 13:03:06 +02:00
|
|
|
|
2022-09-12 00:39:43 +02:00
|
|
|
if invite.status == confirmation_settings.STATUS_REVOKED:
|
|
|
|
raise JsonableError(_("Invitation has already been revoked"))
|
|
|
|
|
2019-02-15 19:09:25 +01:00
|
|
|
do_revoke_multi_use_invite(invite)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|
2019-02-15 19:09:25 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-04-30 21:41:21 +02:00
|
|
|
@require_member_or_admin
|
2017-10-21 03:15:12 +02:00
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def resend_user_invite_email(
|
2024-06-07 17:11:42 +02:00
|
|
|
request: HttpRequest, user_profile: UserProfile, invite_id: int
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2017-12-05 20:01:55 +01:00
|
|
|
try:
|
2024-06-07 17:11:42 +02:00
|
|
|
prereg_user = PreregistrationUser.objects.get(id=invite_id)
|
2017-12-05 20:01:55 +01:00
|
|
|
except PreregistrationUser.DoesNotExist:
|
2017-12-05 20:05:17 +01:00
|
|
|
raise JsonableError(_("No such invitation"))
|
2017-12-05 20:01:55 +01:00
|
|
|
|
2019-03-21 23:37:15 +01:00
|
|
|
# Structurally, any invitation the user can actually access should
|
|
|
|
# have a referred_by set for the user who created it.
|
|
|
|
if prereg_user.referred_by is None or prereg_user.referred_by.realm != user_profile.realm:
|
2017-12-05 20:05:17 +01:00
|
|
|
raise JsonableError(_("No such invitation"))
|
2017-12-05 20:01:55 +01:00
|
|
|
|
2020-06-18 13:03:06 +02:00
|
|
|
if prereg_user.referred_by_id != user_profile.id:
|
2023-06-21 14:33:11 +02:00
|
|
|
check_role_based_permissions(prereg_user.invited_as, user_profile, require_admin=True)
|
2020-04-30 21:41:21 +02:00
|
|
|
|
2024-04-30 22:12:34 +02:00
|
|
|
do_send_user_invite_email(prereg_user, event_time=timezone_now())
|
2024-05-01 06:27:32 +02:00
|
|
|
return json_success(request)
|
2018-03-02 12:27:57 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-08-09 15:06:56 +02:00
|
|
|
@require_member_or_admin
|
2018-03-02 12:27:57 +01:00
|
|
|
@has_request_variables
|
2019-02-06 22:57:14 +01:00
|
|
|
def generate_multiuse_invite_backend(
|
2021-02-12 08:19:30 +01:00
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2022-02-10 11:52:34 +01:00
|
|
|
invite_expires_in_minutes: Optional[int] = REQ(
|
2022-07-19 22:59:47 +02:00
|
|
|
json_validator=check_none_or(check_int), default=INVITATION_LINK_VALIDITY_MINUTES
|
2021-04-05 18:42:45 +02:00
|
|
|
),
|
2022-12-28 00:23:47 +01:00
|
|
|
invite_as: int = REQ(
|
|
|
|
json_validator=check_int_in(
|
|
|
|
list(PreregistrationUser.INVITE_AS.values()),
|
|
|
|
),
|
|
|
|
default=PreregistrationUser.INVITE_AS["MEMBER"],
|
|
|
|
),
|
2021-04-07 22:00:44 +02:00
|
|
|
stream_ids: Sequence[int] = REQ(json_validator=check_list(check_int), default=[]),
|
2023-05-25 16:06:13 +02:00
|
|
|
include_realm_default_subscriptions: bool = REQ(json_validator=check_bool, default=False),
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2023-08-09 15:06:56 +02:00
|
|
|
if not user_profile.can_create_multiuse_invite_to_realm():
|
|
|
|
# Guest users case will not be handled here as it will
|
|
|
|
# be handled by the decorator above.
|
|
|
|
raise JsonableError(_("Insufficient permission"))
|
|
|
|
|
|
|
|
require_admin = invite_as in [
|
|
|
|
# Owners can only be invited by owners, checked by separate
|
|
|
|
# logic in check_role_based_permissions.
|
|
|
|
PreregistrationUser.INVITE_AS["REALM_OWNER"],
|
|
|
|
PreregistrationUser.INVITE_AS["REALM_ADMIN"],
|
|
|
|
PreregistrationUser.INVITE_AS["MODERATOR"],
|
|
|
|
]
|
|
|
|
check_role_based_permissions(invite_as, user_profile, require_admin=require_admin)
|
2020-06-18 13:03:06 +02:00
|
|
|
|
2018-03-02 12:27:57 +01:00
|
|
|
streams = []
|
|
|
|
for stream_id in stream_ids:
|
|
|
|
try:
|
2020-10-16 17:25:48 +02:00
|
|
|
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
2018-03-02 12:27:57 +01:00
|
|
|
except JsonableError:
|
2023-07-17 22:40:33 +02:00
|
|
|
raise JsonableError(
|
2024-04-17 18:18:56 +02:00
|
|
|
_("Invalid channel ID {channel_id}. No invites were sent.").format(
|
2024-04-15 21:24:26 +02:00
|
|
|
channel_id=stream_id
|
2023-07-17 22:40:33 +02:00
|
|
|
)
|
|
|
|
)
|
2018-03-02 12:27:57 +01:00
|
|
|
streams.append(stream)
|
|
|
|
|
2023-05-25 16:06:13 +02:00
|
|
|
if len(streams) and not user_profile.can_subscribe_other_users():
|
|
|
|
raise JsonableError(_("You do not have permission to subscribe other users to channels."))
|
2024-01-05 04:58:39 +01:00
|
|
|
|
2021-04-05 18:42:45 +02:00
|
|
|
invite_link = do_create_multiuse_invite_link(
|
2023-05-25 16:06:13 +02:00
|
|
|
user_profile,
|
|
|
|
invite_as,
|
|
|
|
invite_expires_in_minutes,
|
|
|
|
include_realm_default_subscriptions,
|
|
|
|
streams,
|
2021-04-05 18:42:45 +02:00
|
|
|
)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data={"invite_link": invite_link})
|