2020-06-11 00:54:34 +02:00
|
|
|
import re
|
2024-07-14 19:39:20 +02:00
|
|
|
from typing import Annotated
|
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 _
|
2024-07-14 19:39:20 +02:00
|
|
|
from pydantic import Json
|
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-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
|
2024-07-14 19:39:20 +02:00
|
|
|
from zerver.lib.typed_endpoint import ApiParamConfig, PathOnly, typed_endpoint
|
|
|
|
from zerver.lib.typed_endpoint_validators import check_int_in_validator
|
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
|
2024-07-12 02:30:23 +02:00
|
|
|
INVITATION_LINK_VALIDITY_MINUTES: int | None = 24 * 60 * settings.INVITATION_LINK_VALIDITY_DAYS
|
2022-07-19 22:59:47 +02:00
|
|
|
|
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
|
|
|
|
2024-10-16 12:52:28 +02:00
|
|
|
def access_invite_by_id(user_profile: UserProfile, invite_id: int) -> PreregistrationUser:
|
|
|
|
try:
|
|
|
|
prereg_user = PreregistrationUser.objects.get(id=invite_id)
|
|
|
|
except PreregistrationUser.DoesNotExist:
|
|
|
|
raise JsonableError(_("No such invitation"))
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
raise JsonableError(_("No such invitation"))
|
|
|
|
|
|
|
|
if prereg_user.referred_by_id != user_profile.id:
|
|
|
|
check_role_based_permissions(prereg_user.invited_as, user_profile, require_admin=True)
|
|
|
|
return prereg_user
|
|
|
|
|
|
|
|
|
|
|
|
def access_multiuse_invite_by_id(user_profile: UserProfile, invite_id: int) -> MultiuseInvite:
|
|
|
|
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"))
|
|
|
|
|
|
|
|
if invite.referred_by_id != user_profile.id:
|
|
|
|
check_role_based_permissions(invite.invited_as, user_profile, require_admin=True)
|
|
|
|
|
|
|
|
if invite.status == confirmation_settings.STATUS_REVOKED:
|
|
|
|
raise JsonableError(_("Invitation has already been revoked"))
|
|
|
|
return invite
|
|
|
|
|
|
|
|
|
2019-06-18 16:43:22 +02:00
|
|
|
@require_member_or_admin
|
2024-07-14 19:39:20 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def invite_users_backend(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2024-07-14 19:39:20 +02:00
|
|
|
*,
|
|
|
|
invitee_emails_raw: Annotated[str, ApiParamConfig("invitee_emails")],
|
|
|
|
invite_expires_in_minutes: Json[int | None] = INVITATION_LINK_VALIDITY_MINUTES,
|
|
|
|
invite_as: Annotated[
|
|
|
|
Json[int],
|
|
|
|
check_int_in_validator(list(PreregistrationUser.INVITE_AS.values())),
|
|
|
|
] = PreregistrationUser.INVITE_AS["MEMBER"],
|
|
|
|
notify_referrer_on_join: Json[bool] = True,
|
|
|
|
stream_ids: Json[list[int]],
|
|
|
|
include_realm_default_subscriptions: Json[bool] = 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)
|
|
|
|
|
2024-07-12 02:30:17 +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,
|
2024-04-18 19:38:47 +02:00
|
|
|
notify_referrer_on_join,
|
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
|
|
|
|
2024-07-12 02:30:17 +02: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
|
2024-07-14 19:39:20 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def revoke_user_invite(
|
2024-07-14 19:39:20 +02:00
|
|
|
request: HttpRequest, user_profile: UserProfile, *, invite_id: PathOnly[int]
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2024-10-16 12:52:28 +02:00
|
|
|
prereg_user = access_invite_by_id(user_profile, invite_id)
|
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
|
2024-07-14 19:39:20 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def revoke_multiuse_invite(
|
2024-07-14 19:39:20 +02:00
|
|
|
request: HttpRequest, user_profile: UserProfile, *, invite_id: PathOnly[int]
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2024-10-16 12:52:28 +02:00
|
|
|
invite = access_multiuse_invite_by_id(user_profile, invite_id)
|
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
|
2024-07-14 19:39:20 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def resend_user_invite_email(
|
2024-07-14 19:39:20 +02:00
|
|
|
request: HttpRequest, user_profile: UserProfile, *, invite_id: PathOnly[int]
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2024-10-16 12:52:28 +02:00
|
|
|
prereg_user = access_invite_by_id(user_profile, invite_id)
|
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
|
2024-07-14 19:39:20 +02:00
|
|
|
@typed_endpoint
|
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,
|
2024-07-14 19:39:20 +02:00
|
|
|
*,
|
|
|
|
invite_expires_in_minutes: Json[int | None] = INVITATION_LINK_VALIDITY_MINUTES,
|
|
|
|
invite_as: Annotated[
|
|
|
|
Json[int],
|
|
|
|
check_int_in_validator(list(PreregistrationUser.INVITE_AS.values())),
|
|
|
|
] = PreregistrationUser.INVITE_AS["MEMBER"],
|
|
|
|
stream_ids: Json[list[int]] | None = None,
|
|
|
|
include_realm_default_subscriptions: Json[bool] = False,
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2024-07-14 19:39:20 +02:00
|
|
|
if stream_ids is None:
|
|
|
|
stream_ids = []
|
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})
|