mirror of https://github.com/zulip/zulip.git
mute user: Add backend infrastructure code.
Adds backend code for the mute users feature. This is just infrastructure work (database interactions, helpers, tests, events, API docs etc) and does not involve any behavioral/semantic aspects of muted users. Adds POST and DELETE endpoints, to keep the URL scheme mostly consistent in terms of `users/me`. TODOs: 1. Add tests for exporting `zulip_muteduser` database table. 2. Add dedicated methods to python-zulip-api to be used in place of the current `client.call_endpoint` implementation.
This commit is contained in:
parent
89f6139505
commit
3bfcaa3968
|
@ -10,6 +10,15 @@ below features are supported.
|
||||||
|
|
||||||
## Changes in Zulip 4.0
|
## Changes in Zulip 4.0
|
||||||
|
|
||||||
|
**Feature level 48**
|
||||||
|
|
||||||
|
* [`POST /users/me/muted_users/{muted_user_id}`](/api/mute-user),
|
||||||
|
[`DELETE /users/me/muted_users/{muted_user_id}`](/api/unmute-user):
|
||||||
|
New endpoints added to mute/unmute users.
|
||||||
|
* [`GET /events`](/api/get-events): Added new event type `muted_users`
|
||||||
|
which will be sent to a user when the set of users muted by them has
|
||||||
|
changed.
|
||||||
|
|
||||||
**Feature level 47**
|
**Feature level 47**
|
||||||
|
|
||||||
* [`POST /register`](/api/register-queue): Added a new `giphy_api_key`
|
* [`POST /register`](/api/register-queue): Added a new `giphy_api_key`
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Mute a user
|
||||||
|
|
||||||
|
{generate_api_description(/users/me/muted_users/{muted_user_id}:post)}
|
||||||
|
|
||||||
|
## Usage examples
|
||||||
|
|
||||||
|
{start_tabs}
|
||||||
|
{tab|python}
|
||||||
|
|
||||||
|
{generate_code_example(python)|/users/me/muted_users/{muted_user_id}:post|example}
|
||||||
|
|
||||||
|
{tab|curl}
|
||||||
|
|
||||||
|
{generate_code_example(curl)|/users/me/muted_users/{muted_user_id}:post|example}
|
||||||
|
|
||||||
|
{end_tabs}
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
{generate_api_arguments_table|zulip.yaml|/users/me/muted_users/{muted_user_id}:post}
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
A typical successful JSON response may look like:
|
||||||
|
|
||||||
|
{generate_code_example|/users/me/muted_users/{muted_user_id}:post|fixture(200)}
|
||||||
|
|
||||||
|
|
||||||
|
An example JSON response for when a user is already muted:
|
||||||
|
|
||||||
|
{generate_code_example|/users/me/muted_users/{muted_user_id}:post|fixture(400)}
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Unmute a user
|
||||||
|
|
||||||
|
{generate_api_description(/users/me/muted_users/{muted_user_id}:delete)}
|
||||||
|
|
||||||
|
## Usage examples
|
||||||
|
|
||||||
|
{start_tabs}
|
||||||
|
{tab|python}
|
||||||
|
|
||||||
|
{generate_code_example(python)|/users/me/muted_users/{muted_user_id}:delete|example}
|
||||||
|
|
||||||
|
{tab|curl}
|
||||||
|
|
||||||
|
{generate_code_example(curl)|/users/me/muted_users/{muted_user_id}:delete|example}
|
||||||
|
|
||||||
|
{end_tabs}
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
{generate_api_arguments_table|zulip.yaml|/users/me/muted_users/{muted_user_id}:delete}
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
A typical successful JSON response may look like:
|
||||||
|
|
||||||
|
{generate_code_example|/users/me/muted_users/{muted_user_id}:delete|fixture(200)}
|
||||||
|
|
||||||
|
An example JSON response for when a user is not previously muted:
|
||||||
|
|
||||||
|
{generate_code_example|/users/me/muted_users/{muted_user_id}:delete|fixture(400)}
|
|
@ -50,6 +50,8 @@
|
||||||
* [Update a user group](/api/update-user-group)
|
* [Update a user group](/api/update-user-group)
|
||||||
* [Delete a user group](/api/remove-user-group)
|
* [Delete a user group](/api/remove-user-group)
|
||||||
* [Update user group members](/api/update-user-group-members)
|
* [Update user group members](/api/update-user-group-members)
|
||||||
|
* [Mute a user](/api/mute-user)
|
||||||
|
* [Unmute a user](/api/unmute-user)
|
||||||
|
|
||||||
#### Server & organizations
|
#### Server & organizations
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0"
|
||||||
#
|
#
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in templates/zerver/api/changelog.md.
|
# new level means in templates/zerver/api/changelog.md.
|
||||||
API_FEATURE_LEVEL = 47
|
API_FEATURE_LEVEL = 48
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# 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
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|
|
@ -167,6 +167,7 @@ from zerver.lib.upload import (
|
||||||
upload_emoji_image,
|
upload_emoji_image,
|
||||||
)
|
)
|
||||||
from zerver.lib.user_groups import access_user_group_by_id, create_user_group
|
from zerver.lib.user_groups import access_user_group_by_id, create_user_group
|
||||||
|
from zerver.lib.user_mutes import add_user_mute, get_user_mutes, remove_user_mute
|
||||||
from zerver.lib.user_status import update_user_status
|
from zerver.lib.user_status import update_user_status
|
||||||
from zerver.lib.users import (
|
from zerver.lib.users import (
|
||||||
check_bot_name_available,
|
check_bot_name_available,
|
||||||
|
@ -6486,6 +6487,24 @@ def do_unmute_topic(user_profile: UserProfile, stream: Stream, topic: str) -> No
|
||||||
send_event(user_profile.realm, event, [user_profile.id])
|
send_event(user_profile.realm, event, [user_profile.id])
|
||||||
|
|
||||||
|
|
||||||
|
def do_mute_user(
|
||||||
|
user_profile: UserProfile,
|
||||||
|
muted_user: UserProfile,
|
||||||
|
date_muted: Optional[datetime.datetime] = None,
|
||||||
|
) -> None:
|
||||||
|
if date_muted is None:
|
||||||
|
date_muted = timezone_now()
|
||||||
|
add_user_mute(user_profile, muted_user, date_muted)
|
||||||
|
event = dict(type="muted_users", muted_users=get_user_mutes(user_profile))
|
||||||
|
send_event(user_profile.realm, event, [user_profile.id])
|
||||||
|
|
||||||
|
|
||||||
|
def do_unmute_user(user_profile: UserProfile, muted_user: UserProfile) -> None:
|
||||||
|
remove_user_mute(user_profile, muted_user)
|
||||||
|
event = dict(type="muted_users", muted_users=get_user_mutes(user_profile))
|
||||||
|
send_event(user_profile.realm, event, [user_profile.id])
|
||||||
|
|
||||||
|
|
||||||
def do_mark_hotspot_as_read(user: UserProfile, hotspot: str) -> None:
|
def do_mark_hotspot_as_read(user: UserProfile, hotspot: str) -> None:
|
||||||
UserHotspot.objects.get_or_create(user=user, hotspot=hotspot)
|
UserHotspot.objects.get_or_create(user=user, hotspot=hotspot)
|
||||||
event = dict(type="hotspots", hotspots=get_next_hotspots(user))
|
event = dict(type="hotspots", hotspots=get_next_hotspots(user))
|
||||||
|
|
|
@ -337,6 +337,21 @@ muted_topics_event = event_dict_type(
|
||||||
)
|
)
|
||||||
check_muted_topics = make_checker(muted_topics_event)
|
check_muted_topics = make_checker(muted_topics_event)
|
||||||
|
|
||||||
|
muted_user_type = DictType(
|
||||||
|
required_keys=[
|
||||||
|
("id", int),
|
||||||
|
("timestamp", int),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
muted_users_event = event_dict_type(
|
||||||
|
required_keys=[
|
||||||
|
("type", Equals("muted_users")),
|
||||||
|
("muted_users", ListType(muted_user_type)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
check_muted_users = make_checker(muted_users_event)
|
||||||
|
|
||||||
_check_topic_links = DictType(
|
_check_topic_links = DictType(
|
||||||
required_keys=[
|
required_keys=[
|
||||||
("text", str),
|
("text", str),
|
||||||
|
|
|
@ -45,6 +45,7 @@ from zerver.lib.stream_subscription import handle_stream_notifications_compatibi
|
||||||
from zerver.lib.topic import TOPIC_NAME
|
from zerver.lib.topic import TOPIC_NAME
|
||||||
from zerver.lib.topic_mutes import get_topic_mutes
|
from zerver.lib.topic_mutes import get_topic_mutes
|
||||||
from zerver.lib.user_groups import user_groups_in_realm_serialized
|
from zerver.lib.user_groups import user_groups_in_realm_serialized
|
||||||
|
from zerver.lib.user_mutes import get_user_mutes
|
||||||
from zerver.lib.user_status import get_user_info_dict
|
from zerver.lib.user_status import get_user_info_dict
|
||||||
from zerver.lib.users import get_cross_realm_dicts, get_raw_user_data, is_administrator_role
|
from zerver.lib.users import get_cross_realm_dicts, get_raw_user_data, is_administrator_role
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
|
@ -159,6 +160,9 @@ def fetch_initial_state_data(
|
||||||
if want("muted_topics"):
|
if want("muted_topics"):
|
||||||
state["muted_topics"] = [] if user_profile is None else get_topic_mutes(user_profile)
|
state["muted_topics"] = [] if user_profile is None else get_topic_mutes(user_profile)
|
||||||
|
|
||||||
|
if want("muted_users"):
|
||||||
|
state["muted_users"] = [] if user_profile is None else get_user_mutes(user_profile)
|
||||||
|
|
||||||
if want("presence"):
|
if want("presence"):
|
||||||
state["presences"] = (
|
state["presences"] = (
|
||||||
{} if user_profile is None else get_presences_for_realm(realm, slim_presence)
|
{} if user_profile is None else get_presences_for_realm(realm, slim_presence)
|
||||||
|
@ -969,6 +973,8 @@ def apply_event(
|
||||||
state["alert_words"] = event["alert_words"]
|
state["alert_words"] = event["alert_words"]
|
||||||
elif event["type"] == "muted_topics":
|
elif event["type"] == "muted_topics":
|
||||||
state["muted_topics"] = event["muted_topics"]
|
state["muted_topics"] = event["muted_topics"]
|
||||||
|
elif event["type"] == "muted_users":
|
||||||
|
state["muted_users"] = event["muted_users"]
|
||||||
elif event["type"] == "realm_filters":
|
elif event["type"] == "realm_filters":
|
||||||
state["realm_filters"] = event["realm_filters"]
|
state["realm_filters"] = event["realm_filters"]
|
||||||
elif event["type"] == "update_display_settings":
|
elif event["type"] == "update_display_settings":
|
||||||
|
|
|
@ -160,6 +160,7 @@ ALL_ZULIP_TABLES = {
|
||||||
"zerver_userprofile_user_permissions",
|
"zerver_userprofile_user_permissions",
|
||||||
"zerver_userstatus",
|
"zerver_userstatus",
|
||||||
"zerver_mutedtopic",
|
"zerver_mutedtopic",
|
||||||
|
"zerver_muteduser",
|
||||||
}
|
}
|
||||||
|
|
||||||
# This set contains those database tables that we expect to not be
|
# This set contains those database tables that we expect to not be
|
||||||
|
@ -223,6 +224,7 @@ NON_EXPORTED_TABLES = {
|
||||||
# export before they reach full production status.
|
# export before they reach full production status.
|
||||||
"zerver_defaultstreamgroup",
|
"zerver_defaultstreamgroup",
|
||||||
"zerver_defaultstreamgroup_streams",
|
"zerver_defaultstreamgroup_streams",
|
||||||
|
"zerver_muteduser",
|
||||||
"zerver_submessage",
|
"zerver_submessage",
|
||||||
# This is low priority, since users can easily just reset themselves to away.
|
# This is low priority, since users can easily just reset themselves to away.
|
||||||
"zerver_userstatus",
|
"zerver_userstatus",
|
||||||
|
|
|
@ -45,6 +45,7 @@ from zerver.models import (
|
||||||
Huddle,
|
Huddle,
|
||||||
Message,
|
Message,
|
||||||
MutedTopic,
|
MutedTopic,
|
||||||
|
MutedUser,
|
||||||
Reaction,
|
Reaction,
|
||||||
Realm,
|
Realm,
|
||||||
RealmAuditLog,
|
RealmAuditLog,
|
||||||
|
@ -111,6 +112,7 @@ ID_MAP: Dict[str, Dict[int, int]] = {
|
||||||
"recipient_to_huddle_map": {},
|
"recipient_to_huddle_map": {},
|
||||||
"userhotspot": {},
|
"userhotspot": {},
|
||||||
"mutedtopic": {},
|
"mutedtopic": {},
|
||||||
|
"muteduser": {},
|
||||||
"service": {},
|
"service": {},
|
||||||
"usergroup": {},
|
"usergroup": {},
|
||||||
"usergroupmembership": {},
|
"usergroupmembership": {},
|
||||||
|
@ -1068,6 +1070,13 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea
|
||||||
update_model_ids(MutedTopic, data, "mutedtopic")
|
update_model_ids(MutedTopic, data, "mutedtopic")
|
||||||
bulk_import_model(data, MutedTopic)
|
bulk_import_model(data, MutedTopic)
|
||||||
|
|
||||||
|
if "zerver_muteduser" in data:
|
||||||
|
fix_datetime_fields(data, "zerver_muteduser")
|
||||||
|
re_map_foreign_keys(data, "zerver_muteduser", "user_profile", related_table="user_profile")
|
||||||
|
re_map_foreign_keys(data, "zerver_muteduser", "muted_user", related_table="user_profile")
|
||||||
|
update_model_ids(MutedUser, data, "muteduser")
|
||||||
|
bulk_import_model(data, MutedUser)
|
||||||
|
|
||||||
if "zerver_service" in data:
|
if "zerver_service" in data:
|
||||||
re_map_foreign_keys(data, "zerver_service", "user_profile", related_table="user_profile")
|
re_map_foreign_keys(data, "zerver_service", "user_profile", related_table="user_profile")
|
||||||
fix_service_tokens(data, "zerver_service")
|
fix_service_tokens(data, "zerver_service")
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import datetime
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
|
from zerver.models import MutedUser, UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_mutes(user_profile: UserProfile) -> List[Dict[str, int]]:
|
||||||
|
rows = MutedUser.objects.filter(user_profile=user_profile).values(
|
||||||
|
"muted_user__id",
|
||||||
|
"date_muted",
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": row["muted_user__id"],
|
||||||
|
"timestamp": datetime_to_timestamp(row["date_muted"]),
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def add_user_mute(
|
||||||
|
user_profile: UserProfile,
|
||||||
|
muted_user: UserProfile,
|
||||||
|
date_muted: Optional[datetime.datetime] = None,
|
||||||
|
) -> None:
|
||||||
|
if date_muted is None:
|
||||||
|
date_muted = timezone_now()
|
||||||
|
MutedUser.objects.create(
|
||||||
|
user_profile=user_profile,
|
||||||
|
muted_user=muted_user,
|
||||||
|
date_muted=date_muted,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_user_mute(user_profile: UserProfile, muted_user: UserProfile) -> None:
|
||||||
|
MutedUser.objects.get(user_profile=user_profile, muted_user=muted_user).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def user_is_muted(user_profile: UserProfile, muted_user: UserProfile) -> bool:
|
||||||
|
return MutedUser.objects.filter(user_profile=user_profile, muted_user=muted_user).exists()
|
|
@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
("date_muted", models.DateTimeField(default=django.utils.timezone.now)),
|
("date_muted", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
(
|
(
|
||||||
"muted_user_profile",
|
"muted_user",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="+",
|
related_name="+",
|
||||||
|
@ -41,7 +41,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"unique_together": {("user_profile", "muted_user_profile")},
|
"unique_together": {("user_profile", "muted_user")},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1847,14 +1847,14 @@ class MutedTopic(models.Model):
|
||||||
|
|
||||||
class MutedUser(models.Model):
|
class MutedUser(models.Model):
|
||||||
user_profile = models.ForeignKey(UserProfile, related_name="+", on_delete=CASCADE)
|
user_profile = models.ForeignKey(UserProfile, related_name="+", on_delete=CASCADE)
|
||||||
muted_user_profile = models.ForeignKey(UserProfile, related_name="+", on_delete=CASCADE)
|
muted_user = models.ForeignKey(UserProfile, related_name="+", on_delete=CASCADE)
|
||||||
date_muted: datetime.datetime = models.DateTimeField(default=timezone_now)
|
date_muted: datetime.datetime = models.DateTimeField(default=timezone_now)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("user_profile", "muted_user_profile")
|
unique_together = ("user_profile", "muted_user")
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"<MutedUser: {self.user_profile.email} -> {self.muted_user_profile.email}>"
|
return f"<MutedUser: {self.user_profile.email} -> {self.muted_user.email}>"
|
||||||
|
|
||||||
|
|
||||||
class Client(models.Model):
|
class Client(models.Model):
|
||||||
|
|
|
@ -601,6 +601,32 @@ def toggle_mute_topic(client: Client) -> None:
|
||||||
validate_against_openapi_schema(result, "/users/me/subscriptions/muted_topics", "patch", "200")
|
validate_against_openapi_schema(result, "/users/me/subscriptions/muted_topics", "patch", "200")
|
||||||
|
|
||||||
|
|
||||||
|
@openapi_test_function("/users/me/muted_users/{muted_user_id}:post")
|
||||||
|
def add_user_mute(client: Client) -> None:
|
||||||
|
ensure_users([10], ["hamlet"])
|
||||||
|
# {code_example|start}
|
||||||
|
# Mute user with ID 10
|
||||||
|
muted_user_id = 10
|
||||||
|
result = client.call_endpoint(url=f"/users/me/muted_users/{muted_user_id}", method="POST")
|
||||||
|
# {code_example|end}
|
||||||
|
|
||||||
|
validate_against_openapi_schema(result, "/users/me/muted_users/{muted_user_id}", "post", "200")
|
||||||
|
|
||||||
|
|
||||||
|
@openapi_test_function("/users/me/muted_users/{muted_user_id}:delete")
|
||||||
|
def remove_user_mute(client: Client) -> None:
|
||||||
|
ensure_users([10], ["hamlet"])
|
||||||
|
# {code_example|start}
|
||||||
|
# Unmute user with ID 10
|
||||||
|
muted_user_id = 10
|
||||||
|
result = client.call_endpoint(url=f"/users/me/muted_users/{muted_user_id}", method="DELETE")
|
||||||
|
# {code_example|end}
|
||||||
|
|
||||||
|
validate_against_openapi_schema(
|
||||||
|
result, "/users/me/muted_users/{muted_user_id}", "delete", "200"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@openapi_test_function("/mark_all_as_read:post")
|
@openapi_test_function("/mark_all_as_read:post")
|
||||||
def mark_all_as_read(client: Client) -> None:
|
def mark_all_as_read(client: Client) -> None:
|
||||||
|
|
||||||
|
@ -1352,6 +1378,8 @@ def test_users(client: Client, owner_client: Client) -> None:
|
||||||
add_alert_words(client)
|
add_alert_words(client)
|
||||||
remove_alert_words(client)
|
remove_alert_words(client)
|
||||||
deactivate_own_user(client, owner_client)
|
deactivate_own_user(client, owner_client)
|
||||||
|
add_user_mute(client)
|
||||||
|
remove_user_mute(client)
|
||||||
|
|
||||||
|
|
||||||
def test_streams(client: Client, nonadmin_client: Client) -> None:
|
def test_streams(client: Client, nonadmin_client: Client) -> None:
|
||||||
|
|
|
@ -1696,6 +1696,50 @@ paths:
|
||||||
[["Denmark", "topic", 1594825442]],
|
[["Denmark", "topic", 1594825442]],
|
||||||
"id": 0,
|
"id": 0,
|
||||||
}
|
}
|
||||||
|
- type: object
|
||||||
|
description: |
|
||||||
|
Event sent to a user's clients when that user's set of
|
||||||
|
configured muted users have changed.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 4.0 (feature level 48).
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/EventIdSchema"
|
||||||
|
type:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/EventTypeSchema"
|
||||||
|
- enum:
|
||||||
|
- muted_users
|
||||||
|
muted_users:
|
||||||
|
type: array
|
||||||
|
description: |
|
||||||
|
A list of dictionaries where each dictionary describes
|
||||||
|
a muted user.
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
description: |
|
||||||
|
Object containing the user id and timestamp of a muted user.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
The ID of the muted user.
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
An integer UNIX timestamp representing when the user was muted.
|
||||||
|
additionalProperties: false
|
||||||
|
example:
|
||||||
|
{
|
||||||
|
"type": "muted_users",
|
||||||
|
"muted_users":
|
||||||
|
[
|
||||||
|
{"id": 1, "timestamp": 1594825442},
|
||||||
|
{"id": 22, "timestamp": 1654865392},
|
||||||
|
],
|
||||||
|
"id": 0,
|
||||||
|
}
|
||||||
- type: object
|
- type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
description: |
|
description: |
|
||||||
|
@ -5590,6 +5634,77 @@ paths:
|
||||||
- $ref: "#/components/schemas/JsonError"
|
- $ref: "#/components/schemas/JsonError"
|
||||||
- example:
|
- example:
|
||||||
{"msg": "Topic is not muted", "result": "error"}
|
{"msg": "Topic is not muted", "result": "error"}
|
||||||
|
/users/me/muted_users/{muted_user_id}:
|
||||||
|
post:
|
||||||
|
operationId: mute_user
|
||||||
|
tags: ["users"]
|
||||||
|
description: |
|
||||||
|
This endpoint mutes a user. Messages sent by users you've muted will
|
||||||
|
be automatically marked as read and hidden.
|
||||||
|
|
||||||
|
`POST {{ api_url }}/v1/users/me/muted_users/{muted_user_id}`
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 4.0 (feature level 48).
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/MutedUserId"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Success.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/JsonSuccess"
|
||||||
|
- example: {"msg": "", "result": "success"}
|
||||||
|
"400":
|
||||||
|
description: Bad request.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- allOf:
|
||||||
|
- $ref: "#/components/schemas/JsonError"
|
||||||
|
- example: {"msg": "Cannot mute self", "result": "error"}
|
||||||
|
- allOf:
|
||||||
|
- $ref: "#/components/schemas/JsonError"
|
||||||
|
- example: {"msg": "No such user", "result": "error"}
|
||||||
|
- allOf:
|
||||||
|
- $ref: "#/components/schemas/JsonError"
|
||||||
|
- example:
|
||||||
|
{"msg": "User already muted", "result": "error"}
|
||||||
|
delete:
|
||||||
|
operationId: unmute_user
|
||||||
|
tags: ["users"]
|
||||||
|
description: |
|
||||||
|
This endpoint unmutes a user.
|
||||||
|
|
||||||
|
`DELETE {{ api_url }}/v1/users/me/muted_users/{muted_user_id}`
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 4.0 (feature level 48).
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/MutedUserId"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Success.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/JsonSuccess"
|
||||||
|
- example: {"msg": "", "result": "success"}
|
||||||
|
"400":
|
||||||
|
description: Bad request.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- allOf:
|
||||||
|
- $ref: "#/components/schemas/JsonError"
|
||||||
|
- example: {"msg": "No such user", "result": "error"}
|
||||||
|
- allOf:
|
||||||
|
- $ref: "#/components/schemas/JsonError"
|
||||||
|
- example: {"msg": "User is not muted", "result": "error"}
|
||||||
|
|
||||||
/users/{user_id}/subscriptions/{stream_id}:
|
/users/{user_id}/subscriptions/{stream_id}:
|
||||||
get:
|
get:
|
||||||
operationId: get_subscription_status
|
operationId: get_subscription_status
|
||||||
|
@ -6660,6 +6775,29 @@ paths:
|
||||||
oneOf:
|
oneOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: integer
|
- type: integer
|
||||||
|
muted_users:
|
||||||
|
type: array
|
||||||
|
description: |
|
||||||
|
Present if `muted_users` is present in `fetch_event_types`.
|
||||||
|
|
||||||
|
A list of dictionaries where each dictionary describes
|
||||||
|
a muted user.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 4.0 (feature level 48).
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
description: |
|
||||||
|
Object containing the user id and timestamp of a muted user.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
The ID of the muted user.
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
An integer UNIX timestamp representing when the user was muted.
|
||||||
presences:
|
presences:
|
||||||
type: object
|
type: object
|
||||||
description: |
|
description: |
|
||||||
|
@ -11059,6 +11197,15 @@ components:
|
||||||
type: integer
|
type: integer
|
||||||
example: 12
|
example: 12
|
||||||
required: true
|
required: true
|
||||||
|
MutedUserId:
|
||||||
|
name: muted_user_id
|
||||||
|
in: path
|
||||||
|
description: |
|
||||||
|
The ID of the user to mute/un-mute.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
example: 10
|
||||||
|
required: true
|
||||||
StreamPostPolicy:
|
StreamPostPolicy:
|
||||||
name: stream_post_policy
|
name: stream_post_policy
|
||||||
in: query
|
in: query
|
||||||
|
|
|
@ -852,7 +852,7 @@ class FetchQueriesTest(ZulipTestCase):
|
||||||
with mock.patch("zerver.lib.events.always_want") as want_mock:
|
with mock.patch("zerver.lib.events.always_want") as want_mock:
|
||||||
fetch_initial_state_data(user)
|
fetch_initial_state_data(user)
|
||||||
|
|
||||||
self.assert_length(queries, 29)
|
self.assert_length(queries, 30)
|
||||||
|
|
||||||
expected_counts = dict(
|
expected_counts = dict(
|
||||||
alert_words=1,
|
alert_words=1,
|
||||||
|
@ -862,6 +862,7 @@ class FetchQueriesTest(ZulipTestCase):
|
||||||
hotspots=0,
|
hotspots=0,
|
||||||
message=1,
|
message=1,
|
||||||
muted_topics=1,
|
muted_topics=1,
|
||||||
|
muted_users=1,
|
||||||
presence=1,
|
presence=1,
|
||||||
realm=0,
|
realm=0,
|
||||||
realm_bot=1,
|
realm_bot=1,
|
||||||
|
|
|
@ -59,6 +59,7 @@ from zerver.lib.actions import (
|
||||||
do_invite_users,
|
do_invite_users,
|
||||||
do_mark_hotspot_as_read,
|
do_mark_hotspot_as_read,
|
||||||
do_mute_topic,
|
do_mute_topic,
|
||||||
|
do_mute_user,
|
||||||
do_reactivate_user,
|
do_reactivate_user,
|
||||||
do_regenerate_api_key,
|
do_regenerate_api_key,
|
||||||
do_remove_alert_words,
|
do_remove_alert_words,
|
||||||
|
@ -81,6 +82,7 @@ from zerver.lib.actions import (
|
||||||
do_set_user_display_setting,
|
do_set_user_display_setting,
|
||||||
do_set_zoom_token,
|
do_set_zoom_token,
|
||||||
do_unmute_topic,
|
do_unmute_topic,
|
||||||
|
do_unmute_user,
|
||||||
do_update_embedded_data,
|
do_update_embedded_data,
|
||||||
do_update_message,
|
do_update_message,
|
||||||
do_update_message_flags,
|
do_update_message_flags,
|
||||||
|
@ -109,6 +111,7 @@ from zerver.lib.event_schema import (
|
||||||
check_invites_changed,
|
check_invites_changed,
|
||||||
check_message,
|
check_message,
|
||||||
check_muted_topics,
|
check_muted_topics,
|
||||||
|
check_muted_users,
|
||||||
check_presence,
|
check_presence,
|
||||||
check_reaction_add,
|
check_reaction_add,
|
||||||
check_reaction_remove,
|
check_reaction_remove,
|
||||||
|
@ -1000,6 +1003,14 @@ class NormalActionsTest(BaseAction):
|
||||||
events = self.verify_action(lambda: do_unmute_topic(self.user_profile, stream, "topic"))
|
events = self.verify_action(lambda: do_unmute_topic(self.user_profile, stream, "topic"))
|
||||||
check_muted_topics("events[0]", events[0])
|
check_muted_topics("events[0]", events[0])
|
||||||
|
|
||||||
|
def test_muted_users_events(self) -> None:
|
||||||
|
muted_user = self.example_user("othello")
|
||||||
|
events = self.verify_action(lambda: do_mute_user(self.user_profile, muted_user))
|
||||||
|
check_muted_users("events[0]", events[0])
|
||||||
|
|
||||||
|
events = self.verify_action(lambda: do_unmute_user(self.user_profile, muted_user))
|
||||||
|
check_muted_users("events[0]", events[0])
|
||||||
|
|
||||||
def test_change_avatar_fields(self) -> None:
|
def test_change_avatar_fields(self) -> None:
|
||||||
events = self.verify_action(
|
events = self.verify_action(
|
||||||
lambda: do_change_avatar_fields(
|
lambda: do_change_avatar_fields(
|
||||||
|
|
|
@ -107,6 +107,7 @@ class HomeTest(ZulipTestCase):
|
||||||
"max_message_id",
|
"max_message_id",
|
||||||
"message_content_in_email_notifications",
|
"message_content_in_email_notifications",
|
||||||
"muted_topics",
|
"muted_topics",
|
||||||
|
"muted_users",
|
||||||
"narrow",
|
"narrow",
|
||||||
"narrow_stream",
|
"narrow_stream",
|
||||||
"needs_tutorial",
|
"needs_tutorial",
|
||||||
|
@ -261,7 +262,7 @@ class HomeTest(ZulipTestCase):
|
||||||
set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
|
set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_length(queries, 39)
|
self.assert_length(queries, 40)
|
||||||
self.assert_length(cache_mock.call_args_list, 5)
|
self.assert_length(cache_mock.call_args_list, 5)
|
||||||
|
|
||||||
html = result.content.decode("utf-8")
|
html = result.content.decode("utf-8")
|
||||||
|
@ -341,7 +342,7 @@ class HomeTest(ZulipTestCase):
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
self.check_rendered_logged_in_app(result)
|
self.check_rendered_logged_in_app(result)
|
||||||
self.assert_length(cache_mock.call_args_list, 6)
|
self.assert_length(cache_mock.call_args_list, 6)
|
||||||
self.assert_length(queries, 36)
|
self.assert_length(queries, 37)
|
||||||
|
|
||||||
def test_num_queries_with_streams(self) -> None:
|
def test_num_queries_with_streams(self) -> None:
|
||||||
main_user = self.example_user("hamlet")
|
main_user = self.example_user("hamlet")
|
||||||
|
@ -372,7 +373,7 @@ class HomeTest(ZulipTestCase):
|
||||||
with queries_captured() as queries2:
|
with queries_captured() as queries2:
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
|
|
||||||
self.assert_length(queries2, 34)
|
self.assert_length(queries2, 35)
|
||||||
|
|
||||||
# Do a sanity check that our new streams were in the payload.
|
# Do a sanity check that our new streams were in the payload.
|
||||||
html = result.content.decode("utf-8")
|
html = result.content.decode("utf-8")
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
|
from zerver.lib.user_mutes import add_user_mute, get_user_mutes, user_is_muted
|
||||||
|
|
||||||
|
|
||||||
|
class MutedUsersTests(ZulipTestCase):
|
||||||
|
def test_get_user_mutes(self) -> None:
|
||||||
|
othello = self.example_user("othello")
|
||||||
|
cordelia = self.example_user("cordelia")
|
||||||
|
|
||||||
|
muted_users = get_user_mutes(othello)
|
||||||
|
self.assertEqual(muted_users, [])
|
||||||
|
mute_time = datetime(2021, 1, 1, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
with mock.patch(
|
||||||
|
"zerver.lib.user_mutes.timezone_now",
|
||||||
|
return_value=mute_time,
|
||||||
|
):
|
||||||
|
add_user_mute(user_profile=othello, muted_user=cordelia)
|
||||||
|
|
||||||
|
muted_users = get_user_mutes(othello)
|
||||||
|
self.assertEqual(len(muted_users), 1)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
muted_users[0],
|
||||||
|
{
|
||||||
|
"id": cordelia.id,
|
||||||
|
"timestamp": datetime_to_timestamp(mute_time),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_add_muted_user_mute_self(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
url = "/api/v1/users/me/muted_users/{}".format(user.id)
|
||||||
|
result = self.api_post(user, url)
|
||||||
|
self.assert_json_error(result, "Cannot mute self")
|
||||||
|
|
||||||
|
def test_add_muted_user_mute_bot(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
bot_info = {
|
||||||
|
"full_name": "The Bot of Hamlet",
|
||||||
|
"short_name": "hambot",
|
||||||
|
"bot_type": "1",
|
||||||
|
}
|
||||||
|
result = self.client_post("/json/bots", bot_info)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
muted_id = result.json()["user_id"]
|
||||||
|
|
||||||
|
url = "/api/v1/users/me/muted_users/{}".format(muted_id)
|
||||||
|
result = self.api_post(user, url)
|
||||||
|
# Currently we do not allow muting bots. This is the error message
|
||||||
|
# from `access_user_by_id`.
|
||||||
|
self.assert_json_error(result, "No such user")
|
||||||
|
|
||||||
|
def test_add_muted_user_mute_twice(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
muted_user = self.example_user("cordelia")
|
||||||
|
muted_id = muted_user.id
|
||||||
|
|
||||||
|
add_user_mute(
|
||||||
|
user_profile=user,
|
||||||
|
muted_user=muted_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
url = "/api/v1/users/me/muted_users/{}".format(muted_id)
|
||||||
|
result = self.api_post(user, url)
|
||||||
|
self.assert_json_error(result, "User already muted")
|
||||||
|
|
||||||
|
def test_add_muted_user_valid_data(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
muted_user = self.example_user("cordelia")
|
||||||
|
muted_id = muted_user.id
|
||||||
|
|
||||||
|
mock_date_muted = datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp()
|
||||||
|
with mock.patch(
|
||||||
|
"zerver.views.muting.timezone_now",
|
||||||
|
return_value=datetime(2021, 1, 1, tzinfo=timezone.utc),
|
||||||
|
):
|
||||||
|
url = "/api/v1/users/me/muted_users/{}".format(muted_id)
|
||||||
|
result = self.api_post(user, url)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
self.assertIn({"id": muted_id, "timestamp": mock_date_muted}, get_user_mutes(user))
|
||||||
|
self.assertTrue(user_is_muted(user, muted_user))
|
||||||
|
|
||||||
|
def test_remove_muted_user_unmute_before_muting(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
muted_user = self.example_user("cordelia")
|
||||||
|
muted_id = muted_user.id
|
||||||
|
|
||||||
|
url = "/api/v1/users/me/muted_users/{}".format(muted_id)
|
||||||
|
result = self.api_delete(user, url)
|
||||||
|
self.assert_json_error(result, "User is not muted")
|
||||||
|
|
||||||
|
def test_remove_muted_user_valid_data(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
muted_user = self.example_user("cordelia")
|
||||||
|
muted_id = muted_user.id
|
||||||
|
|
||||||
|
add_user_mute(
|
||||||
|
user_profile=user,
|
||||||
|
muted_user=muted_user,
|
||||||
|
date_muted=datetime(2021, 1, 1, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
url = "/api/v1/users/me/muted_users/{}".format(muted_id)
|
||||||
|
result = self.api_delete(user, url)
|
||||||
|
|
||||||
|
mock_date_muted = datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp()
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertNotIn({"id": muted_id, "timestamp": mock_date_muted}, get_user_mutes(user))
|
||||||
|
self.assertFalse(user_is_muted(user, muted_user))
|
|
@ -5,7 +5,7 @@ from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from zerver.lib.actions import do_mute_topic, do_unmute_topic
|
from zerver.lib.actions import do_mute_topic, do_mute_user, do_unmute_topic, do_unmute_user
|
||||||
from zerver.lib.request import REQ, has_request_variables
|
from zerver.lib.request import REQ, has_request_variables
|
||||||
from zerver.lib.response import json_error, json_success
|
from zerver.lib.response import json_error, json_success
|
||||||
from zerver.lib.streams import (
|
from zerver.lib.streams import (
|
||||||
|
@ -16,6 +16,8 @@ from zerver.lib.streams import (
|
||||||
check_for_exactly_one_stream_arg,
|
check_for_exactly_one_stream_arg,
|
||||||
)
|
)
|
||||||
from zerver.lib.topic_mutes import topic_is_muted
|
from zerver.lib.topic_mutes import topic_is_muted
|
||||||
|
from zerver.lib.user_mutes import user_is_muted
|
||||||
|
from zerver.lib.users import access_user_by_id
|
||||||
from zerver.lib.validator import check_int
|
from zerver.lib.validator import check_int
|
||||||
from zerver.models import UserProfile
|
from zerver.models import UserProfile
|
||||||
|
|
||||||
|
@ -85,3 +87,29 @@ def update_muted_topic(
|
||||||
stream_name=stream,
|
stream_name=stream,
|
||||||
topic_name=topic,
|
topic_name=topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mute_user(request: HttpRequest, user_profile: UserProfile, muted_user_id: int) -> HttpResponse:
|
||||||
|
if user_profile.id == muted_user_id:
|
||||||
|
return json_error(_("Cannot mute self"))
|
||||||
|
|
||||||
|
muted_user = access_user_by_id(user_profile, muted_user_id, allow_bots=False, for_admin=False)
|
||||||
|
date_muted = timezone_now()
|
||||||
|
|
||||||
|
if user_is_muted(user_profile, muted_user):
|
||||||
|
return json_error(_("User already muted"))
|
||||||
|
|
||||||
|
do_mute_user(user_profile, muted_user, date_muted)
|
||||||
|
return json_success()
|
||||||
|
|
||||||
|
|
||||||
|
def unmute_user(
|
||||||
|
request: HttpRequest, user_profile: UserProfile, muted_user_id: int
|
||||||
|
) -> HttpResponse:
|
||||||
|
muted_user = access_user_by_id(user_profile, muted_user_id, allow_bots=False, for_admin=False)
|
||||||
|
|
||||||
|
if not user_is_muted(user_profile, muted_user):
|
||||||
|
return json_error(_("User is not muted"))
|
||||||
|
|
||||||
|
do_unmute_user(user_profile, muted_user)
|
||||||
|
return json_success()
|
||||||
|
|
|
@ -80,7 +80,7 @@ from zerver.views.message_flags import (
|
||||||
update_message_flags,
|
update_message_flags,
|
||||||
)
|
)
|
||||||
from zerver.views.message_send import render_message_backend, send_message_backend, zcommand_backend
|
from zerver.views.message_send import render_message_backend, send_message_backend, zcommand_backend
|
||||||
from zerver.views.muting import update_muted_topic
|
from zerver.views.muting import mute_user, unmute_user, update_muted_topic
|
||||||
from zerver.views.portico import (
|
from zerver.views.portico import (
|
||||||
app_download_link_redirect,
|
app_download_link_redirect,
|
||||||
apps_view,
|
apps_view,
|
||||||
|
@ -462,6 +462,7 @@ v1_api_and_json_patterns = [
|
||||||
),
|
),
|
||||||
# muting -> zerver.views.muting
|
# muting -> zerver.views.muting
|
||||||
rest_path("users/me/subscriptions/muted_topics", PATCH=update_muted_topic),
|
rest_path("users/me/subscriptions/muted_topics", PATCH=update_muted_topic),
|
||||||
|
rest_path("users/me/muted_users/<int:muted_user_id>", POST=mute_user, DELETE=unmute_user),
|
||||||
# used to register for an event queue in tornado
|
# used to register for an event queue in tornado
|
||||||
rest_path("register", POST=events_register_backend),
|
rest_path("register", POST=events_register_backend),
|
||||||
# events -> zerver.tornado.views
|
# events -> zerver.tornado.views
|
||||||
|
|
Loading…
Reference in New Issue