From 1c10f9a916d248924dbee77ab36a74ba363c0f7b Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 29 Aug 2025 12:54:02 -0700 Subject: [PATCH 1/9] api: Add realm-level restrict-message-deleting fields to InitialSnapshot --- lib/api/model/initial_snapshot.dart | 32 +++++++++++++++++++++++++++ lib/api/model/initial_snapshot.g.dart | 27 ++++++++++++++++++++++ test/example_data.dart | 10 +++++++++ 3 files changed, 69 insertions(+) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 26e1948d9d..0943e148ba 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -72,6 +72,17 @@ class InitialSnapshot { final List? userTopics; // TODO(server-6) + final GroupSettingValue? realmCanDeleteAnyMessageGroup; // TODO(server-10) + + final GroupSettingValue? realmCanDeleteOwnMessageGroup; // TODO(server-10) + + /// The policy for who can delete their own messages, + /// on supported servers below version 10. + /// + /// Removed in FL 291, so absent in the current API doc; + /// see zulip/zulip@0cd51f2fe. + final RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy; // TODO(server-10) + /// The policy for who can use wildcard mentions in large channels. /// /// Search for "realm_wildcard_mention_policy" in https://zulip.com/api/register-queue. @@ -87,6 +98,8 @@ class InitialSnapshot { /// https://zulip.com/api/roles-and-permissions#determining-if-a-user-is-a-full-member final int realmWaitingPeriodThreshold; + final int? realmMessageContentDeleteLimitSeconds; + final bool realmAllowMessageEditing; final int? realmMessageContentEditLimitSeconds; @@ -158,9 +171,13 @@ class InitialSnapshot { required this.userStatuses, required this.userSettings, required this.userTopics, + required this.realmCanDeleteAnyMessageGroup, + required this.realmCanDeleteOwnMessageGroup, + required this.realmDeleteOwnMessagePolicy, required this.realmWildcardMentionPolicy, required this.realmMandatoryTopics, required this.realmWaitingPeriodThreshold, + required this.realmMessageContentDeleteLimitSeconds, required this.realmAllowMessageEditing, required this.realmMessageContentEditLimitSeconds, required this.realmEnableReadReceipts, @@ -196,6 +213,21 @@ enum RealmWildcardMentionPolicy { int? toJson() => apiValue; } +@JsonEnum(valueField: 'apiValue') +enum RealmDeleteOwnMessagePolicy { + members(apiValue: 1), + admins(apiValue: 2), + fullMembers(apiValue: 3), + moderators(apiValue: 4), + everyone(apiValue: 5); + + const RealmDeleteOwnMessagePolicy({required this.apiValue}); + + final int apiValue; + + int toJson() => apiValue; +} + /// An item in `realm_default_external_accounts`. /// /// For docs, search for "realm_default_external_accounts:" diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index 481b3184b7..4a4d153e2c 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -81,6 +81,18 @@ InitialSnapshot _$InitialSnapshotFromJson( userTopics: (json['user_topics'] as List?) ?.map((e) => UserTopicItem.fromJson(e as Map)) .toList(), + realmCanDeleteAnyMessageGroup: + json['realm_can_delete_any_message_group'] == null + ? null + : GroupSettingValue.fromJson(json['realm_can_delete_any_message_group']), + realmCanDeleteOwnMessageGroup: + json['realm_can_delete_own_message_group'] == null + ? null + : GroupSettingValue.fromJson(json['realm_can_delete_own_message_group']), + realmDeleteOwnMessagePolicy: $enumDecodeNullable( + _$RealmDeleteOwnMessagePolicyEnumMap, + json['realm_delete_own_message_policy'], + ), realmWildcardMentionPolicy: $enumDecode( _$RealmWildcardMentionPolicyEnumMap, json['realm_wildcard_mention_policy'], @@ -88,6 +100,8 @@ InitialSnapshot _$InitialSnapshotFromJson( realmMandatoryTopics: json['realm_mandatory_topics'] as bool, realmWaitingPeriodThreshold: (json['realm_waiting_period_threshold'] as num) .toInt(), + realmMessageContentDeleteLimitSeconds: + (json['realm_message_content_delete_limit_seconds'] as num?)?.toInt(), realmAllowMessageEditing: json['realm_allow_message_editing'] as bool, realmMessageContentEditLimitSeconds: (json['realm_message_content_edit_limit_seconds'] as num?)?.toInt(), @@ -157,9 +171,14 @@ Map _$InitialSnapshotToJson( 'user_status': instance.userStatuses.map((k, e) => MapEntry(k.toString(), e)), 'user_settings': instance.userSettings, 'user_topics': instance.userTopics, + 'realm_can_delete_any_message_group': instance.realmCanDeleteAnyMessageGroup, + 'realm_can_delete_own_message_group': instance.realmCanDeleteOwnMessageGroup, + 'realm_delete_own_message_policy': instance.realmDeleteOwnMessagePolicy, 'realm_wildcard_mention_policy': instance.realmWildcardMentionPolicy, 'realm_mandatory_topics': instance.realmMandatoryTopics, 'realm_waiting_period_threshold': instance.realmWaitingPeriodThreshold, + 'realm_message_content_delete_limit_seconds': + instance.realmMessageContentDeleteLimitSeconds, 'realm_allow_message_editing': instance.realmAllowMessageEditing, 'realm_message_content_edit_limit_seconds': instance.realmMessageContentEditLimitSeconds, @@ -174,6 +193,14 @@ Map _$InitialSnapshotToJson( 'cross_realm_bots': instance.crossRealmBots, }; +const _$RealmDeleteOwnMessagePolicyEnumMap = { + RealmDeleteOwnMessagePolicy.members: 1, + RealmDeleteOwnMessagePolicy.admins: 2, + RealmDeleteOwnMessagePolicy.fullMembers: 3, + RealmDeleteOwnMessagePolicy.moderators: 4, + RealmDeleteOwnMessagePolicy.everyone: 5, +}; + const _$RealmWildcardMentionPolicyEnumMap = { RealmWildcardMentionPolicy.everyone: 1, RealmWildcardMentionPolicy.members: 2, diff --git a/test/example_data.dart b/test/example_data.dart index f079f5d719..6c33a47701 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -1243,9 +1243,13 @@ InitialSnapshot initialSnapshot({ Map? userStatuses, UserSettings? userSettings, List? userTopics, + GroupSettingValue? realmCanDeleteAnyMessageGroup, + GroupSettingValue? realmCanDeleteOwnMessageGroup, + RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy, RealmWildcardMentionPolicy? realmWildcardMentionPolicy, bool? realmMandatoryTopics, int? realmWaitingPeriodThreshold, + int? realmMessageContentDeleteLimitSeconds, bool? realmAllowMessageEditing, int? realmMessageContentEditLimitSeconds, bool? realmEnableReadReceipts, @@ -1291,9 +1295,15 @@ InitialSnapshot initialSnapshot({ presenceEnabled: true, ), userTopics: userTopics, + // no default; allow `null` to simulate servers without this + realmCanDeleteAnyMessageGroup: realmCanDeleteAnyMessageGroup, + // no default; allow `null` to simulate servers without this + realmCanDeleteOwnMessageGroup: realmCanDeleteOwnMessageGroup, + realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy, realmWildcardMentionPolicy: realmWildcardMentionPolicy ?? RealmWildcardMentionPolicy.everyone, realmMandatoryTopics: realmMandatoryTopics ?? true, realmWaitingPeriodThreshold: realmWaitingPeriodThreshold ?? 0, + realmMessageContentDeleteLimitSeconds: realmMessageContentDeleteLimitSeconds, realmAllowMessageEditing: realmAllowMessageEditing ?? true, realmMessageContentEditLimitSeconds: realmMessageContentEditLimitSeconds, realmEnableReadReceipts: realmEnableReadReceipts ?? true, From 1225c09d69d930ed86ea7e8f76104279c6260f97 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 29 Aug 2025 13:03:33 -0700 Subject: [PATCH 2/9] channel: Add per-channel restrict-message-deleting fields --- lib/api/model/events.dart | 2 ++ lib/api/model/events.g.dart | 2 ++ lib/api/model/model.dart | 10 ++++++++++ lib/api/model/model.g.dart | 18 ++++++++++++++++++ lib/model/channel.dart | 4 ++++ test/example_data.dart | 8 ++++++++ 6 files changed, 44 insertions(+) diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart index 2f0f846d49..2adf9250a9 100644 --- a/lib/api/model/events.dart +++ b/lib/api/model/events.dart @@ -675,6 +675,8 @@ class ChannelUpdateEvent extends ChannelEvent { case ChannelPropertyName.channelPostPolicy: return ChannelPostPolicy.fromApiValue(value as int); case ChannelPropertyName.canAddSubscribersGroup: + case ChannelPropertyName.canDeleteAnyMessageGroup: + case ChannelPropertyName.canDeleteOwnMessageGroup: case ChannelPropertyName.canSubscribeGroup: return GroupSettingValue.fromJson(value); case ChannelPropertyName.streamWeeklyTraffic: diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart index 6e7aadf362..c77b8f8ca3 100644 --- a/lib/api/model/events.g.dart +++ b/lib/api/model/events.g.dart @@ -462,6 +462,8 @@ const _$ChannelPropertyNameEnumMap = { ChannelPropertyName.messageRetentionDays: 'message_retention_days', ChannelPropertyName.channelPostPolicy: 'stream_post_policy', ChannelPropertyName.canAddSubscribersGroup: 'can_add_subscribers_group', + ChannelPropertyName.canDeleteAnyMessageGroup: 'can_delete_any_message_group', + ChannelPropertyName.canDeleteOwnMessageGroup: 'can_delete_own_message_group', ChannelPropertyName.canSubscribeGroup: 'can_subscribe_group', ChannelPropertyName.streamWeeklyTraffic: 'stream_weekly_traffic', }; diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index a821474325..5c2be49b7d 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -642,6 +642,8 @@ class ZulipStream { // final bool isAnnouncementOnly; // deprecated for `channelPostPolicy`; ignore GroupSettingValue? canAddSubscribersGroup; // TODO(server-10) + GroupSettingValue? canDeleteAnyMessageGroup; // TODO(server-11) + GroupSettingValue? canDeleteOwnMessageGroup; // TODO(server-11) GroupSettingValue? canSubscribeGroup; // TODO(server-10) // TODO(server-8): added in FL 199, was previously only on [Subscription] objects @@ -660,6 +662,8 @@ class ZulipStream { required this.messageRetentionDays, required this.channelPostPolicy, required this.canAddSubscribersGroup, + required this.canDeleteAnyMessageGroup, + required this.canDeleteOwnMessageGroup, required this.canSubscribeGroup, required this.streamWeeklyTraffic, }); @@ -679,6 +683,8 @@ class ZulipStream { messageRetentionDays: subscription.messageRetentionDays, channelPostPolicy: subscription.channelPostPolicy, canAddSubscribersGroup: subscription.canAddSubscribersGroup, + canDeleteAnyMessageGroup: subscription.canDeleteAnyMessageGroup, + canDeleteOwnMessageGroup: subscription.canDeleteOwnMessageGroup, canSubscribeGroup: subscription.canSubscribeGroup, streamWeeklyTraffic: subscription.streamWeeklyTraffic, ); @@ -711,6 +717,8 @@ enum ChannelPropertyName { @JsonValue('stream_post_policy') channelPostPolicy, canAddSubscribersGroup, + canDeleteAnyMessageGroup, + canDeleteOwnMessageGroup, canSubscribeGroup, streamWeeklyTraffic; @@ -792,6 +800,8 @@ class Subscription extends ZulipStream { required super.messageRetentionDays, required super.channelPostPolicy, required super.canAddSubscribersGroup, + required super.canDeleteAnyMessageGroup, + required super.canDeleteOwnMessageGroup, required super.canSubscribeGroup, required super.streamWeeklyTraffic, required this.desktopNotifications, diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index b5bb32da2a..b5d87ecbdc 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -253,6 +253,12 @@ ZulipStream _$ZulipStreamFromJson(Map json) => ZulipStream( canAddSubscribersGroup: json['can_add_subscribers_group'] == null ? null : GroupSettingValue.fromJson(json['can_add_subscribers_group']), + canDeleteAnyMessageGroup: json['can_delete_any_message_group'] == null + ? null + : GroupSettingValue.fromJson(json['can_delete_any_message_group']), + canDeleteOwnMessageGroup: json['can_delete_own_message_group'] == null + ? null + : GroupSettingValue.fromJson(json['can_delete_own_message_group']), canSubscribeGroup: json['can_subscribe_group'] == null ? null : GroupSettingValue.fromJson(json['can_subscribe_group']), @@ -273,6 +279,8 @@ Map _$ZulipStreamToJson(ZulipStream instance) => 'message_retention_days': instance.messageRetentionDays, 'stream_post_policy': instance.channelPostPolicy, 'can_add_subscribers_group': instance.canAddSubscribersGroup, + 'can_delete_any_message_group': instance.canDeleteAnyMessageGroup, + 'can_delete_own_message_group': instance.canDeleteOwnMessageGroup, 'can_subscribe_group': instance.canSubscribeGroup, 'stream_weekly_traffic': instance.streamWeeklyTraffic, }; @@ -303,6 +311,12 @@ Subscription _$SubscriptionFromJson(Map json) => Subscription( canAddSubscribersGroup: json['can_add_subscribers_group'] == null ? null : GroupSettingValue.fromJson(json['can_add_subscribers_group']), + canDeleteAnyMessageGroup: json['can_delete_any_message_group'] == null + ? null + : GroupSettingValue.fromJson(json['can_delete_any_message_group']), + canDeleteOwnMessageGroup: json['can_delete_own_message_group'] == null + ? null + : GroupSettingValue.fromJson(json['can_delete_own_message_group']), canSubscribeGroup: json['can_subscribe_group'] == null ? null : GroupSettingValue.fromJson(json['can_subscribe_group']), @@ -331,6 +345,8 @@ Map _$SubscriptionToJson(Subscription instance) => 'message_retention_days': instance.messageRetentionDays, 'stream_post_policy': instance.channelPostPolicy, 'can_add_subscribers_group': instance.canAddSubscribersGroup, + 'can_delete_any_message_group': instance.canDeleteAnyMessageGroup, + 'can_delete_own_message_group': instance.canDeleteOwnMessageGroup, 'can_subscribe_group': instance.canSubscribeGroup, 'stream_weekly_traffic': instance.streamWeeklyTraffic, 'desktop_notifications': instance.desktopNotifications, @@ -492,6 +508,8 @@ const _$ChannelPropertyNameEnumMap = { ChannelPropertyName.messageRetentionDays: 'message_retention_days', ChannelPropertyName.channelPostPolicy: 'stream_post_policy', ChannelPropertyName.canAddSubscribersGroup: 'can_add_subscribers_group', + ChannelPropertyName.canDeleteAnyMessageGroup: 'can_delete_any_message_group', + ChannelPropertyName.canDeleteOwnMessageGroup: 'can_delete_own_message_group', ChannelPropertyName.canSubscribeGroup: 'can_subscribe_group', ChannelPropertyName.streamWeeklyTraffic: 'stream_weekly_traffic', }; diff --git a/lib/model/channel.dart b/lib/model/channel.dart index 8ef01054bd..f20fd4586c 100644 --- a/lib/model/channel.dart +++ b/lib/model/channel.dart @@ -378,6 +378,10 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore { stream.channelPostPolicy = event.value as ChannelPostPolicy; case ChannelPropertyName.canAddSubscribersGroup: stream.canAddSubscribersGroup = event.value as GroupSettingValue; + case ChannelPropertyName.canDeleteAnyMessageGroup: + stream.canDeleteAnyMessageGroup = event.value as GroupSettingValue; + case ChannelPropertyName.canDeleteOwnMessageGroup: + stream.canDeleteOwnMessageGroup = event.value as GroupSettingValue; case ChannelPropertyName.canSubscribeGroup: stream.canSubscribeGroup = event.value as GroupSettingValue; case ChannelPropertyName.streamWeeklyTraffic: diff --git a/test/example_data.dart b/test/example_data.dart index 6c33a47701..771559e339 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -449,6 +449,8 @@ ZulipStream stream({ int? messageRetentionDays, ChannelPostPolicy? channelPostPolicy, GroupSettingValue? canAddSubscribersGroup, + GroupSettingValue? canDeleteAnyMessageGroup, + GroupSettingValue? canDeleteOwnMessageGroup, GroupSettingValue? canSubscribeGroup, int? streamWeeklyTraffic, }) { @@ -470,6 +472,8 @@ ZulipStream stream({ messageRetentionDays: messageRetentionDays, channelPostPolicy: channelPostPolicy ?? ChannelPostPolicy.any, canAddSubscribersGroup: canAddSubscribersGroup ?? GroupSettingValueNamed(nobodyGroup.id), + canDeleteAnyMessageGroup: canDeleteAnyMessageGroup ?? GroupSettingValueNamed(nobodyGroup.id), + canDeleteOwnMessageGroup: canDeleteOwnMessageGroup ?? GroupSettingValueNamed(nobodyGroup.id), canSubscribeGroup: canSubscribeGroup ?? GroupSettingValueNamed(nobodyGroup.id), streamWeeklyTraffic: streamWeeklyTraffic, ); @@ -510,6 +514,8 @@ Subscription subscription( messageRetentionDays: stream.messageRetentionDays, channelPostPolicy: stream.channelPostPolicy, canAddSubscribersGroup: stream.canAddSubscribersGroup, + canDeleteAnyMessageGroup: stream.canDeleteAnyMessageGroup, + canDeleteOwnMessageGroup: stream.canDeleteOwnMessageGroup, canSubscribeGroup: stream.canSubscribeGroup, streamWeeklyTraffic: stream.streamWeeklyTraffic, desktopNotifications: desktopNotifications ?? false, @@ -1183,6 +1189,8 @@ ChannelUpdateEvent channelUpdateEvent( case ChannelPropertyName.channelPostPolicy: assert(value is ChannelPostPolicy); case ChannelPropertyName.canAddSubscribersGroup: + case ChannelPropertyName.canDeleteAnyMessageGroup: + case ChannelPropertyName.canDeleteOwnMessageGroup: case ChannelPropertyName.canSubscribeGroup: assert(value is GroupSettingValue); case ChannelPropertyName.streamWeeklyTraffic: From 772b71d8598f237da08f4133fcfaa717acf2f569 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 29 Aug 2025 13:28:06 -0700 Subject: [PATCH 3/9] realm [nfc]: Order updatable realm-setting fields by realm/update_dict doc And leave a comment so we remember to keep this order when adding more fields. --- lib/model/realm.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/model/realm.dart b/lib/model/realm.dart index 02db21c56e..f2156c7759 100644 --- a/lib/model/realm.dart +++ b/lib/model/realm.dart @@ -37,16 +37,18 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { //|////////////////////////////// // Realm settings found in realm/update_dict events: // https://zulip.com/api/get-events#realm-update_dict + // + // In order of appearance in the realm/update_dict event doc. // TODO(#668): update all these realm settings on events. bool get realmAllowMessageEditing; + bool get realmEnableReadReceipts; bool get realmMandatoryTopics; int get maxFileUploadSizeMib; Duration? get realmMessageContentEditLimit => realmMessageContentEditLimitSeconds == null ? null : Duration(seconds: realmMessageContentEditLimitSeconds!); int? get realmMessageContentEditLimitSeconds; - bool get realmEnableReadReceipts; bool get realmPresenceDisabled; int get realmWaitingPeriodThreshold; @@ -146,14 +148,14 @@ mixin ProxyRealmStore on RealmStore { @override bool get realmAllowMessageEditing => realmStore.realmAllowMessageEditing; @override + bool get realmEnableReadReceipts => realmStore.realmEnableReadReceipts; + @override bool get realmMandatoryTopics => realmStore.realmMandatoryTopics; @override int get maxFileUploadSizeMib => realmStore.maxFileUploadSizeMib; @override int? get realmMessageContentEditLimitSeconds => realmStore.realmMessageContentEditLimitSeconds; @override - bool get realmEnableReadReceipts => realmStore.realmEnableReadReceipts; - @override bool get realmPresenceDisabled => realmStore.realmPresenceDisabled; @override int get realmWaitingPeriodThreshold => realmStore.realmWaitingPeriodThreshold; @@ -261,14 +263,14 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { @override final bool realmAllowMessageEditing; @override + final bool realmEnableReadReceipts; + @override final bool realmMandatoryTopics; @override final int maxFileUploadSizeMib; @override final int? realmMessageContentEditLimitSeconds; @override - final bool realmEnableReadReceipts; - @override final bool realmPresenceDisabled; @override final int realmWaitingPeriodThreshold; From dd70226712ecb384572cf3c210c0346525f4761a Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 29 Aug 2025 13:46:44 -0700 Subject: [PATCH 4/9] realm: Add realm-level restrict-message-deleting fields to RealmStore --- lib/model/realm.dart | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/model/realm.dart b/lib/model/realm.dart index f2156c7759..782740405d 100644 --- a/lib/model/realm.dart +++ b/lib/model/realm.dart @@ -42,9 +42,12 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { // TODO(#668): update all these realm settings on events. bool get realmAllowMessageEditing; + GroupSettingValue? get realmCanDeleteAnyMessageGroup; // TODO(server-10) + GroupSettingValue? get realmCanDeleteOwnMessageGroup; // TODO(server-10) bool get realmEnableReadReceipts; bool get realmMandatoryTopics; int get maxFileUploadSizeMib; + int? get realmMessageContentDeleteLimitSeconds; Duration? get realmMessageContentEditLimit => realmMessageContentEditLimitSeconds == null ? null : Duration(seconds: realmMessageContentEditLimitSeconds!); @@ -57,6 +60,7 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { // but now deprecated. RealmWildcardMentionPolicy get realmWildcardMentionPolicy; // TODO(#662): replaced by can_mention_many_users_group + RealmDeleteOwnMessagePolicy? get realmDeleteOwnMessagePolicy; // TODO(server-10) remove //|////////////////////////////// // Realm settings that lack events. @@ -148,12 +152,18 @@ mixin ProxyRealmStore on RealmStore { @override bool get realmAllowMessageEditing => realmStore.realmAllowMessageEditing; @override + GroupSettingValue? get realmCanDeleteAnyMessageGroup => realmStore.realmCanDeleteAnyMessageGroup; + @override + GroupSettingValue? get realmCanDeleteOwnMessageGroup => realmStore.realmCanDeleteOwnMessageGroup; + @override bool get realmEnableReadReceipts => realmStore.realmEnableReadReceipts; @override bool get realmMandatoryTopics => realmStore.realmMandatoryTopics; @override int get maxFileUploadSizeMib => realmStore.maxFileUploadSizeMib; @override + int? get realmMessageContentDeleteLimitSeconds => realmStore.realmMessageContentDeleteLimitSeconds; + @override int? get realmMessageContentEditLimitSeconds => realmStore.realmMessageContentEditLimitSeconds; @override bool get realmPresenceDisabled => realmStore.realmPresenceDisabled; @@ -162,6 +172,8 @@ mixin ProxyRealmStore on RealmStore { @override RealmWildcardMentionPolicy get realmWildcardMentionPolicy => realmStore.realmWildcardMentionPolicy; @override + RealmDeleteOwnMessagePolicy? get realmDeleteOwnMessagePolicy => realmStore.realmDeleteOwnMessagePolicy; + @override String get realmEmptyTopicDisplayName => realmStore.realmEmptyTopicDisplayName; @override Map get realmDefaultExternalAccounts => realmStore.realmDefaultExternalAccounts; @@ -197,13 +209,17 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { serverTypingStoppedWaitPeriodMilliseconds = initialSnapshot.serverTypingStoppedWaitPeriodMilliseconds, serverTypingStartedWaitPeriodMilliseconds = initialSnapshot.serverTypingStartedWaitPeriodMilliseconds, realmAllowMessageEditing = initialSnapshot.realmAllowMessageEditing, + realmCanDeleteAnyMessageGroup = initialSnapshot.realmCanDeleteAnyMessageGroup, + realmCanDeleteOwnMessageGroup = initialSnapshot.realmCanDeleteOwnMessageGroup, realmMandatoryTopics = initialSnapshot.realmMandatoryTopics, maxFileUploadSizeMib = initialSnapshot.maxFileUploadSizeMib, + realmMessageContentDeleteLimitSeconds = initialSnapshot.realmMessageContentDeleteLimitSeconds, realmMessageContentEditLimitSeconds = initialSnapshot.realmMessageContentEditLimitSeconds, realmEnableReadReceipts = initialSnapshot.realmEnableReadReceipts, realmPresenceDisabled = initialSnapshot.realmPresenceDisabled, realmWaitingPeriodThreshold = initialSnapshot.realmWaitingPeriodThreshold, realmWildcardMentionPolicy = initialSnapshot.realmWildcardMentionPolicy, + realmDeleteOwnMessagePolicy = initialSnapshot.realmDeleteOwnMessagePolicy, _realmEmptyTopicDisplayName = initialSnapshot.realmEmptyTopicDisplayName, realmDefaultExternalAccounts = initialSnapshot.realmDefaultExternalAccounts, customProfileFields = _sortCustomProfileFields(initialSnapshot.customProfileFields); @@ -263,12 +279,18 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { @override final bool realmAllowMessageEditing; @override + final GroupSettingValue? realmCanDeleteAnyMessageGroup; + @override + final GroupSettingValue? realmCanDeleteOwnMessageGroup; + @override final bool realmEnableReadReceipts; @override final bool realmMandatoryTopics; @override final int maxFileUploadSizeMib; @override + final int? realmMessageContentDeleteLimitSeconds; + @override final int? realmMessageContentEditLimitSeconds; @override final bool realmPresenceDisabled; @@ -277,6 +299,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { @override final RealmWildcardMentionPolicy realmWildcardMentionPolicy; + @override + final RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy; @override String get realmEmptyTopicDisplayName { From db5f9bdab9b3f2ae12b30c865389aa2bfdb1997e Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 29 Aug 2025 14:42:20 -0700 Subject: [PATCH 5/9] channel: Add isArchived field --- lib/api/model/events.dart | 3 +++ lib/api/model/events.g.dart | 1 + lib/api/model/model.dart | 10 ++++++++++ lib/api/model/model.g.dart | 5 +++++ lib/model/channel.dart | 2 ++ test/example_data.dart | 6 ++++++ 6 files changed, 27 insertions(+) diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart index 2adf9250a9..94bf662e30 100644 --- a/lib/api/model/events.dart +++ b/lib/api/model/events.dart @@ -664,6 +664,9 @@ class ChannelUpdateEvent extends ChannelEvent { final value = json['value']; switch (ChannelPropertyName.fromRawString(json['property'] as String)) { case ChannelPropertyName.name: + return value as String; + case ChannelPropertyName.isArchived: + return value as bool; case ChannelPropertyName.description: return value as String; case ChannelPropertyName.firstMessageId: diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart index c77b8f8ca3..398802892a 100644 --- a/lib/api/model/events.g.dart +++ b/lib/api/model/events.g.dart @@ -456,6 +456,7 @@ Map _$ChannelUpdateEventToJson(ChannelUpdateEvent instance) => const _$ChannelPropertyNameEnumMap = { ChannelPropertyName.name: 'name', + ChannelPropertyName.isArchived: 'is_archived', ChannelPropertyName.description: 'description', ChannelPropertyName.firstMessageId: 'first_message_id', ChannelPropertyName.inviteOnly: 'invite_only', diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index 5c2be49b7d..2a6c9f6aa1 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -627,6 +627,12 @@ class ZulipStream { final int streamId; String name; + + // Servers that don't send this property will only send non-archived channels; + // default to false for those servers. + @JsonKey(defaultValue: false) + bool isArchived; // TODO(server-10) remove default and its comment + String description; String renderedDescription; @@ -652,6 +658,7 @@ class ZulipStream { ZulipStream({ required this.streamId, required this.name, + required this.isArchived, required this.description, required this.renderedDescription, required this.dateCreated, @@ -674,6 +681,7 @@ class ZulipStream { streamId: subscription.streamId, name: subscription.name, description: subscription.description, + isArchived: subscription.isArchived, renderedDescription: subscription.renderedDescription, dateCreated: subscription.dateCreated, firstMessageId: subscription.firstMessageId, @@ -706,6 +714,7 @@ class ZulipStream { enum ChannelPropertyName { // streamId is immutable name, + isArchived, description, // renderedDescription is updated via its own [ChannelUpdateEvent] field // dateCreated is immutable @@ -791,6 +800,7 @@ class Subscription extends ZulipStream { required super.streamId, required super.name, required super.description, + required super.isArchived, required super.renderedDescription, required super.dateCreated, required super.firstMessageId, diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index b5d87ecbdc..b21b2ee29e 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -238,6 +238,7 @@ Map _$SavedSnippetToJson(SavedSnippet instance) => ZulipStream _$ZulipStreamFromJson(Map json) => ZulipStream( streamId: (json['stream_id'] as num).toInt(), name: json['name'] as String, + isArchived: json['is_archived'] as bool? ?? false, description: json['description'] as String, renderedDescription: json['rendered_description'] as String, dateCreated: (json['date_created'] as num).toInt(), @@ -269,6 +270,7 @@ Map _$ZulipStreamToJson(ZulipStream instance) => { 'stream_id': instance.streamId, 'name': instance.name, + 'is_archived': instance.isArchived, 'description': instance.description, 'rendered_description': instance.renderedDescription, 'date_created': instance.dateCreated, @@ -297,6 +299,7 @@ Subscription _$SubscriptionFromJson(Map json) => Subscription( streamId: (json['stream_id'] as num).toInt(), name: json['name'] as String, description: json['description'] as String, + isArchived: json['is_archived'] as bool? ?? false, renderedDescription: json['rendered_description'] as String, dateCreated: (json['date_created'] as num).toInt(), firstMessageId: (json['first_message_id'] as num?)?.toInt(), @@ -335,6 +338,7 @@ Map _$SubscriptionToJson(Subscription instance) => { 'stream_id': instance.streamId, 'name': instance.name, + 'is_archived': instance.isArchived, 'description': instance.description, 'rendered_description': instance.renderedDescription, 'date_created': instance.dateCreated, @@ -502,6 +506,7 @@ const _$PresenceStatusEnumMap = { const _$ChannelPropertyNameEnumMap = { ChannelPropertyName.name: 'name', + ChannelPropertyName.isArchived: 'is_archived', ChannelPropertyName.description: 'description', ChannelPropertyName.firstMessageId: 'first_message_id', ChannelPropertyName.inviteOnly: 'invite_only', diff --git a/lib/model/channel.dart b/lib/model/channel.dart index f20fd4586c..1d856a4a24 100644 --- a/lib/model/channel.dart +++ b/lib/model/channel.dart @@ -366,6 +366,8 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore { stream.name = event.value as String; streamsByName.remove(streamName); streamsByName[stream.name] = stream; + case ChannelPropertyName.isArchived: + stream.isArchived = event.value as bool; case ChannelPropertyName.description: stream.description = event.value as String; case ChannelPropertyName.firstMessageId: diff --git a/test/example_data.dart b/test/example_data.dart index 771559e339..0b24342f6e 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -440,6 +440,7 @@ ZulipStream stream({ int? streamId, String? name, String? description, + bool? isArchived, String? renderedDescription, int? dateCreated, int? firstMessageId, @@ -462,6 +463,7 @@ ZulipStream stream({ return ZulipStream( streamId: effectiveStreamId, name: effectiveName, + isArchived: isArchived ?? false, description: effectiveDescription, renderedDescription: renderedDescription ?? '

$effectiveDescription

', dateCreated: dateCreated ?? 1686774898, @@ -504,6 +506,7 @@ Subscription subscription( return Subscription( streamId: stream.streamId, name: stream.name, + isArchived: stream.isArchived, description: stream.description, renderedDescription: stream.renderedDescription, dateCreated: stream.dateCreated, @@ -1178,6 +1181,9 @@ ChannelUpdateEvent channelUpdateEvent( }) { switch (property) { case ChannelPropertyName.name: + assert(value is String); + case ChannelPropertyName.isArchived: + assert(value is bool); case ChannelPropertyName.description: assert(value is String); case ChannelPropertyName.firstMessageId: From 9c37ed4516bc721915d362f555780ce4120364f3 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 29 Aug 2025 17:34:42 -0700 Subject: [PATCH 6/9] initial_snapshot: Fill in SupportedPermissionSettings.fixture.realm --- lib/api/model/initial_snapshot.dart | 136 +++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 0943e148ba..a8e99dde28 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -457,7 +457,141 @@ class SupportedPermissionSettings { /// or a similar API, and switch to using that. See thread: /// https://chat.zulip.org/#narrow/channel/378-api-design/topic/server_supported_permission_settings/near/2247549 static SupportedPermissionSettings fixture = SupportedPermissionSettings( - realm: {}, // Please go ahead and fill this in when we come to need it. + realm: { + // From the server's Realm.REALM_PERMISSION_GROUP_SETTINGS, + // in zerver/models/realms.py. Current as of 6ab30fcce, 2025-08. + 'create_multiuse_invite_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.ADMINISTRATORS, + ), + 'can_access_all_users_group': PermissionSettingsItem( + // require_system_group=True, + // allow_nobody_group=False, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.EVERYONE, + // # Note that user_can_access_all_other_users in the web + // # app is relying on members always have access. + // allowed_system_groups=[SystemGroups.EVERYONE, SystemGroups.MEMBERS], + ), + 'can_add_subscribers_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_add_custom_emoji_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_create_bots_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_create_groups': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_create_public_channel_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_create_private_channel_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_create_web_public_channel_group': PermissionSettingsItem( + // require_system_group=True, + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.OWNERS, + // allowed_system_groups=[ + // SystemGroups.MODERATORS, + // SystemGroups.ADMINISTRATORS, + // SystemGroups.OWNERS, + // SystemGroups.NOBODY, + // ], + ), + 'can_create_write_only_bots_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_delete_any_message_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.ADMINISTRATORS, + ), + 'can_delete_own_message_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.EVERYONE, + ), + 'can_invite_users_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_manage_all_groups': PermissionSettingsItem( + // allow_nobody_group=False, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.OWNERS, + ), + 'can_manage_billing_group': PermissionSettingsItem( + // allow_nobody_group=False, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.ADMINISTRATORS, + ), + 'can_mention_many_users_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.ADMINISTRATORS, + ), + 'can_move_messages_between_channels_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_move_messages_between_topics_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.EVERYONE, + ), + 'can_resolve_topics_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.EVERYONE, + ), + 'can_set_delete_message_policy_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: false, + // default_group_name=SystemGroups.MODERATORS, + ), + 'can_set_topics_policy_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.MEMBERS, + ), + 'can_summarize_topics_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.EVERYONE, + ), + 'direct_message_initiator_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.EVERYONE, + ), + 'direct_message_permission_group': PermissionSettingsItem( + // allow_nobody_group=True, + allowEveryoneGroup: true, + // default_group_name=SystemGroups.EVERYONE, + ), + }, group: {}, // Please go ahead and fill this in when we come to need it. stream: { // From the server's Stream.stream_permission_group_settings, From f978e51e8e9643d4ceae661a954543cec136c47f Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 5 Sep 2025 14:30:51 -0700 Subject: [PATCH 7/9] channel [nfc]: Add HasChannelStore for other substores to use Like HasUserStore for data about users (added in 623bcb46b), and HasRealmStore for data about the realm, this will help make it convenient for other substores to refer to data about channels. --- lib/model/channel.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/model/channel.dart b/lib/model/channel.dart index 1d856a4a24..ae864e7ed5 100644 --- a/lib/model/channel.dart +++ b/lib/model/channel.dart @@ -16,6 +16,9 @@ import 'user.dart'; /// /// The data structures described here are implemented at [ChannelStoreImpl]. mixin ChannelStore on UserStore { + @protected + UserStore get userStore; + /// All known channels/streams, indexed by [ZulipStream.streamId]. /// /// The same [ZulipStream] objects also appear in [streamsByName]. @@ -248,6 +251,18 @@ mixin ProxyChannelStore on ChannelStore { channelStore.debugTopicVisibility; } +/// A base class for [PerAccountStore] substores +/// that need access to [ChannelStore] as well as to its prerequisites +/// [CorePerAccountStore], [RealmStore], and [UserStore]. +abstract class HasChannelStore extends HasUserStore with ChannelStore, ProxyChannelStore { + HasChannelStore({required ChannelStore channels}) + : channelStore = channels, super(users: channels.userStore); + + @protected + @override + final ChannelStore channelStore; +} + /// The implementation of [ChannelStore] that does the work. /// /// Generally the only code that should need this class is [PerAccountStore] From e5b43df56a3b281e0a764cea632f89b673abd185 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 5 Sep 2025 14:33:34 -0700 Subject: [PATCH 8/9] messages [nfc]: Provide ChannelStore to MessageStore This way, this substore becomes a valid home for methods that need to refer to data about both messages and channels. See also fab85ca1e, where we provided UserStore to ChannelStore. --- lib/model/message.dart | 7 ++++--- lib/model/store.dart | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/model/message.dart b/lib/model/message.dart index 48bdbf5a49..5b538d61b8 100644 --- a/lib/model/message.dart +++ b/lib/model/message.dart @@ -11,6 +11,7 @@ import '../api/model/model.dart'; import '../api/route/messages.dart'; import '../log.dart'; import 'binding.dart'; +import 'channel.dart'; import 'message_list.dart'; import 'realm.dart'; import 'store.dart'; @@ -18,7 +19,7 @@ import 'store.dart'; const _apiSendMessage = sendMessage; // Bit ugly; for alternatives, see: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20PerAccountStore.20methods/near/1545809 /// The portion of [PerAccountStore] for messages and message lists. -mixin MessageStore { +mixin MessageStore on ChannelStore { /// All known messages, indexed by [Message.id]. Map get messages; @@ -138,8 +139,8 @@ class _EditMessageRequestStatus { final String newContent; } -class MessageStoreImpl extends HasRealmStore with MessageStore, _OutboxMessageStore { - MessageStoreImpl({required super.realm}) +class MessageStoreImpl extends HasChannelStore with MessageStore, _OutboxMessageStore { + MessageStoreImpl({required super.channels}) : // There are no messages in InitialSnapshot, so we don't have // a use case for initializing MessageStore with nonempty [messages]. messages = {}; diff --git a/lib/model/store.dart b/lib/model/store.dart index 903d1b94be..a5da6aa3f8 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -537,7 +537,7 @@ class PerAccountStore extends PerAccountStoreBase with presence: Presence(realm: realm, initial: initialSnapshot.presences), channels: channels, - messages: MessageStoreImpl(realm: realm), + messages: MessageStoreImpl(channels: channels), unreads: Unreads(core: core, channelStore: channels, initial: initialSnapshot.unreadMsgs), recentDmConversationsView: RecentDmConversationsView(core: core, From 536cbb7f0198e77895f4609e630a6ea97f2a9fa1 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 29 Aug 2025 16:00:31 -0700 Subject: [PATCH 9/9] message: Implement selfCanDeleteMessage Related: #1548 --- lib/model/message.dart | 117 ++++++++ test/example_data.dart | 3 +- test/model/message_test.dart | 506 +++++++++++++++++++++++++++++++++++ 3 files changed, 625 insertions(+), 1 deletion(-) diff --git a/lib/model/message.dart b/lib/model/message.dart index 5b538d61b8..9cc4d76f48 100644 --- a/lib/model/message.dart +++ b/lib/model/message.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import '../api/exception.dart'; import '../api/model/events.dart'; +import '../api/model/initial_snapshot.dart'; import '../api/model/model.dart'; import '../api/route/messages.dart'; import '../log.dart'; @@ -78,6 +79,122 @@ mixin MessageStore on ChannelStore { /// Should only be called when there is a failed request, /// per [getEditMessageErrorStatus]. ({String originalRawContent, String newContent}) takeFailedMessageEdit(int messageId); + + /// Whether the user has permission to delete a message, as of [atDate]. + /// + /// For a value of [atDate], use [ZulipBinding.instance.utcNow]. + bool selfCanDeleteMessage(int messageId, {required DateTime atDate}) { + // Compare web's message_delete.get_deletability. + + final message = messages[messageId]; + if (message == null) { + assert(false); // TODO(log) + return true; + } + + final ZulipStream? channel; + if (message is StreamMessage) { + channel = streams[message.streamId]; + if (channel == null) { + assert(false); // TODO(log) + return true; + } + } else { + channel = null; + } + + if (channel != null && channel.isArchived) { + return false; + } + + // TODO(#1850) really the default should be `role:administrators`: + // https://github.com/zulip/zulip-flutter/pull/1842#discussion_r2331362461 + if (realmCanDeleteAnyMessageGroup != null + && selfHasPermissionForGroupSetting(realmCanDeleteAnyMessageGroup!, + GroupSettingType.realm, 'can_delete_any_message_group')) { + return true; + } + + if (channel != null) { + if (channel.canDeleteAnyMessageGroup != null + && selfHasPermissionForGroupSetting(channel.canDeleteAnyMessageGroup!, + GroupSettingType.stream, 'can_delete_any_message_group')) { + return true; + } + } + + final sender = getUser(message.senderId); + if (sender == null) return false; + + if (!( + sender.userId == selfUserId + || (sender.isBot && sender.botOwnerId == selfUserId) + )) { + return false; + } + + // Web returns false here for local-echoed message objects; + // that's impossible here because `message` can't be an [OutboxMessage] + // (it's a [Message] from [MessageStore.messages]). + + if (realmCanDeleteOwnMessageGroup != null) { + if (!selfHasPermissionForGroupSetting(realmCanDeleteOwnMessageGroup!, + GroupSettingType.realm, 'can_delete_own_message_group')) { + if (channel == null) { + // i.e. this is a DM + return false; + } + + if ( + channel.canDeleteOwnMessageGroup == null + || !selfHasPermissionForGroupSetting(channel.canDeleteOwnMessageGroup!, + GroupSettingType.stream, 'can_delete_own_message_group') + ) { + return false; + } + } + } else if (realmDeleteOwnMessagePolicy != null) { + if (!_selfPassesLegacyDeleteMessagePolicy(messageId, atDate: atDate)) { + return false; + } + } else { + assert(false); // TODO(log) + return true; + } + + if (realmMessageContentDeleteLimitSeconds == null) { + // i.e., no limit + return true; + } + return atDate.millisecondsSinceEpoch ~/ 1000 - message.timestamp + <= realmMessageContentDeleteLimitSeconds!; + } + + bool _selfPassesLegacyDeleteMessagePolicy(int messageId, {required DateTime atDate}) { + assert(realmDeleteOwnMessagePolicy != null); + final role = selfUser.role; + + // (Could early-return true on [UserRole.unknown], + // but pre-291 servers shouldn't be giving us an unknown role.) + + switch (realmDeleteOwnMessagePolicy!) { + case RealmDeleteOwnMessagePolicy.everyone: + return true; + case RealmDeleteOwnMessagePolicy.members: + return role.isAtLeast(UserRole.member); + case RealmDeleteOwnMessagePolicy.fullMembers: { + if (!role.isAtLeast(UserRole.member)) return false; + if (role == UserRole.member) { + return hasPassedWaitingPeriod(selfUser, byDate: atDate); + } + return true; + } + case RealmDeleteOwnMessagePolicy.moderators: + return role.isAtLeast(UserRole.moderator); + case RealmDeleteOwnMessagePolicy.admins: + return role.isAtLeast(UserRole.administrator); + } + } } mixin ProxyMessageStore on MessageStore { diff --git a/test/example_data.dart b/test/example_data.dart index 0b24342f6e..110a2f0ca8 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -271,6 +271,7 @@ User user({ String? dateJoined, bool? isActive, bool? isBot, + int? botOwnerId, UserRole? role, String? avatarUrl, Map? profileData, @@ -286,7 +287,7 @@ User user({ isActive: isActive ?? true, isBot: isBot ?? false, botType: null, - botOwnerId: null, + botOwnerId: botOwnerId, role: role ?? UserRole.member, timezone: 'UTC', avatarUrl: avatarUrl, diff --git a/test/model/message_test.dart b/test/model/message_test.dart index 79aa441771..7dbf28a47d 100644 --- a/test/model/message_test.dart +++ b/test/model/message_test.dart @@ -9,6 +9,7 @@ import 'package:http/http.dart' as http; import 'package:test/scaffolding.dart'; import 'package:zulip/api/exception.dart'; import 'package:zulip/api/model/events.dart'; +import 'package:zulip/api/model/initial_snapshot.dart'; import 'package:zulip/api/model/model.dart'; import 'package:zulip/api/model/submessage.dart'; import 'package:zulip/api/route/messages.dart'; @@ -864,6 +865,331 @@ void main() { })); }); + group('selfCanDeleteMessage', () { + /// Call the method, with setup from [params]. + Future evaluate(CanDeleteMessageParams params) async { + final selfUser = eg.selfUser; + final botUserOwnedBySelf = eg.user(isBot: true, botOwnerId: selfUser.userId); + final botUserNotOwnedBySelf = eg.user(isBot: true, botOwnerId: eg.otherUser.userId); + + final groupWithSelf = eg.userGroup(members: [selfUser.userId]); + final groupWithoutSelf = eg.userGroup(members: [eg.otherUser.userId]); + final groupSettingWithSelf = GroupSettingValueNamed(groupWithSelf.id); + final groupSettingWithoutSelf = GroupSettingValueNamed(groupWithoutSelf.id); + + final GroupSettingValue? realmCanDeleteAnyMessageGroup; + final GroupSettingValue? realmCanDeleteOwnMessageGroup; + final RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy; + + if (params.inRealmCanDeleteAnyMessageGroup != null) { + realmCanDeleteAnyMessageGroup = params.inRealmCanDeleteAnyMessageGroup! + ? groupSettingWithSelf : groupSettingWithoutSelf; + } else { + realmCanDeleteAnyMessageGroup = null; + } + + if (params.inRealmCanDeleteOwnMessageGroup != null) { + assert(params.inRealmCanDeleteAnyMessageGroup != null); // TODO(server-10) + assert(params.realmDeleteOwnMessagePolicy == null); + realmCanDeleteOwnMessageGroup = params.inRealmCanDeleteOwnMessageGroup! + ? groupSettingWithSelf : groupSettingWithoutSelf; + } else { + realmCanDeleteOwnMessageGroup = null; + } + + if (params.realmDeleteOwnMessagePolicy != null) { + assert(params.inRealmCanDeleteOwnMessageGroup == null); + realmDeleteOwnMessagePolicy = params.realmDeleteOwnMessagePolicy!; + } else { + realmDeleteOwnMessagePolicy = null; + } + + final sender = switch (params.senderConfig) { + CanDeleteMessageSenderConfig.unknown => eg.user(), + CanDeleteMessageSenderConfig.self => selfUser, + CanDeleteMessageSenderConfig.otherHuman => eg.otherUser, + CanDeleteMessageSenderConfig.botOwnedBySelf => botUserOwnedBySelf, + CanDeleteMessageSenderConfig.botNotOwnedBySelf => botUserNotOwnedBySelf, + }; + + final channel = eg.stream(); + + final now = testBinding.utcNow(); + final timestamp = (now.millisecondsSinceEpoch ~/ 1000) - 60; + final Message message; + if (params.isChannelArchived != null) { + // testing with a channel message + message = eg.streamMessage(sender: sender, stream: channel, timestamp: timestamp); + channel.isArchived = params.isChannelArchived!; + if ( + params.inChannelCanDeleteAnyMessageGroup != null + && params.inChannelCanDeleteOwnMessageGroup != null + ) { + channel.canDeleteAnyMessageGroup = params.inChannelCanDeleteAnyMessageGroup! + ? groupSettingWithSelf : groupSettingWithoutSelf; + channel.canDeleteOwnMessageGroup = params.inChannelCanDeleteOwnMessageGroup! + ? groupSettingWithSelf : groupSettingWithoutSelf; + } else { + assert(params.inChannelCanDeleteAnyMessageGroup == null); + assert(params.inChannelCanDeleteOwnMessageGroup == null); + channel.canDeleteAnyMessageGroup = null; + channel.canDeleteOwnMessageGroup = null; + } + } else { + // testing with a DM message + final to = sender == selfUser ? [] : [selfUser]; + message = eg.dmMessage(from: sender, to: to, timestamp: timestamp); + } + + final realmMessageContentDeleteLimitSeconds = switch (params.timeLimitConfig) { + CanDeleteMessageTimeLimitConfig.notLimited => null, + CanDeleteMessageTimeLimitConfig.insideLimit => 24 * 60 * 60, + CanDeleteMessageTimeLimitConfig.outsideLimit => 1, + }; + + final store = eg.store( + selfUser: selfUser, + initialSnapshot: eg.initialSnapshot( + realmUsers: [selfUser, eg.otherUser, botUserOwnedBySelf, botUserNotOwnedBySelf], + streams: [channel], + realmUserGroups: [groupWithSelf, groupWithoutSelf], + realmCanDeleteAnyMessageGroup: realmCanDeleteAnyMessageGroup, + realmCanDeleteOwnMessageGroup: realmCanDeleteOwnMessageGroup, + realmMessageContentDeleteLimitSeconds: realmMessageContentDeleteLimitSeconds, + realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy)); + + await store.addMessage(message); + + return store.selfCanDeleteMessage(message.id, atDate: now); + } + + void doTest(bool expected, CanDeleteMessageParams params) { + test('params: ${params.describe()}', () async { + check(await evaluate(params)).equals(expected); + }); + } + + group('channel message', () { + doTest(true, CanDeleteMessageParams.permissiveForChannelMessageExcept()); + doTest(false, CanDeleteMessageParams.restrictiveForChannelMessageExcept()); + + group('denial conditions', () { + doTest(false, CanDeleteMessageParams.permissiveForChannelMessageExcept( + isChannelArchived: true)); + + doTest(false, CanDeleteMessageParams.permissiveForChannelMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + inChannelCanDeleteAnyMessageGroup: false, + senderConfig: CanDeleteMessageSenderConfig.unknown)); + + doTest(false, CanDeleteMessageParams.permissiveForChannelMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + inChannelCanDeleteAnyMessageGroup: false, + senderConfig: CanDeleteMessageSenderConfig.otherHuman)); + + doTest(false, CanDeleteMessageParams.permissiveForChannelMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + inChannelCanDeleteAnyMessageGroup: false, + senderConfig: CanDeleteMessageSenderConfig.botNotOwnedBySelf)); + + doTest(false, CanDeleteMessageParams.permissiveForChannelMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + inChannelCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: false, + inChannelCanDeleteOwnMessageGroup: false)); + + doTest(false, CanDeleteMessageParams.permissiveForChannelMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + inChannelCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: false, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.outsideLimit)); + + doTest(false, CanDeleteMessageParams.permissiveForChannelMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + inChannelCanDeleteAnyMessageGroup: false, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.outsideLimit)); + }); + + group('approval conditions', () { + doTest(true, CanDeleteMessageParams.restrictiveForChannelMessageExcept( + isChannelArchived: false, + inRealmCanDeleteAnyMessageGroup: true)); + + doTest(true, CanDeleteMessageParams.restrictiveForChannelMessageExcept( + isChannelArchived: false, + inChannelCanDeleteAnyMessageGroup: true)); + + doTest(true, CanDeleteMessageParams.restrictiveForChannelMessageExcept( + isChannelArchived: false, + senderConfig: CanDeleteMessageSenderConfig.self, + inRealmCanDeleteOwnMessageGroup: true, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited)); + + doTest(true, CanDeleteMessageParams.restrictiveForChannelMessageExcept( + isChannelArchived: false, + senderConfig: CanDeleteMessageSenderConfig.botOwnedBySelf, + inChannelCanDeleteOwnMessageGroup: true, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.insideLimit)); + }); + }); + + group('dm message', () { + doTest(true, CanDeleteMessageParams.permissiveForDmMessageExcept()); + doTest(false, CanDeleteMessageParams.restrictiveForDmMessageExcept()); + + group('denial conditions', () { + doTest(false, CanDeleteMessageParams.permissiveForDmMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + senderConfig: CanDeleteMessageSenderConfig.unknown)); + + doTest(false, CanDeleteMessageParams.permissiveForDmMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + senderConfig: CanDeleteMessageSenderConfig.otherHuman)); + + doTest(false, CanDeleteMessageParams.permissiveForDmMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + senderConfig: CanDeleteMessageSenderConfig.botNotOwnedBySelf)); + + doTest(false, CanDeleteMessageParams.permissiveForDmMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: false)); + + doTest(false, CanDeleteMessageParams.permissiveForDmMessageExcept( + inRealmCanDeleteAnyMessageGroup: false, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.outsideLimit)); + }); + + group('approval conditions', () { + doTest(true, CanDeleteMessageParams.restrictiveForDmMessageExcept( + inRealmCanDeleteAnyMessageGroup: true)); + + doTest(true, CanDeleteMessageParams.restrictiveForDmMessageExcept( + senderConfig: CanDeleteMessageSenderConfig.self, + inRealmCanDeleteOwnMessageGroup: true, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited)); + + doTest(true, CanDeleteMessageParams.restrictiveForDmMessageExcept( + senderConfig: CanDeleteMessageSenderConfig.self, + inRealmCanDeleteOwnMessageGroup: true, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.insideLimit)); + }); + }); + + group('legacy behavior', () { + group('pre-407', () { + // The channel-level group permissions don't exist, + // so we act as though they were present and denied, + // notably by not throwing. + + test('denial is not forced just because one of the permissions is absent (the any-message one)', () async { + check(await evaluate( + CanDeleteMessageParams.pre407( + senderConfig: CanDeleteMessageSenderConfig.self, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: true, + isChannelArchived: false, + )))..equals(await evaluate( + CanDeleteMessageParams.modern( + senderConfig: CanDeleteMessageSenderConfig.self, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: true, + isChannelArchived: false, + inChannelCanDeleteAnyMessageGroup: false, + inChannelCanDeleteOwnMessageGroup: false))) + ..isTrue(); + }); + + test('exercise both existence checks', () async { + check(await evaluate( + CanDeleteMessageParams.pre407( + senderConfig: CanDeleteMessageSenderConfig.self, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: false, + isChannelArchived: false, + )))..equals(await evaluate( + CanDeleteMessageParams.modern( + senderConfig: CanDeleteMessageSenderConfig.self, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: false, + isChannelArchived: false, + inChannelCanDeleteAnyMessageGroup: false, + inChannelCanDeleteOwnMessageGroup: false))) + ..isFalse(); + }); + }); + + group('pre-291', () { + // The realm-level can-delete-own-message group permission + // doesn't exist, so we follow realmDeleteOwnMessagePolicy instead, + // and we don't error. + + test('allowed', () async { + check(await evaluate( + CanDeleteMessageParams.pre291( + senderConfig: CanDeleteMessageSenderConfig.self, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + isChannelArchived: false, + realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone, + ))) + ..equals(await evaluate( + CanDeleteMessageParams.pre407( + senderConfig: CanDeleteMessageSenderConfig.self, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: true, + isChannelArchived: false))) + ..isTrue(); + }); + + test('denied', () async { + check(await evaluate( + CanDeleteMessageParams.pre291( + senderConfig: CanDeleteMessageSenderConfig.self, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + isChannelArchived: false, + realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.admins, + )))..equals(await evaluate( + CanDeleteMessageParams.pre407( + senderConfig: CanDeleteMessageSenderConfig.self, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + inRealmCanDeleteOwnMessageGroup: false, + isChannelArchived: false))) + ..isFalse(); + }); + }); + + group('pre-281', () { + // The realm-level can-delete-any-message permission + // doesn't exist, so we act as though that's present and denied, + // notably by not throwing. + + test('denied', () async { + check(await evaluate( + CanDeleteMessageParams.pre281( + senderConfig: CanDeleteMessageSenderConfig.otherHuman, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + isChannelArchived: false, + realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone, + )))..equals(await evaluate( + CanDeleteMessageParams.pre291( + senderConfig: CanDeleteMessageSenderConfig.otherHuman, + timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: false, + isChannelArchived: false, + realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone))) + ..isFalse(); + }); + }); + }); + }); + group('handleMessageEvent', () { test('from empty', () async { await prepare(); @@ -1668,3 +1994,183 @@ void main() { }); }); } + +/// Params for testing the logic for +/// whether the self-user has permission to delete a message. +class CanDeleteMessageParams { + final CanDeleteMessageSenderConfig senderConfig; + final CanDeleteMessageTimeLimitConfig timeLimitConfig; + final bool? inRealmCanDeleteAnyMessageGroup; + final bool? inRealmCanDeleteOwnMessageGroup; + final bool? isChannelArchived; + final bool? inChannelCanDeleteAnyMessageGroup; + final bool? inChannelCanDeleteOwnMessageGroup; + final RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy; + + CanDeleteMessageParams._({ + required this.senderConfig, + required this.timeLimitConfig, + required this.inRealmCanDeleteAnyMessageGroup, + required this.inRealmCanDeleteOwnMessageGroup, + required this.isChannelArchived, + required this.inChannelCanDeleteAnyMessageGroup, + required this.inChannelCanDeleteOwnMessageGroup, + required this.realmDeleteOwnMessagePolicy, + }); + + CanDeleteMessageParams.modern({ + required this.senderConfig, + required this.timeLimitConfig, + required this.inRealmCanDeleteAnyMessageGroup, + required this.inRealmCanDeleteOwnMessageGroup, + required this.isChannelArchived, + required this.inChannelCanDeleteAnyMessageGroup, + required this.inChannelCanDeleteOwnMessageGroup, + }) : realmDeleteOwnMessagePolicy = null; + + factory CanDeleteMessageParams.restrictiveForChannelMessageExcept({ + CanDeleteMessageSenderConfig? senderConfig, + CanDeleteMessageTimeLimitConfig? timeLimitConfig, + bool? inRealmCanDeleteAnyMessageGroup, + bool? inRealmCanDeleteOwnMessageGroup, + bool? isChannelArchived, + bool? inChannelCanDeleteAnyMessageGroup, + bool? inChannelCanDeleteOwnMessageGroup, + }) => CanDeleteMessageParams.modern( + senderConfig: senderConfig ?? CanDeleteMessageSenderConfig.unknown, + timeLimitConfig: timeLimitConfig ?? CanDeleteMessageTimeLimitConfig.outsideLimit, + inRealmCanDeleteAnyMessageGroup: inRealmCanDeleteAnyMessageGroup ?? false, + inRealmCanDeleteOwnMessageGroup: inRealmCanDeleteOwnMessageGroup ?? false, + isChannelArchived: isChannelArchived ?? true, + inChannelCanDeleteAnyMessageGroup: inChannelCanDeleteAnyMessageGroup ?? false, + inChannelCanDeleteOwnMessageGroup: inChannelCanDeleteOwnMessageGroup ?? false, + ); + + factory CanDeleteMessageParams.permissiveForChannelMessageExcept({ + CanDeleteMessageSenderConfig? senderConfig, + CanDeleteMessageTimeLimitConfig? timeLimitConfig, + bool? inRealmCanDeleteAnyMessageGroup, + bool? inRealmCanDeleteOwnMessageGroup, + bool? isChannelArchived, + bool? inChannelCanDeleteAnyMessageGroup, + bool? inChannelCanDeleteOwnMessageGroup, + }) => CanDeleteMessageParams.modern( + senderConfig: senderConfig ?? CanDeleteMessageSenderConfig.self, + timeLimitConfig: timeLimitConfig ?? CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: inRealmCanDeleteAnyMessageGroup ?? true, + inRealmCanDeleteOwnMessageGroup: inRealmCanDeleteOwnMessageGroup ?? true, + isChannelArchived: isChannelArchived ?? false, + inChannelCanDeleteAnyMessageGroup: inChannelCanDeleteAnyMessageGroup ?? true, + inChannelCanDeleteOwnMessageGroup: inChannelCanDeleteOwnMessageGroup ?? true, + ); + + factory CanDeleteMessageParams.restrictiveForDmMessageExcept({ + CanDeleteMessageSenderConfig? senderConfig, + CanDeleteMessageTimeLimitConfig? timeLimitConfig, + bool? inRealmCanDeleteAnyMessageGroup, + bool? inRealmCanDeleteOwnMessageGroup, + }) => CanDeleteMessageParams.modern( + senderConfig: senderConfig ?? CanDeleteMessageSenderConfig.unknown, + timeLimitConfig: timeLimitConfig ?? CanDeleteMessageTimeLimitConfig.outsideLimit, + inRealmCanDeleteAnyMessageGroup: inRealmCanDeleteAnyMessageGroup ?? false, + inRealmCanDeleteOwnMessageGroup: inRealmCanDeleteOwnMessageGroup ?? false, + isChannelArchived: null, + inChannelCanDeleteAnyMessageGroup: null, + inChannelCanDeleteOwnMessageGroup: null, + ); + + factory CanDeleteMessageParams.permissiveForDmMessageExcept({ + CanDeleteMessageSenderConfig? senderConfig, + CanDeleteMessageTimeLimitConfig? timeLimitConfig, + bool? inRealmCanDeleteAnyMessageGroup, + bool? inRealmCanDeleteOwnMessageGroup, + }) => CanDeleteMessageParams.modern( + senderConfig: senderConfig ?? CanDeleteMessageSenderConfig.self, + timeLimitConfig: timeLimitConfig ?? CanDeleteMessageTimeLimitConfig.notLimited, + inRealmCanDeleteAnyMessageGroup: inRealmCanDeleteAnyMessageGroup ?? true, + inRealmCanDeleteOwnMessageGroup: inRealmCanDeleteOwnMessageGroup ?? true, + isChannelArchived: null, + inChannelCanDeleteAnyMessageGroup: null, + inChannelCanDeleteOwnMessageGroup: null, + ); + + // TODO(server-11) delete + factory CanDeleteMessageParams.pre407({ + required CanDeleteMessageSenderConfig senderConfig, + required CanDeleteMessageTimeLimitConfig timeLimitConfig, + required bool inRealmCanDeleteAnyMessageGroup, + required bool inRealmCanDeleteOwnMessageGroup, + required bool? isChannelArchived, + }) => CanDeleteMessageParams._( + senderConfig: senderConfig, + timeLimitConfig: timeLimitConfig, + inRealmCanDeleteAnyMessageGroup: inRealmCanDeleteAnyMessageGroup, + inRealmCanDeleteOwnMessageGroup: inRealmCanDeleteOwnMessageGroup, + isChannelArchived: isChannelArchived, + inChannelCanDeleteAnyMessageGroup: null, + inChannelCanDeleteOwnMessageGroup: null, + realmDeleteOwnMessagePolicy: null, + ); + + // TODO(server-10) delete + factory CanDeleteMessageParams.pre291({ + required CanDeleteMessageSenderConfig senderConfig, + required CanDeleteMessageTimeLimitConfig timeLimitConfig, + required bool inRealmCanDeleteAnyMessageGroup, + required bool? isChannelArchived, + required RealmDeleteOwnMessagePolicy realmDeleteOwnMessagePolicy, + }) => CanDeleteMessageParams._( + senderConfig: senderConfig, + timeLimitConfig: timeLimitConfig, + inRealmCanDeleteAnyMessageGroup: inRealmCanDeleteAnyMessageGroup, + inRealmCanDeleteOwnMessageGroup: null, + isChannelArchived: isChannelArchived, + inChannelCanDeleteAnyMessageGroup: null, + inChannelCanDeleteOwnMessageGroup: null, + realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy, + ); + + // TODO(server-10) delete + factory CanDeleteMessageParams.pre281({ + required CanDeleteMessageSenderConfig senderConfig, + required CanDeleteMessageTimeLimitConfig timeLimitConfig, + required bool? isChannelArchived, + required RealmDeleteOwnMessagePolicy realmDeleteOwnMessagePolicy, + }) => CanDeleteMessageParams._( + senderConfig: senderConfig, + timeLimitConfig: timeLimitConfig, + inRealmCanDeleteAnyMessageGroup: null, + inRealmCanDeleteOwnMessageGroup: null, + isChannelArchived: isChannelArchived, + inChannelCanDeleteAnyMessageGroup: null, + inChannelCanDeleteOwnMessageGroup: null, + realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy, + ); + + String describe() { + return [ + 'sender: ${senderConfig.name}', + 'time limit: ${timeLimitConfig.name}', + 'in realmCanDeleteAnyMessageGroup?: ${inRealmCanDeleteAnyMessageGroup ?? 'N/A'}', + 'in realmCanDeleteOwnMessageGroup?: ${inRealmCanDeleteOwnMessageGroup ?? 'N/A'}', + 'channel is archived?: ${isChannelArchived ?? 'N/A'}', + 'in channel.canDeleteAnyMessageGroup?: ${inChannelCanDeleteAnyMessageGroup ?? 'N/A'}', + 'in channel.canDeleteOwnMessageGroup?: ${inChannelCanDeleteOwnMessageGroup ?? 'N/A'}', + 'realmDeleteOwnMessagePolicy: ${realmDeleteOwnMessagePolicy ?? 'N/A'}', + ].join(', '); + } +} + +enum CanDeleteMessageSenderConfig { + unknown, + self, + otherHuman, + botOwnedBySelf, + botNotOwnedBySelf, +} + +enum CanDeleteMessageTimeLimitConfig { + notLimited, + insideLimit, + outsideLimit, +}