Skip to content

Commit

Permalink
settings: Add two realm settings to restrict direct messages.
Browse files Browse the repository at this point in the history
Fixes #24467.
  • Loading branch information
Vector73 committed Jun 20, 2024
1 parent 02aefc6 commit 36e4eb1
Show file tree
Hide file tree
Showing 38 changed files with 992 additions and 61 deletions.
10 changes: 10 additions & 0 deletions api_docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ format used by the Zulip server that they are interacting with.

## Changes in Zulip 9.0

**Feature level 266**

* `PATCH /realm`, [`POST /register`](/api/register-queue),
[`GET /events`](/api/get-events): Added two new realm settings,
`direct_message_initiator_group`, which is the ID of the user group
whose members can initiate direct message thread, and
`direct_message_permission_group`, which is the ID of the user group
of which at least one member must be included as sender or recipient
in all personal and group direct messages.

**Feature level 265**

* [`GET /messages`](/api/get-messages),
Expand Down
3 changes: 3 additions & 0 deletions tools/test-js-with-node
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ EXEMPT_FILES = make_set(
"web/src/messages_overlay_ui.ts",
"web/src/modals.ts",
"web/src/muted_users_ui.ts",
"web/src/narrow_banner.ts",
"web/src/narrow_history.ts",
"web/src/narrow_title.ts",
"web/src/navbar_alerts.ts",
Expand All @@ -166,6 +167,7 @@ EXEMPT_FILES = make_set(
"web/src/overlays.ts",
"web/src/padded_widget.ts",
"web/src/page_params.ts",
"web/src/people.ts",
"web/src/personal_menu_popover.js",
"web/src/playground_links_popover.ts",
"web/src/plotly.js.d.ts",
Expand Down Expand Up @@ -200,6 +202,7 @@ EXEMPT_FILES = make_set(
"web/src/sent_messages.ts",
"web/src/sentry.ts",
"web/src/server_events.js",
"web/src/server_events_dispatch.js",
"web/src/settings.js",
"web/src/settings_account.js",
"web/src/settings_bots.js",
Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
# Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 265
API_FEATURE_LEVEL = 266

# Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump
Expand Down
6 changes: 6 additions & 0 deletions web/src/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ export function build_page() {
language_list,
realm_default_language_name: get_language_name(realm.realm_default_language),
realm_default_language_code: realm.realm_default_language,
realm_direct_message_initiator_group_id: realm.realm_direct_message_initiator_group,
realm_direct_message_permission_group_id: realm.realm_direct_message_permission_group,
realm_waiting_period_threshold: realm.realm_waiting_period_threshold,
realm_new_stream_announcements_stream_id: realm.realm_new_stream_announcements_stream_id,
realm_signup_announcements_stream_id: realm.realm_signup_announcements_stream_id,
Expand Down Expand Up @@ -275,6 +277,10 @@ export function build_page() {

tippy.default($("#realm_can_access_all_users_group_widget_container")[0], opts);
}

settings_org.check_disable_direct_message_initiator_group_dropdown(
realm.realm_direct_message_permission_group,
);
}

export function launch(section, user_settings_tab) {
Expand Down
27 changes: 24 additions & 3 deletions web/src/compose_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import autosize from "autosize";
import $ from "jquery";

import {all_messages_data} from "./all_messages_data";
import * as blueslip from "./blueslip";
import * as compose_banner from "./compose_banner";
import * as compose_fade from "./compose_fade";
Expand All @@ -18,11 +19,11 @@ import type {Message} from "./message_store";
import * as message_viewport from "./message_viewport";
import * as narrow_state from "./narrow_state";
import {page_params} from "./page_params";
import * as people from "./people";
import * as popovers from "./popovers";
import * as reload_state from "./reload_state";
import * as resize from "./resize";
import * as spectators from "./spectators";
import {realm} from "./state_data";
import * as stream_data from "./stream_data";

// Opts sent to `compose_actions.start`.
Expand Down Expand Up @@ -516,8 +517,28 @@ export function on_narrow(opts: NarrowActivateOpts): void {
}
return;
}
// Do not open compose box if organization has disabled sending
// direct messages and recipient is not a bot.
// Do not open compose box if sender is not allowed to send direct message.
const recipient_ids_string = opts.private_message_recipient
.split(",")
.map((email) => people.get_by_email(email)?.user_id)
.join(",");

const previous_messages_exist = all_messages_data
.all_messages()
.find((message) => message.is_private && message.to_user_ids === recipient_ids_string);

if (
(!previous_messages_exist &&
!people.user_can_initiate_direct_message_thread(recipient_ids_string)) ||
!people.user_can_direct_message(recipient_ids_string)
) {
// If we are navigating between direct message conversation,
// we want the compose box to close for non-bot users.
if (compose_state.composing()) {
cancel();
}
return;
}

// Open the compose box, passing the option to skip attempting
// an animated adjustment to scroll position, which is useless
Expand Down
11 changes: 10 additions & 1 deletion web/src/compose_closed_ui.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import $ from "jquery";

import {all_messages_data} from "./all_messages_data";
import * as compose_actions from "./compose_actions";
import {$t} from "./i18n";
import * as message_lists from "./message_lists";
Expand Down Expand Up @@ -129,7 +130,15 @@ export function update_buttons_for_private(): void {
const text_stream = $t({defaultMessage: "Start new conversation"});
const is_direct_message_narrow = true;
const pm_ids_string = narrow_state.pm_ids_string();
if (!pm_ids_string || people.user_can_direct_message(pm_ids_string)) {
const previous_messages_exist = all_messages_data
.all_messages()
.find((message) => message.is_private && message.to_user_ids === pm_ids_string);
if (
!pm_ids_string ||
((previous_messages_exist ??
people.user_can_initiate_direct_message_thread(pm_ids_string)) &&
people.user_can_direct_message(pm_ids_string))
) {
$("#new_conversation_button").attr("data-conversation-type", "direct");
update_buttons(text_stream, is_direct_message_narrow);
return;
Expand Down
49 changes: 45 additions & 4 deletions web/src/compose_recipient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type * as tippy from "tippy.js";

import render_inline_decorated_stream_name from "../templates/inline_decorated_stream_name.hbs";

import {all_messages_data} from "./all_messages_data";
import * as compose_banner from "./compose_banner";
import * as compose_fade from "./compose_fade";
import * as compose_pm_pill from "./compose_pm_pill";
Expand All @@ -20,10 +21,12 @@ import type {Option} from "./dropdown_widget";
import {$t} from "./i18n";
import * as narrow_state from "./narrow_state";
import * as people from "./people";
import * as settings_config from "./settings_config";
import {realm} from "./state_data";
import * as stream_data from "./stream_data";
import * as sub_store from "./sub_store";
import * as ui_util from "./ui_util";
import {get_user_group_from_id} from "./user_groups";
import * as util from "./util";

type MessageType = "stream" | "private";
Expand Down Expand Up @@ -116,8 +119,37 @@ export function get_posting_policy_error_message(): string {
if (compose_state.selected_recipient_id === "direct") {
const recipients = compose_pm_pill.get_user_ids_string();
if (!people.user_can_direct_message(recipients)) {
assert(typeof realm.realm_direct_message_permission_group === "number");
const {name} = get_user_group_from_id(realm.realm_direct_message_permission_group);
if (name === "role:nobody") {
return $t({
defaultMessage:
"You are not allowed to send direct messages in this organization.",
});
}
const display_name = settings_config.system_user_groups_list.find(
(group) => group.name === name,
)?.display_name;
assert(display_name !== undefined);
return $t(
{
defaultMessage:
"{allowed_user_group} must be in every direct message conversation.",
},
{allowed_user_group: display_name.replace(" and ", " or ")},
);
}

const previous_messages_exist = all_messages_data
.all_messages()
.find((message) => message.is_private && message.to_user_ids === recipients);
if (
!previous_messages_exist &&
!people.user_can_initiate_direct_message_thread(recipients)
) {
return $t({
defaultMessage: "You are not allowed to send direct messages in this organization.",
defaultMessage:
"You are not allowed to start direct message conversations in this organization.",
});
}
return "";
Expand Down Expand Up @@ -255,14 +287,23 @@ function item_click_callback(event: JQuery.ClickEvent, dropdown: tippy.Instance)
function get_options_for_recipient_widget(): Option[] {
const options: (Option | DirectMessagesOption)[] =
stream_data.get_options_for_dropdown_widget();

const recipients = compose_pm_pill.get_user_ids_string();
const direct_messages_option = {
is_direct_message: true,
unique_id: compose_state.DIRECT_MESSAGE_ID,
name: $t({defaultMessage: "Direct message"}),
};

options.unshift(direct_messages_option);
const previous_messages_exist = all_messages_data
.all_messages()
.find((message) => message.is_private && message.to_user_ids === recipients);
if (
(previous_messages_exist ?? people.user_can_initiate_direct_message_thread(recipients)) &&
people.user_can_direct_message(recipients)
) {
options.unshift(direct_messages_option);
} else {
options.push(direct_messages_option);
}
return options;
}

Expand Down
9 changes: 3 additions & 6 deletions web/src/compose_tooltips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as compose_recipient from "./compose_recipient";
import * as compose_state from "./compose_state";
import * as compose_validate from "./compose_validate";
import {$t} from "./i18n";
import * as narrow_banner from "./narrow_banner";
import * as narrow_state from "./narrow_state";
import * as popover_menus from "./popover_menus";
import {EXTRA_LONG_HOVER_DELAY, INSTANT_HOVER_DELAY, LONG_HOVER_DELAY} from "./tippyjs";
Expand All @@ -35,7 +36,7 @@ export function initialize(): void {
},
});
tippy.delegate("body", {
target: "#compose_buttons .compose-reply-button-wrapper",
target: ".compose-reply-button-wrapper",
delay: EXTRA_LONG_HOVER_DELAY,
// Only show on mouseenter since for spectators, clicking on these
// buttons opens login modal, and Micromodal returns focus to the
Expand All @@ -47,11 +48,7 @@ export function initialize(): void {
const button_type = $elem.attr("data-reply-button-type");
switch (button_type) {
case "direct_disabled": {
instance.setContent(
parse_html(
$("#compose_reply_direct_disabled_button_tooltip_template").html(),
),
);
instance.setContent(narrow_banner.pick_empty_narrow_banner().title);
return;
}
case "selected_message": {
Expand Down
56 changes: 56 additions & 0 deletions web/src/compose_validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import $ from "jquery";
import assert from "minimalistic-assert";

import * as resolved_topic from "../shared/src/resolved_topic";
import render_compose_banner from "../templates/compose_banner/compose_banner.hbs";
Expand All @@ -8,6 +9,7 @@ import render_stream_wildcard_warning from "../templates/compose_banner/stream_w
import render_wildcard_mention_not_allowed_error from "../templates/compose_banner/wildcard_mention_not_allowed_error.hbs";
import render_compose_limit_indicator from "../templates/compose_limit_indicator.hbs";

import {all_messages_data} from "./all_messages_data";
import * as compose_banner from "./compose_banner";
import * as compose_pm_pill from "./compose_pm_pill";
import * as compose_state from "./compose_state";
Expand All @@ -26,6 +28,7 @@ import * as stream_data from "./stream_data";
import * as sub_store from "./sub_store";
import type {StreamSubscription} from "./sub_store";
import type {UserOrMention} from "./typeahead_helper";
import {get_user_group_from_id} from "./user_groups";
import * as util from "./util";

let user_acknowledged_stream_wildcard = false;
Expand Down Expand Up @@ -619,8 +622,61 @@ function validate_stream_message(scheduling_message: boolean): boolean {
// for now)
function validate_private_message(): boolean {
const user_ids = compose_pm_pill.get_user_ids();
const user_ids_string = util.sorted_ids(user_ids).join(",");
const $banner_container = $("#compose_banners");

if (!people.user_can_direct_message(user_ids.join(","))) {
assert(typeof realm.realm_direct_message_permission_group === "number");
const {name} = get_user_group_from_id(realm.realm_direct_message_permission_group);
if (name === "role:nobody") {
compose_banner.show_error_message(
$t({
defaultMessage:
"You are not allowed to send direct messages in this organization.",
}),
compose_banner.CLASSNAMES.private_messages_disabled,
$banner_container,
$("#private_message_recipient"),
);
return false;
}

const display_name = settings_config.system_user_groups_list.find(
(group) => group.name === name,
)?.display_name;
assert(display_name !== undefined);
compose_banner.show_error_message(
$t(
{
defaultMessage:
"{allowed_user_group} must be in every direct message conversation.",
},
{allowed_user_group: display_name.replace(" and ", " or ")},
),
compose_banner.CLASSNAMES.private_messages_disabled,
$banner_container,
$("#private_message_recipient"),
);
return false;
}
const previous_messages_exist = all_messages_data
.all_messages()
.find((message) => message.is_private && message.to_user_ids === user_ids_string);
if (
!previous_messages_exist &&
!people.user_can_initiate_direct_message_thread(user_ids_string)
) {
compose_banner.show_error_message(
$t({
defaultMessage:
"You are not allowed to start direct message conversations in this organization.",
}),
compose_banner.CLASSNAMES.private_messages_disabled,
$banner_container,
$("#private_message_recipient"),
);
return false;
}
if (compose_state.private_message_recipient().length === 0) {
compose_banner.show_error_message(
$t({defaultMessage: "Please specify at least one valid recipient."}),
Expand Down
Loading

0 comments on commit 36e4eb1

Please sign in to comment.