From 9fb8d844a8dfff67e010b714798a5a37f791904d Mon Sep 17 00:00:00 2001 From: Daksh Date: Fri, 6 Mar 2026 15:41:31 +0100 Subject: [PATCH] Add legacy documentation snapshot from getstream.io/chat/docs Extracted from the Stream Chat documentation site, containing all server-side SDK documentation with code examples for this SDK. Co-Authored-By: Claude Opus 4.6 --- docs/app_and_channel_settings/app_settings.md | 143 ++ .../channel-level_settings.md | 66 + .../app_and_channel_settings/channel_types.md | 182 ++ .../chat_permission_policies.md | 247 +++ .../multi_tenant_chat.md | 444 +++++ .../permissions_reference.md | 797 +++++++++ docs/best_practices/best_practices.md | 27 + docs/best_practices/gdpr.md | 121 ++ .../livestream_best_practices.md | 103 ++ .../marketplace_best_practices.md | 89 + docs/best_practices/moderation.md | 319 ++++ docs/best_practices/query_syntax_operators.md | 20 + docs/channels/channel_management/archiving.md | 26 + .../channel_management/batch-updates.md | 186 +++ .../channel_management/channel_invites.md | 47 + docs/channels/channel_management/deleting.md | 37 + docs/channels/channel_management/disabling.md | 25 + docs/channels/channel_management/freezing.md | 31 + docs/channels/channel_management/hiding.md | 18 + docs/channels/channel_management/muting.md | 32 + docs/channels/channel_management/overview.md | 51 + docs/channels/channel_management/pinning.md | 33 + .../channels/channel_management/truncating.md | 33 + docs/channels/channel_members.md | 105 ++ docs/channels/channel_pagination.md | 57 + docs/channels/channel_update.md | 49 + docs/channels/creating_channels.md | 54 + docs/channels/query_channels.md | 443 +++++ docs/channels/query_members.md | 90 + docs/debugging_and_cli/api_budget.md | 422 +++++ docs/debugging_and_cli/api_errors_response.md | 50 + docs/debugging_and_cli/cli_introduction.md | 136 ++ docs/debugging_and_cli/datadog_integration.md | 34 + .../push_-_common_issues_and_faq.md | 117 ++ docs/debugging_and_cli/rate_limits.md | 154 ++ docs/features/advanced/audit_logs.md | 94 ++ docs/features/advanced/drafts.md | 90 + .../features/advanced/dynamic_partitioning.md | 59 + docs/features/advanced/pending_messages.md | 137 ++ docs/features/advanced/private_messaging.md | 61 + .../advanced/slow_mode_and_throttling.md | 53 + .../advanced/user_average_response_time.md | 36 + docs/features/campaign_api.md | 561 +++++++ docs/features/events.md | 145 ++ docs/features/location_sharing.md | 120 ++ docs/features/overview.md | 35 + docs/features/polls_api.md | 602 +++++++ docs/features/presence_format.md | 32 + docs/features/translation.md | 158 ++ docs/features/typing_indicators.md | 36 + docs/features/unread.md | 92 + docs/init_and_users/authless_users.md | 38 + docs/init_and_users/init_and_users.md | 40 + .../tokens_and_authentication.md | 128 ++ docs/init_and_users/update_users.md | 208 +++ docs/messages/file_uploads.md | 93 ++ docs/messages/message_receipts.md | 101 ++ docs/messages/message_reminders.md | 145 ++ docs/messages/pinned_messages.md | 42 + docs/messages/search.md | 80 + docs/messages/send_message.md | 391 +++++ docs/messages/send_reaction.md | 151 ++ docs/messages/silent_messages.md | 47 + docs/messages/threads.md | 230 +++ docs/messages/unread_reminders.md | 119 ++ docs/migrating/exporting_channels.md | 93 ++ docs/migrating/import.md | 500 ++++++ docs/migrating/migration_guide.md | 216 +++ docs/push/legacy_push_system.md | 41 + docs/push/push_introduction.md | 127 ++ docs/push/push_preferences.md | 75 + docs/push/push_providers_and_multi_bundle.md | 46 + docs/push/push_template.md | 391 +++++ docs/push/push_test.md | 76 + docs/push/registering_push_devices.md | 55 + .../quick_start/architecture_and_benchmark.md | 87 + docs/quick_start/backend_quickstart.md | 149 ++ docs/quick_start/roadmap_and_changelog.md | 33 + docs/webhooks/sns.md | 83 + docs/webhooks/sqs.md | 178 ++ docs/webhooks/webhook_events.md | 1484 +++++++++++++++++ .../before_message_send_webhook.md | 164 ++ .../custom_commands_webhook.md | 232 +++ .../webhooks_overview/webhooks_overview.md | 258 +++ 84 files changed, 13200 insertions(+) create mode 100644 docs/app_and_channel_settings/app_settings.md create mode 100644 docs/app_and_channel_settings/channel-level_settings.md create mode 100644 docs/app_and_channel_settings/channel_types.md create mode 100644 docs/app_and_channel_settings/chat_permission_policies.md create mode 100644 docs/app_and_channel_settings/multi_tenant_chat.md create mode 100644 docs/app_and_channel_settings/permissions_reference.md create mode 100644 docs/best_practices/best_practices.md create mode 100644 docs/best_practices/gdpr.md create mode 100644 docs/best_practices/livestream_best_practices.md create mode 100644 docs/best_practices/marketplace_best_practices.md create mode 100644 docs/best_practices/moderation.md create mode 100644 docs/best_practices/query_syntax_operators.md create mode 100644 docs/channels/channel_management/archiving.md create mode 100644 docs/channels/channel_management/batch-updates.md create mode 100644 docs/channels/channel_management/channel_invites.md create mode 100644 docs/channels/channel_management/deleting.md create mode 100644 docs/channels/channel_management/disabling.md create mode 100644 docs/channels/channel_management/freezing.md create mode 100644 docs/channels/channel_management/hiding.md create mode 100644 docs/channels/channel_management/muting.md create mode 100644 docs/channels/channel_management/overview.md create mode 100644 docs/channels/channel_management/pinning.md create mode 100644 docs/channels/channel_management/truncating.md create mode 100644 docs/channels/channel_members.md create mode 100644 docs/channels/channel_pagination.md create mode 100644 docs/channels/channel_update.md create mode 100644 docs/channels/creating_channels.md create mode 100644 docs/channels/query_channels.md create mode 100644 docs/channels/query_members.md create mode 100644 docs/debugging_and_cli/api_budget.md create mode 100644 docs/debugging_and_cli/api_errors_response.md create mode 100644 docs/debugging_and_cli/cli_introduction.md create mode 100644 docs/debugging_and_cli/datadog_integration.md create mode 100644 docs/debugging_and_cli/push_-_common_issues_and_faq.md create mode 100644 docs/debugging_and_cli/rate_limits.md create mode 100644 docs/features/advanced/audit_logs.md create mode 100644 docs/features/advanced/drafts.md create mode 100644 docs/features/advanced/dynamic_partitioning.md create mode 100644 docs/features/advanced/pending_messages.md create mode 100644 docs/features/advanced/private_messaging.md create mode 100644 docs/features/advanced/slow_mode_and_throttling.md create mode 100644 docs/features/advanced/user_average_response_time.md create mode 100644 docs/features/campaign_api.md create mode 100644 docs/features/events.md create mode 100644 docs/features/location_sharing.md create mode 100644 docs/features/overview.md create mode 100644 docs/features/polls_api.md create mode 100644 docs/features/presence_format.md create mode 100644 docs/features/translation.md create mode 100644 docs/features/typing_indicators.md create mode 100644 docs/features/unread.md create mode 100644 docs/init_and_users/authless_users.md create mode 100644 docs/init_and_users/init_and_users.md create mode 100644 docs/init_and_users/tokens_and_authentication.md create mode 100644 docs/init_and_users/update_users.md create mode 100644 docs/messages/file_uploads.md create mode 100644 docs/messages/message_receipts.md create mode 100644 docs/messages/message_reminders.md create mode 100644 docs/messages/pinned_messages.md create mode 100644 docs/messages/search.md create mode 100644 docs/messages/send_message.md create mode 100644 docs/messages/send_reaction.md create mode 100644 docs/messages/silent_messages.md create mode 100644 docs/messages/threads.md create mode 100644 docs/messages/unread_reminders.md create mode 100644 docs/migrating/exporting_channels.md create mode 100644 docs/migrating/import.md create mode 100644 docs/migrating/migration_guide.md create mode 100644 docs/push/legacy_push_system.md create mode 100644 docs/push/push_introduction.md create mode 100644 docs/push/push_preferences.md create mode 100644 docs/push/push_providers_and_multi_bundle.md create mode 100644 docs/push/push_template.md create mode 100644 docs/push/push_test.md create mode 100644 docs/push/registering_push_devices.md create mode 100644 docs/quick_start/architecture_and_benchmark.md create mode 100644 docs/quick_start/backend_quickstart.md create mode 100644 docs/quick_start/roadmap_and_changelog.md create mode 100644 docs/webhooks/sns.md create mode 100644 docs/webhooks/sqs.md create mode 100644 docs/webhooks/webhook_events.md create mode 100644 docs/webhooks/webhooks_overview/before_message_send_webhook.md create mode 100644 docs/webhooks/webhooks_overview/custom_commands_webhook.md create mode 100644 docs/webhooks/webhooks_overview/webhooks_overview.md diff --git a/docs/app_and_channel_settings/app_settings.md b/docs/app_and_channel_settings/app_settings.md new file mode 100644 index 0000000..173363f --- /dev/null +++ b/docs/app_and_channel_settings/app_settings.md @@ -0,0 +1,143 @@ +Application level settings control + +- The primary storage location +- Production & Development modes +- Authentication behaviour +- Push providers +- Action handlers webhooks +- CDN settings + +See also the [User Average Response Time](/chat/docs/python/user_average_response_time/) feature for tracking user responsiveness. + +The easiest way to edit this is the dashboard. The docs below show how to change it with the API. + +### Edge network & Storage Location + +Stream runs an edge network of servers around the world. This ensures that when users connect the chat loads quickly. +We also support offline storage & optimistic UI updates in all SDKs, ensuring a fast user experience. + +At the app level you can control the primary region. This is where your data is stored. +Connections always happen at the edge, but data is stored in this primary region. + +### Production & Development Mode + +Stream apps can be configured to be either in `development mode` or in `production mode`. You can select which mode your app should be in when you create it in the Stream dashboard, and you can easily switch between modes later on. + +When your app is in production mode, certain destructive features in the dashboard are disabled. This prevents you from accidentally deleting user data or disabling mission-critical features. + +### Authentication Behaviour + +Application level settings allow you to configure settings that impact all the channel types in your app. Our backend SDKs make it easy to change the app settings. You can also change most of these using the CLI or the dashboard. Here's an example on changing the disable_auth_checks setting: + +```python +# disable auth checks, allows dev token usage +client.update_app_settings(disable_auth_checks=True) + +# re-enable auth checks +client.update_app_settings(disable_auth_checks=False) +``` + +These 2 settings are important. Never run with disable_auth_checks or disable_permission_checks in production. + +| NAME | DESCRIPTION | DEFAULT | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| disable_auth_checks | Disabled authentication. Convenient during testing and allows you to use devTokens on the client side. Should not be used in production. | false | +| disable_permissions_checks | Gives all users full permissions to edit messages, delete them etc. Again not recommended in a production setting, only useful for development. | false | + +## Other app settings + +### Push + +| NAME | DESCRIPTION | DEFAULT | +| --------------- | ----------------------------------------------------------------------------------------- | ------- | +| apn_config | APN config object. See [details](/chat/docs/python#settings-updateapp-request). | | +| firebase_config | Firebase config object. See [details](/chat/docs/python#settings-updateapp-request). | | +| huawei_config | Huawei config object. See [details](/chat/docs/python#settings-updateapp-request). | | +| xiaomi_config | Xiaomi config object. See [details](/chat/docs/python#settings-updateapp-request). | | +| push_config | Global config object. See [details](/chat/docs/python#settings-updateapp-request). | | + +### CDN + +| NAME | DESCRIPTION | DEFAULT | +| ---------------------- | ------------------------------------------------------------------------------------------ | ----------------- | +| cdn_expiration_seconds | CDN URL expiration time. See [details](/chat/docs/python#settings-updateapp-request). | 1209600 (14 days) | + +### Hooks + +#### Custom Action Handler and Before Message Send Webhooks + +| NAME | DESCRIPTION | DEFAULT | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------- | ------- | +| custom_action_handler_url | This webhook reacts to custom /slash commands and actions on those commands/ | - | +| before_message_send_hook_url | This webhook allows you to modify or moderate message content before sending it to the chat for everyone to see | - | + +### Webhooks, SQS, SNS, and pending messages + +Webhooks, SQS, SNS, and pending messages async moderation now use the `event_hooks` array configuration. See the [Webhooks](/chat/docs/python/webhooks_overview/) documentation for complete details. + +### Moderation & Translation + +The following settings allow you to control moderation for your chat: + +| NAME | DESCRIPTION | DEFAULT | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| image_moderation_labels | Moderation scores returned from the external image moderation API | - | +| image_moderation_enabled | If image moderation AI should be turned on | - | +| enforce_unique_usernames | If Stream should enforce username uniqueness. This prevents people from joining the chat as "elonmusk" while "elonmusk" is presenting. | - | +| auto_translation_enabled | If Stream should automatically translate messages | - | +| async_url_enrich_enabled | If url enrichment should be done async. It will trigger message.updated event | - | + +### File Uploads + +You can set restrictions on file uploads by including a `file_upload_config` object. You can set either an inclusive list using `allowed_file_extensions` and `allowed_mime_types` or an exclusive list using `blocked_file_extensions` and `blocked_mime_types` . + +The `file_upload_config` object accepts the following fields: + +| NAME | DESCRIPTION | Example | Default | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------- | +| allowed_file_extensions | An array of file types that the user can submit. Files with an extension that does not match the values in this array will be rejected. | [".tar", ".png", ".jpg"] | - | +| blocked_file_extensions | An array of file types that the user can submit. Files with an extension that does not match the values in this array will be rejected. | [".tar", ".png", ".jpg"] | - | +| allowed_mime_types | An array of file MIME types that the user can submit. Files with an MIME type that does not match the values in this array will be rejected. Must follow the type/ subtype pattern. | ["text/css", "text/plain", "image/png"] | - | +| blocked_mime_types | An array of file types that the user can submit. Files with an MIME type that does not match the values in this array will be rejected. Must follow the type/ subtype pattern. | ["text/css", "text/plain", "image/png"] | - | +| size_limit | A number that represents the maximum accepted file size in bytes. In case its 0 the default maximum is used. | 10485760 | 0 | + +For example, the following code shows how to block all attempts to upload any files that are not .csv: + +```python +# Only accept .CSV files + +client.update_app_settings({ + "file_upload_config": { + "allowed_file_extensions": [".csv"], + "allowed_mime_types": ["text/csv"], +}}) +``` + +### Image Uploads + +You can set restrictions on file uploads by including an `image_upload_config` object. You can set either an inclusive list using `allowed_file_extensions` and `allowed_mime_types` or an exclusive list using `blocked_file_extensions` and `blocked_mime_types` . + +The `image_upload_config` object accepts the following fields: + +| NAME | DESCRIPTION | Example | Default | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------- | +| allowed_file_extensions | An array of file types that the user can submit. Files with an extension that does not match the values in this array will be rejected. | [".gif", ".png", ".jpg"] | - | +| blocked_file_extensions | An array of file types that the user can submit. Files with an extension that does not match the values in this array will be rejected. | [".tar", ".tiff", ".jpg"] | - | +| allowed_mime_types | An array of file MIME types that the user can submit. Files with an MIME type that does not match the values in this array will be rejected. Must follow the type/ subtype pattern. | ["image/jpeg", "image/svg+xml", "image/png"] | - | +| blocked_mime_types | An array of file types that the user can submit. Files with an MIME type that does not match the values in this array will be rejected. Must follow the type/ subtype pattern. | ["text/css", "text/plain", "image/tiff"] | - | +| size_limit | A number that represents the maximum accepted file size in bytes. In case its 0 the default maximum is used. | 10485760 | 0 | + +For example, the following code shows how to block all attempts to upload any files that are not gif, jpeg, or png files: + +```python +# Only accept gif, jpeg, or png files. + +client.update_app_settings({ + "image_upload_config": { + "allowed_file_extensions": [".gif", ".jpeg", ".png"], + "allowed_mime_types": ["image/gif", "image/jpeg", "image/png"], +}}) +``` + +> [!NOTE] +> Stream allowed types for images are: `image/bmp` , `image/gif` , `image/jpeg` , `image/png` , `image/webp` , `image/heic` , `image/heic-sequence` , `image/heif` , `image/heif-sequence` , `image/svg+xml` . Applications can set a more restrictive list, but would not be allowed to set a less restrictive list. diff --git a/docs/app_and_channel_settings/channel-level_settings.md b/docs/app_and_channel_settings/channel-level_settings.md new file mode 100644 index 0000000..69d23eb --- /dev/null +++ b/docs/app_and_channel_settings/channel-level_settings.md @@ -0,0 +1,66 @@ +In most cases, you define your channel settings and features at the [Channel Type](/chat/docs/python/channel_features/) level and then have these inherited by all the channels of the same type. For instance you can configure [Livestream](/chat/docs/python/channel_features/) channels without typing events and enable reactions and replies for all [Messaging](/chat/docs/python/channel_features/) type channels. + +This approach does not work well if your application has many different combination of settings for channels and that is when channel-level settings can be useful. + +Channel-level settings allow you to override one or more settings for a channel without changing other channels of the same type. A few important things to mention about channel-level settings: + +1. Settings that are not overridden at channel level will use the current setting from the channel type + +2. Changing channel-level settings can only be done server-side + +### List of settings that can be overridden + +Not all channel type settings can be configured at the channel level, here is the complete list of settings that can be overridden. + +- **typing_events** : Controls if typing indicators are shown. + +- **reactions** : Controls if users are allowed to add reactions to messages. + +- **replies** : Enables message threads and replies. + +- **uploads** : Allows image and file uploads within messages. + +- **url_enrichment** : When enabled, messages containing URLs will be enriched automatically with image and text related to the message. + +- **commands** : Enable a set of commands for this channel. + +- **max_message_length** : The max message length. + +- **blocklist** : A list of words you can define to moderate chat messages. More information can be found [here](/moderation/docs/engines/blocklists-and-regex-filters/). + +- **blocklist_behavior** : set as  `block`  or  `flag` to determine what happens to a message containing blocked words. + +- **grants** : Allows to modify channel-type permission grants for particular channel. More information can be found [here](/chat/docs/python/chat_permission_policies/) + +- **user_message_reminders** : Allow users to set reminders for messages. More information can be found [here](/chat/docs/python/message_reminders/) + +- **shared_locations** : Allow users to share their current location. More information can be found [here](/chat/docs/python/location_sharing/) + +- **count_messages** : Enables counting messages on new channels. + +### Examples + +#### Use a different blocklist + +```python +channel.update_partial( + { + "config_overrides": { + "blocklist": "medical_blocklist", + "blocklist_behavior": "block", + }, + } +) +``` + +#### Disables replies + +```python +channel.update_partial({"config_overrides": {"replies": False}}) +``` + +#### Remove overrides and go back to default settings + +```python +channel.update_partial({"config_overrides": {}}) +``` diff --git a/docs/app_and_channel_settings/channel_types.md b/docs/app_and_channel_settings/channel_types.md new file mode 100644 index 0000000..676dda1 --- /dev/null +++ b/docs/app_and_channel_settings/channel_types.md @@ -0,0 +1,182 @@ +Channel types allow you to configure which features are enabled, and how permissions work. +For example you can disable typing indicators, give rights to moderators, or configure channels to be accessible even if you're not a member. + +The easiest way to change your channel types is the dashboard. The docs below show how to change channel types via the API. + +### Built-in Channel types + +There are five built-in channel types with good default for these use cases. + +- [Messaging](https://getstream.io/chat/demos/messaging/): Good default for dating, marketplace, and other social app chat use cases +- [AI](https://getstream.io/chat/solutions/gaming/): For creating user to LLM style chat experiences, text, voice & video. +- [Livestream](https://getstream.io/chat/demos/livestream/): For livestreaming or live shopping experiences +- [Team](https://getstream.io/chat/demos/team/): If you want to build your own version of Slack or something similar, start here. +- [Gaming](https://getstream.io/chat/solutions/gaming/): Defaults for adding chat to video games. + +The five default channel types come with good default permission policies. You can find more information on how to manage permissions in the [Channel Types section](/chat/docs/python/chat_permission_policies/). + +### Updating or creating a channel type + +```python +# Create a new channel type +client.create_channel_type( + { + "name": "my-channel-type", + "typing_events": True, + "read_events": True, + "reactions": True, + "replies": True, + } +) + +# Update an existing channel type +client.update_channel_type( + "my-channel-type", + reactions=False, + max_message_length=1000, +) +``` + +### Features you can enable/disable + +Channel types can be configured with specific permissions and features. + +As you can see in the examples below, you can define your own Channel types and configure them to fit your needs. The Channel type allows you to configure these features: + +- **typing_events** : Controls if typing indicators are shown. +- **read_events** : Controls whether the chat shows how far you've read. +- **connect_events** : Determines if events are fired for connecting and disconnecting to a chat. +- **custom_events** : Determines if channel watchers will receive custom events. +- **reactions** : If users are allowed to add reactions to messages. +- **search** : Controls if messages should be searchable. +- **replies** : Enables message threads and replies. +- **quotes** : Allows members to quote messages (inline replies). +- **mutes** : Determines if users are able to mute other users. +- **uploads** : Allows image and file uploads within messages. +- **url_enrichment** : When enabled, messages containing URLs will be enriched automatically with image and text related to the message. This is disabled by default for the livestream channel type and we do not recommend enabling it for performance reasons. +- **count_messages** : Enables message counting on new channels. When enabled the message count will be present in the channel response. +- **user_message_reminders** : Allow users to set reminders for messages. More information can be found [here](/chat/docs/python/message_reminders/). +- **mark_messages_pending** : When enabled, messages marked as pending are only visible to the sender until approved. +- **polls** : Allows channel members to create and vote on polls. +- **skip_last_msg_update_for_system_msgs** : When disabled, system messages will affect the channel's last_message_at timestamp. +- **location_sharing** : Allows members to share their locations with other members. +- **read_receipts** : Allows members to see when messages are delivered (delivery events). +- **partitioning** : Automatically chunks messages into virtual partitions for better performance at larger scales (dynamic partitioning). +- **push_notifications** : If messages are allowed to generate push notifications. + +### Channel Types Fields + +| name | type | description | default | optional | +| ------------------------------------ | -------------- | --------------------------------------------------------------------------------------------------- | ------- | -------- | +| name | string | The name of the channel type must be unique per application | | | +| max_message_length | int | The max message length | 5,000 | ✓ | +| typing_events | boolean | Enable typing events | true | ✓ | +| read_events | boolean | Enable read events | true | ✓ | +| connect_events | boolean | Enable connect events | true | ✓ | +| custom_events | boolean | Enable custom events | true | ✓ | +| reactions | boolean | Enable message reactions | true | ✓ | +| search | boolean | Enable message search | true | ✓ | +| replies | boolean | Enable replies (threads) | true | ✓ | +| quotes | boolean | Allow quotes/inline replies | true | ✓ | +| mutes | boolean | Enable mutes | true | ✓ | +| uploads | boolean | Enable file and image upload | true | ✓ | +| url_enrichment | boolean | Automatically enrich URLs | true | ✓ | +| count_messages | boolean | Enables message counting on new channels | false | ✓ | +| user_message_reminders | boolean | Allow users to set reminders and bookmarks for messages | false | ✓ | +| mark_messages_pending | boolean | Messages marked as pending are only visible to the sender until approved | false | ✓ | +| polls | boolean | Allow channel members to create and vote on polls | false | ✓ | +| skip_last_msg_update_for_system_msgs | boolean | When disabled, system messages will affect the channel's last_message_at timestamp | false | ✓ | +| location_sharing | boolean | Allow members to share their locations with other members | false | ✓ | +| read_receipts | boolean | Allow members to see when messages are delivered (delivery events) | true | ✓ | +| partitioning | boolean | Automatically chunks messages into virtual partitions for better performance at larger scales | false | ✓ | +| push_notifications | boolean | Enable push notifications | true | ✓ | +| automod | string | Disabled, simple or AI are valid options for the Automod (AI based moderation is a premium feature) | simple | ✓ | +| commands | list of string | The commands that are available on this channel type | [] | ✓ | + +> [!WARNING] +> You need to use server-side authentication to create, edit, or delete a channel type. + + +### Creating a Channel Type + +```python +client.create_channel_type( + { + "name": "public", + "mutes": False, + "reactions": False, + } +) +``` + +> [!WARNING] +> If not provided, the permission settings will default to the ones from the built-in "messaging" type. + + +Please note that applications have a hard limit of 50 channel types. If you need more than this please have a look at the [Multi-tenant & Teams](/chat/docs/python/multi_tenant_chat/) section. + +### List Channel Types + +You can retrieve the list of all channel types defined for your application. + +```python +client.list_channel_types() +``` + +### Get a Channel Type + +You can retrieve a channel type definition with this endpoint. + +> [!NOTE] +> Features and commands are also returned by other channel endpoints. + + +```python +client.get_channel_type("public") +``` + +### Edit a Channel Type + +Channel type features, commands and permissions can be changed. Only the fields that must change need to be provided, fields that are not provided to this API will remain unchanged. + +```python +client.update_channel_type( + "public", + replies=False, + commands=["all"], +) +``` + +Features of a channel can be updated by passing the boolean flags: + +```python +client.update_channel_type("public", + typing_events=False, + read_events=True, + connect_events=True, + search=False, + reactions=True, + replies=False, + mutes=True +) +``` + +Settings can also be updated by passing in the desired new values: + +```python +client.update_channel_type( + "public", + automod="disabled", + max_message_length=140, + commands=["ban", "unban"], +) +``` + +### Remove a Channel Type + +```python +client.delete_channel_type("public") +``` + +> [!NOTE] +> You cannot delete a channel type if there are any active channels of that type. diff --git a/docs/app_and_channel_settings/chat_permission_policies.md b/docs/app_and_channel_settings/chat_permission_policies.md new file mode 100644 index 0000000..9310094 --- /dev/null +++ b/docs/app_and_channel_settings/chat_permission_policies.md @@ -0,0 +1,247 @@ +Stream Chat ships with a configurable permission system that allows high resolution control over what users are permitted to do. + +## Getting Started + +There are multiple important terms to understand when it comes to permission management. Each permission check comes down to three things: + +- `Subject` - an actor which attempts to perform certain Action. It could be represented by a User or by a ChannelMember + +- `Resource` - an item that Subject attempts to perform an Action against. It could be a Channel, Message, Attachment or another User + +- `Action` - the exact action that is being performed. For example `CreateChannel` , `DeleteMessage` , `AddLinks` + +The purpose of permission system is to answer a question: **is** `Subject A` **allowed to perform** `Action B` **on** `Resource C` ? + +Stream Chat provides several concepts which help to control which actions are available to whom: + +- `Permission` - an object which represents actions a subject is allowed to perform + +- `Role` - assigned to a User or Channel Member and is used to check their permissions + +- `Grants` - the way permissions are assigned to roles, applicable across the entire application, or specific to a single channel type or channel. + +Also important to know is permissions checking only happens on the client-side calls. Server-side allows everything so long as a valid API key and secret is provided. + +## Role Management + +To make it easy to get started, all Stream applications come with several roles already built in with permissions to represent the most common use cases. These roles can be customized if needed, and new roles can be created specific for your application + +This is the process of assigning a role to users so they can be granted permissions. This represents `Subject A` in the permissions question. Users will have one role which grants them permissions for the entire application and additionally users can have channel roles which grant permissions for a single channel or for all channels with the same channel type. + +By default all users have builtin role `user` assigned. To change the role of the User, you can use UpdateUser API endpoint: + +```python +response = client.update_user_partial( + {"id": "james_bond", "set": {"role": "special_agent"}} +) +``` + +Once you add user to the channel, `channel_member` role will be assigned to user's membership: + +```python +result = channel.add_members([{"user_id": "james_bond"}]) +print(result["members"][0]["channel_role"]) +``` + +In order to change channel-level role of the user, you can either add user to the channel with a different role (if the SDK supports it) or update it later by role assignment: + +```python +# Add user to the channel with role set +result = channel.add_members([{"user_id": "james_bond", "channel_role": "channel_moderator"}]) + +# Assign new channel member role +result = channel.assign_roles([{"user_id": "james_bond", "channel_role": "channel_member"}]) +``` + +> [!NOTE] +> changing channel member roles is not allowed client-side. + + +Subject + +`Subject` can be represented by User or ChannelMember. ChannelMember subject is used only when user interacts with a channel that they are member of. Both User and ChannelMember have Role and permission system takes both roles into consideration when checking permissions. + +## Builtin roles + +There are some builtin roles in Stream Chat that cover basic chat scenarios: + +| Role | Level | Description | +| ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| user | User | Default User role | +| guest | User | Used for guest users created by server-side endpoints. Guests are short-lived temporary users that could be created without a token | +| anonymous | User | Anonymous users are not allowed to perform any actions that write data. You should treat them as unathenticated clients | +| admin | User | Role for users that perform administrative tasks with elevated permissions | +| channel_member | Channel | Default role that gets assigned when user is added to the channel | +| channel_moderator | Channel | Role for channel members that perform administrative tasks with elevated permissions | + +> [!NOTE] +> It's worth noting that you cannot use user-level roles as channel-level roles vice-versa. This restriction only applies to builtin roles + + +## Ownership + +Some Stream Chat entities have an owner and the fact of ownership can be considered when configuring access permissions. Ownership is supported in these entity types: + +1. **Channel** - owned by its creator + +2. **Message** - owned by its creator (sender) + +3. **Attachment** - owned by user who uploaded a file + +4. **User** - authenticated user owns itself + +Using ownership concept, permissions could be set up in such a way that allows entity owners to perform certain actions. For example: + +- **Update Own Message** - allows message senders to edit their messages + +- **Update Own User** - allows users to change their own properties (except role and team) + +- **Send Message in Own Channel** - allows channel creators to send messages in the channels that they created even if they are not members + +## Custom Roles + +In more sophisticated scenarios custom roles could be used. One Stream Chat application could have up to 25 custom roles. Roles are simple, and require only a name to be created. They do nothing until permissions are assigned to the role. To create new custom role you can use CreateRole API endpoint: + +```python +client.create_role("special_agent") +``` + +To delete previously created role you can use DeleteRole API endpoint: + +```python +client.delete_role("agent_006") +``` + +> [!NOTE] +> In order to delete a role, you have to remove all permission grants that this role has and make sure that you don't have non-deleted users with this role assigned. Channel-level roles could be deleted without reassigning them, although, some users could lose access to channels where this role is used. + + +Once you have a role created you can start granting permissions to it. You can also grant or remove permissions for built in roles. + +## Granting permissions + +User access in Chat application is split across multiple scopes. + +- **Application Permissions** : You can grant these using the .app scope. These permissions apply to operations that occur outside of channel-types including accessing and [modifying other users](/chat/docs/python/update_users/), or [using moderation features](/chat/docs/python/moderation/). + +- **Channel-Type Permissions** : These apply permissions to all channels of a particular type. + +- **Channel Permissions** : These apply permissions to a single channel and override channel-type permissions. + +To list all available permissions you can you ListPermissions API endpoint: + +```python +response = client.list_permissions() +``` + +> [!NOTE] +> You can also find all available permissions on [Permissions Reference](/chat/docs/python/permissions_reference/) page + + +Each permission object contains these fields: + +| Type | Description | Description | Example | +| ----------- | ----------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | +| id | string | Unique permission ID | create-message-owner | +| name | string | Human-readable permission name | Create Message in Owned Channel | +| description | string | Human-readable permission description | Grants action CreateMessage which allows to send a new message, user should own a channel | +| action | string | Action which this permission grants | CreateMessage | +| owner | boolean | If true, Subject should be an owner of the Resource | true | +| same_team | boolean | If true, Subject should be a part of the team that Resource is a part of | true | + +To manipulate granted permissions for certain channel type, you can use UpdateChannelType API endpoint: + +```python +# observe current grants of the channel type +response = client.get_channel_type("messaging") +print(repr(response["grants"])) + +# update "channel_member" role grants in "messaging" scope +client.update_channel_type("messaging", grants={ + "channel_member": [ + "read-channel", # allow access to the channel + "create-message", # create messages in the channel + "update-message-owner", # update own user messages + "delete-message-owner", # delete own user messages + ] +}) +``` + +This call will only change grants of roles that were mentioned in the request. You can remove all role grants with providing empty array ( `[]` ) as list of granted permissions: + +```python +client.update_channel_type("messaging", grants={ + "guest": [], # removes all grants of "guest" role + "anonymous": [], # removes all grants of "anonymous" role +}) +``` + +If you want to reset the whole scope to default settings, you can explicitly provide `null` to `grants` field: + +```python +# reset the whole scope to default settings +client.update_channel_type("messaging", grants=None) +``` + +You can manipulate `.app` scope grants using UpdateApp API endpoint in exactly the same way: + +```python +# update grants of multiple roles in ".app" scope +client.update_app_settings(grants={ + "anonymous": [], + "guest": [], + "user": [ + "search-user", + "mute-user", + ], + "admin": [ + "search-user", + "mute-user", + "ban-user", + ], +}) +``` + +## UI for configuring permissions + +Stream Dashboard provides a user interface to edit permission grants. This UI is available on **Chat > Roles & Permissions** page which is available after switching to version 2 of permissions. + +![](https://getstream.imgix.net/docs/4d6a4f5e-6e96-4f3a-9097-f14526b384f7.png?auto=compress&fit=clip&w=800&h=600) + +## Channel-level permissions + +In some cases it makes sense to slightly modify granted permissions for the channel without changing channel-type grants configuration. For this, you can use Grants Modifiers that you can set for each channel individually. Grants Modifiers look almost exactly the same as regular Grants object except it allows to revoke permissions as well as grant new ones. For example, if we want to disallow sending links for users with role "user" in channel "livestream:example" and allow creating reactions, we can do this: + +```python +channel.update_partial(to_set={ + "config_overrides": { + "grants": { + "user": ["!add-links", "create-reaction"], + } + } +}) +``` + +Exclamation mark ( `!` ) here means "revoke" and you can combine any number of "revoke" and "grant" modifiers + +> [!NOTE] +> After modifying the granted channel-level permissions, the API will enrich the channel response with the grants field under data.config.grants + + +> [!NOTE] +> The field `config_overrides` can only be updated using server-side auth + + +## Broadcast and Reply-only Channels + +A common example of changing the permission model of a channel type is to create a Telegram-style broadcast channel where privileged channel members can send messages and other members may have permissions restricted to reading, reactions, or replying. + +The three Permission grants to modify these under the scope of the channel type are + +- Read Channel +- Create Reaction +- Create Reply + +## Multi-Tenancy + +For grouping users into teams (or tenants) to keep their data strictly segregated, see [Multi-Tenancy](/chat/docs/python/multi_tenant_chat/). diff --git a/docs/app_and_channel_settings/multi_tenant_chat.md b/docs/app_and_channel_settings/multi_tenant_chat.md new file mode 100644 index 0000000..4920f2f --- /dev/null +++ b/docs/app_and_channel_settings/multi_tenant_chat.md @@ -0,0 +1,444 @@ +Many apps that add chat have customers of their own. If you're building something like Slack, or a SaaS application like InVision you want to make sure that one customer can't read the messages of another customer. Stream Chat can be configured in multi-tenant mode so that users are organized in separated teams that cannot interact with each other. + +## Teams + +Stream Chat has the concept of teams for users and channels. The purpose of teams is to provide a simple way to separate different groups of users and channels within a single application. + +If a user belongs to a team, the API will ensure that such user will only be able to connect to channels from the same team. Features such as user search are limited so that a user can only search for users from the same team by default. + +> [!NOTE] +> In legacy permission system users can never access users nor channels from other teams. In [Permissions V2](/chat/docs/python/chat_permission_policies/) it is possible to alter this behavior using multi-tenant permissions. + + +When enabling multi-tenant mode all user requests will always ensure that the request applies to a team the user belongs to. For instance, if a user from team "blue" tries to delete a message that was created on a channel from team "red" the API will return an error. If user doesn't have team set, it will only have access to users and channels that don't have team. + +## Enable Teams for your application + +In order to use Teams, your application must have multi-tenant mode enabled. You can ensure your app is in multi-tenant mode by calling the Application Settings endpoint. + +```python +client = StreamChat("{{ api_key }}", "{{ api_secret }}") +client.update_app_settings(multi_tenant_enabled=True) +``` + +> [!NOTE] +> You only need to activate multi-tenancy once per application. + + +> [!WARNING] +> Do not turn off multitenancy on an application without very careful consideration as this will turn off teams checking which gives users the ability to access all channels and messages across all teams. +> +> Make sure to activate multi-tenancy before using teams. + + +## User teams + +When using teams, users must be created from your back-end and specify which teams they are a member of. + +```python +client.update_user_partial( + {"id": user_id, "set": {"teams": ["red", "blue"]}} + ) +``` + +> [!NOTE] +> A user can be a member of a maximum of 250 teams. Team name is limited to 100 bytes + + +> [!WARNING] +> User teams are included in all User object payloads. We recommend to have short team names to reduce response payload sizes + + +> [!WARNING] +> In Permissions v1, user teams can only be changed using server-side auth. This ensures users can't change their own team membership. In Permissions v2 it is possible to update user teams from client-side if `UpdateUserTeam` action is granted to the user + + +## Channel team + +Channels can be associated with a team. Users can create channels client-side but if their user is part of a team, they will have to specify a team or the request will be rejected with an error. + +```python +channel = client.channel("messaging", "red-general", {"team": "red"}) +channel.create() +``` + +> [!NOTE] +> Channel teams allows you to ensure proper permission checking for a multi tenant application. Keep in mind that you will still need to enforce that channel IDs are unique. A very effective approach is to include the team name as a prefix to avoid collisions. (ie. "red-general" and "blue-general" instead of just "general") + + +## User Search + +By default the user search will only return results from teams that user is a part of. API injects filter `{teams: {$in: ["red", "blue"]}}` for every request that doesn't already contain filter for `teams` field. If you want to query users from all teams, you have to provide empty filter like this: `{teams:{}}` . For server-side requests, this filter does not apply. + + +> [!NOTE] +> Users that cannot be displayed to the current user due to lack of permissions will be omitted from response. + + +## Query Channels + +When using multi-tenant, the query channels endpoint will only return channels that match the query **and** are on the same team as the user. API injects filter `{team: {$in: []}}` for every request that doesn't already contain filter for `team` field. If you want to query channels from all teams, you have to provide empty filter like this: `{team:{}}` . For server-side requests, this filter does not apply. + + +> [!NOTE] +> In case if response contains channels that user cannot access, an access error will be returned. + + +## Team based roles + +By default a user will be assigned only 1 role (ie. `user`, `admin`, etc.). If you would like to have different roles depending on the the team the user is part of, you can do so by specifying a separate role per team. This team based role is applicable only on channels that belong to that team. Let's imagine user Jane, she's a user with role `user` throughout the application, however on team `red` we would like to give her elevated permissions and give her the `admin` role. +We can do this by updating the user as follows: + +```python +user = { + "id": "Jane", + "role": "user", + "teams": [ "red", "blue"], + "teams_role": { + "red": "admin", + "blue": "user" + }, +} +response = client.upsert_user(user) +``` + +If no team based role is set for a team, the system uses the role of the user. +For example, user Janet is a member of teams `red`, `blue` and `orange`. She has role `user` and team based roles `{ "red": "admin", "blue": "user" }`: + +- On team red, she will have `admin` level permissions. This means that on channels that belong to team red, she will have admin level permissions. +- On channels from team blue, she has `user` level permissions. +- On channels from team orange, she also has `user` level permissions (because no team role was assigned for this team). + +Please be aware team based roles will only work when multitenancy is enabled. + +## Multi-Tenant Permissions + +In tables below you will find default permission grants for builtin roles that designed for multi-tenant applications. They are useful for [multi-tenant applications](/chat/docs/python/multi_tenant_chat/) only. + +By default, for multi-tenant applications, all objects (users, channels, and messages) must belong to the same team to be able to interact. These multi-tenant permissions enable overriding that behavior, so that certain users can have permissions to interact with objects on any team + +### Scope `video:livestream` + +| Permission ID | +| ------------- | + +### Scope `video:development` + +| Permission ID | +| ------------- | + +### Scope `.app` + +| Permission ID | global_moderator | global_admin | +| --------------------------- | ---------------- | ------------ | +| flag-user-any-team | ✅ | ✅ | +| mute-user-any-team | ✅ | ✅ | +| read-flag-reports-any-team | ✅ | ✅ | +| search-user-any-team | ✅ | ✅ | +| update-flag-report-any-team | ✅ | ✅ | +| update-user-owner | ✅ | ✅ | + +### Scope `video:audio_room` + +| Permission ID | +| ------------- | + +### Scope `video:default` + +| Permission ID | +| ------------- | + +### Scope `messaging` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✅ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-channel-owner-any-team | ✅ | ✖️ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| recreate-channel-owner-any-team | ✅ | ✖️ | +| remove-own-channel-membership-any-team | ✅ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| truncate-channel-owner-any-team | ✅ | ✖️ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✅ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✅ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +### Scope `livestream` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✅ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| remove-own-channel-membership-any-team | ✖️ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✖️ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✖️ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +### Scope `team` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✅ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-channel-owner-any-team | ✅ | ✖️ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| recreate-channel-owner-any-team | ✅ | ✖️ | +| remove-own-channel-membership-any-team | ✅ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| truncate-channel-owner-any-team | ✅ | ✖️ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✅ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✅ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +### Scope `commerce` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✅ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| remove-own-channel-membership-any-team | ✅ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✅ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✅ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +### Scope `gaming` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✖️ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| remove-own-channel-membership-any-team | ✅ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✖️ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✖️ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +## Team Usage Statistics + +For multi-tenant applications, you can query usage statistics broken down by team. This is useful for billing, monitoring, and analytics purposes. The API returns detailed metrics for each team including user counts, message volumes, and activity patterns. + +### Querying Team Usage Stats + +Use the `queryTeamUsageStats` method to retrieve usage statistics. You can query by month or by a custom date range. + +```python +# Query current month's stats +response = client.query_team_usage_stats() + +# Query specific month +response = client.query_team_usage_stats(month="2024-01") + +# Query date range +response = client.query_team_usage_stats( + start_date="2024-01-01", + end_date="2024-01-31" +) + +# With pagination +response = client.query_team_usage_stats(limit=10) +if response.get("next"): + next_page = client.query_team_usage_stats(limit=10, next=response["next"]) +``` + +### Available Metrics + +The response includes statistics for each team with the following metrics: + +| Metric | Description | +| ----------------------------- | -------------------------------------- | +| `users_daily` | Number of unique users active per day | +| `messages_daily` | Number of messages sent per day | +| `translations_daily` | Number of message translations per day | +| `image_moderations_daily` | Number of images moderated per day | +| `concurrent_users` | Peak concurrent users | +| `concurrent_connections` | Peak concurrent connections | +| `users_total` | Total number of users | +| `users_last_24_hours` | Users active in the last 24 hours | +| `users_last_30_days` | Users active in the last 30 days | +| `users_month_to_date` | Users active month to date | +| `users_engaged_last_30_days` | Engaged users in the last 30 days | +| `users_engaged_month_to_date` | Engaged users month to date | +| `messages_total` | Total number of messages | +| `messages_last_24_hours` | Messages sent in the last 24 hours | +| `messages_last_30_days` | Messages sent in the last 30 days | +| `messages_month_to_date` | Messages sent month to date | + +> [!NOTE] +> This API requires server-side authentication. It cannot be called from client-side SDKs. + + +> [!NOTE] +> Use the `month` parameter (format: `YYYY-MM`) for monthly reports, or `start_date` and `end_date` (format: `YYYY-MM-DD`) for custom date ranges. If no parameters are provided, the current month's statistics are returned. + + +### Metric Attribution + +Message metrics (`messages_*`) are attributed based on the channel's `team` field (`channel.team`), while user metrics (`users_*`) are attributed based on the user's `teams` array (`user.teams`). This means you may see messages under a team even when `users_*` metrics are zero for that team, if messages were sent in channels belonging to that team by users who are not members of that team. + +### Empty Team + +The API returns a row for `team=""` (empty string) which represents users and messages that are not assigned to any team. This includes messages in channels without a team set and users without any team membership. + +### Response Modes + +The response shape differs based on query mode: + +**Monthly mode** (using `month` parameter): Returns only the total/aggregated values for each metric. Daily breakdown arrays are omitted. + +**Daily mode** (using `start_date` and `end_date`): Returns both daily breakdown arrays and aggregated totals. The aggregation method depends on the metric type: + +- **SUM**: Daily activity metrics (`users_daily`, `messages_daily`, `translations_daily`, `image_moderations_daily`) - totals are summed across the date range +- **MAX**: Peak metrics (`concurrent_users`, `concurrent_connections`) - totals reflect the maximum value observed +- **LATEST**: Rolling/cumulative metrics (`users_total`, `users_last_24_hours`, `users_last_30_days`, `users_month_to_date`, `users_engaged_last_30_days`, `users_engaged_month_to_date`, `messages_total`, `messages_last_24_hours`, `messages_last_30_days`, `messages_month_to_date`) - totals reflect the most recent value + +### Pagination + +Results are paginated in lexicographic order by team name. The `next` cursor in the response is a base64-encoded team ID. Use this cursor value in subsequent requests to fetch the next page of results. The `limit` parameter is capped at 30 teams per request. + +### Date Range Validation + +When using custom date ranges, the following validations apply: + +- `end_date` must be greater than or equal to `start_date` +- The date range cannot exceed 365 days diff --git a/docs/app_and_channel_settings/permissions_reference.md b/docs/app_and_channel_settings/permissions_reference.md new file mode 100644 index 0000000..9b39d20 --- /dev/null +++ b/docs/app_and_channel_settings/permissions_reference.md @@ -0,0 +1,797 @@ +This reference contains some useful information about permission system, applicable to both versions. + +## Actions + +In the table below you will find all available actions of Stream Chat permission system + +| Action | Resource Type | Description | +| --------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------- | +| AddLinks | Channel | Allows user to add URLs into messages | +| AddOwnChannelMembership | Channel | Allows user to add own channel membership (join channel) | +| BanChannelMember | Channel | Allows user to ban channel members | +| CreateChannel | Channel | Allows user to create a new channel | +| CreateDistinctChannelForOthers | Channel | Allows user to create new distinct channel for other users (e.g. user A creates channel for users B and C) | +| CreateMessage | Channel | Allows user to send a new message | +| CreateAttachment | Channel | Allows user to send a new message with attachments | +| CreateMention | Channel | Allows user to send a new message with mentions | +| CreateReaction | Channel | Allows user to add a reaction to a message | +| CreateSystemMessage | Channel | Allows user to send a new system message | +| DeleteChannel | Channel | Allows user to delete a channel | +| DeleteReaction | Channel | Allows user to delete a reaction | +| FlagMessage | Channel | Allows user to flag messages | +| MuteChannel | Channel | Allows user to mute and unmute channel | +| PinMessage | Channel | Allows user to pin a message | +| ReadChannel | Channel | Allows user to read messages from the channel | +| ReadChannelMembers | Channel | Allows user to read channel members | +| ReadDisabledChannel | User | Allows user to read disabled channels (regardless of channel membership) | +| ReadMessageFlags | Channel | Allows user to access messages that have been flagged | +| RecreateChannel | Channel | Allows user to recreate a channel when it got deleted | +| RemoveOwnChannelMembership | Channel | Allows user to leave the channel (remove own channel membership) | +| SendCustomEvent | Channel | Allows user to send custom events to a channel | +| SkipChannelCooldown | Channel | Allows user to bypass existing cooldown in a channel | +| SkipMessageModeration | Channel | Allows user to bypass automatic message moderation | +| TruncateChannel | Channel | Allows user to truncate a channel | +| UpdateChannel | Channel | Allows user to update channel data | +| UpdateChannelCooldown | Channel | Allows user to set and unset cooldown time for a channel (slow mode) | +| UpdateChannelFrozen | Channel | Allows user to freeze and unfreeze a channel | +| UpdateChannelMembers | Channel | Allows user to add, modify and remove channel members | +| UploadAttachment | Channel | Allows user to upload files and images | +| UseFrozenChannel | Channel | Allows user to send messages and reactions to a frozen channels | +| DeleteMessage | Message | Allows user to delete a message | +| RunMessageAction | Message | Allows user to run an action against a message | +| UnblockMessage | Message | Allows user to unblock message blocked by automatic moderation | +| UpdateMessage | Message | Allows user to update a message | +| DeleteAttachment | Attachment | Allows user to delete uploaded files and images | +| BanUser | User | Allows user to ban users | +| FlagUser | User | Allows user to flag users | +| MuteUser | User | Allows user to mute and unmute users | +| SearchUser | User | Allows user to search for other users | +| UpdateUser | User | Allows user to update users | +| UpdateUserRole | User | Allows user to update user roles | +| UpdateUserTeams | User | Allows user to update user teams | +| CreateRestrictedVisibilityMessage | User | Allows user to create restricted visibility messages | +| ReadRestrictedVisibilityMessage | User | Allows user to read restricted visibility messages | +| BlockUser | Call | Allows user to Block and unblock users on calls | +| CreateCall | Call | Allows user to creates a call | +| CreateCallReaction | Call | Allows user to Add a reaction to a call | +| DeleteRecording | Call | Allows user to Delete recording | +| EndCall | Call | Allows user to terminates a call | +| JoinBackstage | Call | Allows user to joins a call backstage | +| JoinCall | Call | Allows user to joins a call | +| JoinEndedCall | Call | Allows user to joins a call that was marked as ended | +| ListRecordings | Call | Allows user to List recordings | +| MuteUsers | Call | Allows user to MuteUsers | +| PinCallTrack | Call | Allows user to Pin/Unpin a track for everyone in the call | +| ReadCall | Call | Allows user to read a call | +| ReadFlagReports | FlagReport | Allows user to read flag reports | +| RemoveCallMember | Call | Allows user to Remove a participant | +| Screenshare | Call | Allows user to Screenshare | +| SendAudio | Call | Allows user to Send audio | +| SendEvent | Call | Allows user to SendEvent | +| SendVideo | Call | Allows user to Send video | +| StartBroadcasting | Call | Allows user to Start broadcasting | +| StartRecording | Call | Allows user to Start recording | +| StartTranscription | Call | Allows user to Start transcription | +| StopBroadcasting | Call | Allows user to Stop broadcasting | +| StopRecording | Call | Allows user to Stop recording | +| StopTranscription | Call | Allows user to Stop transcription | +| UpdateCall | Call | Allows user to update the data for a call | +| UpdateCallMember | Call | Allows user to Update a participant | +| UpdateCallMemberRole | Call | Allows user to Update role for participants | +| UpdateCallPermissions | Call | Allows user to UpdateCallPermissions | +| UpdateCallSettings | Call | Allows user to updates settings of a call | +| UpdateFlagReport | FlagReport | Allows user to update flag report | + +## Default Grants + +In tables below you will find default permission grants for each builtin channel type as well as `.app` permission scope. + +For each of of the above actions, there are different built in permissions depending on whether the object was created by the user or not. For example, users can be given permissions to `delete-attachment` which allows for deleting any message attachments, or they can be given permissions to `delete-attachment-owned` to restrict this to only attachments added by the current user. + +Every custom channel type that you create using CreateChannelType API endpoint, will have `messaging` scope grants by default. + +### Scope `video:development` + +| Permission ID | admin | user | guest | anonymous | +| ----------------------- | ----- | ---- | ----- | --------- | +| block-user | ✅ | ✅ | ✖️ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | ✖️ | +| create-call-reaction | ✅ | ✅ | ✖️ | ✖️ | +| end-call | ✅ | ✅ | ✖️ | ✖️ | +| join-backstage | ✅ | ✅ | ✖️ | ✖️ | +| join-call | ✅ | ✅ | ✅ | ✅ | +| join-ended-call | ✅ | ✅ | ✖️ | ✖️ | +| list-recordings | ✅ | ✅ | ✖️ | ✖️ | +| mute-users | ✅ | ✅ | ✖️ | ✖️ | +| pin-call-track | ✅ | ✅ | ✖️ | ✖️ | +| read-call | ✅ | ✅ | ✅ | ✅ | +| remove-call-member | ✅ | ✅ | ✖️ | ✖️ | +| screenshare | ✅ | ✅ | ✖️ | ✖️ | +| send-audio | ✅ | ✅ | ✅ | ✖️ | +| send-event | ✅ | ✅ | ✅ | ✖️ | +| send-video | ✅ | ✅ | ✅ | ✖️ | +| start-broadcasting | ✅ | ✅ | ✖️ | ✖️ | +| start-recording | ✅ | ✅ | ✖️ | ✖️ | +| start-transcription | ✅ | ✅ | ✖️ | ✖️ | +| stop-broadcasting | ✅ | ✅ | ✖️ | ✖️ | +| stop-recording | ✅ | ✅ | ✖️ | ✖️ | +| stop-transcription | ✅ | ✅ | ✖️ | ✖️ | +| update-call | ✅ | ✅ | ✖️ | ✖️ | +| update-call-member | ✅ | ✅ | ✖️ | ✖️ | +| update-call-member-role | ✅ | ✅ | ✖️ | ✖️ | +| update-call-permissions | ✅ | ✅ | ✖️ | ✖️ | +| update-call-settings | ✅ | ✅ | ✖️ | ✖️ | + +### Scope `video:livestream` + +| Permission ID | admin | user | anonymous | +| ----------------------------- | ----- | ---- | --------- | +| block-user | ✅ | ✖️ | ✖️ | +| block-user-owner | ✖️ | ✅ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | +| create-call-reaction | ✅ | ✅ | ✖️ | +| end-call | ✅ | ✖️ | ✖️ | +| end-call-owner | ✖️ | ✅ | ✖️ | +| join-backstage | ✅ | ✖️ | ✖️ | +| join-backstage-owner | ✖️ | ✅ | ✖️ | +| join-call | ✅ | ✅ | ✅ | +| join-ended-call | ✅ | ✖️ | ✖️ | +| join-ended-call-owner | ✖️ | ✅ | ✖️ | +| mute-users | ✅ | ✖️ | ✖️ | +| mute-users-owner | ✖️ | ✅ | ✖️ | +| pin-call-track | ✅ | ✖️ | ✖️ | +| pin-call-track-owner | ✖️ | ✅ | ✖️ | +| read-call | ✅ | ✅ | ✅ | +| remove-call-member | ✅ | ✖️ | ✖️ | +| remove-call-member-owner | ✖️ | ✅ | ✖️ | +| screenshare | ✅ | ✖️ | ✖️ | +| screenshare-owner | ✖️ | ✅ | ✖️ | +| send-audio | ✅ | ✖️ | ✖️ | +| send-audio-owner | ✖️ | ✅ | ✖️ | +| send-event | ✅ | ✅ | ✖️ | +| send-video | ✅ | ✖️ | ✖️ | +| send-video-owner | ✖️ | ✅ | ✖️ | +| start-broadcasting | ✅ | ✖️ | ✖️ | +| start-broadcasting-owner | ✖️ | ✅ | ✖️ | +| start-recording | ✅ | ✖️ | ✖️ | +| start-recording-owner | ✖️ | ✅ | ✖️ | +| stop-broadcasting | ✅ | ✖️ | ✖️ | +| stop-broadcasting-owner | ✖️ | ✅ | ✖️ | +| stop-recording | ✅ | ✖️ | ✖️ | +| stop-recording-owner | ✖️ | ✅ | ✖️ | +| update-call | ✅ | ✖️ | ✖️ | +| update-call-member | ✅ | ✖️ | ✖️ | +| update-call-member-owner | ✖️ | ✅ | ✖️ | +| update-call-member-role | ✅ | ✖️ | ✖️ | +| update-call-member-role-owner | ✖️ | ✅ | ✖️ | +| update-call-owner | ✖️ | ✅ | ✖️ | +| update-call-permissions | ✅ | ✖️ | ✖️ | +| update-call-permissions-owner | ✖️ | ✅ | ✖️ | +| update-call-settings | ✅ | ✖️ | ✖️ | + +### Scope `video:audio_room` + +| Permission ID | admin | user | anonymous | +| ----------------------------- | ----- | ---- | --------- | +| block-user | ✅ | ✖️ | ✖️ | +| block-user-owner | ✖️ | ✅ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | +| create-call-reaction | ✅ | ✅ | ✖️ | +| end-call | ✅ | ✖️ | ✖️ | +| end-call-owner | ✖️ | ✅ | ✖️ | +| join-backstage | ✅ | ✖️ | ✖️ | +| join-backstage-owner | ✖️ | ✅ | ✖️ | +| join-call | ✅ | ✅ | ✅ | +| join-ended-call | ✅ | ✖️ | ✖️ | +| join-ended-call-owner | ✖️ | ✅ | ✖️ | +| mute-users | ✅ | ✖️ | ✖️ | +| mute-users-owner | ✖️ | ✅ | ✖️ | +| read-call | ✅ | ✅ | ✅ | +| remove-call-member | ✅ | ✖️ | ✖️ | +| remove-call-member-owner | ✖️ | ✅ | ✖️ | +| screenshare | ✅ | ✖️ | ✖️ | +| send-audio | ✅ | ✖️ | ✖️ | +| send-audio-owner | ✖️ | ✅ | ✖️ | +| send-event | ✅ | ✅ | ✖️ | +| start-broadcasting | ✅ | ✖️ | ✖️ | +| start-broadcasting-owner | ✖️ | ✅ | ✖️ | +| start-recording | ✅ | ✖️ | ✖️ | +| start-recording-owner | ✖️ | ✅ | ✖️ | +| start-transcription | ✅ | ✖️ | ✖️ | +| start-transcription-owner | ✖️ | ✅ | ✖️ | +| stop-broadcasting | ✅ | ✖️ | ✖️ | +| stop-broadcasting-owner | ✖️ | ✅ | ✖️ | +| stop-recording | ✅ | ✖️ | ✖️ | +| stop-recording-owner | ✖️ | ✅ | ✖️ | +| stop-transcription | ✅ | ✖️ | ✖️ | +| stop-transcription-owner | ✖️ | ✅ | ✖️ | +| update-call | ✅ | ✖️ | ✖️ | +| update-call-member | ✅ | ✖️ | ✖️ | +| update-call-member-owner | ✖️ | ✅ | ✖️ | +| update-call-member-role | ✅ | ✖️ | ✖️ | +| update-call-member-role-owner | ✖️ | ✅ | ✖️ | +| update-call-owner | ✖️ | ✅ | ✖️ | +| update-call-permissions | ✅ | ✖️ | ✖️ | +| update-call-permissions-owner | ✖️ | ✅ | ✖️ | +| update-call-settings | ✅ | ✖️ | ✖️ | +| update-call-settings-owner | ✖️ | ✅ | ✖️ | + +### Scope `.app` + +| Permission ID | admin | moderator | user | guest | +| ------------------ | ----- | --------- | ---- | ----- | +| flag-user | ✅ | ✅ | ✅ | ✅ | +| mute-user | ✅ | ✅ | ✅ | ✅ | +| read-flag-reports | ✅ | ✅ | ✖️ | ✖️ | +| search-user | ✅ | ✅ | ✅ | ✅ | +| update-flag-report | ✅ | ✅ | ✖️ | ✖️ | +| update-user-owner | ✅ | ✅ | ✅ | ✅ | + +### Scope `video:default` + +| Permission ID | admin | user | guest | +| ----------------------------- | ----- | ---- | ----- | +| block-user | ✅ | ✖️ | ✖️ | +| block-user-owner | ✖️ | ✅ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | +| create-call-reaction | ✅ | ✅ | ✖️ | +| delete-recording | ✅ | ✖️ | ✖️ | +| end-call | ✅ | ✅ | ✖️ | +| join-backstage | ✅ | ✖️ | ✖️ | +| join-call | ✅ | ✅ | ✅ | +| join-ended-call | ✅ | ✅ | ✖️ | +| list-recordings | ✅ | ✅ | ✖️ | +| mute-users | ✅ | ✖️ | ✖️ | +| mute-users-owner | ✖️ | ✅ | ✖️ | +| pin-call-track | ✅ | ✖️ | ✖️ | +| pin-call-track-owner | ✖️ | ✅ | ✖️ | +| read-call | ✅ | ✅ | ✅ | +| remove-call-member | ✅ | ✅ | ✖️ | +| screenshare | ✅ | ✅ | ✖️ | +| send-audio | ✅ | ✅ | ✅ | +| send-event | ✅ | ✅ | ✅ | +| send-video | ✅ | ✅ | ✅ | +| start-broadcasting | ✅ | ✅ | ✖️ | +| start-recording | ✅ | ✅ | ✖️ | +| start-transcription | ✅ | ✅ | ✖️ | +| stop-broadcasting | ✅ | ✅ | ✖️ | +| stop-recording | ✅ | ✅ | ✖️ | +| stop-transcription | ✅ | ✅ | ✖️ | +| update-call | ✅ | ✖️ | ✖️ | +| update-call-member | ✅ | ✅ | ✖️ | +| update-call-member-role | ✅ | ✖️ | ✖️ | +| update-call-owner | ✖️ | ✅ | ✖️ | +| update-call-permissions | ✅ | ✖️ | ✖️ | +| update-call-permissions-owner | ✖️ | ✅ | ✖️ | +| update-call-settings | ✅ | ✖️ | ✖️ | +| update-call-settings-owner | ✖️ | ✅ | ✖️ | + +### Scope `messaging` + +| Permission ID | admin | moderator | user | channel_member | channel_moderator | +| ----------------------------------- | ----- | --------- | ---- | -------------- | ----------------- | +| add-links | ✅ | ✅ | ✖️ | ✅ | ✅ | +| add-links-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| ban-channel-member | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| ban-user | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-channel | ✅ | ✅ | ✅ | ✖️ | ✖️ | +| create-message | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-attachment | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-mention | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-mention-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-reaction | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-system-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-attachment | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| delete-channel-owner | ✖️ | ✅ | ✅ | ✖️ | ✖️ | +| delete-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-reaction | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| flag-message | ✅ | ✅ | ✖️ | ✅ | ✅ | +| flag-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| join-call | ✅ | ✅ | ✖️ | ✅ | ✅ | +| mute-channel | ✅ | ✅ | ✖️ | ✅ | ✅ | +| mute-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| pin-message | ✅ | ✅ | ✖️ | ✅ | ✅ | +| pin-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| read-channel | ✅ | ✅ | ✖️ | ✅ | ✅ | +| read-channel-members | ✅ | ✅ | ✖️ | ✅ | ✅ | +| read-channel-members-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| read-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| read-message-flags | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| recreate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| recreate-channel-owner | ✖️ | ✅ | ✅ | ✖️ | ✖️ | +| remove-own-channel-membership | ✅ | ✅ | ✖️ | ✅ | ✅ | +| remove-own-channel-membership-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| run-message-action | ✅ | ✅ | ✖️ | ✅ | ✅ | +| run-message-action-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| send-custom-event | ✅ | ✅ | ✖️ | ✅ | ✅ | +| send-custom-event-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| skip-channel-cooldown | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| skip-message-moderation | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| truncate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| truncate-channel-owner | ✖️ | ✅ | ✅ | ✖️ | ✖️ | +| unblock-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-cooldown | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-frozen | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-members | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-members-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| update-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| update-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| upload-attachment | ✅ | ✅ | ✖️ | ✅ | ✅ | +| upload-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | + +### Scope `livestream` + +| Permission ID | admin | moderator | user | channel_moderator | guest | anonymous | +| ----------------------------- | ----- | --------- | ---- | ----------------- | ----- | --------- | +| add-links | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| ban-channel-member | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| ban-user | ✅ | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| create-channel | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| create-message | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| create-attachment | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| create-mention | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| create-reaction | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| create-system-message | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-attachment | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| delete-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| delete-message | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| delete-reaction | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| flag-message | ✅ | ✅ | ✅ | ✖️ | ✅ | ✖️ | +| join-call | ✅ | ✅ | ✅ | ✖️ | ✅ | ✅ | +| mute-channel | ✅ | ✅ | ✅ | ✖️ | ✅ | ✖️ | +| pin-message | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| pin-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| read-channel | ✅ | ✅ | ✅ | ✖️ | ✅ | ✅ | +| read-channel-members | ✅ | ✅ | ✅ | ✖️ | ✅ | ✅ | +| read-message-flags | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| recreate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| remove-own-channel-membership | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| run-message-action | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| send-custom-event | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| skip-channel-cooldown | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| skip-message-moderation | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| truncate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| unblock-message | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| update-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| update-channel-cooldown | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| update-channel-frozen | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| update-channel-members | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| update-message | ✅ | ✅ | ✖️ | ✅ | ✖️ | ✖️ | +| update-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| upload-attachment | ✅ | ✅ | ✅ | ✖️ | ✖️ | ✖️ | + +### Scope `team` + +| Permission ID | admin | moderator | user | channel_member | channel_moderator | +| ----------------------------------- | ----- | --------- | ---- | -------------- | ----------------- | +| add-links | ✅ | ✅ | ✖️ | ✅ | ✅ | +| add-links-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| ban-channel-member | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| ban-user | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-channel | ✅ | ✅ | ✅ | ✖️ | ✖️ | +| create-message | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-attachment | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-mention | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-mention-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-reaction | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-system-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-attachment | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| delete-channel-owner | ✖️ | ✅ | ✅ | ✖️ | ✖️ | +| delete-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-reaction | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| flag-message | ✅ | ✅ | ✖️ | ✅ | ✅ | +| flag-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| join-call | ✅ | ✅ | ✖️ | ✅ | ✅ | +| mute-channel | ✅ | ✅ | ✖️ | ✅ | ✅ | +| mute-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| pin-message | ✅ | ✅ | ✖️ | ✅ | ✅ | +| pin-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| read-channel | ✅ | ✅ | ✖️ | ✅ | ✅ | +| read-channel-members | ✅ | ✅ | ✖️ | ✅ | ✅ | +| read-channel-members-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| read-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| read-message-flags | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| recreate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| recreate-channel-owner | ✖️ | ✅ | ✅ | ✖️ | ✖️ | +| remove-own-channel-membership | ✅ | ✅ | ✖️ | ✅ | ✅ | +| remove-own-channel-membership-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| run-message-action | ✅ | ✅ | ✖️ | ✅ | ✅ | +| run-message-action-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| send-custom-event | ✅ | ✅ | ✖️ | ✅ | ✅ | +| send-custom-event-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| skip-channel-cooldown | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| skip-message-moderation | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| truncate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| truncate-channel-owner | ✖️ | ✅ | ✅ | ✖️ | ✖️ | +| unblock-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-cooldown | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-frozen | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-members | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-members-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| update-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| update-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| upload-attachment | ✅ | ✅ | ✖️ | ✅ | ✅ | +| upload-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | + +### Scope `commerce` + +| Permission ID | admin | moderator | user | channel_member | channel_moderator | guest | +| ----------------------------------- | ----- | --------- | ---- | -------------- | ----------------- | ----- | +| add-links | ✅ | ✅ | ✖️ | ✅ | ✅ | ✅ | +| add-links-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| ban-channel-member | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| ban-user | ✅ | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| create-channel | ✅ | ✅ | ✖️ | ✖️ | ✖️ | ✅ | +| create-message | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| create-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| create-attachment | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| create-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| create-mention | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| create-mention-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| create-reaction | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| create-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| create-system-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| delete-attachment | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| delete-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| delete-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| delete-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| delete-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| delete-reaction | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| delete-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| flag-message | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| flag-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| join-call | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| mute-channel | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| mute-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| pin-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| pin-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| read-channel | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| read-channel-members | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| read-channel-members-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| read-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| read-message-flags | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| recreate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| remove-own-channel-membership | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| remove-own-channel-membership-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| run-message-action | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| run-message-action-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| send-custom-event | ✅ | ✅ | ✖️ | ✅ | ✅ | ✖️ | +| send-custom-event-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | +| skip-channel-cooldown | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| skip-message-moderation | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| truncate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | +| unblock-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| update-channel | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| update-channel-cooldown | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| update-channel-frozen | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| update-channel-members | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| update-channel-members-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| update-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | ✖️ | +| update-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✅ | +| upload-attachment | ✅ | ✅ | ✖️ | ✅ | ✅ | ✅ | +| upload-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | ✖️ | + +### Scope `gaming` + +| Permission ID | admin | moderator | user | channel_member | channel_moderator | +| ----------------------------------- | ----- | --------- | ---- | -------------- | ----------------- | +| add-links | ✅ | ✅ | ✖️ | ✅ | ✅ | +| add-links-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| ban-channel-member | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| ban-user | ✅ | ✅ | ✖️ | ✖️ | ✖️ | +| create-call | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| create-message | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-attachment | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-mention | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-mention-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-reaction | ✅ | ✅ | ✖️ | ✅ | ✅ | +| create-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| create-system-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-attachment | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| delete-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| delete-reaction | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| delete-reaction-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| flag-message | ✅ | ✅ | ✖️ | ✅ | ✅ | +| flag-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| join-call | ✅ | ✅ | ✖️ | ✅ | ✅ | +| mute-channel | ✅ | ✅ | ✖️ | ✅ | ✅ | +| mute-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| pin-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| read-channel | ✅ | ✅ | ✖️ | ✅ | ✅ | +| read-channel-members | ✅ | ✅ | ✖️ | ✅ | ✅ | +| read-channel-members-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| read-channel-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| read-message-flags | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| recreate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| remove-own-channel-membership | ✅ | ✅ | ✖️ | ✅ | ✅ | +| remove-own-channel-membership-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| run-message-action | ✅ | ✅ | ✖️ | ✅ | ✅ | +| run-message-action-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| send-custom-event | ✅ | ✅ | ✖️ | ✅ | ✅ | +| send-custom-event-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| skip-channel-cooldown | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| skip-message-moderation | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| truncate-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| unblock-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| update-channel-cooldown | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-frozen | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-channel-members | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | +| update-message | ✅ | ✅ | ✖️ | ✖️ | ✅ | +| update-message-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | +| upload-attachment | ✅ | ✅ | ✖️ | ✅ | ✅ | +| upload-attachment-owner | ✖️ | ✖️ | ✅ | ✖️ | ✖️ | + +## Multi-Tenant Default Grants + +In tables below you will find default permission grants for builtin roles that designed for multi-tenant applications. They are useful for [multi-tenant applications](/chat/docs/python/multi_tenant_chat/) only. + +By default, for multi-tenant applications, all objects (users, channels, and messages) must belong to the same team to be able to interact. These multi-tenant permissions enable overriding that behavior, so that certain users can have permissions to interact with objects on any team + +### Scope `video:livestream` + +| Permission ID | +| ------------- | + +### Scope `video:development` + +| Permission ID | +| ------------- | + +### Scope `.app` + +| Permission ID | global_moderator | global_admin | +| --------------------------- | ---------------- | ------------ | +| flag-user-any-team | ✅ | ✅ | +| mute-user-any-team | ✅ | ✅ | +| read-flag-reports-any-team | ✅ | ✅ | +| search-user-any-team | ✅ | ✅ | +| update-flag-report-any-team | ✅ | ✅ | +| update-user-owner | ✅ | ✅ | + +### Scope `video:audio_room` + +| Permission ID | +| ------------- | + +### Scope `video:default` + +| Permission ID | +| ------------- | + +### Scope `messaging` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✅ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-channel-owner-any-team | ✅ | ✖️ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| recreate-channel-owner-any-team | ✅ | ✖️ | +| remove-own-channel-membership-any-team | ✅ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| truncate-channel-owner-any-team | ✅ | ✖️ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✅ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✅ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +### Scope `livestream` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✅ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| remove-own-channel-membership-any-team | ✖️ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✖️ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✖️ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +### Scope `team` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✅ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-channel-owner-any-team | ✅ | ✖️ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| recreate-channel-owner-any-team | ✅ | ✖️ | +| remove-own-channel-membership-any-team | ✅ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| truncate-channel-owner-any-team | ✅ | ✖️ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✅ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✅ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +### Scope `commerce` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✅ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| remove-own-channel-membership-any-team | ✅ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✅ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✅ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | + +### Scope `gaming` + +| Permission ID | global_moderator | global_admin | +| -------------------------------------- | ---------------- | ------------ | +| add-links-any-team | ✅ | ✅ | +| ban-channel-member-any-team | ✅ | ✅ | +| ban-user-any-team | ✅ | ✅ | +| create-call-any-team | ✅ | ✅ | +| create-channel-any-team | ✖️ | ✅ | +| create-message-any-team | ✅ | ✅ | +| create-attachment-any-team | ✅ | ✅ | +| create-mention-any-team | ✅ | ✅ | +| create-reaction-any-team | ✅ | ✅ | +| create-system-message-any-team | ✅ | ✅ | +| delete-attachment-any-team | ✅ | ✅ | +| delete-channel-any-team | ✖️ | ✅ | +| delete-message-any-team | ✅ | ✅ | +| delete-reaction-any-team | ✅ | ✅ | +| flag-message-any-team | ✅ | ✅ | +| join-call-any-team | ✅ | ✅ | +| mute-channel-any-team | ✅ | ✅ | +| pin-message-any-team | ✅ | ✅ | +| read-channel-any-team | ✅ | ✅ | +| read-channel-members-any-team | ✅ | ✅ | +| read-message-flags-any-team | ✅ | ✅ | +| recreate-channel-any-team | ✖️ | ✅ | +| remove-own-channel-membership-any-team | ✅ | ✅ | +| run-message-action-any-team | ✅ | ✅ | +| send-custom-event-any-team | ✅ | ✅ | +| skip-channel-cooldown-any-team | ✅ | ✅ | +| skip-message-moderation-any-team | ✅ | ✅ | +| truncate-channel-any-team | ✖️ | ✅ | +| unblock-message-any-team | ✅ | ✅ | +| update-channel-any-team | ✖️ | ✅ | +| update-channel-cooldown-any-team | ✅ | ✅ | +| update-channel-frozen-any-team | ✅ | ✅ | +| update-channel-members-any-team | ✖️ | ✅ | +| update-message-any-team | ✅ | ✅ | +| upload-attachment-any-team | ✅ | ✅ | diff --git a/docs/best_practices/best_practices.md b/docs/best_practices/best_practices.md new file mode 100644 index 0000000..af6448c --- /dev/null +++ b/docs/best_practices/best_practices.md @@ -0,0 +1,27 @@ +## Best Practices + +Here's an overview of guides with best practices + +- [Livestreaming best practices](/chat/docs/python/livestream_best_practices/) +- [Marketplace apps best practices](/chat/docs/python/marketplace_best_practices/) +- [GDPR endpoints](/chat/docs/python/gdpr/) +- Migrating data + +### Launching Quickly + +Stream provides a low level client, offline support and UI components. +This means that you can build any type of chat or messaging UI. + +A few tips for launching quickly + +- Start with a quick tutorial for each SDK team +- Integrate into your app while using most of the default UI components +- Start customizing/ building your own UI components + +We've seen customers integrate in as little as a few days. Though for enterprise customers it's common to take a month. + +### Generating Test Data + +While developing with Stream Chat, you might want to see how your app handles data from several users. You can do this by generating mock data for your application. + +The easiest way to generate mock data is to use the [Data Generator](https://generator.getstream.io/). When you enter your api key, secret, and username, the generator will create a randomized data set for your app. This will include a set of random users, channels, and messages. diff --git a/docs/best_practices/gdpr.md b/docs/best_practices/gdpr.md new file mode 100644 index 0000000..0ec2a1d --- /dev/null +++ b/docs/best_practices/gdpr.md @@ -0,0 +1,121 @@ +Companies doing business in the European Union are bound by law to follow the General Data Protection Act. While most parts of this law don't have much impact on your integration with Stream Chat, the GDPR right to data access and right to erasure involve data stored and managed on Stream's servers. + +Because of this, Stream provides a set of methods that make complying with those portions of the law easy. + +## The Right to Access Data + +GDPR gives EU citizens the right to request access to their information and the right to have access to this information in a portable format. Stream Chat covers this requirement with the [Export User](/chat/docs/python/exporting_channels/) method. + +This method can only be used with server-side authentication. + +```python +response = server_client.export_user(user_id); +``` + +The export will return all data about the user, including: + +- User ID + +- Messages + +- Reactions + +- Custom Data + +Running a user export will return a JSON object like the following example. + +
+Response + +```json +{ + user: { + id: 'waters-malone', + role: 'user', + created_at: '2021-05-17T14:35:23.313097Z', + updated_at: '2021-05-17T21:10:58.028195Z', + last_active: '2021-07-29T17:43:28.795240793Z', + banned: false, + online: false, + image: 'https://getstream.io/random_png/?id=waters-malone&name=waters-malone', + name: 'Malone Waters' + }, + messages: [ + { + id: 'waters-malone-2f419088-b279-47f0-9aa9-14a706b7988a', + text: 'dfasd', + html: '

dfasd

\n', + type: 'regular', + user: null, + attachments: [], + latest_reactions: [], + own_reactions: [], + reaction_counts: null, + reaction_scores: {}, + reply_count: 0, + cid: 'messaging:waters-malone', + created_at: '2021-05-28T19:40:17.482123Z', + updated_at: '2021-05-28T19:40:17.482123Z', + shadowed: false, + mentioned_users: [], + silent: false, + pinned: false, + pinned_at: null, + pinned_by: null, + pin_expires: null + }, + reactions: [ + { + message_id: 'waters-malone-dc533aa2-7a97-491d-a216-503081d2efde', + user_id: 'waters-malone', + user: null, + type: 'angry', + score: 1, + created_at: '2021-05-17T19:26:10.923605Z', + updated_at: '2021-05-17T19:26:10.923605Z' + }, + { + message_id: 'waters-malone-de5626db-26fc-4a07-8a41-a27496a67612', + user_id: 'waters-malone', + user: null, + type: 'haha', + score: 1, + created_at: '2021-05-17T19:26:18.163286Z', + updated_at: '2021-05-17T19:26:18.163286Z' + } + ], + duration: '1.87ms' +} +``` + +
+ +> [!WARNING] +> Users with more than 10,000 messages will throw an error during the export process. The Stream Chat team is actively working on a workaround for this issue and it will be resolved soon. + + +### The Right to Erasure + +GDPR also gives EU citizens the right to request the deletion of their information. Stream Chat provides ways to delete users, channels, and messages depending on the use case. + +There are two server-side functions which can be used: `deleteUsers` and `deleteChannels` . These allow you to delete up to 100 users or channels and optionally all of their messages in one API request. + +- To permanently delete a user and all of their data, use `deleteUsers` and set `mark_messages_deleted` , `hard_delete` , and `delete_conversation_channels` options to true. + +- To permanently delete a channel and all of its messages, use `deleteChannels` and set `hard_delete` to true. + +For more information, and examples, see: + +- [Deleting a batch of users](/chat/docs/python/update_users/#deleting-many-users/) + +- [Deleting a batch of channels](/chat/docs/python/channel_delete/#deleting-many-channels/) + +After deleting a user, the user will no longer be able to: + +- Connect to Stream Chat + +- Send or receive messages + +- Be displayed when querying users + +- Have messages stored in Stream Chat (depending on whether or not `mark_messages_deleted` is set to `true` or `false` ) diff --git a/docs/best_practices/livestream_best_practices.md b/docs/best_practices/livestream_best_practices.md new file mode 100644 index 0000000..63471af --- /dev/null +++ b/docs/best_practices/livestream_best_practices.md @@ -0,0 +1,103 @@ +This guide is designed to provide general best practices for the Livestream and Live Events use cases. + +- Use the **Livestream channel type** which doesn't require membership for read/write access +- **Disable expensive features** like read events, typing indicators, and connect events to improve performance +- Enable **slow mode** and message throttling for high-traffic events +- **Pre-load users** before events to avoid registration bottlenecks +- Use **virtualized lists** on web to limit DOM elements and protect performance +- **Load test your app** at high message volumes to catch UI performance issues +- Implement **moderation tools** like block lists, automod, and flagging + +### Channel Types + +The Livestream channel type is designed to be used in a Livestream setting and has pre-configured permissions that do not require membership as a registered user to read or write to a channel. This channel type is "open" to any user accessing it with an authorized JWT. Users are not required to be added as members, which saves complexity and additional updates to the channel through the [membership endpoint](/chat/docs/python/channel_members/). The channel can be "[watched](/chat/docs/python/creating_channels/#watching-channels)" by users in order to receive real-time updates on the activity (new messages, etc) in the channel. + +> [!NOTE] +> For general information on understanding [channel types](/chat/docs/python/channel_features/), [roles](/chat/docs/python/update_users/), and [permissions](/chat/docs/python/chat_permission_policies/), please refer to those documentation pages. + + +Live events, by contrast, may sometimes be well suited for the Livestream channel type, but frequently require membership for interacting in channels. Creating a new channel type with similar settings to the Livestream channel, but with some tweaks to tailor to your event platform's needs, may be necessary. + +### Channel Features + +A performant website or mobile app can easily be degraded or overwhelmed by excessive API traffic. The Stream API has been designed with this in mind with features to protect clients (see [throttling](/chat/docs/python/slow_mode/) and [slow mode](/chat/docs/python/slow_mode/#channel-slow-mode/)). However, we still recommend taking these additional steps. + +Certain features that are beneficial to remove for Livestream type settings (in order of performance impact) are + +1. Read events + +2. Typing indicators + +3. Connect events + +4. File uploads + +5. Custom messages + +The Stream Chat API automatically starts throttling typing and read events at 100 watchers in a channel, but it is good practice to remove these from the start, as even 100 very active users can be problematic. It's also worth noting that typing and read events lose their value in user experience as active users rise. + +### Message Throttling and Slow Mode + +The Stream API will begin to throttle messages at >5 messages per second and some messages will not be delivered to all clients watching a channel to protect the client from degraded performance. We also recommend considering Slow Mode for events in which traffic is expected to be particularly high. + +> [!NOTE] +> For reference, Stream found that in [this SpaceX launch video](https://www.youtube.com/watch?v=gBELXjq_X-M), the peak message volume was 9 messages per second, and the Chat experience was difficult to follow. + + +### Application Settings + +> [!NOTE] +> It is always recommended to ensure that [Auth and Permission checks are **not** disabled](/chat/docs/python/app_setting_overview/) for any application that is in a production environment. + + +It is common for guest or anonymous users to be integrated into Livestream channels. These users have access to fewer permissions by default, but this is entirely configurable. Guest and Anonymous users do not require a signed JWT from a server. + +### Adding Users Prior to Events + +Stream recommends that users are "pre-loaded" into the Stream API prior to the event. This prevents registration/upsert bottlenecks at the start of the event. In particular, for channels that may have large member counts, it is advised to add members to these ahead of time. + +If this can't be achieved, based on your app's use case, then we recommend batching users being added to Stream and members being added to channels. Both upsertUsers and addMember endpoint accept up to 100 user_id's in a single API call. + +Lastly, if this isn't possible, and you expect to exceed rate limits, please email with your use case and we may seek to make an exception for your application to avoid any disruptions. + +### API Calls to /channels + +There are [several methods](/chat/docs/python/query_channels/#channel-creation-and-watching) that will trigger an API call to /channels. Watch, query, and create. Only one of these is necessary and additional API calls quickly add up and can be problematic. Filtering and sorting generally are not issues for this use case, but for more information on optimizing these queryChannel parameters, take a look at the [best practices](/chat/docs/python/query_channels/#best-practices). + +### Virtualized Lists + +Using a Virtualized list on a web platform is a recommended means to cap the number of messages stored in the DOM and have proven to significantly improve client performance and protect the quality of a video stream. For more information on the Stream Virtualized list components, take a look [here](https://github.com/GetStream/stream-chat-react/blob/85c75524cafdcdf3b64f04995b608de88ac6d23c/src/docs/VirtualizedMessageList.md). The key caveat to the Virtualized List component is that messages should have a fixed height, which can make images, emojis, and custom messages types problematic. + +### Load Testing + +Before launching a live event, test your application under high user and message volumes. It's easy to have subtle mistakes in UI elements that cause performance degradation at scale—issues that aren't apparent during normal development and testing. + +Common issues that only surface under load: + +- **Expensive re-renders**: Components that re-render on every message can cause lag when message volume is high +- **Memory leaks**: Unbounded message lists or event listeners that aren't properly cleaned up +- **Avatar and image loading**: Fetching user avatars or images for every message without caching +- **Complex message formatting**: Rich text parsing, link previews, or emoji rendering that doesn't scale +- **Animation overhead**: CSS animations or transitions that compound with high message frequency + +Use Stream's [benchat](https://github.com/GetStream/benchat) tool to stress test your chat channels and simulate realistic traffic patterns. Test with at least 5-10 messages per second to approximate peak livestream conditions, and monitor your application's CPU usage, memory consumption, and frame rate during these tests. + +> [!TIP] +> Profile your application using browser developer tools or mobile profilers while simulating high traffic. Look for long-running JavaScript tasks, excessive DOM updates, and memory growth over time. + + +### Moderation + +Stream provides a number of moderation tools that can be useful in a Livestream setting: + +1. **Users flagging messages** - Any user has the ability to flag another user's message. Flagged messages are currently sent to the Stream Moderation Dashboard and flagged messages also trigger a Webhook event. + +2. **Moderation Dashboard** - available to all customers and includes a chat Explorer, Flagged Message review area, and a number of API driven features to ban or delete users. + +3. **Block Lists** - a simple but powerful tool to prevent lists of words from being used in a Chat. These are applied on a Channel Type basis and either a Flag or Block behavior can be defined. + +4. **Pre-send message hook** - a customer-hosted solution that provides a means to host your own moderation solution that can hook into any 3rd party solutions, have Regex filters, or more advanced filtering based on your own criteria. + +5. **AI Moderation** - currently in beta testing currently. Please reach out to to learn more. + +6. **Image Moderation** - an addon to Enterprise packages and will flag images that are deemed to be inappropriate by the moderation logic. diff --git a/docs/best_practices/marketplace_best_practices.md b/docs/best_practices/marketplace_best_practices.md new file mode 100644 index 0000000..9b94641 --- /dev/null +++ b/docs/best_practices/marketplace_best_practices.md @@ -0,0 +1,89 @@ +This guide provides best practices for building chat experiences in marketplace applications, where communication between buyers and sellers is critical to successful transactions. + +## Channel Types + +The built-in **Messaging** channel type is designed for marketplace use cases and provides a good default configuration. It requires membership for channel access and supports all the features typically needed for buyer-seller communication. + +For marketplace apps, we recommend: + +- Using the `messaging` channel type as your starting point +- Creating channels with exactly 2 members (buyer and seller) for most transactions + +## Recommended Features + +### Unread Message Reminders + +[Unread Reminders](/chat/docs/python/unread-reminders/) notify users about messages they haven't read, helping ensure timely responses. Use them to trigger emails, push notifications, or SMS when a user has unread messages. + +**Why this matters for marketplaces:** + +- Buyers waiting for seller responses stay engaged +- Sellers don't miss potential sales opportunities +- Improves overall transaction completion rates + +Enable reminders for your channel type: + + +> [!NOTE] +> Reminders work on channels with 10 or fewer members, making them ideal for typical buyer-seller conversations. + + +### User Average Response Time + +The [User Average Response Time](/chat/docs/python/user_average_response_time/) feature tracks how quickly users typically respond to messages. This is particularly valuable in marketplaces where prompt communication often determines transaction success. + +**Benefits for marketplaces:** + +- Buyers can see seller responsiveness before initiating contact +- Marketplaces can highlight responsive sellers with badges or sorting options +- Helps set expectations for communication timelines + +Enable this feature in your app settings: + + +Once enabled, the `avg_response_time` field will be included in user responses, allowing you to display responsiveness indicators in seller profiles. + +### Pending Messages + +For marketplaces requiring message moderation, enable [pending messages](/chat/docs/python/pending_messages/) to review content before delivery. This is useful for: + +- Preventing spam or fraudulent messages +- Filtering contact information to keep transactions on-platform +- Ensuring compliance with marketplace policies + + +## Channel Configuration Tips + +### Recommended Settings + +For most marketplace apps, configure your channel type with these settings: + + +### Custom Data + +Use channel and message custom data to enhance the marketplace experience: + + +## Trust and Safety + +### Moderation + +Implement moderation to maintain a safe marketplace environment: + +- Use [block lists](/moderation/docs/engines/blocklists-and-regex-filters/) to filter prohibited content +- Enable [automod](/chat/docs/python/moderation/) for automatic content filtering +- Set up [webhooks](/chat/docs/python/webhooks_overview/) to monitor suspicious activity + +## Performance Considerations + +- Use [query channels best practices](/chat/docs/python/query_channels/#best-practices) when loading conversation lists +- Filter by `cid` when querying specific channels for best performance +- Always include `members: { $in: [userID] }` in filters for consistent results + +## Analytics and Insights + +Track key metrics to improve your marketplace communication: + +- **Response times**: Use `avg_response_time` to identify and reward responsive users +- **Message volume**: Monitor conversation activity to understand engagement +- **Unread rates**: Track how often reminders are triggered to optimize notification timing diff --git a/docs/best_practices/moderation.md b/docs/best_practices/moderation.md new file mode 100644 index 0000000..a3a0ffe --- /dev/null +++ b/docs/best_practices/moderation.md @@ -0,0 +1,319 @@ +Moderation is essential for a good user experience on chat. +There are also requirements from DSA and the app stores to take into account. + +Stream has advanced AI moderation capabilities for text, images, video and audio. +Before we launched moderation, customers often struggled with the cost and difficulty of integrating external moderation APIs. +You can now setup moderation in minutes at an affordable price point. + +There are 4 layers of moderation: + +- **Limits / chat features**: Restrict what's allowed. Moderator permissions, disabling links or images, slow mode, enforce_unique_usernames, slash commands etc. +- **Simple**: Blocklist, regex, domain allow/block, email allow/block +- **User actions**: Flag, mute, ban etc. +- **[AI moderation](/moderation/docs/)**: AI on text, images, video, audio + +Let's go over each of these and show what's supported. + +## Limits & Chat features + +### Disabling the permission to post links or add attachments + +You can control links and attachments by revoking the relevant permissions for a role. The permissions to manage are: + +- `add-links` - ability to post messages containing URLs +- `create-attachment` - ability to add attachments to messages +- `upload-attachment` - ability to upload files/images + +Update the grants for a channel type to remove these permissions from a role: + +```python +# Remove link and attachment permissions for channel_member role +client.update_channel_type("messaging", grants={ + "channel_member": [ + "read-channel", + "create-message", + "update-message-owner", + "delete-message-owner", + # "add-links" - removed to disable links + # "create-attachment" - removed to disable attachments + # "upload-attachment" - removed to disable uploads + ], +}) +``` + +For more details on permissions, see [User Permissions](/chat/docs/python/chat_permission_policies/). + +### Image & Video file types + +You can restrict which file types users can upload using `image_upload_config` and `file_upload_config`. This allows you to set allowed or blocked file extensions and MIME types, as well as size limits. + +Both configs accept the following fields: + +| Field | Description | +| ------------------------- | ----------------------------------------------------------------- | +| `allowed_file_extensions` | Array of allowed file extensions (e.g., `[".jpg", ".png"]`) | +| `blocked_file_extensions` | Array of blocked file extensions | +| `allowed_mime_types` | Array of allowed MIME types (e.g., `["image/jpeg", "image/png"]`) | +| `blocked_mime_types` | Array of blocked MIME types | +| `size_limit` | Maximum file size in bytes (default allows up to 100MB) | + +```python +# Restrict images to common formats only +client.update_app_settings({ + "image_upload_config": { + "allowed_file_extensions": [".jpg", ".jpeg", ".png", ".gif", ".webp"], + "allowed_mime_types": ["image/jpeg", "image/png", "image/gif", "image/webp"], + "size_limit": 5 * 1024 * 1024, # 5MB + }, + # Restrict file uploads to documents only + "file_upload_config": { + "allowed_file_extensions": [".pdf", ".doc", ".docx"], + "allowed_mime_types": ["application/pdf", "application/msword"], + "size_limit": 10 * 1024 * 1024, # 10MB + }, +}) +``` + +For more details, see [App Settings](/chat/docs/python/app_setting_overview/). + +### Giving moderators more permissions + +Moderators have elevated permissions like the ability to ban users, delete messages, and more. You can assign moderator roles to users at the channel level or across all channels. + +**Add a Moderator to a Channel:** + +```python +# Add a member with moderator role +channel.add_members([{"user_id": "james_bond", "channel_role": "channel_moderator"}]) +``` + +**Make a User a Moderator Across All Channels:** + +To grant a user moderator permissions across all channels in your app, update their global role: + +```python +client.update_user_partial( + {"id": "james_bond", "set": {"role": "admin"}} +) +``` + +For more details on permissions, see [User Permissions](/chat/docs/python/chat_permission_policies/). + +### Slow mode + +Slow mode helps reduce noise on a channel by limiting users to a maximum of 1 message per cooldown interval (1-120 seconds). Moderators, admins, and server-side API calls are not restricted. + +```python +channel.update({ "cooldown": 30 }) # 30 sec +``` + +For more details, see [Slow Mode & Throttling](/chat/docs/python/slow_mode/). + +### Enforce unique usernames + +This setting prevents users from using duplicate usernames. When enabled with `app`, it enforces uniqueness across the entire app. With `team`, it only enforces within the same team. + +```python +# Enable uniqueness constraints on App level +client.update_app_settings(enforce_unique_usernames="app") + +# Enable uniqueness constraints on Team level +client.update_app_settings(enforce_unique_usernames="team") +``` + +### Slash commands for banning + +Stream Chat supports built-in slash commands like `/ban` and `/unban` for quick moderation actions. Enable these commands on your channel type: + +```python +# Enable ban/unban commands for a channel type +client.update_channel_type( + "messaging", + commands=["ban", "unban", "mute", "unmute", "flag"], +) +``` + +## Simple Moderation features + +### Blocklist + +A Blocklist is a list of words that you can use to moderate chat messages. Stream Chat comes with a built-in Blocklist called `profanity_en_2020_v1` which contains over a thousand of the most common profane words. + +You can manage your own blocklists via the Stream dashboard or APIs to a manage blocklists and configure your channel types to use them. Channel types can be configured to block or flag messages from your users based on your blocklists. To do this you need to configure your channel type(s) with these two configurations: `blocklist` and `blocklist_behavior` . The first one refers to the name of the blocklist and the second must be set as `block` or `flag` . + +- Applications can have up to 15 blocklists in total alongside advanced filters + +- A Blocklist can contain up to 10,000 words, each word can be up to 40 characters + +- The blocklist words must be in lowercase + +- Text matching is done with case insensitive word match (no prefix, post-fix support) + +- Messages are split into words using white spaces and hyphens (cookie-monster matches both "cookie" and "monster") + +So for instance, if you have a blocklist with the word "cream" these messages will be blocked or flagged: + +- She jabbed the spoon in the ice cream and sighed + +- Cream is the best + +and it will not affect any of these + +- Is creamcheese a word? + +- I did not enjoy watching Scream + +> [!NOTE] +> The default blocklist contains material that many will find offensive. + + +#### Setup example + +Blocklists can be managed using the APIs like any other Chat feature. Here is a simple example on how to create a Blocklist and use it for a channel type. + +```python +# add a new blocklist for this app +client.create_blocklist(name="no-cakes", words=["fudge", "cream", "sugar"]) + +# use the blocklist for all channels of type messaging +client.update_channel_type("messaging", blocklist="no-cakes", blocklist_behavior="block") +``` + +#### List available blocklists + +All applications have the `profanity_en_2020_v1` blocklist available. This endpoint returns all blocklists available for this application. + +```python +client.list_blocklists() +``` + +#### Describe a blocklist + +```python +client.get_blocklist("no-cakes") +``` + +#### Create new blocklist + +```python +client.create_blocklist(name="no-cakes", words=["fudge", "cream", "sugar"]) +``` + +#### Update a blocklist + +```python +client.update_blocklist("no-cakes", words=["fudge", "cream", "sugar", "vanilla"]) +``` + +#### Delete a blocklist + +When a blocklist is deleted, it will be automatically removed from all channel types that were using it. + +```python +client.delete_blocklist("no-cakes") +``` + +### Regex + +Regex filters allow you to match and moderate messages using regular expressions. This is useful for filtering patterns like phone numbers, URLs, or complex word variations. Configure regex filters via the Stream dashboard under 'Blocklist & Regex Filters'. + +For detailed configuration, see [Regex, Email, and Domain Filters](/moderation/docs/engines/blocklists-and-regex-filters/). + +### Email/domain allow or block + +You can configure domain and email filters to control what URLs and email addresses can be shared in messages. Set up allowlists or blocklists for specific domains via the Stream dashboard. + +For detailed configuration, see [Regex, Email, and Domain Filters](/moderation/docs/engines/blocklists-and-regex-filters/). + +## User Actions + +### Flag + +Any user can flag a message or user. Flagged content is added to your moderation review queue on the Stream Dashboard. + +```python +client.flag_message(msg["id"], user_id=server_user["id"]) +``` + +#### Reasons & custom data + +You can enhance flags by associating them with a specific reason and custom data. It is advisable to utilise a slug or keyword as a designated reason for easy translation or other forms of display customisation. + +The custom data can encompass any object, offering supplementary metadata to the flag. + +The Query Message Flags endpoint retrieves both reasons and custom data, and the reason can also be utilised for filtering these flags. + + +#### Query Flagged Messages + +If you prefer to build your own in-app moderation dashboard, rather than use the Stream dashboard, you can query flagged messages using the `QueryReviewQueue` API endpoint. + +Both server-authenticated and user-authenticated clients can use this method. For client-side requests, the user needs moderator or admin permissions. + + +Please refer to the [Moderation API](/moderation/docs/api/#query-review-queue) documentation for more details. + +### Mute + +Users can mute other users. Mutes are stored at the user level and returned when `connectUser` is called. Messages from muted users are still delivered via websocket but not via push notifications. + +See [Mute in the Moderation API](/moderation/docs/api/flag-mute-ban/#mute) for full documentation and SDK examples. + +### Block + +The user block feature allows users to control their 1-on-1 interactions within the chat application by blocking other users. + + +#### How blocking impacts chat + +When a user is blocked, several changes occur: + +- **Direct Communication Termination**: When a user blocks another user, communication in all 1-on-1 channels are hidden for the blocking user. +- **Adding to Channels**: If a blocked user tries to add the blocking user to a channel as a member, the action is ignored. The channel will not include the blocking user but will have the remaining members. +- **Push Notifications**: The blocking user will not receive push notifications from blocked users for 1-on-1 channels. +- **Channel Events**: The blocking user will not receive any events from blocked users in 1-on-1 channels (e.g., message.new). +- **Group Channels**: Group channels are unaffected by the block. Both the blocking and blocked users can participate, receive push notifications, and events in group channels. +- **Query Channels**: When hidden channels are requested, 1-on-1 channels with blocked users will be returned with a `blocked:true` flag and all the messages. +- **Active Chats and Unread Counts**: Blocked users will not appear in the blocking user's list of active chats. Messages from blocked users will not contribute to unread counts. +- **Unblocking Users**: After unblocking, all previous messages in 1-on-1 channels become visible again, including those sent during the block period. +- **Hidden Channels**: Channels with only the blocked and blocking users are marked as hidden for the blocking user by default. If a blocked user sends a message in a hidden channel, the channel remains hidden for the blocking user. +- **Group Channel Messages**: Messages from blocked users will still appear when retrieving messages from a group channel. +- **WebSocket Connection**: When connecting to the WebSocket, the blocking user receives a list of users they have blocked (user.blocked_users). This is only available for the blocking user's own account. +- **Message Actions**: Actions such as sending, updating, reacting to, and deleting messages will still work in blocked channels. However, since the channels are hidden, these actions will not be visible to the blocking user. + +#### Block User + +Any user is allowed to block another user. Blocked users are stored at the user level and returned with the rest of the user information when connectUser is called. A user will be blocked until the user is unblocked. + + +#### Unblock user + + +#### List of Blocked Users + + +#### Server Side + +**Block User:** + + +**Unblock user:** + + +**Get List of Blocked Users:** + + +## AI moderation + +AI moderation can detect over 40 harms in 50+ different languages. +In addition to these classification models LLM based moderation is also supported. +Moderation APIs are available at additional costs. +It's priced to be cost-effective and typically is a fraction of the cost of other moderation APIs. + +[Read the full AI moderation docs](/moderation/docs/). + +## Ban + +Users can be banned from an app entirely or from a channel. When banned, they cannot post messages until the ban is removed or expires. You can also apply IP bans and optionally delete the user's messages. + +See [Ban in the Moderation API](/moderation/docs/api/flag-mute-ban/#ban) for full documentation, including shadow bans, query endpoints, and SDK examples. diff --git a/docs/best_practices/query_syntax_operators.md b/docs/best_practices/query_syntax_operators.md new file mode 100644 index 0000000..63d09d4 --- /dev/null +++ b/docs/best_practices/query_syntax_operators.md @@ -0,0 +1,20 @@ +The Stream Chat API allows you to specify filters and ordering for several endpoints. You can query channels, users, and messages. The query syntax is similar to that of Mongoose. + +> [!WARNING] +> We do not run MongoDB on the backend. Only a subset of the MongoDB operations are supported. + + +Please have a look below at the complete list of supported query operations: + +| Name | Description | Example | +| --------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| $eq | Matches values that are equal to a specified value. | { "key": { "$eq": "value" }} or the simplest form { "key": "value"} | +| $q | Full text search (matches values where the whole text value matches the specified value | { "key": { "$q": "value }} | +| $gt | Matches values that are greater than a specified value. | { "key": { "$gt": 4 }} | +| $gte | Matches values that are greater than or equal to a specified value. | { "key": { "$gte": 4 }} | +| $lt | Matches values that are less than a specified value. | { "key": { "$lt": 4 }} | +| $lte | Matches values that are less than or equal to a specified value. | { "key": { "$lte": 4 }} | +| $in | Matches any of the values specified in an array. | { "key": { "$in": [ 1, 2, 4 ] }} | +| $and | Matches all the values specified in an array. | { "$and": [ { "key": { "$in": [ 1, 2, 4 ] } }, { "some_other_key": 10 } ]} | +| $or | Matches at least one of the values specified in an array. | { "$or": [ { "key": { "$in": [ 1, 2, 4 ] } }, { "key2": 10 } ]} | +| $contains | Matches array elements on a column that contains an array | { key: { $contains: 'value' } } | diff --git a/docs/channels/channel_management/archiving.md b/docs/channels/channel_management/archiving.md new file mode 100644 index 0000000..08c7a01 --- /dev/null +++ b/docs/channels/channel_management/archiving.md @@ -0,0 +1,26 @@ +Channel members can archive a channel for themselves. This is a per-user setting that does not affect other members. + +Archived channels function identically to regular channels via the API, but your UI can display them separately. When a channel is archived, the timestamp is recorded and returned as `archived_at` in the response. + +When querying channels, filter by `archived: true` to retrieve only archived channels, or `archived: false` to exclude them. + +## Archive a Channel + +```python +# Get a channel +channel = client.channel("messaging", "general") + +# Archive the channel for user amy +user_id = "amy" +response = channel.archive(user_id) + +# Query for channels that are archived +response = client.query_channels({"archived": True}, user_id=user_id) + +# Unarchive the channel +response = channel.unarchive(user_id) +``` + +## Global Archiving + +Channels are archived for a specific member. If the channel should instead be archived for all users, this can be stored as custom data in the channel itself. The value cannot collide with existing fields, so use a value such as `globally_archived: true`. diff --git a/docs/channels/channel_management/batch-updates.md b/docs/channels/channel_management/batch-updates.md new file mode 100644 index 0000000..a2f1985 --- /dev/null +++ b/docs/channels/channel_management/batch-updates.md @@ -0,0 +1,186 @@ +--- +title: Batch updates +slug: /chat/docs/$REPLACE_FRAMEWORK/batch_updates/ +--- + +## Batch updates + +You can perform batch updates on multiple channels at once. This is useful for making changes to a large number of channels without having to update each one individually, which could result in potentially thousands of API calls. + +This functionality, unlike the single version of update channel, works asynchronously. This means that the request will return immediately, and the updates will be processed in the background. + +When the update is requested, a task will be created and the task ID will be returned in the response. You can use this task ID to check the status of the update operation. + +## How to target channels + +In order to perform batch updates, you need to target which channels you want to update. + +You can do this by providing a filter that matches the channels you want to update. + +The filters that are supported for batch updates are: + +| Name | Description | Available Operators | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cids` | Filters channels by Channel ID (CID) in the format `type:id` (e.g., `"messaging:channel1"`). Must use explicit operators. Direct arrays are not supported. | `$eq`: Single CID value (e.g., `{"cids": {"$eq": "messaging:channel1"}}`)
`$in`: Multiple CID values (e.g., `{"cids": {"$in": ["messaging:channel1", "livestream:channel2"]}}`) | +| `types` | Filters channels by channel type (e.g., `"messaging"`, `"livestream"`, `"team"`). Must use explicit operators. Direct arrays are not supported. | `$eq`: Single channel type (e.g., `{"types": {"$eq": "messaging"}}`)
`$in`: Multiple channel types (e.g., `{"types": {"$in": ["messaging", "livestream"]}}`) | + +### Filter examples + +```python +# 1) Filter by type +filters = { + "types": {"$in": ["messaging"]}, +} + +# 2) Filter by specific CIDs +filters_by_cids = { + "cids": { + "$in": [ + "messaging:3b11838a-7734-4ece-8547-4b8524257671", + "messaging:a266bee6-dc3c-4188-a37d-e554d4bfac34", + "messaging:40fef12a-0b7c-4bcf-bd97-3ddf604efed5", + "messaging:2a58963e-d769-4ce3-9309-bff93c14db57", + ], + }, +} +``` + +## Supported operations + +You can perform different operations on the channels but only once at a time. The supported operations are: + +| Operation Name | Description | Parameters | +| ---------------- | ----------------------------------------------------- | ----------- | +| addMembers | Add members to the channels. | members | +| removeMembers | Remove members from the channels. | members | +| addModerators | Add moderators to the channels. | members | +| demoteModerators | Remove moderator status from members in the channels. | members | +| hide | Hide the channels for members. | members | +| show | Show the channels for members. | members | +| archive | Archive the channels for members. | members | +| unarchive | Unarchive the channels for members. | members | +| updateData | Update the channel data for the channels. | channelData | +| assignRoles | Assign roles to members in the channels. | members | +| inviteMembers | Send invites to users to join the channels. | members | + +## Channel data update properties + +When using the `updateData` operation, you can update the following channel properties. All properties are optional - only the provided values will be updated. + +| Property | Type | Description | +| --------------------------- | ------- | -------------------------------------------- | +| `frozen` | boolean | Freeze the channel to prevent new messages | +| `disabled` | boolean | Disable the channel | +| `custom` | object | Custom data fields for the channel | +| `team` | string | Team ID to assign the channel to | +| `config_overrides` | object | Override channel type configuration settings | +| `auto_translation_enabled` | boolean | Enable automatic message translation | +| `auto_translation_language` | string | Language code for auto translation | + +### Config overrides + +The `config_overrides` object allows you to override the default channel type configuration for specific channels: + +| Property | Type | Description | +| -------------------- | ------- | ---------------------------------------- | +| `typing_events` | boolean | Enable/disable typing indicators | +| `reactions` | boolean | Enable/disable message reactions | +| `replies` | boolean | Enable/disable message replies (threads) | +| `quotes` | boolean | Enable/disable message quotes | +| `uploads` | boolean | Enable/disable file uploads | +| `url_enrichment` | boolean | Enable/disable URL preview enrichment | +| `max_message_length` | integer | Maximum message length (1-20000) | +| `blocklist` | string | Name of the blocklist to apply | +| `blocklist_behavior` | string | Blocklist behavior: `flag` or `block` | +| `grants` | object | Permission grants modifiers | +| `commands` | array | List of enabled command names | + +Most of the operations require additional parameters to be specified, such as the _members_ to add or remove, or the _channelData_ to update. + +We've prepared convenience methods for all operations, some examples are shown below: + +```python +# Add members +updater = client.channel_batch_updater() +filter = { + "types": { + "$in": ["messaging"], + }, +} + +members = [ + {"user_id": "user-123"}, +] + +resp = updater.add_members(filter, members) + +# Update channel data +updater = client.channel_batch_updater() +filter = { + "types": { + "$in": ["messaging", "team"], + }, +} + +data = { + "frozen": True, + "custom": { + "color": "blue", + }, +} + +resp = updater.update_data(filter, data) +``` + +## Webhooks + +When an update is started via batch updates, a webhook event `channel_batch_update.started` will be triggered. + +Additionally, for each channel that is updated, the corresponding webhook events will be triggered as well. +For example, if members are added to channels, the `member.added` event will be triggered for each channel that is updated. + +When the batch update operation is completed, a webhook event `channel_batch_update.completed` will be triggered. This event will contain the task ID and the status of the operation (success or failure). + +For the format of the status please see next section. + +## Status + +You can check the status of a batch update operation by using the task ID returned when the operation was started. + +To get the status of the task, you use the `Get Task` endpoint with the task ID. + +```python +task_response = client.get_task(response["task_id"]) +``` + +The response will contain information about the task, including its status, result, and any errors that occurred during the operation. + +```json +{ + "task_id": "23685bb3-d1c7-492b-a02a-3dbaa12855e1", + "status": "completed", + "created_at": "2025-12-13T10:25:15.856428Z", + "updated_at": "2025-12-13T10:35:00.512601Z", + "result": { + "operation": "show", + "status": "completed", + "success_channels_count": 1080, + "task_id": "23685bb3-d1c7-492b-a02a-3dbaa12855e1", + "batch_created_at": "2025-12-13T10:25:16Z", + "failed_channels": [ + { + "reason": "cannot invite members to the distinct channel", + "cids": ["messaging:550e8400-e29b-41d4-a716-446655440000"] + }, + { + "reason": "user not found: user_id 'user_12345' does not exist", + "cids": ["team:7c9e6679-7425-40de-944b-e07fc1f90ae7"] + } + ], + "finished_at": "2025-12-13T10:35:00.512579053Z" + }, + "duration": "36.23ms" +} +``` + +You can try to re apply the operation on the failed channels again by creating a new task with the same operation and just the CIDs of the failed channels. diff --git a/docs/channels/channel_management/channel_invites.md b/docs/channels/channel_management/channel_invites.md new file mode 100644 index 0000000..9e3ac76 --- /dev/null +++ b/docs/channels/channel_management/channel_invites.md @@ -0,0 +1,47 @@ +Invites allow you to add users to a channel with a pending state. The invited user receives a notification and can accept or reject the invite. + +Unread counts are not incremented for channels with a pending invite. + +## Invite Users + +```python +channel.invite_members(["thierry"]) +``` + +## Accept an Invite + +Call `acceptInvite` to accept a pending invite. You can optionally include a `message` parameter to post a system message when the user joins (e.g., "Nick joined this channel!"). + +```python +channel.accept_invite("thierry") +``` + +## Reject an Invite + +Call `rejectInvite` to decline a pending invite. Client-side calls use the currently connected user. Server-side calls require a `user_id` parameter. + +```python +channel.reject_invite("thierry") +``` + +## Query Invites by Status + +Use `queryChannels` with the `invite` filter to retrieve channels based on invite status. Valid values are `pending`, `accepted`, and `rejected`. + +### Query Accepted Invites + +```python +client.query_channels({"invite": "accepted"}) +``` + +### Query Rejected Invites + +```python +client.query_channels({"invite": "rejected"}) +``` + +### Query Pending Invites + +```python +client.query_channels({"invite": "pending"}) +``` diff --git a/docs/channels/channel_management/deleting.md b/docs/channels/channel_management/deleting.md new file mode 100644 index 0000000..3eb2f5d --- /dev/null +++ b/docs/channels/channel_management/deleting.md @@ -0,0 +1,37 @@ +You can delete or truncate a channel to remove its contents. To remove only messages while preserving the channel, see [Truncate Channel](/chat/docs/python/truncate_channel/). + +## Deleting a Channel + +You can delete a single Channel using the  `delete`  method. This marks the channel as deleted and hides all the messages. + +```python +channel.delete() +``` + +> [!NOTE] +> If you recreate this channel, it will show up empty. Recovering old messages is not supported. Use the disable method if you want a reversible change. + + +## Deleting Many Channels + +You can delete up to 100 channels and optionally all of their messages using this method. This can be a large amount of data to delete, so this endpoint processes asynchronously, meaning responses contain a `task ID` which can be polled using the [getTask endpoint](/chat/docs/python#tasks-gettask) to check status of the deletions. Channels will be soft-deleted immediately so that channels no longer return from queries, but permanently deleting the channel and deleting messages takes longer to process. + +By default, messages are soft deleted, which means they are removed from client but are still available via server-side export functions. You can also hard delete messages, which deletes them from everywhere, by setting `"hard_delete": true` in the request. Messages that have been soft or hard deleted cannot be recovered. + +This is currently supported on the following SDK versions (or higher): + +- Javascript 4.3.0, Python 3.14.0, Ruby 2.12.0, PHP 2.6.0, Go 3.13.0, Java 1.4.0, Unity 2.0.0 and .NET 0.22.0 + +```python +# soft deletion +response = client.delete_channels([cid1, cid2]) +# hard deletion +response = client.delete_channels([cid1, cid2], hard_delete=True) + +response = client.get_task(response["task_id"]) +if response['status'] == 'completed': + # success! + pass +``` + +The  `deleteChannels`  response contain a taskID which can be polled using the [getTask endpoint](/chat/docs/python#tasks-gettask) to check the status of the deletions. diff --git a/docs/channels/channel_management/disabling.md b/docs/channels/channel_management/disabling.md new file mode 100644 index 0000000..e79444e --- /dev/null +++ b/docs/channels/channel_management/disabling.md @@ -0,0 +1,25 @@ +Disabling a channel is a visibility and access toggle. The channel and all its data remain intact, but client-side read and write operations return a `403 Not Allowed` error. Server-side access is preserved for admin operations like moderation and data export. + +Disabled channels still appear in query results by default. This means users see the channel in their list but receive errors when attempting to open it. To hide disabled channels from users, filter them out in your queries: + + +Re-enabling a channel restores full client-side access with all historical messages intact. + +## Disable a Channel + +```python +# disable a channel with full update +channel.update({"disabled": True}) + +# disable a channel with partial update +channel.update_partial(to_set={"disabled": True}) + +# enable a channel with full update +channel.update({"disabled": False}) + +# enable a channel with partial update +channel.update_partial(to_set={"disabled": False}) +``` + +> [!NOTE] +> To prevent new messages while still allowing users to read existing messages, use [freeze the channel](/chat/docs/python/freezing_channels/) instead. diff --git a/docs/channels/channel_management/freezing.md b/docs/channels/channel_management/freezing.md new file mode 100644 index 0000000..ea74f23 --- /dev/null +++ b/docs/channels/channel_management/freezing.md @@ -0,0 +1,31 @@ +Freezing a channel prevents users from sending new messages and adding or deleting reactions. + +Sending a message to a frozen channel returns an error message. Attempting to add or delete reactions returns a `403 Not Allowed` error. + +User roles with the `UseFrozenChannel` permission can still use frozen channels normally. By default, no user role has this permission. + +## Freeze a Channel + +```python +channel.update({"frozen": True}) +``` + +## Unfreeze a Channel + +```python +channel.update({"frozen": False}) +``` + +## Granting the Frozen Channel Permission + +Permissions are typically managed in the [Stream Dashboard](https://dashboard.getstream.io/) under your app's **Roles & Permissions** settings. This is the recommended approach for most use cases. + +To grant permissions programmatically, update the channel type using a server-side API call. See [user permissions](/chat/docs/python/chat_permission_policies/) for more details. + +```python +resp = client.get_channel_type("messaging") + +adminGrants = resp["grants"]["admin"] + ["use-frozen-channel"] + +client.update_channel_type("messaging", grants={"admin": adminGrants}) +``` diff --git a/docs/channels/channel_management/hiding.md b/docs/channels/channel_management/hiding.md new file mode 100644 index 0000000..7338ebb --- /dev/null +++ b/docs/channels/channel_management/hiding.md @@ -0,0 +1,18 @@ +Hiding a channel removes it from query channel requests for that user until a new message is added. Only channel members can hide a channel. + +Hidden channels may still have unread messages. Consider [marking the channel as read](/chat/docs/python/unread/) before hiding it. + +You can optionally clear the message history for that user when hiding. When a new message is received, it will be the only message visible to that user. + +## Hide a Channel + +```python +# Hide the channel until a new message is added +channel.hide("john") + +# Show a previously hidden channel +channel.show("john") +``` + +> [!NOTE] +> You can still retrieve the list of hidden channels using the `{ "hidden" : true }` query parameter. diff --git a/docs/channels/channel_management/muting.md b/docs/channels/channel_management/muting.md new file mode 100644 index 0000000..42d2b91 --- /dev/null +++ b/docs/channels/channel_management/muting.md @@ -0,0 +1,32 @@ +Muting a channel prevents it from triggering push notifications, unhiding, or incrementing the unread count for that user. + +By default, mutes remain active indefinitely until removed. You can optionally set an expiration time. The list of muted channels and their expiration times is returned when the user connects. + +## Mute a Channel + +```python +channel.mute("john") + +# With expiration +channel.mute("john", expiration=30000) +``` + +> [!NOTE] +> Messages added to muted channels do not increase the unread messages count. + + +### Query Muted Channels + +Muted channels can be filtered or excluded by using the `muted` in your query channels filter. + +```python +client.query_channels({"muted": True}) +``` + +### Remove a Channel Mute + +Use the unmute method to restore normal notifications and unread behavior for a channel. + +```python +channel.unmute("john") +``` diff --git a/docs/channels/channel_management/overview.md b/docs/channels/channel_management/overview.md new file mode 100644 index 0000000..973e5b7 --- /dev/null +++ b/docs/channels/channel_management/overview.md @@ -0,0 +1,51 @@ +Stream Chat provides a variety of channel management APIs that allow you to control how channels behave within your application. This page provides an overview of the different channel management operations available. + +## Overview + +Channel management operations can be broadly categorized into: + +| Operation | Description | User Impact | Data Impact | +| ------------------------------------------------------- | ---------------------------------------------------- | ------------------ | ---------------------- | +| [Archiving](/chat/docs/python/archiving_channels/) | Mark a channel as archived for a specific user | Per-user state | No data loss | +| [Pinning](/chat/docs/python/pinning_channels/) | Mark a channel as pinned for a specific user | Per-user state | No data loss | +| [Muting](/chat/docs/python/muting_channels/) | Suppress notifications for a channel | Per-user state | No data loss | +| [Hiding](/chat/docs/python/hiding_channels/) | Hide a channel from query results until new messages | Per-user state | Optional history clear | +| [Disabling](/chat/docs/python/disabling_channels/) | Prevent all client-side access to a channel | All users affected | No data loss | +| [Freezing](/chat/docs/python/freezing_channels/) | Prevent new messages and reactions | All users affected | No data loss | +| [Truncating](/chat/docs/python/truncate_channel/) | Remove messages from a channel | All users affected | Message data deleted | +| [Deleting](/chat/docs/python/channel_delete/) | Permanently remove a channel | All users affected | All data deleted | + +## Choosing the Right Operation + +### Per-User Operations + +These operations only affect the individual user and are ideal for personal organization: + +- **Archiving**: Use when a user wants to declutter their channel list but keep the channel accessible +- **Pinning**: Use when a user wants to prioritize certain channels at the top of their list +- **Muting**: Use when a user wants to stay in a channel but not receive notifications +- **Hiding**: Use when a user wants to temporarily remove a channel from view + +### Channel-Wide Operations + +These operations affect all users in the channel and typically require admin or moderator permissions: + +- **Disabling**: Use when you need to completely block access to a channel (e.g., for moderation) +- **Freezing**: Use when you want to preserve a channel's content but prevent new activity (e.g., archived discussions) +- **Truncating**: Use when you need to clear message history but keep the channel active +- **Deleting**: Use when a channel is no longer needed and should be permanently removed + +## Server-Side vs Client-Side + +Most channel management operations can be performed from both client-side and server-side SDKs, but some operations are restricted to server-side only for security reasons: + +| Operation | Client-Side | Server-Side | +| ---------- | ---------------------- | ----------- | +| Archiving | ✅ | ✅ | +| Pinning | ✅ | ✅ | +| Muting | ✅ | ✅ | +| Hiding | ✅ | ✅ | +| Disabling | ❌ | ✅ | +| Freezing | ✅ | ✅ | +| Truncating | ❌ | ✅ | +| Deleting | Depends on permissions | ✅ | diff --git a/docs/channels/channel_management/pinning.md b/docs/channels/channel_management/pinning.md new file mode 100644 index 0000000..13127fd --- /dev/null +++ b/docs/channels/channel_management/pinning.md @@ -0,0 +1,33 @@ +Channel members can pin a channel for themselves. This is a per-user setting that does not affect other members. + +Pinned channels function identically to regular channels via the API, but your UI can display them separately. When a channel is pinned, the timestamp is recorded and returned as `pinned_at` in the response. + +When querying channels, filter by `pinned: true` to retrieve only pinned channels, or `pinned: false` to exclude them. You can also sort by `pinned_at` to display pinned channels first. + +## Pin a Channel + +```python +# Get a channel +channel = client.channel("messaging", "general") + +# Pin the channel for user amy +user_id = "amy" +response = channel.pin(user_id) + +# Query for channels that are pinned +response = client.query_channels({"pinned": True}, user_id=user_id) + +# Query for channels for specific members and show pinned first +response = client.query_channels( + {"members": {"$in": ["amy", "ben"]}}, + {"pinned_at": -1}, + user_id=user_id +) + +# Unpin the channel +response = channel.unpin(user_id) +``` + +## Global Pinning + +Channels are pinned for a specific member. If the channel should instead be pinned for all users, this can be stored as custom data in the channel itself. The value cannot collide with existing fields, so use a value such as `globally_pinned: true`. diff --git a/docs/channels/channel_management/truncating.md b/docs/channels/channel_management/truncating.md new file mode 100644 index 0000000..5730ddf --- /dev/null +++ b/docs/channels/channel_management/truncating.md @@ -0,0 +1,33 @@ +Truncating a channel removes all messages but preserves the channel data and members. To delete both the channel and its messages, use [Delete Channel](/chat/docs/python/channel_delete/) instead. + +Truncation can be performed client-side or server-side. Client-side truncation requires the `TruncateChannel` permission. + +On server-side calls, use the `user_id` field to identify who performed the truncation. + +By default, truncation hides messages. To permanently delete messages, set `hard_delete` to `true`. + +## Truncate a Channel + +```python +channel.truncate() + +# Or with parameters: +channel.truncate( + hard_delete=True, + skip_push=True, + message={ + "text": "Dear Everyone. The channel has been truncated.", + "user_id": random_user["id"], + }, + ) +``` + +## Truncate Options + +| Field | Type | Description | Optional | +| ------------ | ------ | ------------------------------------------------------ | -------- | +| truncated_at | Date | Truncate messages up to this time | ✓ | +| user_id | string | User who performed the truncation (server-side only) | ✓ | +| message | object | A system message to add after truncation | ✓ | +| skip_push | bool | Do not send a push notification for the system message | ✓ | +| hard_delete | bool | Permanently delete messages instead of hiding them | ✓ | diff --git a/docs/channels/channel_members.md b/docs/channels/channel_members.md new file mode 100644 index 0000000..7e03956 --- /dev/null +++ b/docs/channels/channel_members.md @@ -0,0 +1,105 @@ +Channel members are users who have been added to a channel and can participate in conversations. This page covers how to manage channel membership, including adding and removing members, controlling message history visibility, and managing member roles. + +## Adding and Removing Members + +### Adding Members + +Using the `addMembers()` method adds the given users as members to a channel. + +```python +channel.add_members(["thierry", "josh"]) +``` + +> [!NOTE] +> **Note:** You can only add/remove up to 100 members at once. + + +Members can also be added when creating a channel: + + +### Removing Members + +Using the `removeMembers()` method removes the given users from the channel. + +```python +channel.remove_members(["tommaso"]) +``` + +### Leaving a Channel + +Users can leave a channel without moderator-level permissions. Ensure channel members have the `Leave Own Channel` permission enabled. + + +> [!NOTE] +> You can familiarize yourself with all permissions in the [Permissions section](/chat/docs/python/chat_permission_policies/). + + +## Hide History + +When members join a channel, you can specify whether they have access to the channel's message history. By default, new members can see the history. Set `hide_history` to `true` to hide it for new members. + +```python +channel.add_members(["thierry"], hide_history=True) +``` + +### Hide History Before a Specific Date + +Alternatively, `hide_history_before` can be used to hide any history before a given timestamp while giving members access to later messages. The value must be a timestamp in the past in RFC 3339 format. If both parameters are defined, `hide_history_before` takes precedence over `hide_history`. + +```python +from datetime import datetime, timedelta, timezone +cutoff = datetime.now(timezone.utc) - timedelta(days=7) # Last 7 days +channel.add_members(["thierry"], hide_history_before=cutoff) +``` + +## System Message Parameter + +You can optionally include a message object when adding or removing members that client-side SDKs will use to display a system message. This works for both adding and removing members. + +```python +channel.add_members(["tommaso", "josh"], { "text": 'Tommaso joined the channel.', "user_id": 'tommaso' }) +``` + +## Adding and Removing Moderators + +Using the `addModerators()` method adds the given users as moderators (or updates their role to moderator if already members), while `demoteModerators()` removes the moderator status. + +### Add Moderators + +```python +channel.add_moderators(["thierry", "josh"]) +``` + +### Remove Moderators + +```python +channel.demote_moderators(["tommaso"]) +``` + +> [!NOTE] +> These operations can only be performed server-side, and a maximum of 100 moderators can be added or removed at once. + + +## Member Custom Data + +Custom data can be added at the channel member level. This is useful for storing member-specific information that is separate from user-level data. Ensure custom data does not exceed 5KB. + +### Adding Custom Data + + +### Updating Member Data + +Channel members can be partially updated. Only custom data and channel roles are eligible for modification. You can set or unset fields, either separately or in the same call. + +```python +user_id = "amy" + +# Set some fields +response = channel.update_member_partial(user_id, to_set={"hat": "blue"}) + +# Unset some fields +response = channel.update_member_partial(user_id, to_set=None, to_unset=["hat"]) + +# Set and unset in the same call +response = channel.update_member_partial(user_id, to_set={"color": "red"}, to_unset=["hat"]) +``` diff --git a/docs/channels/channel_pagination.md b/docs/channels/channel_pagination.md new file mode 100644 index 0000000..b87ba66 --- /dev/null +++ b/docs/channels/channel_pagination.md @@ -0,0 +1,57 @@ +The channel query endpoint allows you to paginate messages, watchers, and members for a channel. Messages use ID-based pagination for consistency, while members and watchers use offset-based pagination. + +## Message Pagination + +Message pagination uses ID-based parameters rather than simple offset/limit. This approach improves performance and prevents issues when the message list changes while paginating. + +For example, if you fetched the first 100 messages and want to load the next 100, pass the ID of the oldest message (when paginating in descending order) or the newest message (when paginating in ascending order). + +### Pagination Parameters + +| Parameter | Description | +| ----------- | -------------------------------------------------- | +| `id_lt` | Retrieve messages older than (less than) the ID | +| `id_gt` | Retrieve messages newer than (greater than) the ID | +| `id_lte` | Retrieve messages older than or equal to the ID | +| `id_gte` | Retrieve messages newer than or equal to the ID | +| `id_around` | Retrieve messages around a specific message ID | + +```python +# Get the ID of the oldest message on the current page +last_message_id = messages[0]["id"] + +# Fetch older messages +result = channel.query( + messages={"limit": 50, "id_lt": last_message_id}, +) + +# Fetch messages around a specific message +result = channel.query( + messages={"limit": 20, "id_around": message_id}, +) +``` + +## Member and Watcher Pagination + +Members and watchers use `limit` and `offset` parameters for pagination. + +| Parameter | Description | Maximum | +| --------- | --------------------------- | ------- | +| `limit` | Number of records to return | 300 | +| `offset` | Number of records to skip | 10000 | + +```python +# Paginate members and watchers +result = channel.query( + members={"limit": 20, "offset": 0}, + watchers={"limit": 20, "offset": 0}, +) + +result = channel.query( + state=True, + members={"limit": 110, "offset": 0}, +) +``` + +> [!NOTE] +> To retrieve filtered and sorted members in a channel use the [Query Members](/chat/docs/python/query_members/) API diff --git a/docs/channels/channel_update.md b/docs/channels/channel_update.md new file mode 100644 index 0000000..fce85b3 --- /dev/null +++ b/docs/channels/channel_update.md @@ -0,0 +1,49 @@ +There are two ways to update a channel with the Stream API: partial updates and full updates. A partial update preserves existing custom key–value data, while a full update replaces the entire channel object and removes any fields not included in the request. + +## Partial Update + +A partial update lets you set or unset specific fields without affecting the rest of the channel’s custom data — essentially a patch-style update. + +```python +# Here's a channel with some custom field data that might be useful +channel = client.channel(type, id , { + "source": "user", + "source_detail":{ "user_id": 123 }, + "channel_detail":{ "topic": "Plants and Animals", "rating": "pg" } +}) + +# let's change the source of this channel +channel.update_partial(to_set={ "source": "system" }); + +# since it's system generated we no longer need source_detail +channel.update_partial(to_unset=["source_detail"]); + +# and finally update one of the nested fields in the channel_detail +channel.update_partial(to_set={ "channel_detail.topic": "Nature" }); + +# and maybe we decide we no longer need a rating +channel.update_partial(to_unset=["channel_detail.rating"]); +``` + +## Full Update + +The `update` function updates all of the channel data. **Any data that is present on the channel and not included in a full update will be deleted.** + +```python +channel.update( + { + "name": "myspecialchannel", + "color": "green", + }, +) +``` + +### Request Params + +| Name | Type | Description | Optional | +| ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | +| channel data | object | Object with the new channel information. One special field is "frozen". Setting this field to true will freeze the channel. Read more about freezing channels in "Freezing Channels" | | +| text | object | Message object allowing you to show a system message in the Channel that something changed. | Yes | + +> [!NOTE] +> Updating a channel using these methods cannot be used to add or remove members. For this, you must use specific methods for adding/removing members, more information can be found [here](/chat/docs/python/channel_members/). diff --git a/docs/channels/creating_channels.md b/docs/channels/creating_channels.md new file mode 100644 index 0000000..83e24f3 --- /dev/null +++ b/docs/channels/creating_channels.md @@ -0,0 +1,54 @@ +Channels must be created before users can start chatting. Channel creation can occur either client-side or server-side, depending on your app’s requirements. Client-side creation is ideal for apps where users can freely start conversations (for example, a Slack-style workforce management app). Server-side creation is preferred in apps that require business logic before a chat can begin, such as dating apps where users must match first. To limit channel creation to server-side only, remove Create Channel [permissions](/chat/docs/python/chat_permission_policies/) for your users. + +There are two ways to create channels: by specifying a channel ID or by creating distinct channels. + +## Creating a Channel Using a Channel ID + +This approach works best when your app already has a database object that naturally maps to a chat channel. For example, in a Twitch-style live-streaming service, each streamer has a unique ID you can reuse as the channel ID, making it easy to route users to the correct chat. Using explicit IDs keeps channels predictable and easy to reference throughout your application. + +```python +channel = client.channel("messaging", "travel") +channel.create("myuserid") +``` + +## Distinct Channels + +Distinct channels are ideal when you want a single, unique conversation for a specific set of users. By leaving the channel ID empty and specifying only the channel type and members, Stream automatically generates a channel ID by hashing the list of members (order does not matter). This ensures that the same group of users will always reference the same channel, preventing duplicate conversations. + +> [!NOTE] +> You cannot add members for channels created this way, but members can be removed. + + +```python +channel = client.channel("messaging", data=dict(members=["thierry"])) +channel.create("myuserid") +``` + +When you create a channel using one of the above approaches, you'll specify the following fields: + +| name | type | description | default | optional | +| ------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | +| type | string | The channel type. Default types are livestream, messaging, team, gaming and commerce. You can also create your own types. | - | | +| id | string | The channel id (optional). If you don't specify an ID the ID will be generated based on the list of members. (max length 64 characters) | - | ✓ | +| channel data | object | Extra data for the channel. Must not exceed 5KB in size. | default | ✓ | + +## Channel Data + +Channel data can include any number of custom fields as long as the total payload stays under 5KB. Some fields are reserved—such as `members`—and our UI components also use `name` and `image` when rendering channel lists and headers. In general, you should store only the data that's essential to your chat experience and avoid adding fields that change frequently. + +| Name | Type | Description | +| ----------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The channel name. No special meaning, but by default the UI component will try to render this if the property is present. | +| image | string | The channel image. Again there is no special meaning but by default, the UI component will try to render this if the property is present. | +| members | array | The members participating in this Channel. Note that you don't need to specify members for a live stream or other public chat. You only need to specify the members if you want to limit access of this chat to these members and subscribe them to future updates. | +| custom_data | various types | Channels can contain up to 5KB of custom data. | + +## Watching Channels + +Once a channel exists—or as it is being created on the client—it can be watched by client devices. Watching a channel subscribes the client’s WebSocket connection to real-time updates, such as new messages, membership changes, and reactions. This allows the SDKs to keep channel state and UI in sync automatically. + +If your app lets users navigate to a channel client-side, you should use `channel.watch()`. This is a get-or-create method: it fetches and watches the channel if it already exists, or creates and watches it if it doesn't. `channel.watch()` returns the full channel state—including members, watchers, and messages—so your UI can render immediately. + +For loading many channels at once, use [client.queryChannels()](/chat/docs/python/query_channels/)—this fetches and watches multiple channels in a single call, reducing API traffic. + +Note that watching a channel is different from being a channel member. A watcher is a temporary, real-time subscription to updates, while a member is a persistent association with the channel. diff --git a/docs/channels/query_channels.md b/docs/channels/query_channels.md new file mode 100644 index 0000000..15c84fd --- /dev/null +++ b/docs/channels/query_channels.md @@ -0,0 +1,443 @@ +Channel lists are a core part of most messaging applications, and our SDKs make them easy to build using the Channel List components. These lists are powered by the Query Channels API, which retrieves channels based on filter criteria, sorting options, and pagination settings. + +Here's an example of how you can query the list of channels: + +```python +client.query_channels( + {"members": {"$in": ["elon", "jack", "jessie"]}}, + {"last_message_at": 1}, + limit=10, +) +``` + +## Query Parameters + +| Name | Type | Description | Default | Optional | +| ------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------- | +| filters | object | Filter criteria for channel fields. See [Queryable Fields](#channel-queryable-built-in-fields) for available options. | {} | | +| sort | object or array of objects | Sorting criteria based on field and direction. You can sort by **last_updated**, **last_message_at**, **updated_at**, **created_at**, **member_count**, **unread_count**, or **has_unread**. Direction can be ascending (1) or descending (-1). Multiple sort options can be provided. | [{last_updated: -1}] | | +| options | object | Additional query options. See [Query Options](#query-options). | {} | | + +> [!NOTE] +> An empty filter matches all channels in your application. In production, always include at least `members: { $in: [userID] }` to return only channels the user belongs to. + + +The API only returns channels that the user has permission to read. For messaging channels, this typically means the user must be a member. Include appropriate filters to match your channel type's permission model. + +## Common Filters + +Understanding which filters perform well at scale helps you build efficient channel queries. This section covers common filter patterns with their performance characteristics. + +> [!TIP] +> **Performance Summary**: Filters using indexed fields (`cid`, `type`, `members`, `last_message_at`) perform best. See [Performance Considerations](#performance-considerations) for detailed guidance. + + +### Messaging and Team Channels + +For most messaging applications, filter by channel type and membership. This pattern uses indexed fields and performs well at scale. + +> [!WARNING] +> **High membership counts**: For users with a large number of channel memberships (more than a few thousand), filtering by `members: { $in: [userID] }` becomes less selective and may cause performance issues. In these cases, consider adding additional filters (like `last_message_at`) to narrow the result set. + + +```python +filter = {"type": "messaging", "members": {"$in": ["thierry"]}} +``` + +## Channel Queryable Built-In Fields + +The following fields can be used in your filter criteria: + +| Name | Type | Description | Supported Operators | Example | +| ---------------- | ------------------------------------ | ------------------------------------------------------------------------------ | ---------------------------------- | ----------------------- | +| frozen | boolean | Channel frozen status | $eq | false | +| type | string or list of string | Channel type | $in, $eq | messaging | +| cid | string or list of string | Full channel ID (type:id) | $in, $eq | messaging:general | +| members | list of string | User IDs of channel members | $in | [thierry, marcelo] | +| invite | string (pending, accepted, rejected) | Invite status | $eq | pending | +| joined | boolean | Whether the current user has joined the channel | $eq | true | +| muted | boolean | Whether the current user has muted the channel | $eq | true | +| member.user.name | string | Name property of a channel member | $autocomplete, $eq | marc | +| created_by_id | string | ID of the user who created the channel | $eq | marcelo | +| hidden | boolean | Whether the current user has hidden the channel | $eq | false | +| last_message_at | string (RFC3339 timestamp) | Time of the last message | $eq, $gt, $lt, $gte, $lte, $exists | 2021-01-15T09:30:20.45Z | +| member_count | integer | Number of members | $eq, $gt, $lt, $gte, $lte | 5 | +| created_at | string (RFC3339 timestamp) | Channel creation time | $eq, $gt, $lt, $gte, $lte, $exists | 2021-01-15T09:30:20.45Z | +| updated_at | string (RFC3339 timestamp) | Channel update time | $eq, $gt, $lt, $gte, $lte | 2021-01-15T09:30:20.45Z | +| team | string | Team associated with the channel | $eq | stream | +| last_updated | string (RFC3339 timestamp) | Time of last message, or channel creation time if no messages exist | $eq, $gt, $lt, $gte, $lte | 2021-01-15T09:30:20.45Z | +| disabled | boolean | Channel disabled status | $eq | false | +| has_unread | boolean | Whether the user has unread messages (only `true` supported, max 100 channels) | true | true | +| app_banned | string | Filter by application-banned users (only for 2-member channels) | excluded, only | excluded | + +> [!NOTE] +> For supported query operators, see [Query Syntax Operators](/chat/docs/python/query_syntax_operators/). + + +> [!NOTE] +> The `app_banned` filter only works on direct message channels with exactly 2 members. + + +## Query Options + +| Name | Type | Description | Default | Optional | +| ------------- | ------- | ---------------------------------------------------- | ------- | -------- | +| state | boolean | Return channel state | true | ✓ | +| watch | boolean | Subscribe to real-time updates for returned channels | true | ✓ | +| limit | integer | Number of channels to return (max 30) | 10 | ✓ | +| offset | integer | Number of channels to skip (max 1000) | 0 | ✓ | +| message_limit | integer | Messages to include per channel (max 300) | 25 | ✓ | +| member_limit | integer | Members to include per channel (max 100) | 100 | ✓ | + +> [!TIP] +> **Performance Tip**: Setting `state: false` and `watch: false` reduces response size and processing time. Use these options when you only need channel IDs or basic metadata—for example, during background syncs, administrative operations, or when building lightweight channel lists that don't require full state. + + +## Response + +The API returns a list of `ChannelState` objects containing all information needed to render channels without additional API calls. + +### ChannelState Fields + +| Field Name | Description | +| --------------- | --------------------------------------------------------------------------------------------------------------- | +| channel | Channel data | +| messages | Recent messages (based on message_limit) | +| watcher_count | Number of users currently watching | +| read | Read state for up to 100 members, ordered by most recently added (current user's read state is always included) | +| members | Up to 100 members, ordered by most recently added | +| pinned_messages | Up to 10 most recent pinned messages | + +
+Example Response + +```json +[ + { + "id": "f8IOxxbt", + "type": "messaging", + "cid": "messaging:f8IOxxbt", + "last_message_at": "2020-01-10T07:26:46.791232Z", + "created_at": "2020-01-10T07:25:37.63256Z", + "updated_at": "2020-01-10T07:25:37.632561Z", + "created_by": { + "id": "8ce4c6e11118ca103a0a7c633dcf60dd", + "role": "admin", + "created_at": "2019-08-27T17:33:14.442265Z", + "updated_at": "2020-01-10T07:25:36.402819Z", + "last_active": "2020-01-10T07:25:36.395796Z", + "banned": false, + "online": false, + "image": "https://ui-avatars.com/api/?name=mezie&size=192&background=000000&color=6E7FFE&length=1", + "name": "mezie", + "username": "mezie" + }, + "frozen": false, + "config": { + "created_at": "2020-01-20T10:23:44.878185331Z", + "updated_at": "2020-01-20T10:23:44.878185458Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Video Call" + } +] +``` + +
+ +## Pagination + +Use `limit` and `offset` to paginate through results: + +```python +client.query_channels( + {"members": {"$in": ["thierry"]}}, + {"last_message_at": -1}, + limit=20, + offset=10, +) +``` + +> [!NOTE] +> Always include `members: { $in: [userID] }` in your filter to ensure consistent pagination results. Without this filter, channel list changes may cause pagination issues. + + +## Best Practices + +### Channel Creation and Watching + +A channel is not created in the API until one of the following methods is called. Each method has subtle differences: + + +Only one of these is necessary. For example, calling `watch` automatically creates the channel in addition to subscribing to real-time updates—there's no need to call `create` separately. + +With `queryChannels`, a user can watch up to 30 channels in a single API call. This eliminates the need to watch channels individually using `channel.watch()` after querying. Using `queryChannels` can substantially decrease API calls, reducing network traffic and improving performance when working with many channels. + +### Filter Best Practices + +Channel lists often form the backbone of the chat experience and are typically one of the first views users see. Use the most selective filter possible: + +- **Filter by CID** is the most performant query you can use +- **For social messaging** (DMs, group chats), use at minimum `type` and `members: { $in: [userID] }` +- **Avoid overly complex queries** with more than one AND or OR statement +- **Filtering by type alone** is not recommended—always include additional criteria +- **Use Predefined Filters** in production for frequently used query patterns + + +> [!TIP] +> If your filter returns more than a few thousand channels, consider adding more selective criteria. For frequently used query patterns, use [Predefined Filters](#predefined-filters) to enable performance monitoring through the Dashboard. [Contact support](https://getstream.io/contact/support/) if you plan on having millions of channels and need guidance on optimal filters. + + +### Sort Best Practices + +Always specify a sort parameter in your query. The default is `last_updated` (the more recent of `created_at` and `last_message_at`). + +The most optimized sort options are: + +- `last_updated` (default) +- `last_message_at` + + +For the full list of supported query operators, see [Query Syntax Operators](/chat/docs/python/query_syntax_operators/). + +### Recommended Query Patterns + +Following recommended patterns helps ensure your queries perform well as your application scales. Here are examples of good and bad query patterns for all server-side SDKs. + +#### Good Pattern: Selective Filter with Indexed Fields + +Use indexed fields like `type`, `members`, and `last_message_at` for efficient queries: + +```python +# ✅ GOOD: Selective filter using indexed fields +filter = { + "type": "messaging", + "members": {"$in": [user_id]}, + "last_message_at": {"$gte": thirty_days_ago}, +} + +channels = client.query_channels( + filter, + {"last_message_at": -1}, + limit=20, +) +``` + +#### Bad Pattern: Overly Broad or Complex Filters + +Avoid overly broad filters or deep nesting of logical operators, which can cause performance issues at scale and may result in dynamic rate limiting: + +```python +# ❌ BAD: Type-only filter (too broad) +broad_filter = {"type": "messaging"} + +# ❌ BAD: Deep nesting of logical operators +nested_filter = { + "$and": [ + { + "$or": [ + {"frozen": True}, + {"disabled": True}, + ] + }, + { + "$or": [ + {"hidden": True}, + {"muted": True}, + ] + }, + ] +} +``` + +### Using Predefined Filters in Production + +For frequently used query patterns, use [Predefined Filters](#predefined-filters) in production. They provide several benefits: + +- **Consistency**: Define filter logic once and reuse across your application +- **Performance Monitoring**: View performance analysis through the Dashboard +- **Optimization Insights**: Receive recommendations for improving slow queries + +```python +# Production-ready: Use Predefined Filter +channels = client.query_channels( + predefined_filter="user_messaging_channels", + filter_values={"user_id": user_id}, + sort={"last_message_at": -1}, + limit=20, +) +``` + +### Monitoring Query Performance + +Use the [Stream Dashboard](https://beta.dashboard.getstream.io) to monitor and optimize your QueryChannels performance: + +1. **Create Predefined Filters** for your frequently used query patterns +2. **View Performance Analysis** in the Dashboard once filters receive traffic +3. **Review Recommendations** for optimization opportunities +4. **Track Improvements** over time as you optimize your queries + +> [!NOTE] +> **Performance insights availability**: Performance scores and recommendations become available once a filter/sort combination receives significant traffic. Not all filters will show analysis immediately—the system needs sufficient usage data to provide meaningful insights. + + +## Predefined Filters + +Predefined Filters are reusable, templated filter configurations that you create and manage in the [Stream Dashboard](https://beta.dashboard.getstream.io). They provide a recommended approach for production QueryChannels usage. + +### Why Use Predefined Filters + +- **Consistency**: Define filter logic once and reuse it across your application +- **Dashboard Management**: Create, update, and monitor filters through the Dashboard +- **Performance Insights**: View performance analysis for your filters directly in the Dashboard once they receive significant traffic +- **Dynamic Values**: Use placeholders for values that change at query time (like user IDs) + +### Creating Predefined Filters + +Create and manage Predefined Filters in the [Stream Dashboard](https://beta.dashboard.getstream.io). Navigate to your app's settings to define filter templates with placeholders for dynamic values. + +### Using Predefined Filters + +Reference a predefined filter by name and provide values for any placeholders: + +```python +channels = client.query_channels( + predefined_filter="user_messaging_channels", + filter_values={"user_id": "user123"}, + limit=20, +) +``` + +### Placeholder Syntax + +Placeholders use double curly braces: `{{placeholder_name}}` + +When creating a predefined filter in the Dashboard, you can define templates like: + +```json +{ + "type": "{{channel_type}}", + "members": { + "$in": "{{users}}" + } +} +``` + +At query time, provide the actual values via `filter_values`: + +```json +{ + "predefined_filter": "user_messaging_channels", + "filter_values": { + "channel_type": "messaging", + "users": ["user123", "user456"] + } +} +``` + +You can also use placeholders in sort field names. Provide these values via `sort_values`: + +```json +{ + "predefined_filter": "team_channels", + "filter_values": { + "team_id": "engineering" + }, + "sort_values": { + "sort_field": "last_message_at" + } +} +``` + +### Performance Analysis + +The Dashboard displays performance analysis for your Predefined Filters. Performance scores and recommendations become available once a filter receives significant traffic or exhibits notable latency. Not all filters will show analysis immediately—the system needs sufficient usage data to provide meaningful insights. + +## Performance Considerations + +QueryChannels performance depends on your filter complexity and the volume of data. Understanding which fields perform well helps you build efficient queries. + +### Well-Optimized Fields + +These fields are indexed and perform efficiently at scale: + +- `cid` (full channel ID) +- `type` +- `members` +- `created_at` +- `last_message_at` +- `last_updated` + +### Fields to Use with Caution + +These fields may have performance implications at scale: + +- `member_count`: Can be slow for large datasets +- `frozen`: Limited index support +- **Complex nested queries**: Multiple `$and`/`$or` combinations + +### Query Complexity + +Simple, selective filters perform better than complex queries: + +```python +# RECOMMENDED: Simple, selective filter with indexed fields +filter = { + "type": "messaging", + "members": {"$in": [user_id]}, + "last_message_at": {"$gte": thirty_days_ago}, +} +``` + +### Sort Performance + +The most efficient sort fields are: + +- `last_message_at` +- `last_updated` +- `created_at` + +### Pagination Best Practices + +For consistent and efficient pagination: + +- **Use reasonable limits**: The default limit is 10 and max is 30. Larger page sizes increase response time and payload size. +- **Include a members filter**: Always include `members: { $in: [userID] }` in your filter for consistent pagination. Without this, channel list changes during pagination can cause channels to be skipped or duplicated. +- **Respect the offset maximum**: The maximum offset is 1000. For datasets larger than this, use time-based filtering (e.g., `last_message_at` or `created_at`) to paginate through older data. + +```python +# Efficient pagination with members filter +channels = client.query_channels( + {"type": "messaging", "members": {"$in": [user_id]}}, + {"last_message_at": -1}, + limit=20, +) +``` + +### Recommendations + +1. **Use Predefined Filters** for frequently used query patterns in production +2. **Filter by indexed fields** (`cid`, `type`, `members`, `last_message_at`, `created_at`) +3. **Add time-based filters** to limit the scan scope (e.g., `last_message_at` within last 30 days) +4. **Avoid deep nesting** of `$and`/`$or` operators +5. **Monitor performance** through the Dashboard when using Predefined Filters diff --git a/docs/channels/query_members.md b/docs/channels/query_members.md new file mode 100644 index 0000000..6629875 --- /dev/null +++ b/docs/channels/query_members.md @@ -0,0 +1,90 @@ +The `queryMembers` method allows you to list and paginate members for a channel. It supports filtering on numerous criteria to efficiently return member information. This is useful for channels with large member lists where you need to search for specific members or display the complete member roster. + +```python +# Query members by user name +response = channel.query_members( + filter_conditions={"name": "tommaso"}, + sort=[{"field": "created_at", "direction": 1}], +) + +# Autocomplete members by user name +response = channel.query_members(filter_conditions={"name": {"$autocomplete": "tom"}}) + +# Query all members +response = channel.query_members(filter_conditions={}) +``` + +> [!NOTE] +> Stream Chat does not run MongoDB on the backend, only a [subset](/chat/docs/python/#query_syntax/) of the query options are available. + + +### Query Parameters + +| Name | Type | Description | Default | Optional | +| ------- | ------ | --------------------------------------------------------------------------------- | --------------------------- | -------- | +| filters | object | The query filters to use. You can query on any of the custom fields defined above | `{}` | | +| sort | object | The sort parameters | `{ created_at: 1 }` | ✓ | +| options | object | Pagination options | `{ limit: 100, offset: 0 }` | ✓ | + +> [!NOTE] +> By default, when `queryMembers` is called without any filter, it matches all members in the channel. + + +### Member Queryable Built-In Fields + +The following fields can be used to filter your query results: + +| Name | Type | Description | Supported Operators | Example | +| ------------ | ------------------------------------------------------------------ | ---------------------------------------------- | ----------------------------------- | ----------------------- | +| id | string | The ID of the user | `$eq`, `$in` | tom | +| name | string | The name of the user | `$eq`, `$in`, `$autocomplete`, `$q` | Tommaso | +| channel_role | string | The member role | `$eq` | channel_moderator | +| banned | boolean | The banned status | `$eq` | false | +| invite | string, must be one of these values: (pending, accepted, rejected) | The status of the invite | `$eq` | pending | +| joined | boolean | Whether the member has joined the channel | `$eq` | true | +| created_at | string, must be formatted as an RFC3339 timestamp | The time the member was created | `$eq`, `$gt`, `$gte`, `$lt`, `$lte` | 2021-01-15T09:30:20.45Z | +| updated_at | string, must be formatted as an RFC3339 timestamp | The time the member was last updated | `$eq`, `$gt`, `$gte`, `$lt`, `$lte` | 2021-01-15T09:30:20.45Z | +| last_active | string, must be formatted as an RFC3339 timestamp | The time the user was last active | `$eq`, `$gt`, `$gte`, `$lt`, `$lte` | 2021-01-15T09:30:20.45Z | +| cid | string | The CID of the channel the user is a member of | `$eq` | messaging:general | +| user.email | string | The email property of the user | `$eq`, `$in`, `$autocomplete` | | + +You can also filter by any field available in the custom data. + +## Paginating Channel Members + +By default, members are ordered from oldest to newest. You can paginate results using offset-based pagination or by the `created_at` or `user_id` fields. + +Offset-based pagination is the simplest to implement, but it can lead to incorrect results if the member list changes while you are paginating. The recommended approach is to sort by `created_at` or `user_id`. + +```python +# Paginate by user_id in descending order +response = channel.query_members( + filter_conditions={}, + sort=[{"field": "user_id", "direction": 1}], + offset=0, + limit=10, +) + +# Paginate by created_at in ascending order +response = channel.query_members( + filter_conditions={}, + sort=[{"field": "created_at", "direction": -1}], + offset=0, + limit=10, +) +``` + +### Pagination Options + +| Name | Type | Description | Default | Optional | +| -------------------------- | ------- | ------------------------------------------------------------------------------- | ------- | -------- | +| limit | integer | The number of members to return (max is 100) | 100 | ✓ | +| offset | integer | The offset (max is 1000) | 0 | ✓ | +| user_id_lt | string | Pagination option: excludes members with ID greater than or equal to the value | - | ✓ | +| user_id_lte | string | Pagination option: excludes members with ID greater than the value | - | ✓ | +| user_id_gt | string | Pagination option: excludes members with ID less than or equal to the value | - | ✓ | +| user_id_gte | string | Pagination option: excludes members with ID less than the value | - | ✓ | +| created_at_after | string | Pagination option: select members created after the date (RFC3339) | - | ✓ | +| created_at_before | string | Pagination option: select members created before the date (RFC3339) | - | ✓ | +| created_at_before_or_equal | string | Pagination option: select members created before or equal to the date (RFC3339) | - | ✓ | +| created_at_after_or_equal | string | Pagination option: select members created after or equal to the date (RFC3339) | - | ✓ | diff --git a/docs/debugging_and_cli/api_budget.md b/docs/debugging_and_cli/api_budget.md new file mode 100644 index 0000000..522bae8 --- /dev/null +++ b/docs/debugging_and_cli/api_budget.md @@ -0,0 +1,422 @@ +In addition to [rate limits](/chat/docs/python/rate_limits/), Stream enforces **API budgets** on expensive +endpoints. While rate limits count requests, API budgets measure actual **database execution time** in milliseconds. +This prevents a single application from consuming disproportionate database resources with costly queries, even when the +request count stays within limits. + +> [!NOTE] +> API budgets are currently enforced on the **Query Channels** endpoint. Other endpoints may be added in the future. + + +## Why API Budgets Exist + +`rate limits` effectively prevent excessive request volume, but they do not account for the cost of individual requests. +A single expensive query can consume orders of magnitude more database time than an optimized query. + +API budgets addresses this by measuring how long your queries take to execute. Each application receives a **time budget** ( +milliseconds per minute) for registered endpoints. Expensive queries deduct more budget than cheap ones, naturally +throttling resource-heavy usage patterns. + +### Per-Query Cap + +No single query can exhaust your entire budget. Each query's cost is capped at a configurable maximum (default: 3,000 +ms), so even an unusually slow query will not consume your full allowance in one call. + +## Detecting Budget Limits + +### Response Headers + +All responses from budgeted endpoints include headers that let you monitor your usage in real time: + +| Header | Description | +| ----------------------- | --------------------------------------------------------------- | +| `X-Budget-Used-Ms` | Current usage in the sliding window (milliseconds) | +| `X-Budget-Limit-Ms` | Your total budget for this endpoint (milliseconds) | +| `X-Budget-Remaining-Ms` | Available budget before denial (milliseconds) | +| `Retry-After` | Seconds until budget frees up (only present on `429` responses) | + +### HTTP 429 Response + +When your budget is exhausted, the API returns HTTP `429 Too Many Requests`. The response includes the `Retry-After` +header indicating how many seconds to wait before retrying. + +> [!NOTE] +> Budget denials return the same `429` status code as rate limit errors. Check the `X-Budget-Used-Ms` header to +> distinguish between a rate limit and a budget limit. + + +## Reducing Query Cost + +The single most effective way to stay within your budget is to write efficient queries. Not all `QueryChannels` calls +cost the same — a simple filter on indexed fields executes orders of magnitude faster than a complex filter on +custom data. The following guidelines help you write queries that execute quickly and consume less budget. + +### Use Efficient Filter Fields + +These filter fields are optimized and execute efficiently: + +| Filter field | Description | +| ----------------- | ------------------------------------------------------------------ | +| `cid` | Channel ID | +| `type` | Channel type | +| `last_message_at` | Timestamp of last message | +| `last_updated` | Last updated timestamp | +| `created_at` | Channel creation timestamp | +| `updated_at` | Channel updated timestamp | +| `members` | Channel membership (see rules below) | +| `has_unread` | Whether the channel has unread messages (only `true` is supported) | +| `team` | Team identifier | + +Filtering on fields **not** in this list — including `hidden`, `frozen`, `member_count`, `created_by_id`, `muted`, `pinned`, +`archived`, and any **custom field** on the channel — is significantly more expensive. If your query filters on +custom data (e.g., `custom.priority`, `custom.category`), expect higher budget consumption. + +### Use Efficient Sort Fields + +These sort fields use database indexes and execute efficiently: + +| Sort field | Description | +| ----------------- | --------------------------------- | +| `last_updated` | Default when no sort is specified | +| `last_message_at` | Sort by last message timestamp | +| `created_at` | Sort by creation time | +| `updated_at` | Sort by update time | + +Sorting by other fields — including `has_unread`, `unread_count`, `pinned_at`, or any custom field — +requires more processing and increases query cost. + +### Avoid Restricted Operators + +These operators are always expensive regardless of which field they are used on: + +- **`$nin`** — The "not in" operator forces full table scans. Restructure your query to use positive matches ( + `$in`, `$eq`) when possible +- **`$ne`** — The "not equal" operator cannot use indexes efficiently. Filter for the values you want instead of + excluding values you don't +- **`$nor`** — Logical NOR evaluates every row. Replace with positive `$and` conditions when possible +- **`$autocomplete`** — Autocomplete queries use full-text search and are inherently expensive. Use them sparingly + and consider caching results client-side +- **`$contains`** — Pattern matching that cannot leverage indexes. Avoid in high-frequency queries +- **`$q`** — Full-text search operator that requires text-search computation on every candidate row. Use targeted + filters instead when possible + +### Keep Filters Simple + +Query complexity has a direct impact on execution time: + +- **Keep `$in` arrays small** — Queries with `$in` containing 3 or fewer values are efficient. Larger arrays + increase cost significantly. +- **Limit `$and` branches** — Combining more than 3 filter conditions with `$and` increases cost. Each top-level + field in your filter object counts as one `$and` condition, even without an explicit `$and` wrapper +- **Limit `$or` branches** — Queries with more than 2 `$or` branches are expensive. Each branch adds a separate + database execution path +- **Use at most one logical operator** — Combining `$and` inside `$or` (or vice versa) creates complex query + plans. A query should use a single `$and` or a single `$or` at the top level, not both +- **Anchor queries with `members`** — Filtering by membership (e.g., `members: { "$in": ["user-id"] }`) narrows + the candidate set to the user's channels, making all other filters and sorts much faster + +### Examples: Efficient vs Expensive Queries + +The examples below illustrate common query patterns and their relative cost. Efficient queries stick to optimized +fields, simple operators, and indexed sort fields. Expensive queries violate one or more of these rules. + +#### Efficient Queries + +**User inbox** — membership anchor with indexed sort (the most common and fastest pattern): + +```json +{ + "filter": { + "type": "messaging", + "members": { + "$in": ["alice"] + } + }, + "sort": [ + { + "field": "last_message_at", + "direction": -1 + } + ] +} +``` + +**Date range** — range filter on indexed field with matching sort: + +```json +{ + "filter": { + "last_message_at": { + "$gt": "2024-01-01T00:00:00Z" + } + }, + "sort": [ + { + "field": "last_message_at", + "direction": -1 + } + ] +} +``` + +**Direct lookup** — fetching specific channels by `CID`: + +```json +{ + "filter": { + "cid": { + "$in": ["messaging:general", "messaging:support"] + } + }, + "sort": [ + { + "field": "last_message_at", + "direction": -1 + } + ] +} +``` + +**Team filter with membership** — multiple indexed fields within the `$and` limit: + +```json +{ + "filter": { + "type": "messaging", + "members": { + "$in": ["alice"] + }, + "team": "engineering" + }, + "sort": [ + { + "field": "last_updated", + "direction": -1 + } + ] +} +``` + +> [!TIP] +> When all `$or` branches filter on the **same field**, use `$in` instead. For example, +> `"type": { "$in": ["messaging", "livestream"] }` is equivalent to an `$or` on `type` but simpler and more efficient. + + +```json +{ + "filter": { + "$in": ["messaging", "livestream"] + }, + "sort": [ + { + "field": "last_message_at", + "direction": -1 + } + ] +} +``` + +#### Expensive Queries + +**Custom field filter with non-indexed sort** — custom data fields and `pinned_at` sort are both expensive: + +```json +{ + "filter": { + "$or": [ + { + "custom.is_archived": false + }, + { + "custom.is_priority": true + }, + { + "custom.is_flagged": true + } + ] + }, + "sort": [ + { + "field": "pinned_at", + "direction": -1 + } + ] +} +``` + +This query has three problems: custom field filters (`custom.*`), three `$or` branches (limit is 2), and sorting by +`pinned_at` (not indexed). + +**Too many `$and` conditions** — exceeding the branch limit: + +```json +{ + "filter": { + "type": "messaging", + "members": { + "$in": ["alice"] + }, + "last_message_at": { + "$exists": true + }, + "member_count": { + "$eq": 2 + } + } +} +``` + +This query has four top-level conditions (exceeding the `$and` limit of 3) and uses `member_count` (not an optimized +field). + +**Large `$in` array** — too many values: + +```json +{ + "filter": { + "cid": { + "$in": [ + "messaging:ch1", + "messaging:ch2", + "messaging:ch3", + "messaging:ch4", + "messaging:ch5", + "messaging:ch6" + ] + } + } +} +``` + +The `$in` array contains 6 values (limit is 3). Restructure to batch multiple smaller queries instead. + +**Negation operators** — `$nin` and `$ne` are always expensive: + +```json +{ + "filter": { + "members": { + "$nin": ["bob"] + }, + "type": { + "$ne": "livestream" + } + } +} +``` + +These operators force full scans. Use positive matches (`$in`, `$eq`) to filter for what you want instead of excluding +what you don't. + +**Nested logical operators** — combining `$and` and `$or`: + +```json +{ + "filter": { + "$and": [ + { + "type": "messaging" + }, + { + "$or": [ + { + "team": "sales" + }, + { + "team": "support" + } + ] + } + ] + } +} +``` + +This query nests `$or` inside `$and`. Use a single level of logical operators instead. In this case, filtering by +`team: { "$in": ["sales", "support"] }` achieves the same result more efficiently. + +**No membership anchor with broad filter** — querying without narrowing by user: + +```json +{ + "filter": { + "type": "messaging" + }, + "sort": [ + { + "field": "created_at", + "direction": -1 + } + ] +} +``` + +This query uses only optimized fields, but without a `members` filter the database must scan all channels of the given +type. For applications with many channels, this leads to high execution time and budget consumption. Adding +`members: { "$in": ["user-id"] }` narrows the candidate set to the user's channels and dramatically reduces cost. + +### Quick Reference: Optimization Rules + +A query is considered **optimized** only when every part of it meets the criteria below. A single violation makes the +entire query expensive. + +| Rule | Optimized | Expensive | +| -------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| **Filter fields** | `cid`, `type`, `last_message_at`, `last_updated`, `created_at`, `updated_at`, `members`, `has_unread`, `team` | Any other field, including `frozen`, `hidden`, `muted`, `member_count`, and any `custom.*` field | +| **Sort fields** | `last_updated`, `last_message_at`, `created_at`, `updated_at` | Any other field, including `has_unread`, `pinned_at`, and any custom field | +| **`$in` array size** | 3 or fewer values | 4 or more values | +| **`$and` branches** | 3 or fewer conditions | 4 or more conditions | +| **`$or` branches** | 2 or fewer branches | 3 or more branches | +| **Logical nesting** | A single `$and` or `$or` at the top level | Combining `$and` with `$or` at any depth | +| **Operators** | `$eq`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$exists` | `$nin`, `$ne`, `$nor`, `$autocomplete`, `$contains`, `$q` | + +> [!TIP] +> **Start with the user inbox pattern.** The most efficient query shape is `members: { "$in": ["user-id"] }` combined with +> `type` and sorted by `last_message_at`. This anchors the query to the user's channels and uses indexed fields +> throughout. + + +### Monitor Your Usage + +Use the `X-Budget-Used-Ms` and `X-Budget-Remaining-Ms` response headers to track your consumption. If you see usage +consistently approaching the limit, review which queries are most expensive and optimize them using the guidelines +above. + +## Handling Budget Errors + +When you receive a `429` response due to budget exhaustion: + +1. **Read the `Retry-After` header** to determine when budget will be available +2. **Implement exponential back-off** — Wait and retry with increasing delays +3. **Review your query patterns** — Frequent budget exhaustion indicates queries that are too expensive, not just + too many requests + +> [!CAUTION] +> Do **not** simply retry immediately on a `429`. The budget is time-based, so rapid retries will not succeed and may +> delay recovery. + + +## Relationship to Rate Limits + +API budgets and [rate limits](/chat/docs/python/rate_limits/) work together but measure different things: + +| Aspect | Rate Limits | API Budget | +| ------------ | -------------------------------- | ----------------------------------------- | +| **Measures** | Number of requests | Database execution time (ms) | +| **Window** | 1 minute | 1 minute (sliding) | +| **Scope** | Per endpoint, per platform | Per endpoint, per application | +| **Denial** | HTTP `429` | HTTP `429` | +| **Headers** | `X-RateLimit-*` | `X-Budget-*` | +| **Purpose** | Prevent excessive request volume | Prevent excessive database resource usage | + +You can hit budget limits even when well within rate limits, and vice versa. Both constraints must be satisfied for a +request to proceed. + +## Requesting Budget Adjustments + +If your application consistently hits budget limits after optimizing your queries: + +- **Standard plans** — Contact Stream support with details about your query patterns. Stream will review your + usage and may adjust your budget +- **Enterprise plans** — Stream works with you to set appropriate budgets for your production workload + +Budget values are configured per application and can be adjusted without code changes on your side. diff --git a/docs/debugging_and_cli/api_errors_response.md b/docs/debugging_and_cli/api_errors_response.md new file mode 100644 index 0000000..b2c45e7 --- /dev/null +++ b/docs/debugging_and_cli/api_errors_response.md @@ -0,0 +1,50 @@ +Below you can find the complete list of errors that are returned by the API together with the description, API code, and corresponding HTTP status of each error. + +| Name | HTTP Status Code | HTTP Status | Stream Code | Description | +| -------------------------------------- | ---------------- | ------------------------------- | ----------- | ----------------------------------------------------------------------------------------- | +| Input Error | 400 | Bad Request | 4 | When wrong data/parameter is sent to the API | +| Duplicate Username Error | 400 | Bad Request | 6 | When a duplicate username is sent while enforce_unique_usernames is enabled | +| Message Too Long Error | 400 | Bad Request | 20 | Message is too long | +| Event Not Supported Error | 400 | Bad Request | 18 | Event is not supported | +| Channel Feature Not Supported Error | 400 | Bad Request | 19 | The feature is currently disabled on the dashboard (i.e. Reactions & Replies) | +| Multiple Nesting Level Error | 400 | Bad Request | 21 | Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads | +| Custom Command Endpoint Call Error | 400 | Bad Request | 45 | Custom Command handler returned an error | +| Custom Command Endpoint Missing Error | 400 | Bad Request | 44 | App config does not have custom_action_handler_url | +| Authentication Error | 401 | Unauthorised | 5 | Unauthenticated, problem with authentication | +| Authentication Token Expired | 401 | Unauthorised | 40 | Unauthenticated, token expired | +| Authentication Token Before Issued At | 401 | Unauthorised | 42 | Unauthenticated, token date incorrect | +| Authentication Token Not Valid Yet | 401 | Unauthorised | 41 | Unauthenticated, token not valid yet | +| Authentication Token Signature Invalid | 401 | Unauthorised | 43 | Unauthenticated, token signature invalid | +| Access Key Error | 401 | Unauthorised | 2 | Access Key invalid | +| Not Allowed Error | 403 | Forbidden | 17 | Unauthorised / forbidden to make request | +| App Suspended Error | 403 | Forbidden | 99 | App suspended | +| Cooldown Error | 403 | Forbidden | 60 | User tried to post a message during the cooldown period | +| Does Not Exist Error | 404 | Not Found | 16 | Resource not found | +| Request Timeout Error | 408 | Request Timeout | 23 | Request timed out | +| Payload Too Big Error | 413 | Request Entity Too Large | 22 | Payload too big | +| Rate Limit Error | 429 | Too Many Requests | 9 | Too many requests in a certain time frame | +| Maximum Header Size Exceeded Error | 431 | Request Header Fields Too Large | 24 | Request headers are too large | +| Internal System Error | 500 | Internal Server Error | -1 | Triggered when something goes wrong in our system | +| No Access to Channels | 403 | Unauthorised | 70 | No access to requested channels | +| Message Moderation Failed | 400 | Bad Request | 73 | Message did not pass moderation | + +## Common Errors Explained + +This section explains how to solve common API errors. + +### GetOrCreateChannel failed + +The full error you receive is "GetOrCreateChannel failed with error: "either data.created_by or data.created_by_id must be provided when using server side auth." with error code 4. This error is only triggered when using server side authentication. + +You can encounter this error when calling channel.watch(), channel.create() or channel.query(). All three methods call get or create a channel. There are two possible causes for this error: + +**1. You are trying to watch a channel that hasn't been created** + +You are expecting that the channel is already created. For instance if you have the following code: + + +The above code works well if the channel with cid messaging:123 already exists. If it doesn't exist yet it will throw the above error. So you can get this error when calling channel.watch if another part of your code failed to create the channel. + +**2. You want to create a channel but forgot the created_by_id param** + +If you're actually intending to create a channel in this part of your code you need to specify the user as follows: diff --git a/docs/debugging_and_cli/cli_introduction.md b/docs/debugging_and_cli/cli_introduction.md new file mode 100644 index 0000000..c3cee46 --- /dev/null +++ b/docs/debugging_and_cli/cli_introduction.md @@ -0,0 +1,136 @@ +Stream's Command Line Interface (CLI) makes it easy to create and manage your Stream apps directly from the terminal. + +> [!NOTE] +> The repository is available [here](https://github.com/GetStream/stream-cli). The documentation is available [here](https://getstream.github.io/stream-cli/), including a [full documentation](https://getstream.github.io/stream-cli/stream-cli_chat.html) of every command. + + +## Installation + +The Stream CLI is written in Go and precompiled into a single binary. It doesn't have any prerequisites. + +You can find the binaries in the [Release section](https://github.com/GetStream/stream-cli/releases) of the GitHub repository. + +### Via Script + +```bash +# MacOS Intel +$ export URL=$(curl -s https://api.github.com/repos/GetStream/stream-cli/releases/latest | grep Darwin_x86 | cut -d '"' -f 4 | sed '1d') +$ curl -L $URL -o stream-cli.tar.gz +$ tar -xvf stream-cli.tar.gz +# We don't sign our binaries today, so we need to explicitly trust it. +$ xattr -d com.apple.quarantine stream-cli + +# MacOS ARM +$ export URL=$(curl -s https://api.github.com/repos/GetStream/stream-cli/releases/latest | grep Darwin_arm | cut -d '"' -f 4 | sed '1d') +$ curl -L $URL -o stream-cli.tar.gz +$ tar -xvf stream-cli.tar.gz +# We don't sign our binaries today, so we need to explicitly trust it. +$ xattr -d com.apple.quarantine stream-cli + +# Linux x86 +$ export URL=$(curl -s https://api.github.com/repos/GetStream/stream-cli/releases/latest | grep Linux_x86 | cut -d '"' -f 4 | sed '1d') +$ curl -L $URL -o stream-cli.tar.gz +$ tar -xvf stream-cli.tar.gz + +# Linux ARM +$ export URL=$(curl -s https://api.github.com/repos/GetStream/stream-cli/releases/latest | grep Linux_arm64 | cut -d '"' -f 4 | sed '1d') +$ curl -L $URL -o stream-cli.tar.gz +$ tar -xvf stream-cli.tar.gz + +# Windows x86 +> $latestRelease = Invoke-WebRequest "https://api.github.com/repos/GetStream/stream-cli/releases/latest" +> $json = $latestRelease.Content | ConvertFrom-Json +> $url = $json.assets | ? { $_.name -match "Windows_x86" } | select -expand browser_download_url +> Invoke-WebRequest -Uri $url -OutFile "stream-cli.zip" +> Expand-Archive -Path ".\stream-cli.zip" + +# Windows ARM +> $latestRelease = Invoke-WebRequest "https://api.github.com/repos/GetStream/stream-cli/releases/latest" +> $json = $latestRelease.Content | ConvertFrom-Json +> $url = $json.assets | ? { $_.name -match "Windows_arm" } | select -expand browser_download_url +> Invoke-WebRequest -Uri $url -OutFile "stream-cli.zip" +> Expand-Archive -Path ".\stream-cli.zip" +``` + +### Via Homebrew + +```bash +$ brew tap GetStream/stream-cli https://github.com/GetStream/stream-cli +$ brew install stream-cli +``` + +### Compile yourself + +```bash +$ git clone git@github.com:GetStream/stream-cli.git +$ cd stream-cli +$ go build ./cmd/stream-cli +$ ./stream-cli --version +stream-cli version 1.0.0 +``` + +## Getting Started + +In order to initialize the CLI, it’s as simple as: + +![](https://getstream.imgix.net/docs/37515dd9-94a7-4183-8bd0-761de5581e3c.png?auto=compress&fit=clip&w=800&h=600) + +> [!NOTE] +> Note: Your API key and secret can be found on the [Stream Dashboard](https://getstream.io/dashboard) and is specific to your application. + + +## Use Cases and example + +A couple of example use cases can be found [here](https://getstream.github.io/stream-cli/use_cases.html). We’ve also created a separate documentation [for the import feature](https://getstream.github.io/stream-cli/imports.html). + +## 🚨Warning + +We purposefully chose the executable name  `stream-cli`  to avoid conflict with another tool called [`imagemagick`](https://imagemagick.org/index.php) which [already has a  `stream`  executable](https://github.com/GetStream/stream-cli/issues/33). + +If you do not have  `imagemagick`  installed, it might be more comfortable to rename  `stream-cli`  to  `stream` . Alternatively you can set up a symbolic link: + +```bash +$ ln -s ~/Downloads/stream-cli /usr/local/bin/stream +$ stream --version +stream-cli version 1.0.0 +``` + +## Syntax + +Basic commands use the following syntax: + +```bash +$ stream-cli [chat|feeds] [command] [args] [options] +``` + +Example: + +```bash +$ stream-cli chat get-channel -t messaging -i redteam +``` + +The  `--help`  keyword is available every step of the way. Examples: + +```bash +$ stream-cli --help +$ stream-cli chat --help +$ stream-cli chat get-channel --help +``` + +## Auto completion + +We provide autocompletion for the most popular shells (PowerShell, Bash, ZSH, Fish). + +```bash +$ stream-cli completion --help +``` + +## Issues + +If you’re experiencing problems directly related to the CLI, please add an [issue on GitHub](https://github.com/getstream/stream-cli/issues). + +For other issues, submit a [support ticket](https://getstream.io/support). + +## Changelog + +As with any project, things are always changing. If you’re interested in seeing what’s changed in the Stream CLI, the changelog for this project can be tracked in the [Release](https://github.com/GetStream/stream-cli/releases) page of the repository. diff --git a/docs/debugging_and_cli/datadog_integration.md b/docs/debugging_and_cli/datadog_integration.md new file mode 100644 index 0000000..4830769 --- /dev/null +++ b/docs/debugging_and_cli/datadog_integration.md @@ -0,0 +1,34 @@ +## Enable Integration + +To seamlessly integrate Stream metrics with your Datadog account, follow these two essential steps. This feature is included with Stream's Enterprise pricing plans. + +1. Identify your Datadog account's DD_SITE and DD_API_KEY: + +2. Request the Stream team to enable this feature by [contacting support](https://getstream.io/contact/support/) + +Navigate to your [Stream Dashboard](https://dashboard.getstream.io/) and follow these instructions: + +1. Go to the left-side menu and select "Chat Messaging" -> "External Integration." + +2. Save your personal API_KEY and DD_SITE. + +If you are uncertain about your DD_SITE, consult [this](https://docs.datadoghq.com/getting_started/site/#access-the-datadog-site) table in the Datadog documentation to ensure you select the correct value. + +## Metrics Explanation + +For now, we provide basically two important metrics: hits and latency. + +| Name | Description | Type | Tags | +| ------------------------------- | --------------------------------------------------------- | ----- | --------------------------- | +| **streamio.chat.hits** | Number of hit for API for chat/video | Count | host, app_id, action,status | +| **streamio.chat.latency.count** | Number of latency value within a sink before push metrics | Rate | host, app_id, action | +| streamio.chat.latency.median | Median value calculated within a sink | Gauge | host, app_id, action | + +## Tag Explanation + +| Tag | Description | Example | +| ------ | ------------------------------ | ----------------------------- | +| host | Hostname of the server | stream-dublin- | +| app_id | App ID for the customer | 123456 | +| action | Action name of the API/Webhook | send_message | +| status | Status code of the API/Webhook | 200 | diff --git a/docs/debugging_and_cli/push_-_common_issues_and_faq.md b/docs/debugging_and_cli/push_-_common_issues_and_faq.md new file mode 100644 index 0000000..ab9fb19 --- /dev/null +++ b/docs/debugging_and_cli/push_-_common_issues_and_faq.md @@ -0,0 +1,117 @@ +## Delivery Condition Not Matched + +Most of the time, not receiving push notifications results from a use case that the API does not handle. Our current API implementation supports push notifications under specific cases: + +- The target user has at least one device [registered](/chat/docs/python/push_devices/) with the Stream App. + +- The target user is a member of the channel that will send notifications. + +- The target user has **not** [muted](/chat/docs/python/muting_channels/) the channel the pushed message is from. + +- The target user has **not** muted the user that created the pushed message. + +- Push notifications are enabled in the channel type. + +- If using [thread](/chat/docs/python/threads/), the user is part of the thread that messages are sent in (i.e previously posted at least one message or were mentioned in that thread) + +- Message isn't sent with `skip_push=true` flag. + +If one of these conditions is not met, then the API will not trigger any push. As a result, you will not receive any push notification AND you won't see any data on this push notification attempt in the Dashboard Push Logs. + +## Device Registration + +At least one device must be registered for a specific user in order to receive push notifications. This is also a push delivery condition and  a mandatory step in the [push integration logic](/chat/docs/python/push_devices/#register-a-device/). + +One of the common reasons for not receiving a push notification on a device is the absence of a device token. Without this, a device cannot be registered on the Stream database. + +> [!WARNING] +> If this is the issue, it will not be reported in the Dashboard Push logs. + + +This is why we recommend testing your push integration using the [Stream CLI tool](/chat/docs/python/push_test/) first, as it runs some checks such as having a registered device. You can learn more the CLI tool [here](https://github.com/GetStream/chat-push-test/tree/master/react-native). + +You can also check if a device is registered by [retrieving the list of devices for a given user](/chat/docs/python/push_devices/#list-devices/). + +## Token Invalidation + +Push provider token, or much commonly known as a `registrationToken` is a unique token string that is tied to each client app instance. The registration token is required for single device and device group messaging. + +An existing registration token may cease to be valid in a number of scenarios, including: + +- **If the registration token expires.** i.e. token is generated long time ago, not used according to provider timeout, etc. + +- **If the client app is unregistered** , which can happen if the user uninstalls the application. + +- **If the client app is updated** but the new version is **not configured to receive messages** . i.e. any configuration changes on provider settings such project id, host, etc. or your app **asks for new permission** on the device such as reading notifications on lock screen, etc. + +- **If the operating system libraries are updated** . Some providers (Firebase and APN) are closed attached to system libraries and any update on them can cause a new token generation which should be registered to Chat API. + +- **If client app is hard stopped and their cache is removed** . + +Usually, when facing this issue, the Push Logs should report an Unregistered error (i.e your device isn't a valid location to send). This usually means that the token used is no longer valid and a new one must be provided. + +For all these cases : + +- **Check that push configuration is up-to-date.** + +- **Check your refresh token logic** (sometimes integration issues can lead to the device not being registered with the latest token retrieved from the library such as race in async code). + +- You will need to **remove this existing registration** token from the Stream (i.e [Remove Device](/chat/docs/python/push_devices/#unregister-a-device/)) and stop using it to send messages. + +- **Add a new valid registration token** for that device (i.e [Add Device](/chat/docs/python/push_devices/#register-a-device/)). + +> [!NOTE] +> First delete then add isn't needed though, you can call add directly and if you reach limit of 25 devices, API automatically remove one invalid or oldest device. + + +## User Offline/Online Transition + +Stream API supports user presence changes and provides offline/online statuses for users. Push notifications are only sent when a user does not have an active Websocket connection (i.e they are offline). + +**However, when your app goes to the background or is killed, your device will keep the Websocket connection alive for up to 1 minute.** This means it can take up to 1 minute for the API to consider the user offline, and during this time the device will not receive any push notifications. + +> [!WARNING] +> If your app is created after 2022-01-18, push notifications are sent irrespective of online status and online users will can push notifications and it's a flexibility of the your app to handle it or ignore it according to the context. + + +The general best practice for handling this edge case is to set up local push notifications to trigger push notifications when the Stream API Push system cannot (i.e delivery conditions are not met). Some of our mobile SDKs provide the background disconnection logic out-of-the-box and examples. It will ultimately be up to you to implement local push notification logic as you see fit. + +- [Flutter](/chat/docs/sdk/flutter/guides/push-notifications/adding_push_notifications_v2/) + +- [ReactNative](/chat/docs/sdk/react-native/guides/push-notifications/) + +- [iOS](/chat/docs/sdk/ios/client/push-notifications/) + +- [Android](/chat/docs/sdk/android/client/guides/push-notifications/) + +## Push Provider Drops Notification + +Considering the above section on Push Logs, the dashboard reports the push provider response, but not the actual device delivery because it's controlled by the push provider and can take some time if device is online and has enough battery, and priority of the push notifications. + +Therefore, there are cases where the dashboard reports push notifications have been successful (i.e. 200 responses), but you are still unable to see any push notifications on your device. In general, this behavior relates to push provider dropping the push notification after receiving and accepting the request. + +When receiving a push request from Stream, push provider does: + +1. Run protocols to validate it based on the authentication information (credentials, device ID, etc...) and format of the request and replies success on receipt. + +2. Run further validation checks such as Message (payload) data & format while processing and on error, it can be ignored such as a mismatch for a data type _i.e. a string for badge count_. + +If the notification message generated from configured templates and data does not respect the format or has wrong data, the push provider will drop the notification. In this case, the push provider does not provide enough information to Stream since it is hit after successful reception. + +Related issues are mostly due to a bad template configuration. + +> [!NOTE] +> This class of problems are irrelevant to v2 because there is no template to configure and Stream controls the payload and a validation error in payload (i.e. size, type mismatch, JSON marshaling, etc.) is impossible. + + +## Template issue (only v1) + +The majority of issues that are visible to Stream and still prevent devices from receiving push notifications are due to providers dropping the notification. Usually, those are related to a template issue (notification or data). + +Both Firebase and APN have specific protocols and payload keys. You need to ensure the Android data and notification template, respectively APN template will reflect and respect the keys values, types, objects format. For example, common issues are : + +- **APN** : Key type mismatch - e.g set template with `{...,“badge”: “{{ unread_count }} “,...}` is wrong. According to [APNs docs](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification#2943365), the "badge" key should be a Number(integer), however, once {{unread_count}} replaced, it will render the following : `{...,“badge”: “1“,...}` . In this case, APN will drop the notification without notifying 3rd party services like Stream API. + +- **Firebase** : Wrong data passed -According to [Firebase docs](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages) e.g the "click_action" key refers to the action associated with a user click on the notification. By default set to `"click_action":"OPEN_ACTIVITY_1"` , but if referencing a custom activity, you need to make sure it exists in your project. Otherwise, Firebase might drop the notification. + +Please note that the Stream Dashboard and API do not apply any checks regarding your notification templates but only ensure the rendered template respects JSON format. In addition to the [APN docs](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification#2943365) and [Firebase docs](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages), you can also refer to our [Push template](/chat/docs/python/push_template/) documentation section with detailed information and examples for configuration. diff --git a/docs/debugging_and_cli/rate_limits.md b/docs/debugging_and_cli/rate_limits.md new file mode 100644 index 0000000..e4b9b7d --- /dev/null +++ b/docs/debugging_and_cli/rate_limits.md @@ -0,0 +1,154 @@ +Stream applies rate limits to protect both your application and our infrastructure. Rate limits prevent: + +- Integration issues or abuse from degrading your app's performance (excessive API calls trigger client-side events) +- Resource consumption beyond what is provisioned for your plan +- Common integration mistakes, such as opening multiple WebSocket connections per user + +Rate limits are applied per **API endpoint** and **platform** on a 1-minute window. Different platforms (iOS, Android, Web, Server) have independent counters for each endpoint. + +> [!NOTE] +> If 6,000 iOS users and 6,000 Android users connect within one minute, no rate limit is triggered. The 10,000/minute connect limit applies independently to each platform. + + +> [!NOTE] +> **Dynamic Rate Limiting**: Rate limits may be adjusted based on overall platform load and your application's individual usage patterns for query endpoints. During periods of high demand, the platform may temporarily reduce rate limits to ensure stability and fair resource allocation for all users. Monitor the `X-RateLimit-*` headers in API responses to track your current limits. + + +## Inspecting Rate Limits + +Check your current rate limit quotas and usage in the [dashboard](https://dashboard.getstream.io) or via the API. + +```python +# Server-side platform only +limits = client.get_rate_limits(server_side=True) + +# All platforms +limits = client.get_rate_limits() + +# Specific platforms +limits = client.get_rate_limits(ios=True, android=True) + +# Specific endpoints +limits = client.get_rate_limits(endpoints=["QueryChannels", "SendMessage"]) +``` + +The response includes the 1-minute limit, remaining quota, and window reset timestamp. + +## Rate Limit Headers + +All API responses include rate limit information in headers. + +| Header | Description | +| --------------------- | -------------------------------------------------- | +| X-RateLimit-Limit | Total limit for the requested resource (e.g. 5000) | +| X-RateLimit-Remaining | Remaining requests in current window (e.g. 4999) | +| X-RateLimit-Reset | When the current window resets (Unix timestamp) | + +## Types of Rate Limits + +### User Rate Limits + +Each user is limited to **60 requests per minute** per endpoint and platform. This prevents a single user from consuming your entire application quota. Your server is not subject to user rate limits. + +### App Rate Limits + +App rate limits apply per endpoint and platform combination. Stream supports four platforms: + +| Platform | SDKs | +| -------- | ------------------------------------- | +| Server | Node, Python, Ruby, Go, C#, PHP, Java | +| Android | Kotlin, Java, Flutter, React Native | +| iOS | Swift, Flutter, React Native | +| Web | React, Angular, JavaScript | + +Rate limits are not shared across platforms. If a server-side script hits a rate limit, your mobile and web applications are unaffected. + +App rate limits are enforced both per minute and per second. The per-second limit equals the per-minute limit divided by 30 to allow for bursts. + +When a rate limit is exceeded, all calls from the same app, platform, and endpoint return HTTP status `429`. + +## Handling Rate Limit Errors + +When you receive a `429` status code, implement exponential back-off retry logic. Use the `X-RateLimit-Reset` header to determine when to retry. + +### Avoiding Rate Limits + +1. **Add delays to scripts** - The most common cause of rate limits. Add timeouts between successive API calls in batch scripts or cronjobs. + +2. **Use batch endpoints** - Instead of 100 individual calls, use [batch endpoints]() to update multiple users in one request. + +3. **Check client-side rendering logic** - Infinite pagination bugs or other client-side issues can trigger excessive API calls. + +4. **Avoid redundant queries** - Channel creation is an upsert operation. Do not call `QueryChannels` to check if a channel exists before creating it. See [Query Channels](/chat/docs/python/query_channels/). + +5. **Use one WebSocket per user** - Multiple WebSocket connections per user cause performance issues, billing problems, and unexpected behavior. See [Initialization & Users](/chat/docs/python/init_and_users/). + +6. **Follow livestream best practices** - High-volume messaging scenarios require additional optimization. See [Livestream Best Practices](/chat/docs/python/livestream_best_practices/). + +### Requesting Higher Limits + +- **Standard plans** - Stream may increase limits after reviewing your integration to confirm optimal usage of default limits. +- **Enterprise plans** - Stream reviews your architecture and sets appropriate limits for your production application. + +## Rate Limits by Endpoint + +Default rate limits for self-serve plans (per minute, per platform). + +| API Request | Limit/min | +| ---------------------- | --------- | +| Connect | 10,000 | +| Get or Create Channel | 10,000 | +| Get App | 10,000 | +| Mark All Read | 10,000 | +| Mark Read | 10,000 | +| Query Channels | 10,000 | +| Send Event | 10,000 | +| Create Guest | 1,000 | +| Delete Message | 1,000 | +| Delete Reaction | 1,000 | +| Get Message | 1,000 | +| Get Reactions | 1,000 | +| Get Replies | 1,000 | +| Query Users | 1,000 | +| Run Message Action | 1,000 | +| Send Message | 1,000 | +| Send Reaction | 1,000 | +| Stop Watching Channel | 1,000 | +| Update Message | 1,000 | +| Upload File | 1,000 | +| Upload Image | 1,000 | +| Ban | 300 | +| Create Device | 300 | +| Edit Users | 300 | +| Flag | 300 | +| Hide Channel | 300 | +| Mute | 300 | +| Query Members | 300 | +| Search | 300 | +| Show Channel | 300 | +| Unban | 300 | +| Unflag | 300 | +| Unmute | 300 | +| Update Channel | 300 | +| Update Users | 300 | +| Update Users (Partial) | 300 | +| Activate User | 60 | +| Check Push | 60 | +| Create Channel Type | 60 | +| Deactivate User | 60 | +| Delete Channel | 60 | +| Delete Channel Type | 60 | +| Delete Device | 60 | +| Delete File | 60 | +| Delete User | 60 | +| Export Channel | 60 | +| Export User | 60 | +| Get Channel Type | 60 | +| List Channel Types | 60 | +| List Devices | 60 | +| Truncate Channel | 60 | +| Update App | 60 | +| Update Channel Type | 60 | + +> [!NOTE] +> All endpoints also enforce the user rate limit of 60 requests per minute per user. Rate limits can be adjusted based on your use case and plan. diff --git a/docs/features/advanced/audit_logs.md b/docs/features/advanced/audit_logs.md new file mode 100644 index 0000000..b0315e8 --- /dev/null +++ b/docs/features/advanced/audit_logs.md @@ -0,0 +1,94 @@ +Message history lets you keep a history of changes to messages. These **only** include the changes to the following fields: + +- Text + +- Attachments + +- Any custom field + +- Whether the message is soft deleted + +The value of the fields **before the update** , will be stored in the message history. Since the present state is already available and otherwise some information will be lost. + +The **time** when the change was made ( `message_updated_at` ) and the **user** who made the change ( `message_updated_by_id` ) will also be stored. + +> [!NOTE] +> This feature is only available on Stream’s Enterprise pricing plans. Request the Stream team to enable this feature for your app by [contacting support](https://getstream.io/contact/support/). + + +## Example + +Let's assume we have a channel with users **alice** and **bob** . We have the user **admin** with the `admin` role as well. + +- **alice** sends a message: + + +Note that sending a new message won't create a history record. + +- **alice** notices she didn't include her number. So she edits the message to include it: + + +This step will be recorded in the message history. + +- **admin** notices the message and since sending numbers is not allowed, he/she edits the message to remove the number: + + +This step will also be recorded in the message history. + +- Now customer support takes a look at message history by calling the **server-side only** API: + + +There will be two histories: + +```json +[ + { + "message_id": "message-1", + "text": "Hello bob! give me a call. Here is my number: +31 6 12345678", + "message_updated_by_id": "admin", + "message_updated_at" "2024-04-24T15:50:21" + }, + { + "message_id": "message-1", + "text": "Hello bob! give me a call.", + "message_updated_by_id": "alice", + "message_updated_at" "2024-04-24T15:47:46" + } +] +``` + +By default, you get the history sorted by latest first. + +## Querying message history + +It is possible to filter and sort when querying message history. Note that this is a **server-side only** API. + +| Name | type | Description | Supported operations | Example | +| --------------------- | ------------------------------------------------ | ----------------------------------------------- | ------------------------- | -------------------------------------------------------- | +| message_id | string or list of strings | the ID of the message | $in, $eq | { message_id: { $in: [ 'message-1', 'message-2' ] } } | +| message_updated_by_id | string or list of strings | the ID of the user who made updates to messages | $in, $eq | { message_updated_by_id: { $in: [ 'alice', 'bob' ] } } | +| message_updated_at | string, must be formatted as a RFC3339 timestamp | the time the update was made | $eq, $gt, $lt, $gte, $lte | { message_updated_at: {$gte: ‘2024-04-24T15:50:00.00Z’ } | + +Example query: + + +Response: + +```json +[ + { + "message_id": "message-1", + "text": "Hello bob! give me a call.", + "is_deleted": false + "message_updated_by_id": "alice", + "message_updated_at" "2024-04-24T15:47:46" + }, + { + "message_id": "message-1", + "text": "Hello bob! give me a call. Here is my number: +31 6 12345678", + "is_deleted": false, + "message_updated_by_id": "admin", + "message_updated_at" "2024-04-24T15:50:21" + } +] +``` diff --git a/docs/features/advanced/drafts.md b/docs/features/advanced/drafts.md new file mode 100644 index 0000000..1d29130 --- /dev/null +++ b/docs/features/advanced/drafts.md @@ -0,0 +1,90 @@ +Draft messages allow users to save messages as drafts for later use. This feature is useful when users want to compose a message but aren't ready to send it yet. + +## Creating a draft message + +It is possible to create a draft message for a channel or a thread. Only one draft per channel/thread can exist at a time, so a newly created draft overrides the existing one. + +```python +# Create/update a draft message in a channel +response = await channel.create_draft({"text": "This is a draft message"}, user_id) + +# Create/update a draft message in a thread (parent message) +response = await channel.create_draft( + {"text": "This is a draft message", "parent_id": parent_id}, user_id +) +``` + +## Deleting a draft message + +You can delete a draft message for a channel or a thread as well. + +```python +# Channel draft +await channel.delete_draft(user_id) + +# Thread draft +await channel.delete_draft(user_id, parent_id=parent_id) +``` + +## Loading a draft message + +It is also possible to load a draft message for a channel or a thread. Although, when querying channels, each channel will contain the draft message payload, in case there is one. The same for threads (parent messages). So, for the most part this function will not be needed. + +```python +# Channel draft +response = await channel.get_draft(user_id) + +# Thread draft +response = await channel.get_draft(user_id, parent_id=parent_id) +``` + +## Querying draft messages + +The Stream Chat SDK provides a way to fetch all the draft messages for the current user. This can be useful to for the current user to manage all the drafts they have in one place. + +```python +# Query all user drafts +response = await client.query_drafts(user_id=user_id, limit=10) + +# Query drafts for certain channels and sort +response = await client.query_drafts( + user_id, + filter={ + "channel_cid": {"$in": ["messaging:channel-1", "messaging:channel-2"]}, + }, + sort=[{"field": "created_at", "direction": SortOrder.ASC}], +) +``` + +Filtering is possible on the following fields: + +| Name | Type | Description | Supported operations | Example | +| ----------- | -------------------------- | ------------------------------ | ------------------------- | ------------------------------------------------------ | +| channel_cid | string | the ID of the message | $in, $eq | { channel_cid: { $in: [ 'channel-1', 'channel-2' ] } } | +| parent_id | string | the ID of the parent message | $in, $eq, $exists | { parent_id: 'parent-message-id' } | +| created_at | string (RFC3339 timestamp) | the time the draft was created | $eq, $gt, $lt, $gte, $lte | { created_at: { $gt: '2024-04-24T15:50:00.00Z' } | + +Sorting is possible on the `created_at` field. By default, draft messages are returned with the newest first. + +### Pagination + +In case the user has a lot of draft messages, you can paginate the results. + +```python +# Query drafts with a limit +first_page = await client.query_drafts(user_id=user_id, options={"limit": 5}) + +# Query the next page +second_page = await client.query_drafts( + user_id=user_id, options={"limit": 5, "next": first_page["next"]} +) +``` + +## Events + +The following WebSocket events are available for draft messages: + +- `draft.updated`, triggered when a draft message is updated. +- `draft.deleted`, triggered when a draft message is deleted. + +You can subscribe to these events using the Stream Chat SDK. diff --git a/docs/features/advanced/dynamic_partitioning.md b/docs/features/advanced/dynamic_partitioning.md new file mode 100644 index 0000000..c65cfe3 --- /dev/null +++ b/docs/features/advanced/dynamic_partitioning.md @@ -0,0 +1,59 @@ +Stream can support [millions of watchers](https://getstream.io/blog/scaling-chat-5-million-concurrent-connections/) in a channel, but sometimes, this can create a lot of noise when many people send messages simultaneously. Dynamic partitioning allows for splitting the channel into virtual partitions where users only interact with users within the same partition. It boosts engagement by grouping users into smaller partitions with balanced capacities according to configurable settings. + +Dynamic partitioning is particularly useful in scenarios where a large number of people are engaging in a live event, such as a game, a live stream, or a webinar. With so many messages flooding the channel, it can quickly become overwhelming, making it difficult for meaningful engagement to occur among participants. To address this, dynamic partitioning divides watchers into smaller partitions within the channel, boosting the chances of meaningful and enjoyable interactions without restricting the volume of messages sent. + +Dynamic Partitioning is transparent to connected clients, except for receiving fewer messages. + +![Partition split](https://getstream.imgix.net/docs/88b388d1-1367-4fea-a80f-359b0041954f.png?auto=compress&fit=clip&w=800&h=600) + +[System messages](/chat/docs/python/silent_messages/) are always delivered to all partitions. + +### Enabling Dynamic Partitioning + +> [!NOTE] +> This feature is only available on Stream's Enterprise pricing plans. Request the Stream team to enable this feature for your app by [contacting support](https://getstream.io/contact/support/). + + +Dynamic partitioning is configured on a channel type level and can be enabled by setting the `partition_size` to a number for how many people should appear in each partition, such as `100` . When enabled, the channel is divided into smaller partitions that all attempt to keep approximately the desired number of users in each partition. When a user sends a message, it is only delivered to other users on the same partition. + + +The minimum partition size is `10` , and there is no maximum. Dynamic partitioning can only be configured from server clients. + +Partitions are added and removed automatically, and clients are moved to new partitions as needed. Most clients stay on the same partition during a split, but some are moved to balance partitions. For example, assume 1000 clients are connected, and the target partition size is 100. 1000 users / 100 per partition = 10 partitions. If a new client is connected, we end up with 11 partitions and some existing clients are moved to this partition to keep them balanced at ~91/partition. + +Partitions are also removed when a client disconnects so that no partition becomes empty or unbalanced. The system will attempt to keep all partitions at approximately the desired target size. + +![Partition merge](https://getstream.imgix.net/docs/3b8ac296-3d9c-4284-b5ae-5a17f04dc75e.png?auto=compress&fit=clip&w=800&h=600) + +### Updating dynamic partitioning + +The `partition_size` can be changed at any time without impacting existing connected clients. For example, with 5000 connected clients and a `partition_size` of 100, we have 50 partitions. If the value is changed to 200, the partitions merge to 25 with 200 users each. + + +### Disabling dynamic partitioning + +To disable dynamic partitioning, update the channel type and set the `partition_size` to `null` . All connected clients will now receive all messages. + + +### Partition TTL + +When users connect, they are placed on the same partition as before, if it still exists. This allows users to stay close by and see familiar users. However, sometimes, this is not desired if the chat should feel vibrant with new users. We can use Partition TTL to do this without increasing the partition size. This will move users to a random partition at a set interval. Partition TTL is disabled by default. + +![Partition randomization with TTL](https://getstream.imgix.net/docs/942259b4-935a-47ec-87f5-c73ed375c95c.png?auto=compress&fit=clip&w=800&h=600) + +Partition TTL can be enabled by setting `partition_ttl` to a value for how frequently partitions should be randomized. The duration is provided as a string, such as `3h` , `24h` , or `2h30m` . Value units are `s` , `m` and `h` . For example, with a value of `partition_ttl: "6h"` , all partitions are randomly shuffled every 6 hours. The minimum value is 1 minute. There is no maximum value. + + +The value can be updated at any time, and it can be disabled by setting the value to `null` . Updating it (including disabling it) will move any connected users to a new partition. + +### Caveats + +- Users may be moved to a new partition at any time. This means a user may reply to a message, but the original author may not receive the reply as they are now in a different partition. + +- The distribution of the partitions is not perfect by design. For example, if we have a desired size of 100, some partitions may end up with 95 users, and others may end up with 105. Partitions will stay within approximately 10% of the desired size. + +- Reloading the channel's history will return all messages. If this is not desired, the existing messages should be retained on the client side. + +- When reconnecting, the client may not end up on the same partition. + +- Randomizing the partitions with `partition_ttl` is not guaranteed to happen at a specific time of day ( `24h` does not mean every midnight) diff --git a/docs/features/advanced/pending_messages.md b/docs/features/advanced/pending_messages.md new file mode 100644 index 0000000..799e882 --- /dev/null +++ b/docs/features/advanced/pending_messages.md @@ -0,0 +1,137 @@ +Pending Messages features lets you introduce asynchronous moderation on messages being sent on channel. To use this feature please get in touch with support so that we can enable it for your organisation. + +## Sending Pending Messages + +Messages can be made pending by default by setting the channel config property `mark_messages_pending` to true. + +```python +response = client.update_channel_type("messaging", mark_messages_pending=True) +``` + +You can also set the `pending` property on a message to mark it as pending on server side (this will override the channel configuration). **Please note that this is only server-side feature** . + +```python +response = channel.send_message( + {"text": "hi"}, random_user["id"], pending=True, pending_message_metadata={"extra_data": "test"} + ) +``` + +Pending messages will only be visible to the user that sent them. They will not be query-able by other users. + +## Callbacks + +When a pending message is either sent or deleted, the message and its associated pending message metadata are forwarded to your configured callback endpoint via HTTP(s). You may set up to two pending message hooks per application. Only the first commit to a pending message will succeed; any subsequent commit attempts will return an error, as the message is no longer pending. If multiple hooks specify a `timeout_ms`, the system will use the longest timeout value. + +You can configure this callback using the dashboard or server-side SDKs. + +### Using the Dashboard + +1. Go to the [Stream Dashboard](https://getstream.io/dashboard/) +2. Select your app +3. Navigate to your app's settings until "Webhook & Event Configuration" section +4. Click on "Add Integration" +5. Add and configure pending message hook + +![](@chat/_default/_assets/images/pending_message_dashboard.png) + +### Using Server-Side SDKs + +```python +# Note: Any previously existing hooks not included in event_hooks array will be deleted. +# Get current settings first to preserve your existing configuration. + +# STEP 1: Get current app settings to preserve existing hooks +response = client.get_app_settings() +existing_hooks = response.get("event_hooks", []) +print("Current event hooks:", existing_hooks) + +# STEP 2: Add pending message hook while preserving existing hooks +new_pending_message_hook = { + "enabled": True, + "hook_type": "pending_message", + "webhook_url": "https://example.com/pending-messages", + "timeout_ms": 10000, # how long messages should stay pending before being deleted + "callback": { + "mode": "CALLBACK_MODE_REST" + } +} + +# STEP 3: Update with complete array including existing hooks +client.update_app_settings( + event_hooks=existing_hooks + [new_pending_message_hook] +) +``` + +See the [Webhooks](/chat/docs/python/webhooks_overview/) documentation for complete details. + +### Callback Request + +For example, if your callback server url is , we would send callbacks: + +- When pending message is sent + +`POST https://example.com/PassOnPendingMessage` + +- When a pending message is deleted + +`POST https://https://example.com/DeletedPendingMessage` + +In both callbacks, the body of the POST request will be of the form: + +```json +{ + "message": { + // the message object + }, + "metadata": { + // keys and values that you passed as pending_message_metadata + }, + "request_info": { + // request info of the request that sent the pending message. Example: + /* + "type": "client", + "ip": "127.0.0.1", + "user_agent": "Mozilla/5.0...", + "sdk": "stream-chat-js", + "ext": "additional-data" + */ + } +} +``` + +## Deleting pending messages + +Pending messages can be deleted using the normal delete message endpoint. Users are only able to delete their own pending messages. The messages must be hard deleted. Soft deleting a pending message will return an error. + +## Updating pending messages + +Pending messages cannot be updated. + +## Querying pending messages + +A user can retrieve their own pending messages using the following endpoints: + +```python +# To retrieve single message +client.get_message(msg_id) + +# To retrieve multiple messages +client.get_messages(['message-1', 'message-2']) +``` + +## Query channels + +Each channel that is returned from query channels will also have an array of `pending_messages` . These are pending messages that were sent to this channel, and belong to the user who made the query channels call. This array will contain a maximum of 100 messages and these will be the 100 most recently sent messages. + + +## Committing pending messages + +Calling the commit message endpoint will promote a pending message into a normal message. This message will then be visible to other users and any events/push notifications associated with the message will be sent. + +The commit message endpoint is server-side only. + +```python +client.commit_message("message-1") +``` + +If a message has been in the pending state longer than the `timeout_ms` defined for your app, then the pending message will be deleted. The default timeout for a pending message is 3 days. diff --git a/docs/features/advanced/private_messaging.md b/docs/features/advanced/private_messaging.md new file mode 100644 index 0000000..5b38294 --- /dev/null +++ b/docs/features/advanced/private_messaging.md @@ -0,0 +1,61 @@ +Restricted message delivery is a feature that allows sending a message in channel to one or more specific users, thereby limiting visibility to other users in that channel. +If you want to inform 1 specific user in a channel with a system message for example, then restricted message delivery is perfectly feature for it. + +> [!NOTE] +> This feature is only available on Stream's Enterprise pricing plans. Request the Stream team to enable this feature for your app by [contacting support](https://getstream.io/contact/support/). + + +## Sending a message with restricted visibility + +In order to send a message with restricted visibility the user needs to have the `CreateRestrictedVisibilityMessage` permission. +A message with restricted visibility is send by adding a list of users to the `restricted_visibility` property. Please note that updating the list of users who can see the message is not possible after the message has been send. + +```python +# Get a channel +channel = client.channel("messaging", "ride-08467339") + +# Send a message only visible to Jane +response = await channel.send_message({ + "text": "Hi Jane, your driver John will be at your location in 1 minute", + "type": "system", + "restricted_visibility": [ "jane" ] +}, admin_user["id"]) +``` + +## Possible use cases + +### Taxi app (like Uber) use case + +In a taxi app where both the driver and the passenger share a channel, you could provide either of them with additional updates. + +- Alert the driver that the passenger is close by. +- Alert the passenger of where the driver is. + +### Moderation use case + +- The system may wish to send a message to only a single user to alert them that there may be fraudulent activity from the another user in the channel. +- Show a blocked user a message to let them know they have been suspended. + +### Marketplace use case + +- Show alternative listings to a potential buyer, thereby improving customer engagement. +- Show a message the product is reserved for the user. + +## Additional information + +### Visibility to other users + +By default a restricted visibility message is only visible to the sender of the message and the users in the `restricted_visibility` list. If you want other users to be able to view those messages as well, you can give those users the `ReadRestrictedVisibilityMessage` permission. By default this permission is only granted to `admin` roles. + +### Pinning a restricted visibility message + +Pinning a restricted visibility message to a channel is not allowed, simply because pinning a message is meant to bring attention to that message, that is not possible with a message that is only visible to a subset of users. + +### Unread counts + +If a restricted visibility message is send to a channel and the user cannot see that message then the unread count will not be increased for that user. The unread count will be increased for users who can see the message. + +### Truncating and updating channels + +When truncating or updating a channel, the user can choose to send a system message in the same request. However, this message cannot contain a list of restricted visibility users. The reason being is both operations send an event to all channel members. This event includes the optional message. All channel members need to be notified about the channel update or truncation, it is not possible to send a message with restricted visbility. +If you want to send a message with restricted visibility, then the update or truncate the channel first, after that you can send a message with restricted visibility to the channel. diff --git a/docs/features/advanced/slow_mode_and_throttling.md b/docs/features/advanced/slow_mode_and_throttling.md new file mode 100644 index 0000000..29cea6b --- /dev/null +++ b/docs/features/advanced/slow_mode_and_throttling.md @@ -0,0 +1,53 @@ +For live events or concerts, you can sometimes have so many users, that the sheer volume of messages overloads the browser or mobile device. This can cause the UI to freeze, high CPU usage, and degraded user experience. Stream offers 3 features to help with this: + +1. Channel Slow Mode + +2. Automatic feature Throttling + +3. Message Throttling + +Stream scales to 5 million concurrent users on a channel, see [Scaling Chat to 5 Million Concurrent Connections](https://getstream.io/blog/scaling-chat-5-million-concurrent-connections/). + +### Channel Slow Mode + +Slow mode helps reduce noise on a channel by limiting users to a maximum of 1 message per cooldown interval. + +The cooldown interval is configurable and can be anything between 1 and 120 seconds. For instance, if you enable slow mode and set the cooldown interval to 30 seconds a user will be able to post at most 1 message every 30 seconds. + +> [!NOTE] +> Moderators, admins and server-side API calls are not restricted by the cooldown period and can post messages as usual. + + +Slow mode is disabled by default and can be enabled/disabled via the Dashboard, using the Chat Explorer: + +![](https://getstream.imgix.net/docs/Screenshot%202021-08-13%20105802.png?auto=compress&fit=clip&w=800&h=600) + +It can also be enabled/disabled by admins and moderators via SDK. + +```python +channel.update({ "cooldown": 30 }) # 30 sec +``` + +When a user posts a message during the cooldown period, the API returns an error message. You can avoid hitting the APIs and instead show such limitation on the send message UI directly. When slow mode is enabled, channels include a `cooldown` field containing the current cooldown period in seconds. + + +### Automatic Feature Throttling + +When a channel has more than 100 active watchers Stream Chat automatically toggles off some features. This is to avoid performance degradation for end-users. Processing large amount of events can potentially increase CPU and memory usage on mobile and web apps. + +1. Read events and typing indicator events are discarded + +2. Watcher start/stop events are only sent once every 5 seconds + +### Message throttling + +Message throttling that protects the client from message flooding. Chat clients will receive up to 5 messages per second and the API servers will allow small surges of messages to be delivered even if that means exceeding the 5 msg/s rate. + +Here is an example of how message throttling works: + +![](https://user-images.githubusercontent.com/88735/96602562-70938100-12f3-11eb-8379-cb316dc7969f.png) + +In this example, the client will receive several more messages above the 5/s limit (the yellow bar), and once this burst credit is over, the client will stop receiving more than 5 messages per second. The burst credit is set to 10 messages on an 8 seconds rolling window. + +> [!NOTE] +> If you are on an [Enterprise Plan](https://getstream.io/enterprise/), message throttling can be disabled or increased for your application by our support team diff --git a/docs/features/advanced/user_average_response_time.md b/docs/features/advanced/user_average_response_time.md new file mode 100644 index 0000000..e86d62d --- /dev/null +++ b/docs/features/advanced/user_average_response_time.md @@ -0,0 +1,36 @@ +The User Average Response Time feature enables users to view the average response time of other users in their public profiles. This metric helps set expectations for communication responsiveness, which is particularly valuable in marketplace applications where prompt responses are important for successful transactions. + +## Configuration + +To enable user response time tracking, set the `user_response_time_enabled` setting to `true`: + +```python +# Enable user response time tracking +client.update_app_settings(user_response_time_enabled=True) +``` + +Once enabled, the `avg_response_time` field will be included in user responses and displayed in user profiles. + +## Use Cases + +### Marketplace Applications + +- Buyers can see how quickly sellers typically respond before initiating contact +- Marketplaces can highlight responsive sellers with badges or sorting options +- Customer support teams can identify and reward highly responsive users + +### Service Platforms + +- Service providers can demonstrate their responsiveness to potential clients +- Users can select service providers based on communication expectations + +### Customer Support Applications + +- Display agent responsiveness to help manage customer expectations +- Create internal leaderboards based on response times + +## How It Works + +- The system tracks the time between replies in a channel and when they respond +- When a user sends a new message that isn't the first in a channel, the system calculates a new average +- This data is then displayed in the user's public profile or returned in the `avg_response_time` field diff --git a/docs/features/campaign_api.md b/docs/features/campaign_api.md new file mode 100644 index 0000000..d490ae9 --- /dev/null +++ b/docs/features/campaign_api.md @@ -0,0 +1,561 @@ +The Campaign API makes it easy to send a message or send an announcement to a large group of users and/or channels. You can personalize the message using templates. For small campaigns of less than 10,000 users, you can directly send the campaign. If you need to maintain larger segments of users or channels that's also supported. The potential applications of this feature are virtually limitless, yet here are some specific examples: + +> [!WARNING] +> The Campaign API runs with administrative privileges and does not validate permissions - any valid user ID can be used as the sender_id. Be sure to implement appropriate access controls in your application code. + + +- **Marketing Campaigns** : Send promotional messages or announcements to a targeted audience. + +- **Product Updates** : Inform users about new features, bug fixes, or enhancements to your product. + +- **Event Reminders:** Send reminders about upcoming events, webinars, or conferences to registered attendees. + +- **Customer Surveys** : Engage with your user base by sending out surveys or feedback forms to gather feedback. + +- **Announcements** : Broadcast important company news, policy changes, or updates to stakeholders. + +- **Campaign Scheduling** : Plan and schedule campaigns in advance to ensure timely delivery and maximize impact + +Under the hood, campaigns send messages to the specified **target** audience and do so on behalf of a designated user ( **sender** ). If there are no existing channels to deliver messages to the target users, campaigns can automatically create them. Additionally, campaigns might generate more events for creating channels and sending new messages. These events can be sent as **In-App** messages and/or **Push Notifications** to the end users, and as **Webhook** calls to your backend server. + +The Campaigns API is designed for backend-to-backend interactions and is not intended for direct use by client-side applications. + +By default we have rate limits in place to ensure that campaigns don't cause stability issues. The throughput supports sending campaigns with tens of millions of messages. Be sure to reach out to support to collaborate with our team and raise your limits. + +> [!WARNING] +> All paid plans include 3 times the procured MAU volume in message capacity. Ex: if you have a 100,000 MAU plan you can send 300,000 campaign messages each month. If you need to send more messages than this limit reach out to our sales team + + +## Sending a Campaign + +Here's a basic example of how to send a campaign. Note that the sender_id can be any valid user ID since the Campaign API bypasses normal permission checks. + +> [!NOTE] +> You can send the campaign immediately or schedule it to start at a later time. You can also stop the campaign at any time. + + +```python +segment_id = "" # segment_id is optional +# Create a dynamic user segment based on the filter provided. +# e.g., following segment will include all users created after 2020-01-01 +segment = client.segment(SegmentType.USER, segment_id, { + "name": 'New App Users Segment (optional)', + "filter": { + "created_at": { + "$gte": "2020-01-01T00:00:00Z", + } + } +}) +segment.create() + +campaign_id = "" # campaign_id is optional +campaign = client.campaign(campaign_id, data={ + # Users targeted by following segment will receive the message + "segment_ids": [segment_id], + # Alternatively, instead of segment_ids, you can also provide user_ids to send the message to specific users + # user_ids: ["", ""], + "sender_id": "", # mandatory + # Optional, specifies whether to 'exclude' or 'include' the sender from the channel. Defaults to null. + "sender_mode": "exclude", + # Optional, controls the visibility of the new channels for the sender ("hidden" or "archived"). Defaults to null + "sender_visibility": "hidden", + "name": "Campaign name", # optional + "description": "Campaign Description", # optional + "message_template": { + "text": "Hello {{receiver.name}}!", # mandatory, message text template + "attachments": [], # Optional, message attachments + "poll_id": "poll-id", # Optional, send a poll with message + "custom": { "promotional": True }, # Optional, custom fields will be added to message object received by the receiver. + }, + "show_channels": True, # Optional, show hidden channels for receiver + "create_channels": True, # Optional, create channel between sender and receiver if not already present + # channel_template is required if create_channels is true + "channel_template": { + # mandatory, channel type + "type": 'messaging', + # Optional, template for channel id for channel creation + # if not provided, channel id will be generated on server side + "id": "{{receiver.id}}-{{sender.id}}", + # Optional, custom fields will be added to channel object + "custom": { "promotional": True }, + # Optional, if provided (and multi tenancy is enabled), you can limit accessibility to the channel only to a team + "team": "kansas-city-chiefs", + # Optional, if provided following members will be added to each of the newly created channel + # if not provided, only sender and receiver will be added to the channel + # You can use this to add e.g., moderator or admin to each newly created channel + "members": ["user-id-1", "user-id-2"], + # Alternatively, you can use members_template to specify channel roles and custom data for members. + # members and members_template cannot be used together + # "members_template": [ + # { + # "user_id": "user-id-1", + # "channel_role": "channel_moderator", # optional + # "custom": { "key1": "value1" }, # optional + # }, + # { + # "user_id": "user-id-2", + # "channel_role": "channel_moderator", # optional + # }, + # ] + } +}) + +campaign.create() + +# Start sending messages to targeted users +campaign.start() + +# Alternatively you can also schedule the campaign to start at a later time and stop at a specific time +campaign.start( + scheduled_for=datetime.datetime.now() + datetime.timedelta(hours=48), + stop_at=datetime.datetime.now() + datetime.timedelta(hours=72), +) +``` + +The campaign exposes methods to create, get, update, start, stop delete and query campaigns. + +```python +campaign.create() # create a campaign based on data passed above +campaign.get() # check the status of the campaign +campaign.update({ + "segment_ids": ["a869fc0f-2e7e-4fe0-8651-775c892c1718"], + "sender_id": 'Updated-user-id-of-sender', # mandatory + "sender_mode": "include", # optional + "sender_visibility": "hidden", # optional + "name": 'Updated name (optional)', + "message_template": { + "text": "Updated Hello {{receiver.name}}!", + } +}) # updates the campaign data + +# You can start a campaign immediately, which will start sending messages to the users in the segment(s) immediately. +campaign.start() + +# Or you can schedule a campaign to start at a later time. +campaign.start( + # start campaign in 48 hours + scheduled_for=datetime.datetime.now() + datetime.timedelta(hours=48), # optional, campaign will start running after this time + # automatically stop the campaign after 72 hours + stop_at=datetime.datetime.now() + datetime.timedelta(hours=72) # optional, campaign will stop running after this time +}) + +campaign.stop() # stops it +campaign.delete() # delete the campaign + +filter = { + "segments": { "$in": [""] } +} +sort = [{"field":"created_at", "direction": SortOrder.DESC}] +options = { + "limit": 25, + "next": "", +} +result = client.query_campaigns(filter, sort, options) # query campaigns +# result.get("campaigns", []) is a list of campaigns +# result.get("next", None) is a cursor for the next page of results +``` + +## Creating a Campaign + +Here are the supported options for creating a campaign: + +| name | type | description | default | optional | +| ----------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | +| id | string | Specify an ID for your campaign | - | ✓ | +| name | string | The name of the campaign | - | ✓ | +| description | string | The description for the campaign | - | ✓ | +| segment_ids | string | A list of segments to target. Max 25. Use either segment_ids or user_ids to target your campaign. The campaign will automatically remove duplicates if users are present in more than 1 segment. | - | ✓ | +| user_ids | string | A list of user ids to target. Max 10,000 users, for bigger campaigns create a user segment first. Use either user_ids or segment_ids to target your campaign. | - | ✓ | +| sender_id | string | The user id of the user that's sending the campaign. Note: The sender_id is not checked against the permission system - any valid user ID can be used. | - | | +| sender_mode | string | Controls how the campaign sender is added to channels. Possible values:
- `"exclude"`: Don't add sender to any channels
- `"include"`: Add sender to all channels (new and existing)
When parameter is omitted (default behavior): Add sender to new channels only. | - | ✓ | +| sender_visibility | string | Controls the visibility of the new channels for the sender when the sender is included as a member. Possible values:
- `"hidden"`: New channels will be hidden for the sender
- `"archived"`: New channels will be archived for the sender
When parameter is omitted (default behavior): All channels are visible. | - | ✓ | +| message_template | string | A message template | - | | +| show_channels | boolean | If **true** then hidden channels will be shown for receiver | false | ✓ | +| create_channels | boolean | If **true** then channels will be created if they don't exist yet | false | ✓ | +| channel_template | string | The template to use when creating a channel | - | ✓ | +| skip_push | boolean | Do not send push notifications for events generated by this campaign, such as message.new or channel.created | false | ✓ | +| skip_webhook | boolean | Do not call webhooks for events generated by this campaign, such as message.new or channel.created | false | ✓ | + +Note that campaigns can only be sent once. If you want to repeate the same campaign you have to create a new campaign object with the same template and segment ids. + +### Message Template + +The message template uses Django/Jinja style variables. So you can use {{ myvariable }} to customize the message. The following fields are available: + +| VarIABLE | Description | +| -------- | --------------------------------------------------------------------------------------------------------- | +| Sender | User object that's sending this campaign | +| Receiver | The person receiving the message. This is only available in 1-1 channels, and not when sending to a group | +| Channel | The channel the message is being sent to | + +So for example you could use a template like: "Hi {{ receiver.name }} welcome to the community". Messages sent by the campaign API will automatically contain the campaign_id custom field and will have type set to regular. + +```json +{ + "text": "{{ sender.name }} says hello!", + "custom": { + "campaign_data": {{ custom }}, + }, + "attachments": [{ + "type": "image", "url": "https://path/to/image.jpg" + }], + "poll_id": "poll-id", +} +``` + +### Channel Template + +Here's an example channel template. It enables the campaign API to find the right channel for a recipient and sender. + +```json +{ + "type": "messaging", // channel type is required + "id": "{{receiver.id}}-{{sender.id}}", + "team": "kansas-city-chiefs", // optional, if provided (and multi tenancy is enabled), you can limit accessibility to the channel only to a team + "custom": { + // optionally add custom data to channels (only when creating) + } +} +``` + +## Querying Campaigns + +You can query campaigns based on extensive set of filters and sort options + +```python +# Query campaigns with which are scheduled or in progress +filter_options = {"status": {"$in": ["scheduled", "in_progress"]}} +sort = [{"field": "created_at", "direction": SortOrder.DESC}] + +# query first page +options1 = {"limit": 10} +page1 = client.query_campaigns(filter_options, sort, options1) + +# query next page +options2 = {"limit": 10, "next": page1["next"]} +page2 = client.query_campaigns(filter_options, sort, options2) +``` + +Following code sample provides various examples of filters: + +```python +filter_status = { "status": { "$in": ["scheduled", "in_progress"] }} +filter_id = { id: { "$in": ["campaign_id_1", "campaign_id_2"] }} +filter_by_segments = { "segments": { "$in": ["segment_id_1", "segment_id_2"] }} +filter_by_name = { "name": { "$in": ["campaign_name_1", "campaign_name_2"] }} +filter_by_sender = { "sender_id": { "$in": ["sender_1", "sender_2"] }} +filter_by_created = { "created_at": { "$gte": "2021-01-01T00:00:00Z" }} +filter_by_updated = { "updated_at": { "$gte": "2021-01-01T00:00:00Z" }} +``` + +## Paginating Campaign Users + +If you created a campaign targeting a specific list of user IDs, you can retrieve the targeted users using pagination. +The Campaign API limits each response to 1,000 users. +To access additional users beyond this limit, you can paginate using the `limit` and `next` parameters, as shown below. + +```python +# Let's say you have a campaign with 2000 users +campaign = client.campaign(""); + +# Fetch camapgin with the first 1000 users +res1 = campaign.get({ + "users": { + "limit": 1000, # 1000 is max allowed limit + }, +}) +first_page_users = res1["campaign"]["users"] + +# Fetch campaign with the next 1000 users +res2 = campaign.get({ + "users": { + "limit": 1000, # 1000 is max allowed limit + "next": res1["users"]["next"], + # or use prev to get previous page + "prev": res1["users"]["prev"], + }, +}) +second_page_users = res2["campaign"]["users"] +``` + +## Segments for Campaigns + +Segments enable you to target large groups of users. You can either specify a large list of user ids, channel ids, or filters that search the user database. There is no limit on how many users you can have in a segment. + +### User Segments + +```python +type = # mandatory +id = "" # optional +data = { + "filter": { + "team": "commonteam" + }, + "name": "segment_name", + "description": "segment_description" +} # optional + +segment = client.segment(type, id, data) +``` + +You can create, update or delete segments. You can also add users to the segment. The above approach specified a filter to query the users. Alternatively you can also manually provide a list of user ids. + +```python +segment_type = SegmentType.USER # mandatory +segment_id = "" # optional +data = { + "filter": { + #... + } +} # optional +user_segment = client.segment(segment_type, segment_id, data) + +user_segment.create() +user_segment.get() +user_segment.add_targets(user_ids) # a max of 10,000 users can be added in 1 API call +user_segment.remove_targets(user_ids) # no error if doesn't exits +user_segment.target_exists(user_Id) # checks if target exists in the segment +user_segment.query_targets( + filter_conditions={ "target_id": {"$gte": ""} }, + sort=[{"field": "target_id", "direction": SortOrder.DESC }], + options={ + "limit": 10000, + "next": "", # or prev + } +) # queries targets in the segment +user_segment.delete() # deletes segment +``` + +The example below shows how to create a segment with **all users** : + +```python +data = { + "name": "everyone", + "all_users": true +} +user_segment = client.segment(SegmentType.USER, data=data) +user_segment.create(); +``` + +**User** segment supports following options as part of **data** : + +| name | type | description | default | optional | +| ----------- | ------- | ----------------------------------------------------- | ------- | -------- | +| name | string | Name for the segment | - | ✓ | +| description | string | Description of the segment | - | ✓ | +| filter | json | Filter criteria for target users of this segment | null | ✓ | +| all_users | boolean | If true, segment will target all the users of the app | false | ✓ | + +### Channel Segments + +You can also create segments of channels to target. If you target a channel the “receiver” message template variable will not be available. + +```python +# note: channel template is not required for channel segment +segmentType = SegmentType.CHANNEL +segmentId = "segmentId" +data = { + "name": "segment_name", + "description": "segment_description", + "filter": { + "team":"commonteam" + } +} +channel_segment = client.segment(segmentType, segmentId, data) +channel_segment.create() +channel_segment.get() +channel_segment.addTargets(channel_cids) +channel_segment.removeTargets(channel_cids) +channel_segment.targetExists(channel_cid) +channel_segment.delete() +``` + +The example below shows how to create a segment which targets all the channels where sender is member of + +```python +data = { + "name": "All my existing chats", + "all_sender_channels": true +} +channel_segment = client.segment(SegmentType.CHANNEL, data=data) +channel_segment.create(); +``` + +**Channel** segment supports following options as part of the **data** : + +| name | type | description | default | optional | +| ------------------- | ------- | ----------------------------------------------------------------------- | ------- | -------- | +| name | string | Name of the segment | - | ✓ | +| description | string | Description for the segment | - | ✓ | +| filter | json | Filter criteria for target channels of this segment | null | ✓ | +| all_sender_channels | boolean | If true, segment will target all the channels where sender is member of | false | ✓ | + +## Getting Segment + +For getting a specified segment you may use the following code snippet: + +```python +segment = client.segment(segment_type, segment_id) +response = segment.get() +``` + +The received `response` will contain the segment data: + +| name | type | description | default | optional | +| ------------------- | ------- | ------------------------------------------------------------------------ | ------- | -------- | +| id | string | ID of the segment | - | | +| type | string | Type of the segment ("user" or "channel") | - | | +| name | string | Name of the segment | "" | | +| description | string | Description of the segment | "" | | +| filter | object | Filter criteria for target users or channels of this segment | nil | | +| all_users | boolean | If true, then segment targets all the users of the app | false | | +| all_sender_channels | boolean | If true, then segment targets all the channels where sender is member of | false | | +| size | integer | Number of the targets for this segment | 0 | | +| created_at | string | Date when the segment was created | - | | +| updated_at | string | Date when the segment was update | - | | +| deleted_at | string | Date when the segment was deleted | - | ✓ | + +Please, take into account that: + +- Parameters `filter` , `all_users` and `all_sender_channels` are mutually exclusive. + +- The `size` is calculated asynchronously when either `filter` or `all_users` is set. + +- The `size` is calculated in place if you add targets manually using `segment.addTargets(...)` function + +> [!WARNING] +> The `size` won't be calculated at all if `all_sender_channels` is set to `true` . If you want the `size` to be calculated for the `channel` segment types, please provide the `filter` instead. + + +## Sending a Campaign to Segments - Full example + +The example below shows you how to create a segment and send a campaign to it + +### **Create a segment for user’s in the USA** + +```python +data = { + "name": "People in the USA", + "filter": { + "country": "USA" + } +} +segment = client.segment(SegmentType.USER, data=data) +segment.create() +``` + +### **Message the above segment** + +> [!NOTE] +> Remember that the Campaign API allows using any valid user ID as sender_id regardless of permissions. Make sure to validate in your application code that the requesting user has appropriate permissions to send campaigns on behalf of other users. + + +```python +campaign = client.campaign(data={ + "segment_ids": [segment["segment"]["id"]] + "sender_id": "user-id-of-sender", # mandatory + "name": "Campaign name (optional)", + "description": "Optional description", + "message_template": { + "text": "Hi {{ receiver.name }} I\'m {{ sender.name }}!", + } +}) +campaign.create() +campaign.start() + +# Alternatively you can schedule the campaign to start at a later time. +# Also you can stop the campaign at a specific time. E.g., +campaign.start( + scheduled_for="2021-12-31T23:59:59Z", + stop_at="2022-01-01T23:59:59Z" +) +``` + +### **Check the status of the campaign** + +```python +res = campaign.get() +print(res["campaign"]["status"]) # "draft" | "scheduled" | "stopped" | "completed" | "in_progress" +``` + +Campaign status have following possible values: + +- `draft` - Campaign has been created but not scheduled + +- `scheduled` - Campaign has been scheduled + +- `stopped` - Campaign has been stopped manually or using stop_at option + +- `completed` - Campaign has succesfully completed + +- `in_progress` - Campaign is running at the moment + +Sending campaigns is fast but not realtime. It can take several minutes for your campaign to complete sending. A campaign with 60,000 users typically takes ~1 minute to send. + +### Campaign Stats + +The campaign API returns stats when you call campaign.get. + +```python +# campaign.get() +{ + "id": "...", + "stats": { + "started_at": "2021-02-01 00:00:00", + "completed_at": "2024-23-02 00:00:00", + "messages_sent": 1000, + "channels_created": 10, + "stats_progress": 0.97, + "stats_users_sent": 1000, # number of users the campaign message was sent to + "stats_users_read": 567 # number of users who read the campaign message + } +} +``` + +### Webhooks + +Your app will often want to know when a campaign API starts or stops. Your server hooks will receive an event when the campaign starts and another when the campaign is completed. + +Both events include the full campaign object with its status and stats. + +```json +{ + "type": "campaign.started", + "campaign": { + "status": "running", + "stats": {...}, + ... + }, + "created_at": "2024-23-02 00:00:00" +} + +{ + "type": "campaign.completed", + "campaign": { + "status": "completed", + "stats": {...}, + ... + }, + "created_at": "2024-23-02 00:00:00" +} +``` + +### Updating a large Segment + +client.querySegments allows you to paginate over a large segment with up to 10,000 results per page. + +```python +filter = { + "name": "" +} +sort = [{"field":"created_at", "direction": SortOrder.DESC}] +options = { + "limit": 30, + "next": "" +} +response = client.query_segments(filter, sort, options) +``` + +The list of users is sorted by ID ASC. This means that you can easily compare it to your internal list of users in this segment, and call segment.addTargets/addTargets as needed. + +Page updated Feb 20th 5:42 diff --git a/docs/features/events.md b/docs/features/events.md new file mode 100644 index 0000000..ac6dead --- /dev/null +++ b/docs/features/events.md @@ -0,0 +1,145 @@ +All changes to the chat state are exposed as events. +When a new message is sent, a reaction is added, or any other action occurs, the client receives an event in real-time. +You can also send custom events, enabling you to build your own pub/sub functionality on top of a channel. + +### Listening for Events + +The code sample below shows how to listen to events: + + +You can also listen to all events at once: + + +### Event Types + +There are different type of events + +- User level/connection level events. You always receive these events. +- Notification events you receive if you are a member of the channel +- Channel level events you receive if you are watching the channel +- User presence events are sent when you specify presence=true +- Custom events. Your own pub/sub on top of the chat channel. + +The example below shows how to watch a channel and enable user presence events. +You can also watch channels and enable user presence when using query channels. +See these links for more details on user presence, watching and query channels. + + +### Connection Events + +The official SDKs make sure that a connection to Stream is kept alive at all times and that chat state is recovered when the user's internet connection comes back online. Your application can subscribe to changes to the connection using client events. + + +### Stop Listening for Events + +It is a good practice to unregister event handlers once they are not in use anymore. Doing so will save you from performance degradations coming from memory leaks or even from errors and exceptions (i.e. null pointer exceptions) + + +### Custom Events + +Custom events allow you to build your own pub/sub functionality on top of a channel. You can send any event with custom data and have it delivered to all users watching the channel. + +#### To a channel + +Users connected to a channel, either as a watcher or member, can send custom events and have them delivered to all users [watching the channel](/chat/docs/python/creating_channels/). + +```python +channel.send_event({"type": "friendship_request", "text": "Hey there, long time no see!"}, server_user["id"]) +``` + +> [!NOTE] +> Custom events are enabled by default on all channel types, you can disable them using the Dashboard or the API the same way as you would manage other channel features (ie. replies, URL previews, ...) + + +#### Permissions + +Like every client-side API request, sending a custom event includes a permission check. By default users that can read messages on a channel can also send custom events. Check the [Auth & Permission](/chat/docs/python/chat_permission_policies/) section to find out more about how permissions can be customized for your application. + +> [!NOTE] +> Keep in mind a clever user can send their own custom events. We recommend using the type attribute or custom data on the event to limit the kinds of events that are displayed to the recipient to ones that are safe e.g. if a bad actor sends a "password reset" or other malicious events, your client app should just ignore it. + + +#### To a user + +This allows you to send custom events to a connected user. The event is delivered to all connected clients for that user. + +It is only available with server-side authentication. A copy of the event is sent via web-hooks if it is enabled. + +```python +client.send_user_custom_event( + server_user["id"], {"type": "friendship_request", "text": "Tommaso wants to be your friend"} +) +``` + +| name | type | description | default | optional | +| ------------ | ------ | ----------------- | ------- | -------- | +| targetUserID | string | target user ID | - | | +| data | object | event to be sent | - | | +| data.type | string | type of the event | - | | + +If the user doesn't exist, a `404 Not Found` error is returned. + +The type of the event shouldn't contain any `.` character otherwise a `400 Bad Request` error is returned. This is a character used for built-in events, see the Built-in Events section below for more details. + +### Event Object + +| name | type | description | default | optional | +| ------------------ | ------ | --------------------------------------------------------------------------------- | ------- | -------- | +| cid | string | Channel ID | | ✓ | +| type | string | Event type | | | +| message | object | [Message Object](/chat/docs/python/send_message/#message-response-structure) | | ✓ | +| reaction | object | [Reaction Object](/chat/docs/python/send_reaction/) | | ✓ | +| channel | object | [Channel Object](/chat/docs/python/creating_channels/) | | ✓ | +| member | object | User object for the channel member that was added/removed | | ✓ | +| user | object | User object of the current user | | ✓ | +| me | object | User object of the health check user | | ✓ | +| total_unread_count | int | the number of unread messages for current user | | ✓ | +| watcher_count | int | Number of users watching this channel | | ✓ | + +### Built-in Events + +The table below shows an overview of all built-in events: + +| Event | Trigger | Recipients | Type | +| ---------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------- | +| channel.deleted | when a channel is deleted | clients watching the channel | channel event | +| channel.hidden | when a channel is marked as hidden | clients from the user that marked the channel as hidden (see hiding channels) | channel event | +| channel.truncated | when a channel's history is truncated | clients watching the channel | channel event | +| channel.updated | when a channel is updated | clients watching the channel | channel event | +| channel.visible | when a channel is made visible | clients from the user that marked the channel as visible (see hiding channels) | channel event | +| connection.changed | when the state of the connection changed | local event | client event | +| connection.recovered | when the connection to chat servers is back online | local event | client event | +| health.check | every 30 seconds to confirm that the client connection is still alive | all clients | client event | +| member.added | when a member is added to a channel | clients watching the channel | channel event | +| member.removed | when a member is removed from a channel | clients watching the channel | channel event | +| member.updated | when a channel member is updated (promoted to moderator/accepted/rejected the invite) | clients watching the channel | channel event | +| message.deleted | when a message is deleted | clients watching the channel | channel event | +| message.new | when a new message is added on a channel | clients watching the channel | channel event | +| message.read | when a channel is marked as read | clients watching the channel | channel event | +| message.updated | when a message is updated | clients watching the channel | channel event | +| notification.added_to_channel | when the user is added to the list of channel members | clients from the user added that are not watching the channel | notification event | +| notification.channel_deleted | when a channel is deleted | clients from members that are not watching the channel | notification event | +| notification.channel_mutes_updated | when a channel is muted | clients from the user that muted the channel | notification event | +| notification.channel_truncated | when a channel's history is truncated | clients from members that are not watching the channel | notification event | +| notification.invite_accepted | when the user accepts an invite | clients from the user invited that are not watching the channel | notification event | +| notification.invite_rejected | when the user rejects an invite | clients from the user invited that are not watching the channel | notification event | +| notification.invited | when the user is invited to join a channel | clients from the user invited that are not watching the channel | notification event | +| notification.mark_read | when the total count of unread messages (across all channels the user is a member) changes | clients from the user with the new unread count | notification event | +| notification.mark_unread | when the user marks a message as unread | clients from the user with the new unread count | notification event | +| notification.message_new | when a message is added to a channel | clients that are not currently watching the channel | notification event | +| notification.mutes_updated | when the user mutes are updated | clients from the user that updated the list of mutes | notification event | +| notification.removed_from_channel | when a user is removed from the list of channel members | clients from the user removed that are not watching the channel | notification event | +| reaction.deleted | when a message reaction is deleted | clients watching the channel | channel event | +| reaction.new | when a message reaction is added | clients watching the channel | channel event | +| reaction.updated | when a message reaction is updated | clients watching the channel | channel event | +| typing.start | when a user starts typing | clients watching the channel | channel event | +| typing.stop | when a user stops typing | clients watching the channel | channel event | +| user.banned | when the user is banned | clients for the banned user | client event | +| user.deleted | when a user is deleted | clients subscribed to the user status | user presence event | +| user.messages.deleted | when the user's messages are deleted | clients for the banned user | client event | +| user.messages.deleted | when the user's messages are deleted | clients watching the channel where the user was banned | channel event | +| user.presence.changed | when a user status changes (eg. online, offline, away, etc.) | clients subscribed to the user status | user presence event | +| user.unbanned | when the user ban is lifted | clients for the banned user | client event | +| user.updated | when a user is updated | clients subscribed to the user status | user presence event | +| user.watching.start | when a user starts watching a channel | clients watching the channel | channel event | +| user.watching.stop | when a user stops watching a channel | clients watching the channel | channel event | diff --git a/docs/features/location_sharing.md b/docs/features/location_sharing.md new file mode 100644 index 0000000..f837e0b --- /dev/null +++ b/docs/features/location_sharing.md @@ -0,0 +1,120 @@ +Location sharing allows users to send a static position or share their real-time location with other participants in a channel. Stream Chat supports both static and live location sharing. + +There are two types of location sharing: + +- **Static Location**: A one-time location share that does not update over time. +- **Live Location**: A real-time location sharing that updates over time. + +> [!NOTE] +> The SDK handles location message creation and updates, but location tracking must be implemented by the application using device location services. + + +## Enabling location sharing + +The location sharing feature must be activated at the channel level before it can be used. You have two configuration options: activate it for a single channel using configuration overrides, or enable it globally for all channels of a particular type via [channel type settings](/chat/docs/python/channel_features/). + +```python +# Enabling it for a channel type +client.update_channel_type( + "messaging", + shared_locations=True, +) +``` + +## Sending static location + +Static location sharing allows you to send a message containing a static location. + +```python +# Send a static location message +now = datetime.datetime.now(datetime.timezone.utc) +shared_location = { + "created_by_device_id": "test_device_id", + "latitude": 37.7749, + "longitude": -122.4194, + # No 'end_at' for static location +} + +channel.send_message( + {"text": "Message with static location", "shared_location": shared_location}, + user_id, +) +``` + +## Starting live location sharing + +Live location sharing enables real-time location updates for a specified duration. The SDK manages the location message lifecycle, but your application is responsible for providing location updates. + +```python +# Send a live location message (with end_at) +now = datetime.datetime.now(datetime.timezone.utc) +one_hour_later = now + datetime.timedelta(hours=1) +shared_location = { + "created_by_device_id": "test_device_id", + "latitude": 37.7749, + "longitude": -122.4194, + "end_at": one_hour_later.isoformat(), +} + +channel.send_message( + {"text": "Message with live location", "shared_location": shared_location}, + user_id, +) +``` + +## Stopping live location sharing + +You can stop live location sharing for a specific message using the message controller: + +```python +# Update the user's live location (e.g., when device location changes) +location_data = { + "created_by_device_id": "test_device_id", + "latitude": new_latitude, + "longitude": new_longitude, +} +client.update_user_location( + user_id, + message_id, + location_data +) +``` + +## Updating live location + +Your application must implement location tracking and provide updates to the SDK. The SDK handles updating all the current user's active live location messages and provides a throttling mechanism to prevent excessive API calls. + +```python +# Update the user's live location (e.g., when device location changes) +location_data = { + "created_by_device_id": "test_device_id", + "latitude": new_latitude, + "longitude": new_longitude, +} +client.update_user_location( + user_id, + message_id, + location_data +) +``` + +Whenever the location is updated, the message will automatically be updated with the new location. + +The SDK will also notify your application when it should start or stop location tracking as well as when the active live location messages change. + + +## Events + +Whenever a location is created or updated, the following WebSocket events will be sent: + +- `message.new`: When a new location message is created. +- `message.updated`: When a location message is updated. + +> [!NOTE] +> In Dart, these events are resolved to more specific location events: +> +> - `location.shared`: When a new location message is created. +> - `location.updated`: When a location message is updated. + + +You can easily check if a message is a location message by checking the `message.sharedLocation` property. For example, you can use this events to render the locations in a map view. diff --git a/docs/features/overview.md b/docs/features/overview.md new file mode 100644 index 0000000..5ba41bb --- /dev/null +++ b/docs/features/overview.md @@ -0,0 +1,35 @@ +Stream supports the features you're used to from apps like Slack, Whatsapp, Telegram etc. +Here's an overview of the features you might want to enable/integrate into your app + +### Messaging Essentials + +The most commonly used chat features are: + +- Unread counts +- URL previews: URL previews for your app +- Message attachments: Bring other parts of your app into the chat. like sharing a Strava route on the chat. +- User Presence: Show who is online +- Typing Indicators +- Reactions +- Threads/ Replies +- System messages. A notification style message, like "user John joined the channel" +- Events & Custom events: Send any custom event on the chat channel (pubsub style functionality) + +### Advanced Features + +We also expose many advanced features that might be relevant for your app, here's a quick overview: + +- Campaign API: send large batches of personalized messages to users +- Polls API: Add polls to your chat +- Pending Messages: Extra confirmation step for messages +- Draft Messages: Store drafts of messages, edit them on +- Message reminders: Unread message reminders or +- Location Sharing +- Translations: Translate messages automatically +- Pinning messages: Pin a message to the channel +- Slow mode & Throttling: Key for live events, live shopping etc where message volume can become too high +- Channel invites: Invite a user to a channel (common in dating apps) +- Average Response Time: For marketplaces, track the user's average response time. +- Dynamic Partitioning: Split a channel based on the number of participants. (Typically used in gaming apps) +- Audit Trails: (track message changes) +- Restricted Message Delivery: A message is only shown to a subset of users on the channel diff --git a/docs/features/polls_api.md b/docs/features/polls_api.md new file mode 100644 index 0000000..b9865ec --- /dev/null +++ b/docs/features/polls_api.md @@ -0,0 +1,602 @@ +The Polls feature provides a comprehensive API that enables seamless integration of polling capabilities within your application, enhancing engagement and interaction among users. Through this API, developers can effortlessly create, manage, and utilize polls as part of messages, gather user opinions, and make informed decisions based on real-time feedback. + +## Polls at a Quick Glance + + +### Key Features Include + +- **Easy Poll Creation and Configuration** : Developers can create polls with customizable options, descriptions, and configurations, including setting voting visibility (public or anonymous), enforcing unique votes, and specifying the maximum votes a user can cast. Polls can also be designed to allow user-suggested options or open-ended answers, providing flexibility in how you engage with your audience. + +- **Seamless Integration with Messages** : Once created, polls can be sent as part of messages, allowing for a seamless user experience. Users can view poll details and participate directly within the context of a conversation. + +- **Dynamic Poll Management** : Polls are not static. You can update poll details, add or modify options, and even close polls to further responses. These actions can be performed through full or partial updates, giving you control over the poll's lifecycle. + +- **Robust Voting System** : Users can cast votes on options or provide answers to open-ended questions, with the API supporting both single and multiple choice responses. Votes can be changed or removed, ensuring users' opinions are accurately captured. + +- **Comprehensive Query Capabilities** : Retrieve detailed information about polls and votes based on various criteria, including poll status, creation time, and user responses. This enables developers to implement rich, data-driven features in their applications. + +- **Customizability and Extensibility** : In addition to predefined poll properties, the API supports custom properties, allowing developers to tailor polls and options to their specific needs while maintaining performance and scalability. + +## Creating a poll and sending it as part of a message + +Creating a poll is easy. You simply create a poll with your desired configuration, and once created, you send a message with the poll id. + + +> [!NOTE] +> Please take into account that the poll can be sent only by the user who created it in the first place. + + +When creating a poll, the following properties can be configured: + +| name | type | description | default | optional | +| ---------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | +| name | string | The name of the poll | - | | +| description | string | The description of the poll | - | ✓ | +| voting_visibility | enum | Designates whether the votes are casted anonymously | public | ✓ | +| enforce_unique_vote | boolean | Designates whether the poll is multiple choice or single choice | false | ✓ | +| max_votes_allowed | number | Designates how many votes a single user is allowed to cast on a poll. Allowed value is in range from 1 to 10. If null, no limits applied. | null | ✓ | +| allow_user_suggested_options | boolean | Designates weather user can add custom options to the poll | false | ✓ | +| allow_answers | boolean | Designates whether user can add an answer to the poll. Max 1 answer is allowed. This is for open ended polls. | false | | +| is_closed | boolean | Whether the poll is closed for voting or not | false | ✓ | +| options | array of poll option objects | One or more options users can vote on. See below for more information on poll options | - | ✓ | + +### Poll options + +| name | type | description | default | optional | +| ---- | ------ | --------------------------- | ------- | -------- | +| text | string | The text of the poll option | - | ✓ | + +Besides the above mentioned properties, it is also possible to supply your own custom properties for both polls and options: + + +> [!NOTE] +> The total size of all custom properties on a poll cannot exceed 5KB. + + +### Example poll response as part of a message + + +> [!NOTE] +> The  `latest_votes_by_option`  will contain at most the 10 latest votes for that particular option. + + +## Casting a vote + +Once a poll has been send as part of a message (and as long as the poll isn’t closed for voting). Votes can be casted + +### Send vote on option + + +### Send an answer (if answers are configured to be allowed) + + +#### Few points to note here + +- If  `enforce_unique_votes`  is set to **true** on poll, then any vote casted on option will replace the previous vote. Also this api will broadcast an event: + +- `poll.vote_changed`  if  `enforce_unique_votes`  is **true** + +- Otherwise  `poll.vote_casted`  event will be broadcasted + +- Adding an answer will always replace the previous answer. This ensures that user can add maximum `1` answer (similar to what Polly app has) + +- You need  `CastVote`  permission to be able to cast a vote + +- API will return an error if poll is not attached to a message. + +## Removing a vote + +A vote can be removed as well: + + +## Closing a poll + +If you want to prevent any further votes on a poll, you can close a poll for voting: + + +## Retrieving a poll + +If you know the id of a poll you can easily retrieve the poll by using the  `getPoll`  method. If you don’t know the id or if you want to retrieve multiple polls, use the query polls method (see below) + + +## Updating a poll + +There are two ways to update a poll: a **full** poll update and a **partial** update. + +### Full update + + +> [!WARNING] +> All the poll properties that are omitted in the update request will either be removed or set to their default values. + + +### Partial update + + +## Deleting a poll + +Deleting a poll removes the poll, its associated options as well as all the votes on that poll. Be aware that removing a poll can’t be undone. + + +## Adding, updating and deleting poll options + +Poll options can be added, updated or deleted after a poll has been created: + +### **Add poll option** + + +> [!WARNING] +> If  `allow_user_suggested_options`  is set to `true` on poll, then user only needs  `CastVote`  permission to access this endpoint. Otherwise user needs  `UpdatePoll`  permission. + + +### Update poll option + + +### Delete poll option + + +## Querying votes + +You are able to query the votes on a poll: + + +### Votes Queryable Built-In Fields + +| Name | Type | Description | Supported operators | Example | +| ---------- | ------------------------------------------------- | ------------------------------------------------ | ------------------------- | --------------------------------------------------- | +| id | string or list of strings | the ID of the vote | $in, $eq | { id: { $in: [ 'abcd', 'defg' ] } } | +| user_id | string or list of strings | the ID of the user who casted the vote | $in, $eq | { $user_id: { $eq: 'abcd' } } | +| created_at | string, must be formatted as an RFC3339 timestamp | the time the vote was created | $eq, $gt, $lt, $gte, $lte | { created_at: { $gte: '2023-12-04T09:30:20.45Z' } } | +| is_answer | boolean | whether or not the vote is suggested by the user | $eq | { is_answer: { $eq: true } } | +| option_id | string or list of strings | The ID of the option the vote was casted on | $in, $eq, $exists | { option_id: { $in: [ 'abcd', 'defg' ] } } | + +## Querying polls + +It is also possible to query for polls based on certain filter criteria: + + +### Poll Queryable Built-In Fields + +| Name | type | Description | Supported operations | Example | +| ---------------------------- | ------------------------------------------------- | -------------------------------------------------------- | ------------------------------ | ------------------------------------------------ | +| id | string or list of strings | the ID of the vote | $in, $eq | { id: { $in: [ 'abcd', 'defg' ] } } | +| poll_id | string or list of strings | the ID of the poll | $in, $eq | { poll_id: { $in: [ 'abcd', 'defg' ] } } | +| name | string or list of strings | the ID of the user who casted the vote | $in, $eq | { name: { $eq: 'abcd' } } | +| voting_visibility | string | indicates whether the votes are casted anonymously | $eq | { voting_visibility: { $eq: 'anonymous' } } | +| max_votes_allowed | number | the maximum amount of votes per user | $eq, $ne, $gt, $lt, $gte, $lte | { max_votes_allowed: { $gte: 5 } } | +| allow_user_suggested_options | boolean | indicates whether the poll allows user suggested options | $eq | { allow_user_suggested_options: { $eq: false } } | +| allow_answers | boolean | indicates whether the poll allows user answers | $eq | { allow_answers: { $eq: false } } | +| is_closed | boolean | indicates whether the poll is closed for voting | $eq | { is_closed: { $eq: true } } | +| created_at | string, must be formatted as an RFC3339 timestamp | the time the poll was created | $eq, $gt, $lt, $gte, $lte | { created_at: {$gte: ‘2023-12-04T09:30:20.45Z’ } | +| updated_at | string, must be formatted as an RFC3339 timestamp | the time the poll was updated | $eq, $gt, $lt, $gte, $lte | { updated_at: {$gte: ‘2023-12-04T09:30:20.45Z’ } | +| created_by_id | string or list of strings | the ID of the user who created the poll | $in, $eq | { id: { $in: [ 'abcd', 'defg' ] } } | + +## Events + +The following websocket events will be emitted: + +- `poll.updated`  whenever a poll (or its options) gets updated. + +- `poll.closed`  whenever a poll is closed for voting. + +- `poll.deleted`  whenever a poll gets deleted. + +- `poll.vote_casted`  whenever a vote is casted. + +- `poll.vote_removed` whenever a vote is removed. + +- `poll.vote_changed`  whenever a vote is changed (case of enforce_unique_vote as true) + +## Poll updated event + +```json +{ + "type": "poll.updated", + "cid": "messaging-polls:a23de673-dcc4-413f-9923-4d1af0a6f596", + "channel_id": "a23de673-dcc4-413f-9923-4d1af0a6f596", + "channel_type": "messaging-polls", + "message": { + // ... + }, + "poll": { + "id": "3598617e-228b-480a-8004-f441ff195da2", + "name": "Updated poll name", + "description": "", + "voting_visibility": "public", + "enforce_unique_vote": false, + "max_votes_allowed": null, + "allow_user_suggested_options": false, + "allow_answers": false, + "vote_count": 0, + "options": [], + "vote_counts_by_option": {}, + "answers_count": 0, + "latest_votes_by_option": {}, + "latest_answers": [], + "own_votes": [], + "created_by_id": "b3e6cf5b-d431-40f5-8022-27d246b3a890", + "created_by": { + "id": "b3e6cf5b-d431-40f5-8022-27d246b3a890", + "role": "user", + "created_at": "2024-04-09T20:43:39.192829Z", + "updated_at": "2024-04-09T20:43:39.192829Z", + "last_active": "2024-04-09T20:43:39.192829Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:43:39.360335Z", + "updated_at": "2024-04-09T20:43:39.940022Z" + }, + "created_at": "2024-04-09T20:43:39.96665Z", + "received_at": "2024-04-09T20:43:39.971Z" +} +``` + +## Poll closed event + +```json +{ + "type": "poll.closed", + "cid": "messaging-polls:2ac6751f-d1d9-41f2-a800-fe8efec27164", + "channel_id": "2ac6751f-d1d9-41f2-a800-fe8efec27164", + "channel_type": "messaging-polls", + "message": { + // ... + }, + "poll": { + "id": "d9e4bb1c-20b9-40fa-937d-616dff4268fc", + "name": "Updated poll name", + "description": "", + "voting_visibility": "public", + "enforce_unique_vote": false, + "max_votes_allowed": null, + "allow_user_suggested_options": false, + "allow_answers": false, + "is_closed": true, + "vote_count": 0, + "options": [], + "vote_counts_by_option": {}, + "answers_count": 0, + "latest_votes_by_option": {}, + "latest_answers": [], + "own_votes": [], + "created_by_id": "dcc77240-440c-436d-8016-ed666654d1ee", + "created_by": { + "id": "dcc77240-440c-436d-8016-ed666654d1ee", + "role": "user", + "created_at": "2024-04-09T20:54:42.894589Z", + "updated_at": "2024-04-09T20:54:42.894589Z", + "last_active": "2024-04-09T20:54:42.894589Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:54:43.060539Z", + "updated_at": "2024-04-09T20:54:43.567798Z" + }, + "created_at": "2024-04-09T20:54:43.592833Z", + "received_at": "2024-04-09T20:54:43.597Z" +} +``` + +## Poll deleted event + +```json +{ + "type": "poll.deleted", + "cid": "messaging-polls:5671fbb8-b02e-40e9-a93d-5c6da9db7ef0", + "channel_id": "5671fbb8-b02e-40e9-a93d-5c6da9db7ef0", + "channel_type": "messaging-polls", + "message": { + // ... + }, + "poll": { + "id": "3ed6694e-cb16-4190-b573-69fd9586ea74", + "name": "poll-58612adb-cee6-4267-b47f-138aa21593a6", + "description": "", + "voting_visibility": "public", + "enforce_unique_vote": false, + "max_votes_allowed": null, + "allow_user_suggested_options": false, + "allow_answers": false, + "vote_count": 0, + "options": [ + { + "id": "5f430775-45af-4112-87d9-fbe6a115229f", + "text": "Option 1" + }, + { + "id": "93123a94-daf9-464e-a2fc-f00b20764dcd", + "text": "Option 2" + } + ], + "vote_counts_by_option": {}, + "answers_count": 0, + "latest_votes_by_option": {}, + "latest_answers": [], + "own_votes": [], + "created_by_id": "17621af7-fab8-4daf-9812-42f3194526b8", + "created_at": "2024-04-09T20:57:43.644546Z", + "updated_at": "2024-04-09T20:57:43.644546Z" + }, + "user": { + "id": "17621af7-fab8-4daf-9812-42f3194526b8", + "role": "user", + "created_at": "2024-04-09T20:57:43.50881Z", + "updated_at": "2024-04-09T20:57:43.50881Z", + "last_active": "2024-04-09T20:57:43.50881Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:57:44.150552Z", + "received_at": "2024-04-09T20:57:44.159Z" +} +``` + +## Vote casted event + +```json +{ + "type": "poll.vote_casted", + "cid": "messaging-polls:629e4e73-1811-4e3d-bb0a-e9ba7a7a1502", + "channel_id": "629e4e73-1811-4e3d-bb0a-e9ba7a7a1502", + "channel_type": "messaging-polls", + "message": { + // ... + }, + "poll": { + "id": "8613282d-c492-4630-94fb-12537fa95e55", + "name": "poll-10acb1ca-1140-45b7-bc12-61bd86403760", + "description": "", + "voting_visibility": "public", + "enforce_unique_vote": false, + "max_votes_allowed": null, + "allow_user_suggested_options": false, + "allow_answers": false, + "vote_count": 1, + "options": [ + { + "id": "fff6357f-56e4-442b-b566-1312b48faf3a", + "text": "Option 1" + } + ], + "vote_counts_by_option": { + "fff6357f-56e4-442b-b566-1312b48faf3a": 1 + }, + "answers_count": 0, + "latest_votes_by_option": { + "fff6357f-56e4-442b-b566-1312b48faf3a": [ + { + "poll_id": "8613282d-c492-4630-94fb-12537fa95e55", + "id": "7afe9ee6-1cf2-4ba2-a07e-4ee2dd51d4ba", + "option_id": "fff6357f-56e4-442b-b566-1312b48faf3a", + "user_id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "user": { + "id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "role": "user", + "created_at": "2024-04-09T20:58:52.154483Z", + "updated_at": "2024-04-09T20:58:52.154483Z", + "last_active": "2024-04-09T20:58:52.154483Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:58:52.799538Z", + "updated_at": "2024-04-09T20:58:52.799538Z" + } + ] + }, + "latest_answers": [], + "own_votes": [], + "created_by_id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "created_by": { + "id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "role": "user", + "created_at": "2024-04-09T20:58:52.154483Z", + "updated_at": "2024-04-09T20:58:52.154483Z", + "last_active": "2024-04-09T20:58:52.154483Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:58:52.324944Z", + "updated_at": "2024-04-09T20:58:52.324944Z" + }, + "poll_vote": { + "poll_id": "8613282d-c492-4630-94fb-12537fa95e55", + "id": "7afe9ee6-1cf2-4ba2-a07e-4ee2dd51d4ba", + "option_id": "fff6357f-56e4-442b-b566-1312b48faf3a", + "user_id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "user": { + "id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "role": "user", + "created_at": "2024-04-09T20:58:52.154483Z", + "updated_at": "2024-04-09T20:58:52.154483Z", + "last_active": "2024-04-09T20:58:52.154483Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:58:52.799538Z", + "updated_at": "2024-04-09T20:58:52.799538Z" + }, + "created_at": "2024-04-09T20:58:52.82498Z", + "received_at": "2024-04-09T20:58:52.831Z" +} +``` + +## Vote removed event + +```json +{ + "type": "poll.vote_removed", + "cid": "messaging-polls:!members-j8Uw02jyV30DUXTHql5eoq2EHQ8m3lDNMTidgJS833A", + "channel_id": "!members-j8Uw02jyV30DUXTHql5eoq2EHQ8m3lDNMTidgJS833A", + "channel_type": "messaging-polls", + "message": { + // ... + }, + "poll": { + "id": "987b7ffc-a4e5-4c02-b82c-f8d06d2b7f09", + "name": "poll-dbbbe015-0a5b-4073-993c-bd40b83b8458", + "description": "", + "voting_visibility": "public", + "enforce_unique_vote": false, + "max_votes_allowed": null, + "allow_user_suggested_options": false, + "allow_answers": false, + "vote_count": 0, + "options": [ + { + "id": "a89ef738-5443-406e-8f47-7f68ded8caab", + "text": "Option 1" + } + ], + "vote_counts_by_option": {}, + "answers_count": 0, + "latest_votes_by_option": {}, + "latest_answers": [], + "own_votes": [], + "created_by_id": "50d6bf78-c529-45a8-9c69-6755e8a31ef3", + "created_by": { + "id": "50d6bf78-c529-45a8-9c69-6755e8a31ef3", + "role": "user", + "created_at": "2024-04-09T21:47:09.572264Z", + "updated_at": "2024-04-09T21:47:09.572264Z", + "last_active": "2024-04-09T21:47:09.572264Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T21:47:09.617585Z", + "updated_at": "2024-04-09T21:47:09.617585Z" + }, + "poll_vote": { + "poll_id": "987b7ffc-a4e5-4c02-b82c-f8d06d2b7f09", + "id": "dc7e9ea2-8df8-48e1-9268-1fcb574d0442", + "option_id": "a89ef738-5443-406e-8f47-7f68ded8caab", + "user_id": "50d6bf78-c529-45a8-9c69-6755e8a31ef3", + "user": { + "id": "50d6bf78-c529-45a8-9c69-6755e8a31ef3", + "role": "user", + "created_at": "2024-04-09T21:47:09.572264Z", + "updated_at": "2024-04-09T21:47:09.572264Z", + "last_active": "2024-04-09T21:47:09.572264Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T21:47:10.163473Z", + "updated_at": "2024-04-09T21:47:10.163473Z" + }, + "created_at": "2024-04-09T21:47:10.445489Z", + "received_at": "2024-04-09T21:47:10.455Z" +} +``` + +## Vote changed event + +```json +{ + "type": "poll.vote_changed", + "cid": "messaging-polls:629e4e73-1811-4e3d-bb0a-e9ba7a7a1502", + "channel_id": "629e4e73-1811-4e3d-bb0a-e9ba7a7a1502", + "channel_type": "messaging-polls", + "message": { + // ... + }, + "poll": { + "id": "8613282d-c492-4630-94fb-12537fa95e55", + "name": "poll-10acb1ca-1140-45b7-bc12-61bd86403760", + "description": "", + "voting_visibility": "public", + "enforce_unique_vote": false, + "max_votes_allowed": null, + "allow_user_suggested_options": false, + "allow_answers": false, + "vote_count": 1, + "options": [ + { + "id": "fff6357f-56e4-442b-b566-1312b48faf3a", + "text": "Option 1" + } + ], + "vote_counts_by_option": { + "fff6357f-56e4-442b-b566-1312b48faf3a": 1 + }, + "answers_count": 0, + "latest_votes_by_option": { + "fff6357f-56e4-442b-b566-1312b48faf3a": [ + { + "poll_id": "8613282d-c492-4630-94fb-12537fa95e55", + "id": "7afe9ee6-1cf2-4ba2-a07e-4ee2dd51d4ba", + "option_id": "fff6357f-56e4-442b-b566-1312b48faf3a", + "user_id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "user": { + "id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "role": "user", + "created_at": "2024-04-09T20:58:52.154483Z", + "updated_at": "2024-04-09T20:58:52.154483Z", + "last_active": "2024-04-09T20:58:52.154483Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:58:52.799538Z", + "updated_at": "2024-04-09T20:58:52.799538Z" + } + ] + }, + "latest_answers": [], + "own_votes": [], + "created_by_id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "created_by": { + "id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "role": "user", + "created_at": "2024-04-09T20:58:52.154483Z", + "updated_at": "2024-04-09T20:58:52.154483Z", + "last_active": "2024-04-09T20:58:52.154483Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:58:52.324944Z", + "updated_at": "2024-04-09T20:58:52.324944Z" + }, + "poll_vote": { + "poll_id": "8613282d-c492-4630-94fb-12537fa95e55", + "id": "7afe9ee6-1cf2-4ba2-a07e-4ee2dd51d4ba", + "option_id": "fff6357f-56e4-442b-b566-1312b48faf3a", + "user_id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "user": { + "id": "9e673f6f-e32b-46cf-9296-39bc84e86fd4", + "role": "user", + "created_at": "2024-04-09T20:58:52.154483Z", + "updated_at": "2024-04-09T20:58:52.154483Z", + "last_active": "2024-04-09T20:58:52.154483Z", + "banned": false, + "online": true + }, + "created_at": "2024-04-09T20:58:52.799538Z", + "updated_at": "2024-04-09T20:58:52.799538Z" + }, + "created_at": "2024-04-09T20:58:52.82498Z", + "received_at": "2024-04-09T20:58:52.831Z" +} +``` + +## Permissions + +The following permissions can be configured to allow or disallow certain actions on Polls: + +## App permissions + +- `CreatePoll`  allows or disallows a user to create polls. + +- `UpdatePoll`  allows or disallows a user to update polls. + +- `DeletePoll`  allows or disallows a user to delete polls. + +- `ClosePoll`  allows or disallows a user to close a poll. + +- `QueryPolls`  allows or disallows a user to query polls. + +## Channel permissions + +- `SendPoll`  allows or disallows a user to send a poll as part of a message. + +- `CastVote`  allows or disallows a user to cast vote(s) on a poll. + +- `QueryVotes`  allows or disallows a user to query the vote(s) on a poll. diff --git a/docs/features/presence_format.md b/docs/features/presence_format.md new file mode 100644 index 0000000..94a1862 --- /dev/null +++ b/docs/features/presence_format.md @@ -0,0 +1,32 @@ +User presence allows you to show when a user was last active and if they are online right now. This feature can be enabled or disabled per channel type in the [channel type settings](/chat/docs/python/channel_features/). + +## Listening to Presence Changes + +To receive presence updates, you need to watch a channel or query channels with `presence: true`. This allows you to show a user as offline when they leave and update their status in real time. + + +A users online status change can be handled via event delegation by subscribing to the `user.presence.changed` event the same you do for any other event. + +## Presence Data Format + +Whenever you read a user the presence data will look like this: + + +> [!NOTE] +> The online field indicates if the user is online. The status field stores text indicating the current user status. + + +> [!NOTE] +> The last_active field is updated when a user connects and then refreshed every 15 minutes. + + +## Invisible + +To mark your user as invisible, you can update your user to set the invisible property to _true_. Your user will remain invisible even if you disconnect and reconnect. You must explicitly set invisible to _false_ in order to become visible again. + + +You can also set your user to invisible when connecting by setting the invisible property to _true_. You can also set a custom status message at the same time: + + +> [!NOTE] +> When invisible is set to _true,_ the current user will appear as offline to other users. diff --git a/docs/features/translation.md b/docs/features/translation.md new file mode 100644 index 0000000..1ba1c96 --- /dev/null +++ b/docs/features/translation.md @@ -0,0 +1,158 @@ +Chat messages can be translated on-demand or automatically, this allows users speaking different languages on the same channel. + +### Message Translation Endpoint + +This API endpoint translates an existing message to another language. The source language is inferred from the user language or detected automatically by analyzing its text. If possible it is recommended to store the user language, see " **Set user language** " section later in this page. + +```python +resp = client.translate_message(msg_id, "fr") + +resp["message"]["i18n"]["fr_text"] +``` + +The endpoint returns the translated message, updates it and sends a **message.updated** event to all users on the channel. + +> [!NOTE] +> Only the text field is translated, custom fields and attachments are not included. + + +### i18n data + +When a message is translated, the `i18n` object is added. The `i18n` includes the message text in all languages and the code of the original language. + +The i18n object has one field for each language named using this convention `language-code_text` + +Here is an example after translating a message from english into French and Italian. + +```json +{ + "fr_text": "Bonjour, J'aimerais avoir plus d'informations sur votre produit.", + "it_text": "Ciao, vorrei avere maggiori informazioni sul tuo prodotto.", + "language": "en" +} +``` + +### Automatic translation + +Automatic translation translates all messages immediately when they are added to a channel and are delivered to the other users with the translated text directly included. + +Automatic translation works really well for 1-1 conversations or group channels with two main languages. + +Let's see how this works in practice: + +1. A user sends a message and automatic translation is enabled + +2. The language set for that user is used as source language (if not the source language will be automatically detected) + +3. The message text is translated into the other language in used on the channel by its members + +> [!NOTE] +> When using auto translation, it is recommended setting the language for all users and add them as channel members + + +### Enabling automatic translation + +Automatic translation is not enabled by default. You can enable it for your application via API or CLI from your backend. You can also enable auto translation on a channel basis. + +```python +# enable auto-translation only for this channel +channel.update({"auto_translation_enabled": True}) + +# ensure all messages are translated in english for this channel +channel.update({"auto_translation_enabled": True, "auto_translation_language": "en"}) + +# auto translate messages for all channels +client.update_app_settings(auto_translation_enabled=True) +``` + +### Set user language + +In order for auto translation to work, you must set the user language or specify a destination language for the channel using the `auto_translation_language` field (see previous code example). + +```python +# sets the user language +user = {"id": "userId", "language": "en"} +client.update_user(user) +``` + +> [!NOTE] +> Messages are automatically translated from the user language that posts the message to the most common language in use by the other channel members. + + +### Caveats and limits + +- Translation is only done for messages with up to 5,000 characters. Blowin' In The Wind from Bob Dylan contains less than 1,000 characters + +- Error messages and commands are not translated (ie. /giphy hello) + +- When a message is updated, translations are recomputed automatically + +- Changing translation settings or user language have no effect on messages that are already translated + +- If there are three or more languages being used by channel members, auto-translate will default to the most common language used by the channel members. Therefore, this feature is best suited for groups with a maximum of **two** main languages. + +> [!NOTE] +> A workaround to support more than two languages is to use the translateMessage endpoint to store translated messages for multiple languages, and render the appropriate translation depending on the current users language. + + +### Available Languages + +| Language name | Language code | +| --------------------- | ------------- | +| Afrikaans | af | +| Albanian | sq | +| Amharic | am | +| Arabic | ar | +| Azerbaijani | az | +| Bengali | bn | +| Bosnian | bs | +| Bulgarian | bg | +| Chinese (Simplified) | zh | +| Chinese (Traditional) | zh-TW | +| Croatian | hr | +| Czech | cs | +| Danish | da | +| Dari | fa-AF | +| Dutch | nl | +| English | en | +| Estonian | et | +| Finnish | fi | +| French | fr | +| French (Canada) | fr-CA | +| Georgian | ka | +| German | de | +| Greek | el | +| Haitian Creole | ht | +| Hausa | ha | +| Hebrew | he | +| Hindi | hi | +| Hungarian | hu | +| Indonesian | id | +| Italian | it | +| Japanese | ja | +| Korean | ko | +| Latvian | lv | +| Lithuanian | lt | +| Malay | ms | +| Norwegian | no | +| Persian | fa | +| Pashto | ps | +| Polish | pl | +| Portuguese | pt | +| Romanian | ro | +| Russian | ru | +| Serbian | sr | +| Slovak | sk | +| Slovenian | sl | +| Somali | so | +| Spanish | es | +| Spanish (Mexico) | es-MX | +| Swahili | sw | +| Swedish | sv | +| Tagalog | tl | +| Tamil | ta | +| Thai | th | +| Turkish | tr | +| Ukrainian | uk | +| Urdu | ur | +| Vietnamese | vi | diff --git a/docs/features/typing_indicators.md b/docs/features/typing_indicators.md new file mode 100644 index 0000000..affc081 --- /dev/null +++ b/docs/features/typing_indicators.md @@ -0,0 +1,36 @@ +If you use Stream's UI components, typing indicators are automatically handled. +The typing indicators can be turned on or off in the channel type settings. +This example below shows how to integrate typing indicators into your own message input UI. + +## Sending Typing Events + +When a user starts typing call the keystroke method. Optionally you can specify a thread id to have a thread specific typing indicator. +A few seconds after a user stops typing use stopTyping. + + +### Receiving typing indicator events + +Listening to typing indicators uses the event system, an example is shown below + + +> [!NOTE] +> Because clients might fail at sending `typing.stop` event all Chat clients periodically prune the list of typing users. + + +### Typing Privacy Settings + +Please take into account that `typing.start` and `typing.stop` events delivery can be controlled by user privacy settings: + +```json +// user object with privacy settings where typing indicators are disabled +{ + // other user fields + "privacy_settings": { + "typing_indicators": { + "enabled": false + } + } +} +``` + +If `privacy_settings.typing_indicators.enabled` is set to `false` , then `typing.start` and `typing.stop` events will be ignored for this user by Stream's server and these events will not be sent to other users. In other words other users will not know that the current user was typing. diff --git a/docs/features/unread.md b/docs/features/unread.md new file mode 100644 index 0000000..b8a0f0c --- /dev/null +++ b/docs/features/unread.md @@ -0,0 +1,92 @@ +The following unread counts are provided by Stream + +- A total count of unread messages +- Number of unread channels +- A count of unread threads +- Unread @mentions +- Unread messages per channel +- Unread @mentions per channel +- Unread counts by team +- Unread counts by channel type + +Unread counts are first fetched when a user connects. +After that they are updated by events. (new message, mark read, delete message, delete channel etc.) + +### Reading Unread Counts + +Unread counts are returned when a user connects. After that, you can listen to events to keep them updated in real-time. + + +Note that the higher level SDKs offer convenient endpoints for this. Hooks on react, stateflow on Android etc. +So you only need to use the events manually if you're using plain JS. + +### Unread Counts - Server side + +The unread endpoint can fetch unread counts server-side, eliminating the need for a client-side connection. It can also be used client-side without requiring a persistent connection to the chat service. This can be useful for including an unread count in notifications or for gently polling when a user loads the application to keep the client up to date without loading up the entire chat. + +> [!NOTE] +> A user_id whose unread count is fetched through this method is automatically counted as a Monthly Active User. This may affect your bill. + + +```python +response = client.unread_counts(userID) +print(response["total_unread_count"]) # total unread count for user +print(response["channels"]) # distribution of unread counts across channels +print(response["channel_type"]) # distribution of unread counts across channel types +print(response["total_unread_threads_count"]) # total unread threads +print(response["threads"]) # list of unread counts per thread +``` + +> [!NOTE] +> This endpoint will return the last 100 unread channels, they are sorted by last_message_at. + + +#### Batch Fetch Unread + +The batch unread endpoint works the same way as the non-batch version with the exception that it can handle multiple user IDs at once and that it is restricted to server-side only. + +```python +response = client.unread_counts_batch([userID1, userID2]) +print(response["counts_by_user"][userID1]["total_unread_count"]) # total unread count for userID1 +print(response["counts_by_user"][userID1]["channels"]) # distribution of unread counts across channels for userID1 +print(response["counts_by_user"][userID1]["channel_type"]) # distribution of unread counts across channel types for userID1 +print(response["counts_by_user"][userID1]["total_unread_threads_count"]) # total unread threads count for userID1 +print(response["counts_by_user"][userID1]["threads"]) # list of unread counts per thread for userID1 +``` + +> [!NOTE] +> If a user ID is not returned in the response then the API couldn't find a user with that ID + + +### Mark Read + +By default the UI component SDKs (React, React Native, ...) mark messages as read automatically when the channel is visible. You can also make the call manually like this: + + +The `markRead` function can also be executed server-side by passing a user ID as shown in the example below: + + +It's also possible to mark an already read message as unread: + + +The mark unread operation can also be executed server-side by passing a user ID: + + +#### Mark All As Read + +You can mark all channels as read for a user like this: + + +## Read State - Showing how far other users have read + +When you retrieve a channel from the API (e.g. using query channels), the read state for all members is included in the response. This allows you to display which messages are read by each user. For each member, we include the last time they marked the channel as read. + + +### Unread Messages Per Channel + +You can retrieve the count of unread messages for the current user on a channel like this: + + +### Unread Mentions Per Channel + +You can retrieve the count of unread messages mentioning the current user on a channel like this: diff --git a/docs/init_and_users/authless_users.md b/docs/init_and_users/authless_users.md new file mode 100644 index 0000000..ab4326a --- /dev/null +++ b/docs/init_and_users/authless_users.md @@ -0,0 +1,38 @@ +Stream Chat also lets you give unauthenticated users access to a limited subset of Stream's capabilities. This is done by using either Guest or Anonymous users. + +These two user types are ideal for use cases where users either need to be able to see chat activity prior to authenticating or in scenarios where the additional friction of creating a user account might be unnecessary for a user. + +## Guest Users + +Guest sessions can be created client-side and do not require any server-side authentication. Support and livestreams are common use cases for guest users because often you want a visitor to be able to use chat on your application without (or before) they have a regular user account. + +Guest users are not available to applications using multi-tenancy (teams). + +> [!NOTE] +> Guest users are counted towards your MAU usage. + + +Guest users have a limited set of permissions. You can read more about how to configure permissions [here](/chat/docs/python/chat_permission_policies/). You can create a guest user session by using `setGuestUser` instead of `connectUser`. + +You can generate a guest user in a front end client by using the following code: + + +> [!NOTE] +> The user object schema is the same as the one described in the [Initialization and Users](/chat/docs/python/init_and_users/) portion of the docs. + + +> [!NOTE] +> Creation of guest users can be disabled for your application in the [dashboard](https://getstream.io/dashboard/). + + +## Anonymous Users + +If a user is not logged in, you can call the `connectAnonymousUser` method. While you're anonymous, you can't do much by default, but for the **livestream** channel type, you're still allowed to read the chat conversation. + + +> [!NOTE] +> Anonymous users are not counted toward your MAU number and only have an impact on the number of concurrent connected clients. + + +> [!NOTE] +> Anonymous users are not allowed to perform any write operations. diff --git a/docs/init_and_users/init_and_users.md b/docs/init_and_users/init_and_users.md new file mode 100644 index 0000000..c93ca51 --- /dev/null +++ b/docs/init_and_users/init_and_users.md @@ -0,0 +1,40 @@ +The code below creates a chat client instance for interacting with Stream APIs. A singleton client instance means the Chat client is created once and reused throughout your app, ensuring consistent state, avoiding duplicate connections, and simplifying resource management. + + +## Connecting Users + +Once the client is initialized, your app authenticates the user and establishes a Websocket connection by calling `connectUser`. This function uses your token provider function to request a token from your server. + +The `connectUser` function acts as an upsert for the user object and is a primary method for creating users client-side. + +Before attempting subsequent API requests to Stream, it is important that the `connectUser` function fully resolves. + + +### Connect User Parameters + +| name | type | description | default | optional | +| --------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | -------- | +| user | object | The user object. Must have an id field. **User Ids can only contain characters a-z, 0-9, and special characters @ \_ and -** It can have as many custom fields as you want, as long as the total size of the object is less than 5KB | | | +| userToken | string/function | The Token Provider function or authentication token. See [Tokens & Authentication](/chat/docs/python/tokens_and_authentication/) for details | default | | + +## Disconnecting Users + +The client-side SDKs handle WebSocket disconnection logic, but if a manual disconnect is required in your application, there are the following options: + + +## XHR Fallback + +Most browsers support WebSocket connections as an efficient mode of real-time data transfer. However, sometimes the connection cannot be established due to network or a corporate firewall. In such cases, the client will establish or switch to XHR fallback mechanisms and gently poll our service to keep the client up-to-date. + +The fallback mechanism can be enabled with the flag `enableWSFallback` + + +## Privacy Settings + +Additionally, when connecting the user, you can include the `privacy_settings` as part of the user object. + + +| name | type | description | default | optional | +| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | +| typing_indicators | object | if **enabled** is set to **false** , then **typing.start** and **typing.stop** events will be ignored for this user and these events will not be sent to others | enabled: true | ✓ | +| read_receipts | object | If **enabled** is set to **false** , then the **read_state** of this user will not be exposed to others. Additionally, **read_state** related events will not be delivered to others when this user reads messages. | enabled: true | ✓ | diff --git a/docs/init_and_users/tokens_and_authentication.md b/docs/init_and_users/tokens_and_authentication.md new file mode 100644 index 0000000..e784c7b --- /dev/null +++ b/docs/init_and_users/tokens_and_authentication.md @@ -0,0 +1,128 @@ +## Authentication + +Stream uses JWT (JSON Web Tokens) to authenticate users so they can open WebSocket connections and send API requests. When a user opens your app, they first pass through your own authentication system. After that, the Stream SDK is initialized and a client instance is created. The device then requests a Stream token from your server. Your server verifies the request and returns a valid token. Once the device receives this token, the user is authenticated and ready to start using chat. + +## Generating Tokens + +You can generate tokens on the server by creating a Server Client and then using the Create Token method. + +If generating a token to use client-side, the token must include the userID claim in the token payload, whereas server tokens do not. When using the create token method, pass the user_id parameter to generate a client-side token. + +```python +# pip install stream-chat +import stream_chat + +server_client = stream_chat.StreamChat( + api_key="{{ api_key }}", api_secret="{{ api_secret }}" +) +token = server_client.create_token("john") +``` + +### Manually Generating Tokens + +You can use the JWT generator on this page to generate a User Token JWT without needing to set up a server client. You can use this token for prototyping and debugging; usually by hardcoding this into your application or passing it as an environment value at initialization. + +You will need the following values to generate a token: + +- `User ID` : A unique string to identify a user. + +- `API Secret` : You can find this value in the [Dashboard](https://getstream.io/dashboard/). + +To generate a token, provide a `User ID` and your `API Secret` to the following generator: + +_Use the [Stream Dashboard](https://getstream.io/dashboard/) to generate tokens for testing._ + +For more information on how JWT works, please visit . + +## Setting Automatic Token Expiration + +By default, user tokens are valid indefinitely. You can set an expiration to tokens by passing it as the second parameter. The expiration should contain the number of seconds since Unix epoch (00:00:00 UTC on 1 January 1970). + +```python +# creates a token valid for 1 hour +token = chat_client.create_token( + 'john', + exp=datetime.datetime.utcnow() + datetime.timedelta(hours=1) +) +``` + +## Token Providers + +A concept we will refer to throughout the docs is a Token Provider. At a high level, the Token Provider is an endpoint on your server that can perform the following sequence of tasks: + +1. Receive information about a user from the front end. + +2. Validate that user information against your system. + +3. Provide a User-ID corresponding to that user to the server client's token creation method. + +4. Return that token to the front end. + +To learn more about Token Providers, read on in our [Initialization & Users](/chat/docs/python/init_and_users/) section. + +## Developer Tokens + +For development applications, it is possible to disable token authentication and use client-side generated tokens or a manually generated static token. Disabling auth checks is not suitable for a production application and should only be done for proofs-of-concept and applications in the early development stage. To enable development tokens, you need to change your application configuration. + +On the [Dashboard](https://getstream.io/dashboard/): + +1. Select the App you want to enable developer tokens on and ensure it is in Development mode + +2. Click _App_ name to enter the _Chat Overview_ + +3. Scroll to the _General_ section + +4. Toggle _Disable Authentication Checks_ + +This disables the authentication check, but does not remove the requirement to send a token. Send either a client generated development token, or manually create one and hard code it into your application. + + +## Manual Token Expiration + +Token Revocation is a way to manually expire tokens for a single user or for many users by setting a `revoke_tokens_issued_before` time, and any tokens issued before this will be considered expired and will fail to authenticate.  This can be reversed by setting the field to null. + +### Token Revocation by User + +You can revoke all tokens that belong to a certain user or a list of users. + +```python +client.revoke_user_token("user-id", revokeDate) +client.revoke_users_token(["user1-id", "user2-id"], revokeDate) +``` + +Note: Your tokens must include the `iat` (issued at time) claim, which will be compared to the time in the `revoke_tokens_issued_before` field to determine whether the token is valid or expired.  Tokens which have no `iat` will be considered invalid. + +### Undoing the revoke + +To undo user-level token revocation, you can simply set revocation date to `null`: + +```python +client.revoke_user_token("user-id", None) +client.revoke_users_token(["user1-id", "user2-id"], None) +``` + +### Token Revocation by Application + +It is possible to revoke tokens for all users of an application. This should be used with caution as it will expire every user's token, regardless of whether the token has an `iat` claim. + +```python +client.revoke_tokens(revokeTime) +# revokeTime is a datetime object +``` + +### Undoing the revoke + +To undo app-level token revocation, you can simply set revocation date to `null`: + +```python +client.revoke_tokens(None) +``` + +### Adding iat claim to token + +By default, user tokens generated through the createToken function do not contain information about time of issue. You can change that by passing the issue date as the third parameter while creating tokens. This is a security best practice, as it enables revoking tokens. + +```python +client.create_token(self, user_id, exp=expiryTime, iat=issuedAt) +#issueTime should be a Unix timestamp +``` diff --git a/docs/init_and_users/update_users.md b/docs/init_and_users/update_users.md new file mode 100644 index 0000000..0cd2c71 --- /dev/null +++ b/docs/init_and_users/update_users.md @@ -0,0 +1,208 @@ +The Stream user object is central to the chat system and appears in many API responses, effectively following the user throughout the platform. Only an `id` is required to create a user but you can store additional custom data. We recommend only storing what is necessary for Chat such as a username and image URL. + +## Client-side User Creation + +The `connectUser` method automatically creates _and_ updates the user. If you are looking to onboard your userbase lazily, this is typically a perfectly viable option. + +However, it is also common to add your users to Stream before going Live and keep properties of your user base in sync with Stream. For this you'll want to use the `upsertUsers` function server-side and send users in bulk. + +## Creating and Updating Users Server-Side + +The `upsertUser` method creates or updates a user, replacing its existing data with the new payload (see below for partial updates). To create or update users in batches of up to 100, use the `upsertUsers` or `partialUpdateUsers` APIs, which accept an array of user objects. + +Depending on the permission configuration of your application, you may also allow users to update their own user objects client-side. + +```python +client.upsert_users([{"id": user_id, "role": "admin", "book": "dune"}]) +``` + +And for a batch of users, simply add additional entries (up to 100) into the array you pass to `upsertUsers` : + +```python +client.upsert_users([ + {"id": user_id1, "role": "admin", "book": "dune"}, + {"id": user_id2, "role": "user", "book": "1984"}, + {"id": user_id3, "role": "admin", "book": "Fahrenheit 451"}, +]) +``` + +> [!NOTE] +> If any user in a batch of users contains an error, the entire batch will fail, and the first error encountered will be returned. + + +## Server-side Partial Updates + +If you need to update a subset of properties for a user(s), you can use a partial update method. Both set and unset parameters can be provided to add, modify, or remove attributes to or from the target user(s). The set and unset parameters can be used separately or combined. + +```python +# make partial update call for userID +# it set's user.role to "admin", sets user.field = {'text': 'value'} +# and user.field2.subfield = 'test'. + +# NOTE: +# changing role is available only for server-side auth. +# field name should not contain dots or spaces, as dot is used as path separator. + +update = { + "id": "userID", + "set": { + "role": "admin", + "field": { + "text": 'value' + }, + 'field2.subfield': 'test', + }, + "unset": ['field.unset'], +}; + +# response will contain user object with updated users info +client.update_user_partial(update); + +# partial update for multiple users +updates = [ + { + "id": "userID", + "set": {"field": "value"} + }, + { + "id": "userID2", + "unset": ["field.value"] + } +] + +# response will contain object {userID => user} with updated users info +client.update_users_partial(updates) +``` + +> [!NOTE] +> Partial updates support batch requests, similar to the upsertUser endpoint. + + +## Unique Usernames + +Clients can set a username, by setting the `name` custom field. The field is optional and by default has no uniqueness constraints applied to it, however this is configurable by setting the `enforce_unique_username` to either _app_ or _team_. + +When checking for uniqueness, the name is _normalized_, by removing any white-space or other special characters, and finally transforming it to lowercase. So "John Doe" is considered a duplicate of "john doe", "john.doe", etc. + +With the setting at **app**, creating or updating a user fails if the username already exists anywhere in the app. With **team**, it only fails if the username exists within the same team. + +```python +# Enable uniqueness constraints on App level +client.update_app_settings(enforce_unique_usernames="app") + +# Enable uniqueness constraints on Team level +client.update_app_settings(enforce_unique_usernames="team") +``` + +> [!NOTE] +> Enabling this setting will only enforce the constraint going forward and will not try to validate existing usernames. + + +## Deactivate a User + +To deactivate a user, Stream Chat exposes a server-side `deactivateUser` method. A deactivated user cannot connect to Stream Chat but will be present in user queries and channel history. + +```python +response = client.deactivate_user(user_id) + +response = client.deactivate_user(user_id, + mark_messages_deleted=True, + created_by_id="joe") +``` + +## Deactivate Many Users + +Many users (up to 100) can be deactivated and reactivated with a single call. The operation runs asynchronously, and the response contains a task_id which can be polled using the [getTask endpoint](/chat/docs/python#tasks-gettask) to check the status of the operation. + + +| Name | Type | Description | Default | Optional | +| --------------------- | ------- | ------------------------------------------------- | ------- | -------- | +| mark_messages_deleted | boolean | Soft deletes all of the messages sent by the user | false | ✓ | + +## Reactivate a User + +To reinstate the user as active, use the `reactivateUser` method by passing the users ID as a parameter: + +```python +response = client.reactivate_user(user_id) + +response = client.reactivate_user(user_id, + restore_messages=True, + name="I am back", + created_by_id="joe") +``` + +## Deleting Many Users + +You can delete up to 100 users and optionally all of their channels and messages using this method. First the users are marked deleted synchronously so the user will not be directly visible in the API. Then the process deletes the user and related objects asynchronously. + +```python +response = client.delete_users( + ['userID1', 'userID2'], + "soft", + messages="hard" +) + +response = client.get_task(response["task_id"]) + +if response['status'] == 'completed': + # success! + pass +``` + +The `deleteUsers` method is an asynchronous API where the response contains a task_id which can be polled using the [getTask endpoint](/chat/docs/python#tasks-gettask) to check the status of the deletions. + +These are the request parameters which determine what user data is deleted: + +| name | type | description | default | optional | +| -------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | +| user_ids | array | List of users who will be deleted | - | | +| user | enum (soft, pruning, hard) | Soft: marks user as deleted and retains all user data. Pruning: marks user as deleted and nullifies user information. Hard: deletes user completely - this requires hard option for **messages** and **conversation** as well. | - | ✓ | +| conversations | enum (soft, hard) | Soft: marks all conversation channels as deleted (same effect as Delete Channels with 'hard' option disabled). Hard: deletes channel and all its data completely including messages (same effect as Delete Channels with 'hard' option enabled). | | ✓ | +| messages | enum (soft, pruning, hard) | Soft: marks all user messages as deleted without removing any related message data. Pruning: marks all user messages as deleted, nullifies message information and removes some message data such as reactions and flags. Hard: deletes messages completely with all related information. | - | ✓ | +| new_channel_owner_id | string | Channels owned by hard-deleted users will be transferred to this userID. | - | ✓ | + +> [!NOTE] +> When deleting a user, if you wish to transfer ownership of their channels to another user, provide that user's ID in the `new_channel_owner_id` field. Otherwise, the channel owner will be updated to a system generated ID like `delete-user-8219f6578a7395g` + + +## Restoring deleted users + +If users are _soft_ deleted, they can be restored using the server-side client. However, only the user's metadata is restored; memberships, messages, reactions, etc. are not restored. + +You can restore up to 100 users per call: + +```python +client.restore_users(['userID1', 'userID2']) +``` + +## Querying Users + +The Query Users method lets you search for users, though in many cases it's more practical to query your own user database instead. Like other Stream query APIs, it accepts filter, sort, and options parameters. + +```python +client.query_users( + {"id": {"$in": ["john", "jack", "jessie"]}}, + {"last_active": -1}, + limit=10, + offset=0 +) +``` + + + +### Querying with Autocomplete + +You can use the `$autocomplete` operator to search for users by name or ID with partial matching. + +```python +client.query_users({"name": {"$autocomplete": "ro"}}) +``` + +### Querying Inactive Users + +You can use the `last_active` field with the `$exists` operator to find users who have never connected. Use `$exists: false` for users who have never been active, or `$exists: true` for users who have connected at least once. + +```python +users = client.query_users({"last_active": {"$exists": False}}) +``` diff --git a/docs/messages/file_uploads.md b/docs/messages/file_uploads.md new file mode 100644 index 0000000..c84c2a0 --- /dev/null +++ b/docs/messages/file_uploads.md @@ -0,0 +1,93 @@ +Stream Chat allows you to upload images, videos, and other files to the Stream CDN or your own CDN. Uploaded files can be used as message attachments, user avatars, or channel images. + +> [!NOTE] +> Stream's UI SDKs (React, React Native, Flutter, SwiftUI, Jetpack Compose, etc.) handle file uploads automatically through their message composer components. The upload process, progress tracking, and attachment handling are built into these components. Use the methods described on this page only if you need custom upload behavior or are building a custom UI. + + +## Uploading Files to a Channel + +Files uploaded to a channel can be attached to messages. You can either upload a file first and then attach it to a message, or let the SDK handle the upload when sending a message with attachments. + +```python +channel.send_image( + "http://example.com/image.jpg", + "image.jpg", + {"id": "user-id"}, + "image/jpeg", +) +``` + +## Uploading Standalone Files + +Files not tied to a specific channel can be used for user avatars, channel images, or other application needs. + + +## Deleting Files + +Delete uploaded files to free storage space. Deleting a file from the CDN does not remove it from message attachments that reference it. + +```python +channel.delete_file(url) +channel.delete_image(url) +``` + +## File Requirements + +### Images + +| Requirement | Value | +| ----------------- | ---------------------------------------------------------------------------- | +| Supported formats | BMP, GIF, JPEG, PNG, WebP, HEIC, HEIC-sequence, HEIF, HEIF-sequence, SVG+XML | +| Maximum file size | 100 MB | + +### Other Files + +| Requirement | Value | +| ----------------- | ---------------------------------------------------------------------------------------------- | +| Supported formats | All file types are allowed by default. Different clients may handle certain types differently. | +| Maximum file size | 100 MB | + +You can configure a more restrictive list of allowed file types for your application. + +## Configuring Allowed File Types + +Stream allows all file extensions by default. To restrict allowed file types: + +- **Dashboard**: Go to Chat Overview > Upload Configuration +- **API**: Use the [App Settings](/chat/docs/python/app_setting_overview/#file-uploads/) endpoint + +## Access Control and Link Expiration + +Stream CDN URLs include a signature that validates access to the file. Only channel members can access files uploaded to that channel. + +| Behavior | Description | +| ----------------- | --------------------------------------------------------------------------------------------- | +| Access control | URLs are signed and only accessible by channel members | +| Link expiration | URLs expire after 14 days | +| Automatic refresh | Links are refreshed automatically when messages are retrieved (e.g., when querying a channel) | +| Manual refresh | Call `getMessage` to retrieve fresh URLs for expired attachments | + +To check when a link expires, examine the `Expires` query parameter in the URL (Unix timestamp). + +## Image Resizing + +Append query parameters to Stream CDN image URLs to resize images on the fly. + +| Parameter | Type | Values | Description | +| --------- | ------ | -------------------------------- | -------------------- | +| w | number | | Width in pixels | +| h | number | | Height in pixels | +| resize | string | clip, crop, scale, fill | Resizing mode | +| crop | string | center, top, bottom, left, right | Crop anchor position | + +> [!WARNING] +> Images can only be resized if the source image has 16,800,000 pixels or fewer. An image of 4000x4000 pixels (16,000,000) would be accepted, but 4100x4100 (16,810,000) would fail. + + +> [!NOTE] +> Resized images count against your storage quota. + + +## Using Your Own CDN + +All SDKs support custom CDN implementations. Implement a custom file uploader to use your own storage solution. diff --git a/docs/messages/message_receipts.md b/docs/messages/message_receipts.md new file mode 100644 index 0000000..df39f05 --- /dev/null +++ b/docs/messages/message_receipts.md @@ -0,0 +1,101 @@ +Messages go through multiple states reflecting recipient interaction: + +- **Sent** - The message reached the Stream server. Confirmed via the `message.new` WebSocket event. +- **Delivered** - The recipient's device confirmed delivery. Confirmed via the `message.delivered` WebSocket event. Disabled by default. +- **Read** - The recipient marked the channel as read. Confirmed via the `message.read` WebSocket event. + +For marking channels as read/unread and retrieving unread counts, see [Unread Counts](/chat/docs/python/unread/). + +## Delivery Receipts + +Delivery receipts track when messages are delivered to recipient devices. + +> [!NOTE] +> Contact support to enable message delivery tracking for your app. + + +> [!WARNING] +> The Android SDK requires the [offline plugin](/chat/docs/sdk/android/client/guides/offline-support/) for delivery receipts to function correctly. + + +### Enabling Delivery Receipts + +#### Channel Type Configuration + +Enable delivery tracking for all channels of a type. + +```python +# When creating a channel type +client.create_channel_type({"name": "targetChannelType", "delivery_events": True}) + +# When updating an existing channel type +client.update_channel_type("targetChannelType", delivery_events=True) +``` + +You can also enable this in the Dashboard under channel type configuration. + +#### User Privacy Settings + +Control whether a user's delivery status is shared with others. + + +When `privacy_settings.delivery_receipts.enabled` is `false`, the user's delivery status is not exposed to others, and the `message.delivered` event is not sent when this user confirms delivery. + +### Automatic Delivery Confirmation + +The SDK automatically handles delivery confirmation, including request throttling and duplicate prevention. + +> [!NOTE] +> Delivery tracking is currently supported for channel messages only, not thread replies. + + +### Delivery Events + +The `message.delivered` event is triggered when a message is delivered to a recipient's device. The event includes: + +- `last_delivered_at` - Timestamp when messages were last confirmed as delivered +- `last_delivered_message_id` - ID of the last message confirmed as delivered + +## Read Receipts + +Read receipts track when users have read messages in a channel. + +### Enabling Read Receipts + +#### Channel Type Configuration + +Enable read tracking for all channels of a type. + +```python +# When creating a channel type +client.create_channel_type({"name": "targetChannelType", "read_events": True}) + +# When updating an existing channel type +client.update_channel_type("targetChannelType", read_events=True) +``` + +You can also enable this in the Dashboard under channel type configuration. + +#### User Privacy Settings + +Control whether a user's read status is shared with others. + + +When `privacy_settings.read_receipts.enabled` is `false`, the user's read state is not exposed to others, and `message.read` and `notification.mark_read` events are not sent when this user reads messages. + +### Read Events + +The following events are triggered for read status: + +- `message.read` - When any channel member marks the channel as read +- `notification.mark_read` - When the connected user marks a channel as read +- `notification.mark_unread` - When the connected user marks a message as unread + +For handling these events and updating unread counts, see [Unread Counts](/chat/docs/python/unread/). + +## Push Notification Delivery Confirmation + +By default, when a push notification is received while the app is inactive, the message is not marked as delivered. To mark messages as delivered from push notifications, customize your push notification handling. + +- [iOS Custom Push Notifications](/chat/docs/sdk/ios/client/push-notifications/#customising-remote-push-notifications) +- [Android Custom Push Notifications](/chat/docs/sdk/android/client/guides/push-notifications/#customizing-push-notifications) diff --git a/docs/messages/message_reminders.md b/docs/messages/message_reminders.md new file mode 100644 index 0000000..7c2044d --- /dev/null +++ b/docs/messages/message_reminders.md @@ -0,0 +1,145 @@ +Message reminders let users schedule notifications for specific messages, making it easier to follow up later. When a reminder includes a timestamp, it's like saying "remind me later about this message," and the user who set it will receive a notification at the designated time. If no timestamp is provided, the reminder functions more like a bookmark, allowing the user to save the message for later reference. + +Reminders require Push V3 to be enabled - see details [here](/chat/docs/python/push_template/) + +## Enabling Reminders + +The Message Reminders feature must be activated at the channel level before it can be used. You have two configuration options: activate it for a single channel using configuration overrides, or enable it globally for all channels of a particular type. + +```python +# Enabling it for a channel +channel.update_partial( + config_overrides={ + "user_message_reminders": True + } +) + +# Enabling it for a channel type +client.update_channel_type("messaging", user_message_reminders=True) +``` + +Message reminders allow users to: + +- schedule a notification after given amount of time has elapsed +- bookmark a message without specifying a deadline + +## Limits + +- A user cannot have more than 250 reminders scheduled +- A user can only have one reminder created per message + +## Creating a Message Reminder + +You can create a reminder for any message. When creating a reminder, you can specify a reminder time or save it for later without a specific time. + +```python +from datetime import datetime, timedelta + +# Create a reminder with a specific due date +remind_at = datetime.now() + timedelta(hours=1) +reminder = client.create_reminder('message-id', 'user-id', remind_at) + +# Create a "Save for later" reminder without a specific time +reminder = client.create_reminder('message-id', 'user-id') +``` + +## Updating a Message Reminder + +You can update an existing reminder for a message to change the reminder time. + +```python +from datetime import datetime, timedelta + +# Update a reminder with a new due date +remind_at = datetime.now() + timedelta(hours=2) +updated_reminder = client.update_reminder('message-id', 'user-id', remind_at) + +# Convert a timed reminder to "Save for later" +updated_reminder = client.update_reminder('message-id', 'user-id', None) +``` + +## Deleting a Message Reminder + +You can delete a reminder for a message when it's no longer needed. + +```python +# Delete the reminder for the message +client.delete_reminder('message-id', 'user-id') +``` + +## Querying Message Reminders + +The SDK allows you to fetch all reminders of the current user. You can filter, sort, and paginate through all the user's reminders. + +```python +# Query reminders for a user +reminders = client.query_reminders('user-id') + +# Query reminders with filters +filter_conditions = {'channel_cid': 'messaging:general'} +reminders = client.query_reminders('user-id', filter_conditions) +``` + +### Filtering Reminders + +You can filter the reminders based on different criteria: + +- `message_id` - Filter by the message that the reminder is created on. +- `remind_at` - Filter by the reminder time. +- `created_at` - Filter by the creation date. +- `channel_cid` - Filter by the channel ID. + +The most common use case would be to filter by the reminder time. Like filtering overdue reminders, upcoming reminders, or reminders with no due date (saved for later). + +```python +from datetime import datetime + +# Filter overdue reminders +overdue_filter = {'remind_at': {'$lt': datetime.now()}} +overdue_reminders = client.query_reminders('user-id', overdue_filter) + +# Filter upcoming reminders +upcoming_filter = {'remind_at': {'$gt': datetime.now()}} +upcoming_reminders = client.query_reminders('user-id', upcoming_filter) + +# Filter reminders with no due date (saved for later) +saved_filter = {'remind_at': None} +saved_reminders = client.query_reminders('user-id', saved_filter) +``` + +### Pagination + +If you have many reminders, you can paginate the results. + +```python +# Load reminders with pagination +options = {'limit': 10, 'offset': 0} +reminders = client.query_reminders('user-id', {}, options) + +# Load next page +next_page_options = {'limit': 10, 'offset': 10} +next_reminders = client.query_reminders('user-id', {}, next_page_options) +``` + +## Events + +The following WebSocket events are available for message reminders: + +- `reminder.created` - Triggered when a reminder is created +- `reminder.updated` - Triggered when a reminder is updated +- `reminder.deleted` - Triggered when a reminder is deleted +- `notification.reminder_due` - Triggered when a reminder's due time is reached + +When a reminder's due time is reached, the server also sends a push notification to the user. Ensure push notifications are configured in your app. + + +## Webhooks + +The same events are available as webhooks to notify your backend systems: + +- `reminder.created` +- `reminder.updated` +- `reminder.deleted` +- `notification.reminder_due` + +These webhook events contain the same payload structure as their WebSocket counterparts. For more information on configuring webhooks, see the [Webhooks documentation](/chat/docs/python/webhook_events/). diff --git a/docs/messages/pinned_messages.md b/docs/messages/pinned_messages.md new file mode 100644 index 0000000..abe66b9 --- /dev/null +++ b/docs/messages/pinned_messages.md @@ -0,0 +1,42 @@ +Pinned messages highlight important content in a channel. Use them for announcements, key information, or temporarily promoted content. Each channel can have multiple pinned messages, with optional expiration times. + +## Pinning and Unpinning Messages + +Pin an existing message using `pinMessage`, or create a pinned message by setting `pinned: true` when sending. + +```python +# Create a pinned message +response = channel.send_message({"pinned": True, "text": "Important announcement"}, user["id"]) + +# Pin message for 120 seconds +client.pin_message(response["message"]["id"], user["id"], 120) + +# Unpin message +client.unpin_message(response["message"]["id"], user["id"]) +``` + +### Pin Parameters + +| Name | Type | Description | Default | Optional | +| ----------- | ------- | ---------------------------------------------------------------------- | ------- | -------- | +| pinned | boolean | Whether the message is pinned | false | ✓ | +| pinned_at | string | Timestamp when the message was pinned | - | ✓ | +| pin_expires | string | Timestamp when the pin expires. Null means the message does not expire | null | ✓ | +| pinned_by | object | The user who pinned the message | - | ✓ | + +> [!NOTE] +> Pinning a message requires the `PinMessage` permission. See [Permission Resources](/chat/docs/python/permissions_reference/) and [Default Permissions](/chat/docs/python/chat_permission_policies/) for details. + + +## Retrieving Pinned Messages + +Query a channel to retrieve the 10 most recent pinned messages from `pinned_messages`. + +```python +channel_state = channel.query(watch=False, state=False, presence=False) +pinned_messages = channel_state["pinned_messages"] +``` + +## Paginating Pinned Messages + +Use the dedicated pinned messages endpoint to retrieve all pinned messages with pagination. diff --git a/docs/messages/search.md b/docs/messages/search.md new file mode 100644 index 0000000..a85124b --- /dev/null +++ b/docs/messages/search.md @@ -0,0 +1,80 @@ +Search messages across channels using full-text search or specific field filters. Enable or disable search indexing per channel type in the Stream Dashboard. + +## Searching Messages + +Search requires a channel filter and either a text query or message filter conditions. + +```python +channel_filters = {"cid": "messaging:my-channel"} +message_filters = {"text": {"$autocomplete": "supercali"}} + +page1 = client.search( + channel_filters, + message_filters, + sort=[{"relevance": -1}, {"updated_at": 1}], + limit=10 +) +``` + +### Query Parameters + +| Name | Type | Description | Default | Optional | +| ------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------- | -------- | +| filter_conditions | object | Channel filters. Maximum 500 channels are searched. See [Querying Channels](/chat/docs/python/query_channels/). | - | | +| message_filter_conditions | object | Message filters. See Message Filter Conditions below. Either this or `query` is required. | - | ✓ | +| query | string | Full-text search string. Equivalent to `{text: {$q: }}`. Either this or `message_filter_conditions` is required. | - | ✓ | +| limit | integer | Number of messages to return. | 100 | ✓ | +| offset | integer | Pagination offset. Cannot be used with `sort` or `next`. | 0 | ✓ | +| sort | object/array | Sort order. Use field names with `1` (ascending) or `-1` (descending). | [{relevance: -1}, {id: 1}] | ✓ | +| next | string | Pagination cursor. See Pagination below. | - | ✓ | + +### Message Filter Conditions + +| Field | Description | Operators | +| ------------------ | ------------------------------------------------------ | ------------------------------------------------- | +| id | Message ID | $eq, $gt, $gte, $lt, $lte, $in | +| text | Message text | $q, $autocomplete, $eq, $gt, $gte, $lt, $lte, $in | +| type | Message type. System and deleted messages are excluded | $eq, $gt, $gte, $lt, $lte, $in | +| parent_id | Parent message ID (for replies) | $eq, $gt, $gte, $lt, $lte, $in | +| reply_count | Number of replies | $eq, $gt, $gte, $lt, $lte, $in | +| attachments | Whether message has attachments | $exists, $eq, $gt, $gte, $lt, $lte, $in | +| attachments.type | Attachment type | $eq, $in | +| mentioned_users.id | Mentioned user ID | $contains | +| user.id | Sender user ID | $eq, $gt, $gte, $lt, $lte, $in | +| created_at | Creation timestamp | $eq, $gt, $gte, $lt, $lte, $in | +| updated_at | Update timestamp | $eq, $gt, $gte, $lt, $lte, $in | +| pinned | Whether message is pinned | $eq | +| custom field | Any custom field on the message | $eq, $gt, $gte, $lt, $lte, $in | + +## Sorting + +Results are sorted by relevance by default, with message ID as a tiebreaker. If your query does not use `$q` or `$autocomplete`, all results are equally relevant. + +Sort by any filterable field, including custom fields. Numeric custom fields are sorted numerically; string fields are sorted lexicographically. + +## Pagination + +Two pagination methods are available: + +**Offset pagination** allows access to up to 1,000 results. Results are sorted by relevance and message ID. You cannot use custom sorting with offset pagination. + +**Cursor pagination** (using `next`/`previous`) allows access to all matching results with custom sorting. The response includes `next` and `previous` cursors to navigate between pages. + +```python +channel_filters = {"cid": "messaging:my-channel"} +message_filters = {"text": {"$autocomplete": "supercali"}} + +# First page +page1 = client.search( + channel_filters, + message_filters, + sort=[{"relevance": -1}, {"updated_at": 1}, {"my_custom_field": -1}], + limit=10 +) + +# Next page +page2 = client.search(channel_filters, message_filters, next=page1["next"], limit=10) + +# Previous page +page1_again = client.search(channel_filters, message_filters, next=page2["previous"], limit=10) +``` diff --git a/docs/messages/send_message.md b/docs/messages/send_message.md new file mode 100644 index 0000000..a7b6e81 --- /dev/null +++ b/docs/messages/send_message.md @@ -0,0 +1,391 @@ +Messages are the core building blocks of a chat application. This page covers sending, retrieving, updating, and deleting messages, as well as how Stream processes and formats message content. + +## Sending a Message + +To send a message to a channel, use the `sendMessage` method: + +```python +message = {"text": "Hello, world!"} +channel.send_message(message, user_id) +``` + +> [!NOTE] +> Server-side SDKs require a `user_id` parameter to specify who is sending the message. Client-side SDKs set this automatically based on the connected user. + + +### Message Parameters + +| Name | Type | Description | Default | Optional | +| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------ | ------- | -------- | +| text | string | The message text. Supports markdown and automatic URL enrichment. | | ✓ | +| attachments | array | A list of attachments (audio, video, image, text). Maximum 30 attachments per message with a combined size limit of 5KB. | | ✓ | +| user_id | object | Required for server-side SDKs. Set automatically in client-side mode. | | ✓ | +| mentioned_users | array | A list of user IDs mentioned in the message. You receive back the full user data in the response. Maximum 25 users. | | ✓ | +| custom data | object | Extra data for the message. Must not exceed 5KB in size. | | ✓ | +| skip_push | bool | If true, do not send a push notification for this message. | false | ✓ | +| restricted_visibility | array | Send the message only to specific channel members, identified by their user IDs. | | ✓ | + +### Sending Messages with Attachments + +Messages can include attachments such as images, videos, audio files, and custom content. The following example shows how to send a message with an image attachment and user mentions: + +```python +message = { + "text": "@Josh Check out this image!", + "attachments": [ + { + "type": "image", + "asset_url": "https://bit.ly/2K74TaG", + "thumb_url": "https://bit.ly/2Uumxti", + "myCustomField": 123, + } + ], + "mentioned_users": ["josh-id"], + "priority": "high", +} + +channel.send_message(message, user_id) +``` + +### Supported Attachment Types + +Stream's UI components support the following attachment types by default: + +- **Audio**: Audio files and recordings +- **Video**: Video files +- **Image**: Photos and images +- **Text**: Text-based attachments + +You can define custom attachment types as long as you implement the frontend rendering logic to handle them. Common use cases include embedding products (with photos, descriptions, and links) or sharing user locations. + +The [React tutorial](https://getstream.io/chat/react-chat/tutorial/) explains how to customize the `Attachment` component. + +## Message Processing + +When you send a message, Stream performs several processing steps: + +1. **Markdown parsing**: The message text is parsed for markdown formatting. +2. **URL enrichment**: The first URL in the message text is scraped for Open Graph data, adding preview information automatically. +3. **Slash commands**: Commands like `/giphy`, `/ban`, and `/flag` are processed and executed. + +### URL Enrichment + +When a message contains a URL, Stream automatically scrapes the page for Open Graph metadata and creates an attachment with the preview information: + +```python +message = {"text": "Check this out https://imgur.com/r/bears/4zmGbMN"} +channel.send_message(message, user_id) +``` + +The resulting message includes an automatically generated attachment: + +```json +{ + "id": "message-id", + "text": "Check this out https://imgur.com/r/bears/4zmGbMN", + "attachments": [ + { + "type": "image", + "author_name": "Imgur", + "title": "An update: Dushi made it safe to Bear Sanctuary", + "title_link": "https://imgur.com/4zmGbMN", + "text": "1678 views on Imgur", + "image_url": "https://i.imgur.com/4zmGbMN.jpg?fb", + "thumb_url": "https://i.imgur.com/4zmGbMN.jpg?fb", + "og_scrape_url": "https://imgur.com/r/bears/4zmGbMN" + } + ] +} +``` + +### URL Attachment Fields + +| Name | Type | Description | +| ------------- | ------ | --------------------------------------------------------------------------- | +| type | string | The attachment type based on the URL resource: `audio`, `image`, or `video` | +| author_name | string | The name of the author | +| title | string | The attachment title | +| title_link | string | The link the attachment points to | +| text | string | The attachment description text | +| image_url | string | The URL to the attached image | +| thumb_url | string | The URL to the attachment thumbnail | +| asset_url | string | The URL to the audio, video, or image resource | +| og_scrape_url | string | The original URL that was scraped | + +> [!NOTE] +> The Open Graph scraper uses this user agent: `getstream.io/opengraph-bot facebookexternalhit/1.1`. If you control the target website, ensure this user agent is not blocked for optimal results. + + +## Message Response Structure + +The API returns a message object containing all information about the message, including the author, attachments, reactions, and metadata. + +### Message Fields + +| Field Name | Description | +| ----------------------- | ---------------------------------------------------------------------------------- | +| id | Unique message identifier. Maximum 255 characters; cannot contain `,` or `%`. | +| text | The raw message text | +| html | Safe HTML generated from the text. Can only be set via server-side APIs or import. | +| type | Message type: `regular`, `ephemeral`, `error`, `reply`, `system`, or `deleted` | +| cid | The channel ID in the format `type:id` | +| user | The author user object | +| attachments | List of attachments (maximum 30) | +| mentioned_users | Users mentioned in the message | +| reaction_counts | Reaction counts by type (deprecated, use `reaction_groups`) | +| reaction_scores | Reaction scores by type | +| reaction_groups | Reaction statistics grouped by type with count, scores, and timestamps | +| latest_reactions | The 10 most recent reactions | +| own_reactions | Reactions added by the current user | +| reply_count | Number of replies to this message | +| thread_participants | Users who have participated in the thread | +| parent_id | ID of the parent message if this is a reply | +| quoted_message_id | ID of a quoted message | +| pinned | Whether the message is pinned | +| pinned_at | When the message was pinned | +| pinned_by | User who pinned the message | +| pin_expires | When the pin expires (null for no expiration) | +| silent | Whether this is a silent message (no push notifications) | +| created_at | When the message was created | +| updated_at | When the message was last updated | +| deleted_at | When the message was deleted | +| message_text_updated_at | When the message text was last updated | + +
+Example Response + +```json +{ + "id": "msg-a8f3b2c1-4d5e-6f7a-8b9c-0d1e2f3a4b5c", + "text": "Hey @sarah-miller, the new design mockups are ready! Let me know what you think 🎨", + "html": "", + "type": "regular", + "cid": "messaging:project-apollo", + "user": { + "id": "alex-chen", + "name": "Alex Chen", + "image": "https://cdn.example.com/avatars/alex-chen.jpg", + "role": "user", + "created_at": "2024-03-12T09:15:00.000Z", + "updated_at": "2024-11-28T16:42:00.000Z" + }, + "attachments": [ + { + "type": "image", + "image_url": "https://cdn.example.com/uploads/mockup-v2-homepage.png", + "thumb_url": "https://cdn.example.com/uploads/thumbs/mockup-v2-homepage.png", + "title": "Homepage Redesign v2", + "fallback": "Homepage Redesign v2" + }, + { + "type": "image", + "image_url": "https://cdn.example.com/uploads/mockup-v2-dashboard.png", + "thumb_url": "https://cdn.example.com/uploads/thumbs/mockup-v2-dashboard.png", + "title": "Dashboard Redesign v2", + "fallback": "Dashboard Redesign v2" + } + ], + "mentioned_users": [ + { + "id": "sarah-miller", + "name": "Sarah Miller", + "image": "https://cdn.example.com/avatars/sarah-miller.jpg" + } + ], + "reaction_counts": { + "love": 3, + "fire": 2, + "thumbsup": 1 + }, + "reaction_scores": { + "love": 3, + "fire": 2, + "thumbsup": 1 + }, + "reaction_groups": { + "love": { + "count": 3, + "sum_scores": 3, + "first_reaction_at": "2024-12-11T14:32:00.000Z", + "last_reaction_at": "2024-12-11T15:18:00.000Z" + }, + "fire": { + "count": 2, + "sum_scores": 2, + "first_reaction_at": "2024-12-11T14:35:00.000Z", + "last_reaction_at": "2024-12-11T14:52:00.000Z" + }, + "thumbsup": { + "count": 1, + "sum_scores": 1, + "first_reaction_at": "2024-12-11T16:05:00.000Z", + "last_reaction_at": "2024-12-11T16:05:00.000Z" + } + }, + "latest_reactions": [ + { + "type": "thumbsup", + "user_id": "sarah-miller", + "created_at": "2024-12-11T16:05:00.000Z" + }, + { + "type": "love", + "user_id": "mike-johnson", + "created_at": "2024-12-11T15:18:00.000Z" + }, + { + "type": "fire", + "user_id": "emma-wilson", + "created_at": "2024-12-11T14:52:00.000Z" + } + ], + "own_reactions": [], + "reply_count": 2, + "deleted_reply_count": 0, + "parent_id": "", + "show_in_channel": false, + "thread_participants": [ + { + "id": "sarah-miller", + "name": "Sarah Miller" + }, + { + "id": "alex-chen", + "name": "Alex Chen" + } + ], + "quoted_message_id": "", + "quoted_message": null, + "pinned": true, + "pinned_at": "2024-12-11T17:00:00.000Z", + "pinned_by": { + "id": "sarah-miller", + "name": "Sarah Miller" + }, + "pin_expires": null, + "silent": false, + "shadowed": false, + "i18n": {}, + "image_labels": {}, + "custom": {}, + "restricted_visibility": [], + "poll_id": "", + "poll": null, + "created_at": "2024-12-11T14:30:00.000Z", + "updated_at": "2024-12-11T17:00:00.000Z" +} +``` + +
+ +### Message Types + +| Type | Description | +| --------- | -------------------------------------------------------------------------------------------------------------- | +| regular | A standard message posted to the channel. This is the default type. | +| ephemeral | A temporary message delivered only to one user. Not stored in channel history. Used by commands like `/giphy`. | +| error | An error message from a failed command. Ephemeral and only delivered to one user. | +| reply | A message in a reply thread. Messages with a `parent_id` are automatically this type. | +| system | A message generated by a system event, such as updating the channel or muting a user. | +| deleted | A soft-deleted message. | + +## Retrieving a Message + +Use `getMessage` to retrieve a single message by its ID: + +```python +response = client.get_message(msg_id) +``` + +### Get Message Options + +| Name | Type | Description | Default | Optional | +| -------------------- | ------- | --------------------------------------------------------------- | ------- | -------- | +| show_deleted_message | boolean | If true, returns the original content of a soft-deleted message | false | ✓ | + +> [!NOTE] +> The `show_deleted_message` option is only available for server-side calls. + + +## Updating a Message + +To update a message, call `updateMessage` with a message object that includes the message ID: + +```python +client.update_message({ + "id": msg_id, + "text": "Updated message text", + "user_id": user_id, +}) +``` + +### Partial Update + +Use partial updates to modify specific fields without replacing the entire message. This is useful when you want to retain existing custom data: + +```python +# Set fields +client.update_message_partial( + msg_id, + {"set": {"text": "Updated text", "details.status": "complete"}}, + user["id"], +) + +# Unset fields +client.update_message_partial( + msg_id, + {"unset": ["color"]}, + user["id"], +) +``` + +## Deleting a Message + +Messages can be deleted in three ways: + +- **Soft delete**: The message is marked as deleted but data is preserved. Can be undeleted. +- **Hard delete**: The message and all related data (reactions, replies) are permanently removed. +- **Delete for me**: The message is marked as deleted only for the current user. Other channel members are not affected. + +> [!WARNING] +> Deleting a message does not delete its file attachments. See [deleting attachments](/chat/docs/python/file_uploads/#deleting-files-and-images/) for more information. + + +```python +# Soft delete +client.delete_message(msg_id) + +# Hard delete +client.delete_message(msg_id, hard=True) +``` + +### Delete Type Comparison + +| Behavior | Soft Delete | Hard Delete | Delete for Me | +| ----------------------------- | ----------- | ----------- | ------------- | +| Can be done client-side | ✓ | ✓ | ✓ | +| Message type set to "deleted" | ✓ | - | ✓ | +| Data preserved | ✓ | - | ✓ (for user) | +| Reactions and replies kept | ✓ | - | ✓ | +| Can be undeleted | ✓ | - | - | +| Affects other users | ✓ | ✓ | - | +| Recoverable | ✓ | - | - | + +> [!NOTE] +> Delete for me is limited to 100 messages per user per channel. Contact support to increase this limit. + + +### Undeleting a Message + +Soft-deleted messages can be restored using a server-side call: + +```python +client.undelete_message(msg_id, user_id) +``` + +Messages can be undeleted if: + +- The message was soft-deleted (not hard-deleted) +- The channel has not been deleted +- It is not a reply to a deleted message (the parent must be undeleted first) +- The user performing the undelete is valid diff --git a/docs/messages/send_reaction.md b/docs/messages/send_reaction.md new file mode 100644 index 0000000..034afe3 --- /dev/null +++ b/docs/messages/send_reaction.md @@ -0,0 +1,151 @@ +Stream Chat supports message reactions such as likes, hearts, and custom reaction types. Users can react to messages, and reactions can include custom data and scores for cumulative reactions. + +## Sending a Reaction + +Add a reaction to a message using the `sendReaction` method. Each user can have one reaction of each type per message. + +```python +channel.send_reaction(message_id, {"type": "love", "custom_field": 123}, user_id) +``` + +### Reaction Parameters + +| Name | Type | Description | Default | Optional | +| -------------- | ------- | ------------------------------------------------------------------------ | ------- | -------- | +| message_id | string | ID of the message to react to | | | +| type | string | Reaction type. Each user can have one reaction of each type per message. | | | +| score | integer | Score for cumulative reactions | 1 | ✓ | +| user_id | string | User ID (required for server-side calls) | | ✓ | +| enforce_unique | boolean | If true, replaces all existing reactions from this user with the new one | false | ✓ | +| skip_push | boolean | If true, do not send a push notification | false | ✓ | +| emoji_code | string | Unicode emoji for push notification display | | ✓ | +| custom data | object | Custom fields for the reaction | | ✓ | + +> [!WARNING] +> Custom data for reactions is limited to 1KB. + + +## Removing a Reaction + +Remove a reaction by specifying the message ID and reaction type. + +```python +channel.delete_reaction(message_id, "love", user_id) +``` + +## Retrieving Reactions + +Reactions are included in the message object. Messages returned by the API include the 10 most recent reactions. + +### Reaction Fields in Messages + +| Field | Type | Description | +| ---------------- | ------ | ----------------------------------------------------------------------------------------------- | +| reaction_counts | object | Count of reactions per type. Example: `{"love": 3, "fire": 2}` | +| reaction_scores | object | Sum of scores per type. Equals counts for standard reactions; differs for cumulative reactions. | +| reaction_groups | object | Detailed statistics per type including count, sum_scores, first_reaction_at, last_reaction_at | +| latest_reactions | array | The 10 most recent reactions with type, user_id, and created_at | +| own_reactions | array | The current user's reactions on this message | + +
+Example Reaction Data + +```json +{ + "reaction_counts": { + "love": 3, + "fire": 2, + "thumbsup": 1 + }, + "reaction_scores": { + "love": 3, + "fire": 2, + "thumbsup": 1 + }, + "reaction_groups": { + "love": { + "count": 3, + "sum_scores": 3, + "first_reaction_at": "2024-12-11T14:32:00.000Z", + "last_reaction_at": "2024-12-11T15:18:00.000Z" + }, + "fire": { + "count": 2, + "sum_scores": 2, + "first_reaction_at": "2024-12-11T14:35:00.000Z", + "last_reaction_at": "2024-12-11T14:52:00.000Z" + }, + "thumbsup": { + "count": 1, + "sum_scores": 1, + "first_reaction_at": "2024-12-11T16:05:00.000Z", + "last_reaction_at": "2024-12-11T16:05:00.000Z" + } + }, + "latest_reactions": [ + { + "type": "thumbsup", + "user_id": "sarah-miller", + "created_at": "2024-12-11T16:05:00.000Z" + }, + { + "type": "love", + "user_id": "mike-johnson", + "created_at": "2024-12-11T15:18:00.000Z" + }, + { + "type": "fire", + "user_id": "emma-wilson", + "created_at": "2024-12-11T14:52:00.000Z" + } + ], + "own_reactions": [] +} +``` + +
+ +> [!NOTE] +> Use `reaction_groups` instead of `reaction_counts` for if you're building a custom implementation. The `reaction_groups` field provides additional metadata including timestamps and is the recommended approach. + + +To retrieve more than 10 reactions, use pagination. + +### Paginating Reactions + +Retrieve reactions with pagination using `limit` and `offset` parameters. + +| Parameter | Maximum Value | +| --------- | ------------- | +| limit | 300 | +| offset | 1000 | + +```python +# Get the first 10 reactions +channel.get_reactions(message_id, limit=10) + +# Get reactions 11-13 +channel.get_reactions(message_id, limit=3, offset=10) +``` + +## Querying Reactions + +Filter reactions by type or user on a specific message. This endpoint requires the user to have read permission on the channel when called client-side. + + +## Cumulative Reactions + +Cumulative reactions allow users to react multiple times to the same message, with the total score tracked. This is useful for features like Medium's "clap" functionality. + +Set a `score` value when sending the reaction. The API returns: + +- `sum_scores`: Total score across all users +- Individual user scores + +```python +# User claps 5 times +channel.send_reaction(message_id, {"type": "clap", "score": 5}, user_id) + +# Same user claps 20 more times +channel.send_reaction(message_id, {"type": "clap", "score": 25}, user_id) +``` diff --git a/docs/messages/silent_messages.md b/docs/messages/silent_messages.md new file mode 100644 index 0000000..3f91dc8 --- /dev/null +++ b/docs/messages/silent_messages.md @@ -0,0 +1,47 @@ +Silent and system messages provide ways to add informational content to channels without disrupting users. + +## Silent Messages + +Silent messages do not increment unread counts or mark channels as unread. Use them for transactional or automated content such as: + +- "Your ride is waiting" +- "James updated the trip information" +- "You and Jane are now matched" + +Set `silent: true` when sending a message. + +```python +message = { + "text": "You completed your trip", + "silent": True, +} +channel.send_message(message, user_id) +``` + +> [!NOTE] +> Existing messages cannot be converted to silent messages. + + +> [!NOTE] +> Silent replies still increment the parent message's `reply_count`. + + +> [!NOTE] +> Silent messages still trigger push notifications by default. Set `skip_push: true` to disable push notifications. See [Messages Overview](/chat/docs/python/send_message/) for details. + + +## System Messages + +System messages have a distinct visual presentation, typically styled differently from user messages. Set `type: "system"` to create a system message. + +Stream's UI SDKs include default styling for system messages. However, customizing their appearance is common to match your application's design. See your platform's UI customization documentation for details on styling system messages. + +Client-side users require the `Create System Message` permission. Server-side system messages do not require this permission. + +```python +message = { + "text": "You completed your trip", + "type": "system", +} +channel.send_message(message, user_id) +``` diff --git a/docs/messages/threads.md b/docs/messages/threads.md new file mode 100644 index 0000000..1518793 --- /dev/null +++ b/docs/messages/threads.md @@ -0,0 +1,230 @@ +Threads allow users to reply to specific messages without cluttering the main channel conversation. A thread is created when a message is sent with a `parent_id` referencing another message. + +## Starting a Thread + +Send a message with a `parent_id` to start a thread or add a reply to an existing thread. + +```python +channel.send_message( + {"text": "This is a reply in a thread", "parent_id": parent_message_id}, + user_id, +) +``` + +### Thread Parameters + +| Name | Type | Description | Default | Optional | +| --------------- | ------- | ------------------------------------------------------------------ | ------- | -------- | +| parent_id | string | ID of the parent message to reply to | | | +| show_in_channel | boolean | If true, the reply appears both in the thread and the main channel | false | ✓ | + +> [!NOTE] +> Messages in threads support the same features as regular messages: reactions, attachments, and mentions. + + +## Paginating Thread Replies + +When querying a channel, thread replies are not included by default. The parent message includes a `reply_count` field. Use `getReplies` to fetch thread messages. + +```python +# Get the first 20 replies +channel.get_replies(parent_message_id, limit=20) + +# Get older replies +channel.get_replies(parent_message_id, limit=20, id_lte="42") +``` + +## Inline Replies + +Reply to a message inline without creating a thread. The referenced message appears within the new message. Use `quoted_message_id` instead of `parent_id`. + +```python +channel.send_message( + {"text": "I agree with this point", "quoted_message_id": original_message_id}, + user_id, +) +``` + +When querying messages, the `quoted_message` field is automatically populated: + +```json +{ + "id": "new-message-id", + "text": "I agree with this point", + "quoted_message_id": "original-message-id", + "quoted_message": { + "id": "original-message-id", + "text": "The original message text" + } +} +``` + +> [!WARNING] +> Inline replies are only available one level deep. If Message A replies to Message B, and Message B replies to Message C, you cannot access Message C through Message A. Fetch Message B directly to access its referenced message. + + +## Thread List + +Query all threads that the current user participates in. This is useful for building thread list views similar to Slack or Discord. + +### Querying Threads + +Threads are returned with unread replies first, sorted by the latest reply timestamp in descending order. + + +
+Example Response + +```json +{ + "threads": [ + { + "channel_cid": "messaging:general", + "channel": { + "id": "general", + "type": "messaging", + "name": "General" + }, + "parent_message_id": "parent-123", + "parent_message": { + "id": "parent-123", + "text": "Original message", + "type": "regular" + }, + "created_by_user_id": "user-1", + "reply_count": 5, + "participant_count": 3, + "thread_participants": [ + { + "user_id": "user-1", + "user": { "id": "user-1", "name": "Alice" } + }, + { + "user_id": "user-2", + "user": { "id": "user-2", "name": "Bob" } + } + ], + "last_message_at": "2024-12-11T15:30:00Z", + "latest_replies": [ + { + "id": "reply-1", + "text": "Latest reply", + "type": "reply" + } + ], + "read": [ + { + "user": { "id": "user-1" }, + "last_read": "2024-12-11T15:00:00Z", + "unread_messages": 2 + } + ] + } + ] +} +``` + +
+ +### Query Options + +| Name | Type | Description | Default | Optional | +| ----------------- | ------- | ------------------------------------------------- | ------- | -------- | +| reply_limit | number | Number of latest replies to fetch per thread | 2 | ✓ | +| participant_limit | number | Number of thread participants to fetch per thread | 100 | ✓ | +| limit | number | Maximum number of threads to return | 10 | ✓ | +| watch | boolean | If true, watch channels for the returned threads | true | ✓ | +| member_limit | number | Number of members to fetch per thread channel | 100 | ✓ | + +### Filtering and Sorting + +Filter and sort threads using MongoDB-style query operators. + +#### Supported Filter Fields + +| Field | Type | Operators | Description | +| -------------------- | ------------------------- | ----------------------------------- | ------------------------- | +| `channel_cid` | string or list of strings | `$eq`, `$in` | Channel CID | +| `channel.disabled` | boolean | `$eq` | Channel disabled status | +| `channel.team` | string or list of strings | `$eq`, `$in` | Channel team | +| `parent_message_id` | string or list of strings | `$eq`, `$in` | Parent message ID | +| `created_by_user_id` | string or list of strings | `$eq`, `$in` | Thread creator's user ID | +| `created_at` | string (RFC3339) | `$eq`, `$gt`, `$lt`, `$gte`, `$lte` | Thread creation timestamp | +| `updated_at` | string (RFC3339) | `$eq`, `$gt`, `$lt`, `$gte`, `$lte` | Thread update timestamp | +| `last_message_at` | string (RFC3339) | `$eq`, `$gt`, `$lt`, `$gte`, `$lte` | Last message timestamp | + +#### Supported Sort Fields + +- `active_participant_count` +- `created_at` +- `last_message_at` +- `parent_message_id` +- `participant_count` +- `reply_count` +- `updated_at` + +Use `1` for ascending order and `-1` for descending order. + +```python +filter_conditions = { + "created_by_user_id": {"$eq": "user-1"}, + "updated_at": {"$gte": "2024-01-01T00:00:00Z"}, +} +sort_conditions = [{"created_at": -1}] + +response = client.query_threads( + filter=filter_conditions, + sort=sort_conditions, + limit=10, + user_id=user_id, +) + +# Get next page +if response.get("next"): + next_page = client.query_threads( + filter=filter_conditions, + sort=sort_conditions, + limit=10, + user_id=user_id, + next=response["next"], + ) +``` + +### Getting a Thread by ID + +Retrieve a specific thread using the parent message ID. + + +### Updating Thread Title and Custom Data + +Assign a title and custom data to a thread. + + +## Thread Unread Counts + +### Total Unread Threads + +The total number of unread threads is available after connecting. + + +### Marking Threads as Read or Unread + + +### Unread Count Per Thread + + +## Thread Manager + +The `ThreadManager` class provides built-in pagination and state management for threads. + + +### Event Handling + +Register subscriptions to receive real-time updates for threads. + + +> [!NOTE] +> The `watch` parameter is required when querying threads to receive real-time updates. + + +For `ThreadManager`, call `registerSubscriptions` once to automatically manage subscriptions for all loaded threads: diff --git a/docs/messages/unread_reminders.md b/docs/messages/unread_reminders.md new file mode 100644 index 0000000..11ae4ce --- /dev/null +++ b/docs/messages/unread_reminders.md @@ -0,0 +1,119 @@ +Unread reminders notify users about messages they have not read. Use them to trigger emails, push notifications, or SMS. + +When enabled, Stream Chat sends a webhook or SQS event when a user has an unread message in a channel (with 10 or fewer members) for longer than the configured interval. + +## Enabling Unread Reminders + +Enable reminders for a channel type and configure the reminder interval. + +```python +# Enable reminders for the messaging channel type +client.update_channel_type("messaging", reminders=True) + +# Change reminders interval to 1 hour +client.update_app_settings(reminders_interval=3600) +``` + +The reminder interval can be set between 60 seconds and 86,400 seconds (24 hours). The default is 5 minutes. + +### Requirements + +Reminders are sent only when all conditions are met: + +- The channel has 10 members or fewer +- The channel type has `reminders` enabled +- The channel has at least one unread message +- The channel has `read_events` enabled +- The unread message type is `regular` or `system` +- The message is not deleted +- The channel is not deleted + +> [!NOTE] +> The default channel member limit is 10. To increase this limit, [upgrade to an Enterprise plan](https://getstream.io/chat/pricing) and [contact support](https://getstream.io/contact/support/). + + +## Reminder Event + +Reminder events contain all information needed to send notifications without additional API calls. + +### Event Fields + +| Field | Description | +| ---------- | ------------------------------------------------------------------- | +| type | Event type: `user.unread_message_reminder` | +| user | The user who has unread messages | +| channels | Object containing channels with unread messages (up to 10 channels) | +| created_at | Timestamp when the event was sent | + +### Channel Object Fields + +| Field | Description | +| -------- | ---------------------------------------------------- | +| channel | Channel object including member list | +| messages | Last 5 messages in the channel (in descending order) | + +
+Example Event + +```json +{ + "type": "user.unread_message_reminder", + "created_at": "2022-09-22T12:11:01.258013863Z", + "user": { + "id": "jose", + "role": "user", + "created_at": "2021-08-20T08:16:15.591073Z", + "updated_at": "2022-09-22T12:07:39.675943Z", + "last_active": "2022-09-21T09:49:09.750498Z", + "banned": false, + "online": false, + "teams": ["blue"], + "name": "jose" + }, + "channels": { + "messaging:!members-NsJg6rJv7n1wrpi4NyA5zNGBCpih_eYaQdY6KARmEHo": { + "channel": { + "id": "!members-NsJg6rJv7n1wrpi4NyA5zNGBCpih_eYaQdY6KARmEHo", + "type": "messaging", + "cid": "messaging:!members-NsJg6rJv7n1wrpi4NyA5zNGBCpih_eYaQdY6KARmEHo", + "last_message_at": "2022-09-22T12:10:01.833367Z", + "created_at": "2022-08-24T17:19:28.792836Z", + "updated_at": "2022-08-24T17:19:28.792836Z", + "frozen": false, + "disabled": false, + "member_count": 2, + "members": [ + { + "user_id": "jose", + "user": { + "id": "jose", + "name": "jose" + }, + "role": "member" + }, + { + "user_id": "pepe", + "user": { + "id": "pepe" + }, + "role": "owner" + } + ] + }, + "messages": [ + { + "id": "8009180a-ef37-4818-b4fd-e69a7312d77e", + "text": "hola", + "type": "regular", + "user": { + "id": "pepe" + }, + "created_at": "2022-09-22T12:05:05.641171Z" + } + ] + } + } +} +``` + +
diff --git a/docs/migrating/exporting_channels.md b/docs/migrating/exporting_channels.md new file mode 100644 index 0000000..b83bd71 --- /dev/null +++ b/docs/migrating/exporting_channels.md @@ -0,0 +1,93 @@ +Export channels and users to retrieve messages, metadata, and associated data. All exports run asynchronously and return a task ID for tracking status. + +> [!NOTE] +> All export endpoints require server-side authentication. + + +## Exporting Channels + +```python +response = client.export_channel( + "livestream", + "white-room", + messages_since="2020-11-10T09:30:00.000Z", + messages_until="2020-11-10T11:30:00.000Z", + include_truncated_messages=True, + include_soft_deleted_channels=True, +) + +task_id = response["task_id"] + +# Export multiple channels +response = client.export_channels( + [{"type": "livestream", "id": "white-room"}, + {"type": "livestream", "id": "white-room2"}] +) + +task_id = response["task_id"] +``` + +### Channel Export Options + +| Parameter | Description | +| ------------------------------- | ----------------------------------------------------------- | +| `type` | Channel type (required) | +| `id` | Channel ID (required) | +| `messages_since` | Export messages after this timestamp (RFC3339 format) | +| `messages_until` | Export messages before this timestamp (RFC3339 format) | +| `include_truncated_messages` | Include messages that were truncated (default: `false`) | +| `include_soft_deleted_channels` | Include soft-deleted channels (default: `false`) | +| `version` | Export format: `v1` (default) or `v2` (line-separated JSON) | + +> [!NOTE] +> A single request can export up to 25 channels. + + +### Export Format (v2) + +Add `version: "v2"` for line-separated JSON output, where each entity appears on its own line. + +```python +response = client.export_channel( + "livestream", + "white-room", + version="v2", +) +``` + +### Checking Export Status + +Poll the task status using the returned task ID. When the task completes, the response includes a URL to download the JSON export file. + +```python +response = client.get_export_channel_status(task_id) + +print(response["status"]) # Task status +print(response["result"]) # Result object (when completed) +print(response["result"]["url"]) # Download URL +print(response["error"]) # Error description (if failed) +``` + +> [!NOTE] +> - Download URLs expire after 24 hours but are regenerated on each status request +> - Export files remain available for 60 days +> - Timestamps use UTC in RFC3339 format (e.g., `2021-02-17T08:17:49.745857Z`) + + +## Exporting Users + +Export user data including messages, reactions, calls, and custom data. The export uses line-separated JSON format (same as channel export v2). + + +> [!NOTE] +> A single request can export up to 25 users with a maximum of 10,000 messages per user. [Contact support](https://getstream.io/contact/support/) to export users with more than 10,000 messages. + + +### Checking Export Status + +```python +response = client.get_task(task_id) + +if response['status'] == 'completed': + print(response['result']['url']) +``` diff --git a/docs/migrating/import.md b/docs/migrating/import.md new file mode 100644 index 0000000..3033f12 --- /dev/null +++ b/docs/migrating/import.md @@ -0,0 +1,500 @@ +Stream offers built-in tooling to help you migrate from your current chat provider while keeping the process smooth. + +To import data into Stream, create an import file and upload it either through the dashboard or by using the CLI. + +You can refer to the [File Format](/chat/docs/python/import/#file-format) section below for details about the expected format. + +To get started, you can [download a sample file](/_astro-assets/sample-import.jsonl) to familiarize yourself with the expected structure, and then import it using the [CLI](/chat/docs/python/import/#import-with-the-cli). + +## Import with the CLI + +### 1. Install the CLI + +The easiest way to install the Stream CLI is via Homebrew: + +```bash +$ brew tap GetStream/stream-cli https://github.com/GetStream/stream-cli +$ brew install stream-cli +``` + +For other installation methods, see the [CLI Introduction](/chat/docs/python/cli_introduction/). + +### 2. Configure Authentication + +Before using the CLI, you need to authenticate with your Stream credentials: + +```bash +$ stream-cli config new +``` + +This will prompt you for your API key and secret, which can be found on the [Stream Dashboard](https://getstream.io/dashboard). + +### 3. Upload a file + +Once validated, upload your file to start the import. The maximum file size supported for upload is 300 MB. + +```bash +$ stream-cli chat upload-import my-data.jsonl +{ + "created_at": "2022-05-16T09:02:37.991181Z", + "path": "s3://stream-import/1171432/7e7fbaf4-e266-4877-96da-fbacf650d0a1/my-data.jsonl", + "mode": "upsert", + "id": "79502357-3f4b-486e-9a78-400a184a1088", + "state": "uploaded", + "size": 1230 +} +``` + +You can also specify the import mode using `--mode insert` to only insert new items and skip existing ones. + +### 4. Check Import Status + +Monitor the status of your import using the import ID returned from the upload: + +```bash +$ stream-cli chat get-import 79502357-3f4b-486e-9a78-400a184a1088 +``` + +Use the `--watch` flag to continuously poll the import status until completion: + +```bash +$ stream-cli chat get-import 79502357-3f4b-486e-9a78-400a184a1088 --watch +``` + +To list all imports for your application: + +```bash +$ stream-cli chat list-imports +``` + +For more detailed descriptions of all CLI import commands, please refer to the [Stream CLI import docs](https://getstream.github.io/stream-cli/imports.html). + +### Upsert vs Insert Mode + +#### Upsert + +The Upsert mode will import all the data on the file, including data that already exists on our system. + +- If an item exists, only specific fields will be overwritten. See the **upserted** column in each type's field table for details. +- Custom data will be replaced. + +> [!WARNING] +> Since some omitted fields may be overwritten, it is safest to include all the data you want to persist for each item. + + +#### Insert + +The Insert mode will skip import items if they already exist. + +- It will check for existence of an item by its unique identifier. If it exists it will be skipped, even if the fields provided differ from what exists in the database. For some types, the identifier is a composite key (e.g., members are identified by channel and user, reactions by message, user, and type). +- If it does not exist, the whole object will be inserted. +- This mode is **only available on the Stream CLI**. + +## Import from the Dashboard + +> [!NOTE] +> While we work on improving the import experience, importing from the dashboard is temporarily disabled. +> Please use the [CLI](/chat/docs/python/import/#import-with-the-cli) in the meantime. + + + + +## File Format + +As you prepare your import file, keep the following requirements in mind—otherwise the import may fail during validation. + +- **File Structure:** The file should be generated using the [JSON Lines](https://jsonlines.org/) format, where each line in the file should be a valid JSON object. Each object represents one item of type `user`, `device`, `channel`, `member`, `message`, `reaction`, or `future_channel_ban` that will be imported into your application. + +- **Item order:** Items in the file should be in a specific order, otherwise the validation step will fail. This order makes reference validation more efficient. + For a Chat import, the items in the file should be defined in the following order: + - users + - devices + - future_channel_ban + - channels + - members + - messages + - reactions + +- **Object References:** Every object you reference in the import should either appear as its own object in the file, or the record should already exist in your Stream application. + For example, if you import a message that references a `user_id`, your import file may include a separate user with the same ID, or this user should already exist in your application with the same ID. + This part of the validation process can be tricky, especially with large files. Be cautious with this as it can cause the import to fail. + +- **Read State:** By default, all imported messages will be marked as read. If you provide the last_read timestamp on a member item, then that member’s unread count will be determined based on the amount of messages that have been created after the last_read timestamp. + +- **Distinct Channels:** Distinct channels are channels that are created by providing a list of member IDs rather than a channel ID. Under the hood, our API generates a unique channel ID from the member IDs. To import a distinct channel, include the `member_ids` field and omit the `id` field entirely. + +- **Timestamps:** All timestamps must use the same format as the API ([RFC 3339](https://www.rfc-editor.org/rfc/rfc3339#section-5.8)), for example: `1985-04-12T23:20:50.52Z`. + +## Object format + +As mentioned earlier, each line in the file should be a valid JSON object with the following format: + +| Name | Value | Description | +| ---- | ------ | ----------------------------------------------------------------------------------------------------------- | +| type | string | the item type for this object, allowed values: `user`, `device`, `channel`, `member`, `message`, `reaction` | +| data | object | the data for this object, see below for the format of each type | + +Here's an example of a valid file: + +```json +{"type":"user","data":{"id":"user_001","name":"Jesse","image":"http://getstream.com","created_at":"2017-01-01T01:00:00Z","role":"moderator","invisible":true,"description":"Taj Mahal guitar player at some point"}} +{"type":"device","data":{"id":"device_001","user_id":"user_001","push_provider_type":"firebase","push_provider_name":"firebase"}} +{"type":"channel","data":{"id":"channel_001","type":"messaging","created_by":"user_001","name":"Rock'n Roll Circus"}} +{"type":"member","data":{"channel_type":"messaging","channel_id":"channel_001","user_id":"user_001","is_moderator":true,"created_at":"2017-02-01T02:00:00Z"}} +{"type":"message","data":{"id":"message_001","channel_type":"messaging","channel_id":"channel_001","user":"user_001","text":"Learn how to build a chat app with Stream","type":"regular","created_at":"2017-02-01T02:00:00Z","attachments":[{"type":"video","asset_url":"https://www.youtube.com/watch?v=o-am4BY-dhs","image_url":"https://i.ytimg.com/vi/o-am4BY-dhs/mqdefault.jpg","thumb_url":"https://i.ytimg.com/vi/o-am4BY-dhs/mqdefault.jpg"}]}} +{"type":"reaction","data":{"message_id":"message_001","type":"love","user_id":"user_001","created_at":"2019-03-02T15:00:00Z"}} +``` + +## Data format + +> [!NOTE] +> All time fields should be in **RFC 3339** format + + +> [!NOTE] +> Note that you can add custom fields to users, channels, members, messages (including attachments) and reactions. The limit is 5KB of custom field data per object. + + +### User Type + +The `user` type fields are shown below: + +| name | type | description | required | upserted | +| ---------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | +| blocked_user_ids | array | list of blocked users (only user IDs) | | ✓ | +| channel_mutes | array | list of muted channels (only channel CIDs) | | | +| created_at | string | creation time (default to import time) | | ✓ | +| deactivated_at | string | [deactivation](/chat/docs/python/update_users/#deactivate-a-user) time | | ✓ | +| deleted_at | string | deletion time | | ✓ | +| id | string | unique user ID (**required**) | ✓ | | +| invisible | boolean | [visibility](/chat/docs/javascript/presence_format/#invisible) state (default to `false`) | | ✓ | +| language | string | [language](/chat/docs/python/translation/#set-user-language) | | ✓ | +| privacy_settings | object | control user's privacy settings: [delivery receipts](/chat/docs/python/message-delivery-and-read-status/#enabling-delivery-receipts), [typing indicators](/chat/docs/python/typing_indicators/#typing-privacy-settings) and [read receipts](/chat/docs/python/message-delivery-and-read-status/#read-receipts) | | ✓ | +| push_preferences | object | [push preferences](/chat/docs/python/push_preferences/#chat-push-preferences-support-three-levels-of-notifications) | | ✓ | +| role | string | the user's role (default to `user`) | | ✓ | +| teams | array | list of teams the user is part of | | ✓ | +| teams_role | object | mapping of teams to user roles ([see more](/chat/docs/python/multi_tenant_chat/#team-based-roles)) | | ✓ | +| user_mutes | array | list of muted users (only user IDs) | | | +| \* | string/array/object | add as many custom fields as needed (up to 5 KiB) | | ✓ | + + + +```json +{"type":"user","data":{"id":"user_001","name":"Jesse","image":"http://getstream.com","created_at":"2017-01-01T01:00:00Z","role":"moderator","invisible":true,"teams":["admins"],"teams_role":{"admins":"team_moderator"},"description":"Taj Mahal guitar player at some point"}} +``` + +```json +{"type":"user","data":{"id":"user_001","privacy_settings":{"delivery_receipts":{"enabled":true},"typing_indicators":{"enabled":true},"read_receipts":{"enabled":true}}}} +``` + +```json +{"type":"user","data":{"id":"user_001","push_preferences":{"chat_level":"mentions","disabled_until":"2042-01-01T00:00:01Z"}}} +``` + + + +### Device Type + +Importing devices is the equivalent of [registering devices](/chat/docs/python/push_devices/) with Stream. + +This is useful when migrating from another chat provider to Stream because: + +- Users already have devices registered for push notifications +- You want to preserve these registrations so users continue receiving notifications immediately after migration +- Without importing devices, users would miss notifications until they open the app again + +The `device` type fields are shown below: + +| name | type | description | required | upserted | +| ------------------ | ------ | -------------------------------------------------------------------- | -------- | -------- | +| created_at | string | creation time (default to import time) | | ✓ | +| id | string | unique device id | ✓ | | +| push_provider_type | string | must be one of the following: `firebase`, `apn`, `huawei` or`xiaomi` | ✓ | ✓ | +| push_provider_name | string | name that matches the Push Configuration on your app | ✓ | ✓ | +| user_id | string | user ID | ✓ | | + + + +```json +{"type":"device","data":{"id":"device_001","user_id":"user_001","created_at": "2019-01-11T02:00:00Z","push_provider_type":"firebase","push_provider_name":"production-firebase-config"}} +``` + + + +### Channel Type + +The `channel` type fields are shown below: + +| name | type | description | required | upserted | +| ------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | +| banned_users | array | list of banned users (only user IDs) | | | +| created_at | string | creation time (default to import time) | | ✓ | +| created_by | string | user who created the channel (user ID) | ✓ | ✓ | +| disabled | boolean | [disabled](/chat/docs/python/disabling_channels/) status (default to `false`) | | ✓ | +| frozen | boolean | [frozen](/chat/docs/python/freezing_channels/) status (default to `false`) | | ✓ | +| id | string | unique channel ID (**required only if `member_ids` is not provided**) | ✓ | | +| member_ids | array | user IDs used for [distinct channels](/chat/docs/python/creating_channels#distinct-channels) (**required only if `id` is not provided**) | | | +| team | string | channel [team](/chat/docs/python/multi_tenant_chat/#teams) | | ✓ | +| truncated_at | string | [truncation](/chat/docs/python/truncate_channel/) time | | ✓ | +| type | string | [channel type](/chat/docs/python/channel_features) (**required**) | ✓ | | +| \* | string/list/object | add as many custom fields as needed (up to 5 KiB) | | ✓ | + + + +```json +// with channel ID +{"type": "channel","data": {"id": "channel_001","type": "livestream","created_by": "user_001","name": "Rock'n Roll Circus"}} +``` + +```json +// with member_ids +{"type": "channel","data":{"member_ids":["user_001","user_002"],"type":"livestream","created_by":"user_001","name":"Rock'n Roll Circus"}} +``` + + + +### Member Type + +Channel members store the mapping between users and channels. The fields are shown below: + +| name | type | description | required | upserted | +| -------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | +| archived_at | string | time when the channel was [archived](/chat/docs/python/archiving_channels) | | ✓ | +| channel_id | string | channel ID (**required only if `channel_member_ids` is not provided**) | ✓ | | +| channel_role | string | member role (default to `channel_member`) | | ✓ | +| channel_type | string | channel type | ✓ | | +| created_at | string | creation time (default to import time) | | ✓ | +| channel_member_ids | array | user IDs for [distinct channels](/chat/docs/python/creating_channels#distinct-channels) (**required only if `id` is not provided**) | ✓ | | +| hide_channel | boolean | [hidden](/chat/docs/python/hiding_channels/) status (default to `false`) | | | +| hide_messages_before | string | messages will be hidden before this time | | ✓ | +| invited | boolean | whether the user was invited (default to `false`) | | ✓ | +| invited_accepted_at | string | time when the user accepted the invite | | ✓ | +| invited_rejected_at | string | time when the user rejected the invite | | ✓ | +| last_read | string | last time the member read the channel | | | +| user_id | string | user ID | ✓ | | +| \* | string/list/object | add as many custom fields as needed (up to 5 KiB) | | ✓ | + +> [!NOTE] +> If your app uses multi-tenancy, the referenced `channel` and `user` items must have a matching team. + + + + +```json +{"type":"member","data":{"channel_id":"channel_001","channel_type":"livestream","user_id":"user_001","channel_role":"channel_member","created_at":"2017-02-01T02:00:00Z"}} +``` + +```json +{"type":"member","data":{"channel_member_ids":["user_001","user_002"],"channel_type":"livestream","user_id":"user_001","channel_role":"channel_member","created_at": "2017-02-01T02:00:00Z"}} +``` + + + +### Message Type + +The `message` type fields are shown below: + +| name | type | description | required | upserted | +| --------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | +| attachments | array | message attachments, see the attachment section below | | ✓ | +| channel_id | string | channel ID (**required only if `channel_member_ids` is not provided**) | ✓ | ✓ | +| channel_member_ids | list of strings | user IDs for [distinct channels](/chat/docs/python/creating_channels#distinct-channels) (**required only if `id` is not provided**) | ✓ | ✓ | +| channel_type | string | channel type | ✓ | ✓ | +| created_at | string | creation time (default to import time) | | ✓ | +| deleted_at | string | deletion time | | ✓ | +| html | string | safe HTML generated from the text | | ✓ | +| id | string | unique message ID | ✓ | | +| mentioned_users_ids | array | mentioned user IDs | | ✓ | +| parent_id | string | parent message ID (`type` should be `"reply"`) | | ✓ | +| pin_expires | string | time when pin expires (requires `pinned_at` and `pinned_by_id`) | | ✓ | +| pinned_at | string | time when message was pinned (requires `pin_expires` and `pinned_by_id`) | | ✓ | +| pinned_by_id | string | pinned_by user ID (requires `pinned_at` and `pin_expires`) | | ✓ | +| quoted_message_id | string | quoted message ID | | ✓ | +| restricted_visibility | array | user IDs that can see this message (see [documentation](/chat/docs/python/private_messaging) for more information) | | ✓ | +| show_in_channel | bool | define if reply should be shown in the channel as well (default to `false`) | | ✓ | +| text | string | message text | | ✓ | +| type | string | message type (available type: `regular`, `reply`, `deleted` or `system`) | ✓ | ✓ | +| user | string | user ID who posted the message | ✓ | ✓ | +| \* | string/list/object | add as many custom fields as needed (up to 5 KiB) | | ✓ | + + + +```json +{"type":"message","data":{"id":"message_001","channel_type":"livestream","channel_id":"channel_001","user":"user_001","text":"Such a great song, check out my solo at 2:25","type":"regular","created_at":"2017-02-01T02:00:00Z"}} +``` + + + +#### Message Attachments + +The attachments are a great way to extend Stream's functionality. If you want to have a custom product attachment, location attachment, checkout, etc., attachments are the way to go. +The fields below are automatically picked up and shown by our component libraries. + +> [!NOTE] +> Note that all attachment URLs must be publicly accessible, otherwise the import will fail. + + +| name | type | description | required | +| ----------------- | ------------------ | ---------------------------------------------------------------------- | -------- | +| asset_url | string | URL to the audio, video, or image resource | | +| image_url | string | URL to the attached image | | +| migrate_resources | boolean | if `true`, attachment will be migrated to our CDN (default to `false`) | | +| thumb_url | string | URL to the attachment thumbnail (recommended for images and videos) | | +| type | string | attachment type (built-in types: `audio`, `video`, `image` and `text`) | | +| \* | string/list/object | add as many custom fields as needed (up to 5 KiB) | | + + + +```json +{"type":"message","data":{...,"attachments":[{"type":"image","image_url":"https://my.domain.com/image.jpg","thumb_url":"https://my.domain.com/image-thumb.jpg"},{"type":"video","asset_url":"https://my.domain.com/video.mp4","thumb_url":"https://my.domain.com/video-thumb.jpg"}]}} +``` + + + +For attachment migration, only `image_url`, `thumb_url` and `asset_url` fields will be migrated to our CDN and the original URL will be replaced with the new one. The files should not be empty. The import will fail if resource migration fails. In the error you can see the URL and message ID for the failed migration. + +### Reaction Type + +The `reaction` type fields are shown below: + +| name | type | description | required | upserted | +| ---------- | ------------------ | ------------------------------------------------- | -------- | -------- | +| created_at | string | creation time | ✓ | ✓ | +| message_id | string | message ID | ✓ | | +| type | string | reaction type | ✓ | | +| user_id | string | user ID | ✓ | | +| \* | string/list/object | add as many custom fields as needed (up to 5 KiB) | | ✓ | + + + +```json +{"type":"reaction","data":{"message_id":"message_001","type":"love","user_id":"user_001","created_at":"2019-03-02T15:00:00Z"}} +``` + + + +### Future Channel Ban Type + +The `future_channel_ban` type fields are shown below: + +| name | type | description | required | upserted | +| ---------- | ------- | ----------------------------------------------- | -------- | -------- | +| created_at | string | creation time | | ✓ | +| created_by | string | user ID who initiated this future channel ban | ✓ | | +| target_id | string | user ID who will be ban for all future channels | ✓ | | +| shadow | boolean | determine if the future channel ban is shadowed | | ✓ | +| reason | string | determine the reason for the ban | | ✓ | + + + +```json +{"type":"future_channel_ban","data":{"created_by":"user_001","target_id":"user_002","shadow":true,"created_at":"2019-03-02T15:00:00Z"}} +``` + + + +## Validation + +We use **[JSON Schema](https://json-schema.org/)** to define and validate the structure of our data. +The Chat schema files are available **[here](https://github.com/GetStream/protocol/tree/d29b9ca915d05ea0c11ed2e2df6f47f5ae542c5c/jsonschemas)**. + +These schema files cover approximately **99% of our validation rules**. Some validations depend on your specific configuration, such as **[custom permission policies](/chat/docs/python/chat_permission_policies)** or **[custom channel type configurations](/chat/docs/python/channel_features)**. + +To validate that your data is in the correct format, you can either: + +- validate the data on the fly (while the data is generated), or +- validate the data once the file is generated + +There are many **[JSON Schema validators](https://json-schema.org/tools?query=&sortBy=name&sortOrder=ascending&groupBy=toolingTypes&licenses=&languages=&drafts=&toolingTypes=&environments=&showObsolete=false&supportsBowtie=false#validator)** available for different programming languages that you can use to validate your data. + +## Error Messages + +When problems occur during analysis, they will show up in the dashboard. A list of errors will be shown in JSON format. Where applicable, the offending item will be included, for example: + +```json +{ + "errors": [ + { + "error": "Validation error: max channelID length exceeded (64)", + "item_type": "channel", + "item": { + "id": "waytoolongwaytoolongwaytoolongwaytoolongwaytoolongwaytoolongwaytoolong", + "type": "messaging", + "created_by": "userA-7D3CA510-CB3C-479E-B5FA-69FC2D48410F", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + } + ], + "stats": { + "total": { + "messages": 0, + "members": 0, + "reactions": 0, + "channels": 1, + "users": 0 + } + } +} +``` + +| Error | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Validation error: max "`field`" length exceeded (`field-length`) | Maximum length of field exceeded | +| Validation error: either channel.id or channel.member_ids should be provided, but not both | Either define channel as a regular channel or a distinct channel, but not both | +| Validation error: channel.id or channel.member_ids required, but not both | At least one of `channel.id` or `channel.member_ids` must be provided | +| Validation error: "`field`" required | Missing required field | +| Validation error: "`field`" is a reserved field | Field provided is reserved | +| Validation error: duplicated `item` "`id`" | Item and id combination is duplicated | +| Validation error: created_by user `id` doesn't exist (channel "messaging:abc"). please include all users as separate user entries | All users referenced by all objects, for example in `channel.created_by`, should be included in the import file | +| Validation error: '_value_' is not a valid _field_ | The value provided for a particular field is not valid. For example, a `channel.id` contains invalid characters | +| Validation error: user `id` with teams `X` cannot be a member of channel `Y` with team `Z` | The member item references a user and channel that do not have a matching team | +| Parse error: invalid item type "foobar" | An item was included with an invalid item type, only: `user`, `device`, `channel`, `member`, `message` and `reaction` are allowed | +| Parse error: invalid character ',' looking for beginning of value | The import contains malformed JSON | + +This is not an exhaustive list of possible errors, but these are the most common ones. diff --git a/docs/migrating/migration_guide.md b/docs/migrating/migration_guide.md new file mode 100644 index 0000000..efa11fe --- /dev/null +++ b/docs/migrating/migration_guide.md @@ -0,0 +1,216 @@ +Stream has migrated over customers with >100m users and TBs of data. +The following steps enable you to move over from in-house or competing solutions quickly: + +![img_1.png](@chat/_default/_assets/images/img_1.png) + +**Step 1: SDK Proof of Concept.** 1 day proof of concept on each SDK you need to support. Start with relevant tutorials. +See the chat tutorials for: [React](https://getstream.io/chat/react-chat/tutorial/), [React Native](https://getstream.io/chat/react-native-chat/tutorial/), [Flutter](https://getstream.io/chat/flutter/tutorial/), [iOS](https://getstream.io/tutorials/ios-chat/), [Android](https://getstream.io/chat/android/tutorial/), and [Unity](https://getstream.io/chat/unity/tutorial/). + +**Step 2: Backend Integration.** Configuring users and setting up tokens is done on the backend. +After these 2 steps your team will have a good understanding of the API works. +Next you want to do the following in parallel: + +**Step 3a: Bi-Directional Webhook Sync.** +When your old system receives a message send it to Stream, when Stream receives a message send it to your old API. +We've had many Sendbird customers switch, so for Sendbird specifically we offer this functionality out of the box. +Simply work with our support team to enable it. + +**Step 3b: Prepare Import File.** The [import section](/chat/docs/python/import/) has the full details. First practice with a small import. +This is a good best practice to find any issues before you generate a large export from your old chat. +Data can be imported with the CLI or in the dashboard. + +**Step 3c: Polish UI and UX.** Stream offers a low level client, offline support library and UI components. +This means you can build any type of chat or messaging UI. Take a moment to customize or build UI components. +And iterate on how the chat integrates with your app. + +**Step 4: Final Import.** Next, [Import](/chat/docs/python/import/) all historical data. +Note that imports can take several days to process. Keep that in mind for your release timeline. + +**Step 5: Deploy.** Deploy your new apps with Stream as the chat experience. +After all apps are updated eventually disable your old chat. + +The whole process typically takes 4 weeks. Alternatively if you're in a hurry, some apps skip the webhook sync. +This leads to an easier/faster switch but involves some disruption to chat. + +> [!NOTE] +> For large-scale migrations, we recommend premium support for real-time engagement with our engineering team. [Contact support](https://getstream.io/contact/support/) to discuss your requirements. + + +## Migration Approaches + +Choose an approach based on your requirements for downtime, complexity, and user experience. + +### No Sync (Hard Switch) + +Import your data and switch to Stream at a scheduled time. This is the simplest approach but requires a service interruption. + +**Process:** + +1. Schedule a maintenance window +2. Export chat data in Stream's [import format](/chat/docs/python/import/) +3. Import data via the Stream Dashboard +4. Validate the import +5. Deploy your updated application + +**Characteristics:** + +- Simplest to implement +- Requires downtime +- Users must update their app + +### Uni-Directional Sync + +Sync data from your current provider to Stream in real-time, then switch when ready. This is the most common approach. + +**Process:** + +1. Set up a mechanism (webhook) to replicate data from your current provider to Stream +2. Export and import historical chat data +3. Once sync is operational and imports are complete, deploy your updated application + +**Characteristics:** + +- Zero downtime +- Momentary interruption only +- Not complex to implement +- **Most common approach** + +### Bi-Directional Sync + +Sync data in both directions, allowing both services to work in tandem during transition. This is useful when you cannot control user upgrade timing (e.g., mobile apps). + +**Process:** + +1. Set up forward sync from your current provider to Stream +2. Set up reverse sync from Stream back to your current provider +3. Export and import historical chat data +4. Roll out app updates gradually—users on old and new versions can communicate + +**Characteristics:** + +- Zero downtime +- Zero service interruption +- No forced app updates required +- More complex to implement + +## Field Mapping + +Map your existing data fields to Stream's data model. This is critical for both imports and real-time sync. + +### Users + +| Stream Field | Your Field | Notes | +| ------------ | -------------------------- | ------------------------- | +| id | id | Required, string | +| name | name / nickname | Display name | +| image | profile_image / profileUrl | Avatar URL | +| \* | custom_data | Custom fields (up to 5KB) | + +### Channels + +| Stream Field | Your Field | Notes | +| ------------ | ---------------------- | ------------------------------------------------- | +| id | id | Required, string | +| type | channel_type / private | Map to Stream types (messaging, livestream, etc.) | +| created_by | created_by_id | User ID of creator | +| name | name | Channel name | +| members | member_user_ids | List of user IDs | +| \* | custom_data | Custom fields | + +### Messages + +| Stream Field | Your Field | Notes | +| ------------ | ---------------------- | --------------------------- | +| id | id | Required, string | +| channel_type | - | Inherit from parent channel | +| channel_id | - | Inherit from parent channel | +| user | sender_id | User ID of sender | +| text | text / content | Message text | +| type | - | Usually "regular" | +| attachments | attachments / parts | File attachments | +| created_at | created_at / timestamp | RFC3339 format | +| \* | custom_data | Custom fields | + +> [!NOTE] +> Provide a sample data export for Stream to review before your migration. This helps identify mapping issues early. + + +## Sendbird Migration + +Stream has migrated enough Sendbird customers that we have developed specific tooling for syncing Sendbird data in real-time. + +Watch the [Sendbird sync tool in action](https://www.loom.com/share/98779f38fd264dea8028a5c321a4f025) to see how bi-directional sync works. + +### Real-Time Sync Setup + +> [!NOTE] +> Real-time sync from Sendbird is available on Enterprise plans. [Contact support](https://getstream.io/contact/support/) to enable this feature. + + +**Configuration:** + +1. Provide Stream with your Sendbird app ID and token +2. Configure your Sendbird webhook URL: + +```text +https://chat.stream-io-api.com/sendbird/webhook?api_key= +``` + +Stream processes each webhook payload in a persistent queue, enabling replay if failures occur and guaranteeing chronological processing. + +### Sendbird Field Mappings + +| Sendbird | Stream | +| ----------------- | ------------------------------------------------------------- | +| User `nickname` | User `name` | +| User `profileUrl` | User `image` | +| `ADMM` message | `system` message type | +| Other messages | `regular` message type | +| Channel ID | Same ID with `sendbird_{group,open}_channel_` prefix stripped | + +**Channel type mapping:** + +- Uses `custom_type` if set on the Sendbird channel +- Uses `public` if the Sendbird channel is open or public +- Uses `messaging` otherwise + +**Defaults:** + +- Admin user: `data-migration-admin` +- Default channel role: `channel_member` + +These can be customized by request. + +### Supported Sendbird Events + +**Open Channels:** + +- `open_channel:create`, `open_channel:remove` +- `open_channel:enter`, `open_channel:exit` +- `open_channel:message_send`, `open_channel:message_update`, `open_channel:message_delete` + +**Group Channels:** + +- `group_channel:create`, `group_channel:changed`, `group_channel:remove` +- `group_channel:invite`, `group_channel:join`, `group_channel:decline_invite`, `group_channel:leave` +- `group_channel:message_send`, `group_channel:message_read`, `group_channel:message_update`, `group_channel:message_delete` +- `group_channel:reaction_add`, `group_channel:reaction_delete` + +Stream can handle a subset of these events if needed. + +### Sendbird vs Stream Differences + +| Sendbird | Stream | +| ------------------------------ | ---------------------------------- | +| Open channels + Group channels | 5 built-in types + custom types | +| `getChannel` + `channel.enter` | Single `channel.watch()` call | +| UserMessage / FileMessage | Message with attachments list | +| Thumbnails specified up-front | Image sizes requested at read time | +| Private vs Public groups | Handled via permission system | + +## Next Steps + +1. [Contact Stream support](https://getstream.io/contact/support/) to discuss your migration +2. Review the [Importing Data](/chat/docs/python/import/) guide +3. Provide a sample data export for review +4. Plan your migration timeline based on your chosen approach diff --git a/docs/push/legacy_push_system.md b/docs/push/legacy_push_system.md new file mode 100644 index 0000000..00bb242 --- /dev/null +++ b/docs/push/legacy_push_system.md @@ -0,0 +1,41 @@ +This page covers the legacy versions of push notifications v1 and v2. + +> [!NOTE] +> Version 1 of push notifications is using Firebase's legacy FCM APIs. These APIs are no longer supported by Firebase. In order for your app to be able to send push notifications, it is mandatory to upgrade to version 3. + + +- v1 has a template-based customization system that requires users to learn the details of this template system. + +- v2 has a common message payload where SDKs automatically enrich messages and channels at runtime. They then call the callback with this data, providing a familiar programming environment where any customization is possible. + +## Templates in v2 + +In **v2**, template functionality is partially dropped to simplify configurations: + +1. Standard template, there is nothing to configure + +2. By default, data payload size is limited to 4KB. Data is enriched automatically + +3. Offline storage is synced under the hood for more performant experience + +4. Templates have a steep learning curve and are hard to use and they have their own quirks such as a lack of advanced programming constructs + +SDKs receive a standard payload and enrich it by calling API and syncing their storage then they pass data to the user to be used in their familiar environment. + +It works well for native platforms but has some deficiencies for non-native SDKs such as React Native and Flutter. In this case, a limited version of templating is supported because the app isn't woken up to process notifications if it has been killed by OS or user. + +### Default Firebase APN template + +Following is the default template if you leave **apn_template** or **firebase_apn_template** empty. To see definition of available fields that can be included in `aps` object please see [Apple documentation](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification#2943360). + + +### Updating Firebase APN template + + +### Default Firebase Notification template + +By default, there is only a data message and no notification in the payload. + +If a template is set, then the template will be processed and put into the notification key in the payload. To see available fields and their description please follow [FCM documentation](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidNotification). + +Following is a sample template for an example: diff --git a/docs/push/push_introduction.md b/docs/push/push_introduction.md new file mode 100644 index 0000000..f8c7732 --- /dev/null +++ b/docs/push/push_introduction.md @@ -0,0 +1,127 @@ +Stream Chat supports push notifications through Firebase Cloud Messaging, Apple Push Notification (APN), Huawei Push and Xiaomi Push. + +- Push notifications can be sent for new messages, message edits and reactions. You can use [Webhooks](/chat/docs/python/webhooks_overview/) to send push notifications on other types of events. + +- Supports customization of push payloads, including the ability to add custom data. But you don't have to configure it as it comes with well-designed default templates. + +- Supports enabling/disabling push notifications for each notification type, such as new messages, message edits, reactions. By default, all notification types are disabled. + +- A common message payload where SDKs automatically enrich messages and channels in the runtime and call the callback with these data where it's a familiar way of building by programming and any customization is possible. + +- Multi bundle support for push providers. + +- Multi-tenancy and continuous delivery are covered in a single Stream app. + +- Customization in multiple levels; _app, provider, channel type, user._ + +- **No channel member count limitation** + +## Setting up Push + +Push is available to Stream Chat integrations running in a mobile environment. + +Depending on which Stream Chat SDK you are using, the process for setting up push notifications will be slightly different. Follow the guide for your chosen SDK: + +- [React Native](/chat/docs/sdk/react-native/guides/push-notifications/) + +- [Flutter](/chat/docs/sdk/flutter/guides/push-notifications/adding_push_notifications_v2/) + +- [Android](/chat/docs/sdk/android/client/guides/push-notifications/) + +- [iOS](/chat/docs/sdk/ios/client/push-notifications/) + +## Push Delivery Rules + +Push message delivery behaves according to these rules: + +- Push notifications can be configured for new messages, message edits, message reactions and more. +- Only channel members receive a push notification. +- Members receive push notifications regardless of their online status. +- Replies inside a [thread](/chat/docs/python/threads/) are only sent to users that are part of that thread: + - They posted at least one message. + - They were mentioned. +- Push from muted users are not sent. +- Push [preferences](/chat/docs/python/push_preferences/) for a user are respected: + - Preferences at user level are "all", "none" or "mentions". + - Preferences at channel level are "all", "none" or "mentions". +- Push for a [private message](/chat/docs/python/private_messaging/) are sent only to the restricted users of the message. +- Push notification are sent to all registered devices for a user (up to 25) . +- `skip_push` is marked as `false` , as described [here](/chat/docs/python/send_message/). +- `push_notifications` is enabled (default) on the channel type for message is sent. + +> [!WARNING] +> Push notifications require membership. Watching a channel isn't enough. + + +## Handling Push Notifications on the Foreground + +Both iOS and Android discard push notifications when your application is on the foreground. + +You can configure this on your application and decide what to do when a push notification is received while the app is on the foreground. + + +## Push Notification Payload + +Push notifications are delivered as data payloads that the SDK can use to convert into the same data types that are received when working with the APIs. + +When a message received by the Chat API, according to the delivery rules, it kicks a job that sends a regular data message (as below) to configured push providers on your app. According to the battery and the online status of the device, push providers deliver this payload to the actual devices. When a device receives the payload, it's passed to the SDK which connects to Chat API to receive regular message and channel records and unmarshals them into in-memory objects and gives control to you by passing these objects. At this point, your application can use these objects to generate any push notification to be shown to the user. + +This is the main payload which will be sent to each configured provider: + +> [!NOTE] +> The version field in the data payload is set to **v2**. It is to ensure backward compatibility with the existing SDKs. + + +```json +{ + "sender": "stream.chat", + "type": "message.new", + "version": "v2", + "message_id": "d152f6c1-8c8c-476d-bfd6-59c15c20548a", + "id": "d152f6c1-8c8c-476d-bfd6-59c15c20548a", + "channel_type": "messaging", + "channel_id": "company-chat", + "cid": "messaging:company-chat" +} +``` + +On both Android and iOS the SDK will convert the payload in channel and message types and allow you to customize the notification message. + +You can find more details, examples and guides on specific use-cases on the specific SDK docs. + +## Push Notification Configuration + +### Firebase + +- Requires a service worker account from your FCM project. + + **Firebase console → project settings (top left)  → service accounts (4th sub header) → generate a new private key** + +Export your key and upload in the firebase credentials field in dashboard. A sample content: + +```json +{ + "type": "service_account", + "project_id": "chat", + "private_key_id": "", + "private_key": "", + "client_email": "@.iam.gserviceaccount.com", + "client_id": "", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/%40.iam.gserviceaccount.com" +} +``` + +> [!NOTE] +> For other providers, follow SDK specific docs. + + +## Troubleshooting + +Push notifications are not always intuitive to implement because they involve systems outside of Stream Chat with a number of moving parts. We've put together some resources on [common errors](/chat/docs/python/push_-_common_issues_and_faq/) and how to resolve them. + +## Other Push Providers + +While Stream Chat doesn't have first class integration for Push providers besides Firebase, APN, Huawei and Xiaomi, it is entirely possible to integrate with additional providers using [Webhooks](/chat/docs/python/webhooks_overview/). diff --git a/docs/push/push_preferences.md b/docs/push/push_preferences.md new file mode 100644 index 0000000..7279a25 --- /dev/null +++ b/docs/push/push_preferences.md @@ -0,0 +1,75 @@ +Push preferences allow users to control how they receive push notifications. You can set preferences at both the user level (global preferences) and channel level (per-channel preferences), giving users fine-grained control over their notification experience. + +## How Push Preferences Work + +> [!WARNING] +> Users must be channel members to receive push notifications regardless of their preferences. + + +### Chat push preferences operate on two levels + +- **User-level preferences**: These act as global preferences that apply to all channels for a user. +- **Channel-level preferences**: These override the global preferences for specific channels. + +### Chat push preferences support three levels of notifications + +- **all**: Receive all push notifications **(default)**. +- **mentions**: Receive push notifications only when mentioned. +- **none**: Do not receive push notifications. + +Additionally, you can temporarily disable push notifications until a specific time using the `disabled_until` parameter. + +### The system evaluates preferences in the following priority order + +1. **Channel-level preferences** are checked first (if they exist for the specific channel). +2. If no channel-level preference exists, **user-level (global) preferences** are used. +3. If no preferences are set at all, the default behavior is "all". +4. **Temporary disabling**: If `disabled_until` is set and the current time is before that timestamp, notifications are disabled regardless of other preferences. + +## Setting Push Preferences + +### User-Level Preferences + +Set global push preferences that apply to all channels for a user: + + +### Channel-Level Preferences + +Set preferences for specific channels, which override user-level preferences: + + +## Client-Side vs Server-Side Usage + +### Client-Side Usage + +When using client-side authentication, users can only update their own push preferences: + + +### Server-Side Usage + +Server-side requests can update preferences for any user: + + +## Practical Examples + +### 1: Creating a "Do Not Disturb" Mode + + +### 2: Channel-Specific Notification Settings + +You can set different preferences for each individual channel, allowing users to customize their notification experience on a per-channel basis. + + +### 3: Temporarily Disabling Push Notifications + +You can temporarily disable push notifications until a specific time using the `disabled_until` parameter. This is useful for implementing "Do Not Disturb" periods or scheduled quiet hours. + + +## Call Push Preferences + +You can set preferences for call-related push notifications using the `call_level` field. + +### Call push preferences support two levels of notifications + +- **all**: Receive all call push notifications **(default)**. +- **none**: Do not receive call push notifications. diff --git a/docs/push/push_providers_and_multi_bundle.md b/docs/push/push_providers_and_multi_bundle.md new file mode 100644 index 0000000..e30420d --- /dev/null +++ b/docs/push/push_providers_and_multi_bundle.md @@ -0,0 +1,46 @@ +## Push Provider + +A push provider is a configuration of a push API with one of four different types: APN, Firebase, Huawei and Xiaomi at the moment. + +Multiple providers can be added to the same Stream application to support, for example: + +- Multi-tenancy: there can be different builds of the same application such as prod vs staging, regular vs admin, etc. + +- Multi-platform: there can be specific customizations for different target platforms such as starting React Native and adapting native Android/iOS SDKs along the way. + +> [!NOTE] +> Following endpoints, management of push providers only works if your app is upgraded to v2 or v3. Otherwise, the update app settings endpoint must be used for a single provider config per type (APN, Firebase, Huawei, Xiaomi). + + +### Upsert a Push Provider + +In the same endpoint, a new config can be created or updated. + +> [!WARNING] +> Up to 25 push providers can be added to a single application. + + +> [!NOTE] +> If the authentication information is updated, linked devices might be invalidated in the next push message sent retry. + + +### List Push Providers + + +### Delete a Push Provider + + +### Push Providers & Devices + +By default, adding a device doesn't require a push provider linking due to backward compatibility where old configurations don't have a `name` , so their names are empty. + +- If the configuration name is not provided when adding a device, devices will be matched with configurations according to only their types. + +- If the configuration name is provided, but invalid, the request will fail with a bad request error. + +When devices are added, they can be linked to a provider to inherit their configuration. + + +## Updating non-multi bundle configs (nameless) + +If you're not interested in multi-bundle support, you can leverage `updateAppSettings` endpoint to add push configuration for a single APN, Firebase, Huawei or Xiaomi provider. diff --git a/docs/push/push_template.md b/docs/push/push_template.md new file mode 100644 index 0000000..73424d0 --- /dev/null +++ b/docs/push/push_template.md @@ -0,0 +1,391 @@ +> [!NOTE] +> Customizing push content is optional, as we provide well-designed default templates. Just make sure to enable push notifications for each notification type you plan to support. + + +Once push notifications are enabled for your app, you can customize the templates to match your app’s needs. This lets you control how notifications appear on users' devices. You can do this through the dashboard or via the API, as shown below: + +## Enabling and Customizing Push Templates + +The [Upsert Push Template REST endpoint](https://getstream.github.io/protocol/?urls.primaryName=Chat#/product%3Achat/UpsertPushTemplate) allows you to enable push notifications notification type and optionally define custom templates. Supported event types include `message.new`, `message.updated`, `reaction.new` and more. + +Following is a sample payload for enabling push notifications with default template: + +```json +{ + "enable_push": true, + "event_type": "message.new", + "push_provider_type": "apn", + "push_provider_name": "apn" +} +``` + +Following is a sample payload for enabling push notifications with custom template: + +```json +{ + "enable_push": true, + "event_type": "message.new", + "push_provider_type": "firebase", + "push_provider_name": "firebase", + "template": "{\"data\":{\"version\":\"v2\",\"sender\":\"stream.chat\",\"type\":\"{{ event_type }}\",\"id\":\"{{ message.id }}\",\"message_id\":\"{{ message.id }}\",\"channel_type\":\"{{ channel.type }}\",\"channel_id\":\"{{ channel.id }}\",\"cid\":\"{{ channel.cid }}\",\"receiver_id\":\"{{ receiver.id }}\"},\"android\":{\"priority\":\"high\"},\"apns\":{\"payload\":{\"aps\":{\"alert\":{\"title\":\"New message from {{ sender.name }}\",\"body\":\"{{ truncate message.text 150 }}\"},\"badge\":{{ unread_count }},\"sound\":\"default\",\"mutable-content\":1,\"content-available\":0}}}}" +} +``` + +**Available Fields:** + +| Field Name | Type | Description | +| -------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `enable_push` | Boolean | Indicates whether push notifications are enabled for this event type. | +| `event_type` | String
(message.new, message.updated, reaction.new) | The type of event used to apply the corresponding custom push configuration. | +| `push_provider_type` | String | The type of push provider | +| `push_provider_name` | String | The name of the configured push provider instance. Can be left empty if you're not using multi-bundle support. | +| `template` | String | The push notification template as a stringified JSON object. | + +## Default Templates + +Push v3 supports templating for both Firebase and APNs. Configuring templates is optional — if no custom templates are provided, Stream will automatically use the default templates for Firebase and APNs. + +> [!NOTE] +> The version field in the data payload is set to **v2**. It is to ensure backward compatibility with the existing SDKs. + + +### Firebase default template + +```json +{ + "data": { + "version": "v2", + "sender": "stream.chat", + "type": "{{ event_type }}", + "id": "{{ message.id }}", + "message_id": "{{ message.id }}", + "channel_type": "{{ channel.type }}", + "channel_id": "{{ channel.id }}", + "cid": "{{ channel.cid }}", + "receiver_id": "{{ receiver.id }}" + }, + "android": { + "priority": "high" + }, + "apns": { + "payload": { + "aps": { + "alert": { + "title": "New message from {{ sender.name }}", + "body": "{{ truncate message.text 150 }}" + }, + "badge": {{ unread_count }}, + "sound": "default", + "mutable-content": 1, + "content-available": 0 + } + } + } +} +``` + +By default, there is only a data message and no notification field in the android payload. + +If a template is set, then the template will be processed and put into the notification key in the payload. To see available fields and their description please follow [FCM documentation](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidNotification). + +Following is a sample android template with notification: + +```json +{ + "android": { + "notification": { + "title": "{{ sender.name }} @ {{ channel.name }}", + "body": "{{ truncate message.text 150 }}", + "click_action": "OPEN_ACTIVITY_1", + "sound": "default" + }, + "priority": "high" + } +} +``` + +### APN default template + +```json +{ + "payload": { + "aps": { + "alert": { + "title": "You have a new message", + "body": "{{ truncate message.text 150 }}" + }, + "badge": {{ unread_count }}, + "sound": "default", + "mutable-content": 1, + "content-available": 0 + }, + "stream": { + "version": "v2", + "sender": "stream.chat", + "type": "{{ event_type }}", + "id": "{{ message.id }}", + "cid": "{{ channel.cid }}", + "receiver_id": "{{ receiver.id }}" + } + } +} +``` + +## Context Variables + +For both Firebase and APN, the payload that is being sent is rendered using the [handlebars](http://handlebarsjs.com/) templating language, to ensure full configurability for your app. + +Stream provides the following variables in the template rendering context: + +| Name | Type | Description | +| --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| sender | object | Sender object. You can access the user name, id or any other custom field you have defined for the user | +| receiver | object | Receiver object. You can access the user name, id or any other custom field you have defined for the user | +| message | object | Message object. You can access the text of the message (or a preview of it if the message is too large) or any other custom field you have defined for the message | +| isThread | boolean | Indicates whether the message is a thread message or not | +| isMentioned | boolean | Indicates whether the user is mentioned in the message or not. | +| channel | object | Channel object. You can access the channel name and any other custom field you have defined for this channel | +| unread_count | integer | Number of unread messages | +| unread_channels | integer | Number of unread channels for this user | +| reaction | object | Reaction object. You can access the reaction type and any other custom field you have defined for this channel | +| members | array | Channel members. You can access the user name, id and any other custom field of each member (i.e. excluding sender) | +| otherMembers | array | Like members but the user who will be receiving the notification is excluded (i.e. excluding sender and receiver) | + +## Limitations + +There are some limitations that Stream imposes on the push notification handlebars template to make sure no malformed payloads are being sent to push providers. + +### 1: Custom Arrays Can't Be Indexed + +For example, given the context: + +```json +{ + "sender": { + "name": "Bob", + "some_array": ["foo", "bar"] + } +} +``` + +And the template: + +```json +"title": {{ sender.some_array.[0] }} +``` + +The rendered payload will be: + +```json +"title": "" +``` + +### 2: Interpolating Whole Lists and Objects Isn't Allowed + +For example, given the context: + +```json +{ + "sender": { + "name": "bob", + "some_array": ["foo", "bar"], + "address": { + "street": "willow str" + } + } +} +``` + +And the template: + +```json +"title": "{{ sender.some_array }} {{ sender.address }}" +``` + +The rendered payload will be: + +```json +"title": "[] {}" +``` + +### 3: Unquoted fields that aren't in the context will be rendered as empty strings + +For example, given the context: + +```json +{ + "sender": { + "name": "bob" + } +} +``` + +And the template: + +```json +"title": {{ sender.missing_field }} +``` + +The rendered payload will be: + +```json +"title": "" +``` + +## Advanced Use Cases + +For advanced use cases (e.g. A list of channel members in the notification title, conditional rendering, etc), Stream provides some handlebars helper functions. + +### Helper Functions + +| name | type | description | +| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| implodemembers | function | takes the list of channel members and implodes it into a single string, using a custom limit, separator and suffix. | +| json | function | renders passed parameter as JSON (e.g {"channel":{{{ json channel }}}} ) | +| each | function | For loop. Use this to access the current variable, @index for the current index and @first and @last as convenience booleans to determine if the iteration is at its first/last element | +| if | function | If function. Tests trueness of given parameter. Supports else statement. (e.g {{#if sender.name}}{{ sender.name }}{{/if}} ) | +| unless | function | Unless function. Tests falseness of given parameter. Supports else statement. (e.g {{#unless sender.name}}Missing name{{/unless}} ) | +| equal | function | Equality check function. Tests equality of the given 2 parameters. Supports else statement. (e.g {{#equal channel.type "messaging" }}This is the messaging channel{{else}}This is another channel{{/equal}} ) | +| unequal | function | Inequality check function. Tests inequality of the given 2 parameters. Supports else statement. (e.g {{#unequal channel.type "messaging" }}This is another channel{{else}}This is the messaging channel{{/unequal}} ) | +| ifLt | function | If less than. Supports else statement. | +| ifLte | function | If less than or equal. Supports else statement. | +| ifGt | function | If greater than. Supports else statement. | +| ifGte | function | If greater than or equal. Supports else statement. | +| remainder | function | Calculates the difference between the length of an array and an integer (e.g {{remainder otherMembers 2}} | +| truncate | function | Truncate given text to given length (e.g {{ truncate message.text 150 }}) | + +Most of the functions above are straight forward, except for `implodeMembers` , which will be detailed further. + +The full function signature is: `{{implodeMembers otherMembers|members [limit=] [separator=] [nameField=] [suffixFmt=]}}` + +### Function Parameters + +| name | type | description | default | +| ------------ | ------- | --------------------------------------------------------------------------------------------- | ----------------------------- | +| otherMembers | members | array | Which member array to implode | +| limit | integer | How many member names to show before adding the suffix | 3 | +| nameField | string | Field name from which field to retrieve the member's name. **Note:** does not support nesting | name | +| separator | string | Separator to use for channel members | , | +| suffixFmt | string | Format string to use for the suffix. **Note:** only %d is allowed for formatting | and %d other(s) | + +### Data-only push notifications + +Data-only push notifications carry no UI alert—just a small key/value payload your app handles. Use them to poke the app to sync or fetch fresh data: send only a data payload (no notification body). On iOS, mark the APNs/FCM message as silent (`content-available: 1`) and delivery is opportunistic and not guaranteed. On Android, data messages go to your background handler; if work is long or the OS is aggressive, escalate priority and hand off to a foreground service. Keep payloads tiny and idempotent, and treat them as a trigger to fetch real content—not as a guaranteed delivery mechanism. + +They are also particularly useful whenever we want to consume and display the notification ourselves, without involving the operating system directly. + +Provided below is an example notification template configuration that we can use to allow for data-only notifications on both iOS and Android: + +```json +{ + "data": { + "version": "v2", + "sender": "stream.chat", + "type": "{{ event_type }}", + "id": "{{ message.id }}", + "channel_type": "{{ channel.type }}", + "channel_id": "{{ channel.id }}", + "cid": "{{ channel.cid }}", + "receiver_id": "{{ receiver.id }}", + "my_custom_notification_body": "BODY: {{ truncate message.text 150 }}" + }, + "android": { + "priority": "high" + }, + "apns": { + "headers": { + "apns-push-type": "background", + "apns-priority": "5" + }, + "payload": { + "aps": { + "content-available": 1 + }, + "stream": { + "title": "New message from {{ sender.name }}", + "body": "{{ truncate message.text 150 }}" + } + } + } +} +``` + +Since we are no longer relying on `apns.payload.aps.alert` to control our `body` and `title`, we have to have a way to ingest it within our `data`. However, since both of these are blacklisted fields to set to our `data` object they have to be set in the `stream` object directly and used from there for these types of notifications. + +Whenever crafting custom payload templates, it is very important to keep the `version` to `v2`, which is done for backwards compatibility reasons. + +Another important part to understand is the fact that data-only push notifications must not exceed `4KB`. Due to this, we have to be careful never to send the full payload (and only handpick certain parts of it that we need). We also have to be careful to truncate the text in the event of having very, very long messages whose text might go above this limit. + +### Examples + +Let's put these helpers to use in a few examples: + +#### Example 1 + +Rendering channel members in the notification title. Each member's name is stored in the  `fullName`  field. + +What we want to achieve: + +```json +{ + "aps": { + "alert": { + "title": "Bob Jones, Jessica Wright, Tom Hadle and 4 other(s)", + "body": "Bob Jones: Hello there fellow channel members" + }, + "badge": 0 + } +} +``` + +How we will achieve it: using  `implodeMembers`  with a custom name field (leaving others empty so that defaults will be used): + +```json +{ + "aps": { + "alert": { + "title": "{{implodeMembers otherMembers nameField="fullName"}}", + "body": "{{ sender.fullName }}: {{ message.text }}" + }, + "badge": {{ unread_count }} + } +} +``` + +#### Example 2 + +Rendering channel members in the notification title. Each member's name is stored in the  **nested**   `details.name`  field. + +What we want to achieve: + +```json +{ + "aps": { + "alert": { + "title": "Bob Jones, Jessica Wright, Tom Hadle and 4 other(s)", + "body": "Bob Jones: Hello there fellow channel members" + }, + "badge": 0 + } +} +``` + +How we will achieve it: since  `implodeMembers`  doesn't support nested fields, we need to use a bunch of helpers such as  `each` , `ifLte` . Note how the use of  `~`  will trim the whitespaces so that the title in rendered in a single row: + +```json +{ + "aps": { + "alert": { + "title": " + {{~#each otherMembers}} + {{#ifLte @index 2}} + {{~this.details.name}}{{#ifLt @index 2 }}, {{/ifLt~}} + {{~else if @last~}} + {{{ " " }}} and {{remainder otherMembers 3}} other(s) + {{~/ifLte~}} + {{/each~}}", + "body": "{{ sender.details.name }}: {{ message.text }}" + }, + "badge": {{ unread_count }} + } +``` diff --git a/docs/push/push_test.md b/docs/push/push_test.md new file mode 100644 index 0000000..8dafa33 --- /dev/null +++ b/docs/push/push_test.md @@ -0,0 +1,76 @@ +Once you're all set up with push notifications, you can use the CLI to test how these notifications will look for your devices. + +**Pre-requirements** : + +- Your app has push notifications configured for at least one provider [in your app settings](/chat/docs/python/app_setting_overview/) + +- You have a user that has at least one [device](/chat/docs/python/#push_devices/) associated + +- To skip sending to devices but only to see the payload, pass `skip_devices=true` + +The base command for testing push notifications is: + +```python +client.check_push( + { + "message_id": msg["id"], + "skip_devices": True, + "user_id": user["id"], + } +) +``` + +This command provides the following functionality for you: + +1. Picks a random message from a channel that this user is part of + +2. Uses the notification templates configured for your push provider to render the payload using this message + +3. Sends this payload to all of the user's devices + +> [!NOTE] +> This particular use case is ideal for testing a newly configured app, to make sure the push notifications are configured exactly as desired. + + +In some cases, you might want to test the new notification template that you configured on the dashboard. + +For example, let's say you want to test a new APN notification template: + +```python +client.check_push( + { + "message_id": msg["id"], + "skip_devices": True, + "user_id": user["id"], + "apn_template": "{\"aps\":{\"alert\":{\"title\":\"{{ sender.name }} @ {{ channel.name }}\",\"body\":\"testing out new stuff:{{ message.text }}\"},\"category\":\"NEW_MESSAGE_2\"}}", + } +) +``` + +As you can see below, the new template will be used for sending the push notification only this time. The existing clients will leverage the default configuration. + +![undefined](https://getstream.imgix.net/images/docs/chat_push/cli_push_test.png?auto=compress&fit=clip&w=800&h=600) + +Here's a full list of parameters that you can use with the test command: + +### Push Test Parameters + +| name | type | description | default | optional | +| ------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------- | ------- | -------- | +| user_id | string | The user ID | - | | +| message_id | string | ID of the message that should be used instead of a random one. If the message doesn't exist, an error will occur | - | ✓ | +| apn_notification_template | string | Notification template to be used instead of the configured APN one. This is one time only. (v1 only) | - | ✓ | +| firebase_notification_template | string | Notification template to be used instead of the configured APN one. This is one time only. (v1 only) | - | ✓ | +| firebase_data_template | string | Data template to be used instead of the configured Firebase one. This is one time only. (v1 only) | - | ✓ | +| skip_devices | boolean | Skip requiring device tokens and sending. | False | ✓ | + +### Push Test Payload + +| name | type | description | default | optional | +| -------------------------- | ---------------- | ------------------------------------------------------------ | ------- | -------- | +| device_errors | object | A map of errors indexed by device tokens. | - | ✓ | +| general_errors | array of strings | A list of errors encountered with providers. | - | ✓ | +| skip_devices | boolean | Given skip_devices flag is passed back. | - | ✓ | +| rendered_apn_template | string | Your executed APN template (v1 only). | - | ✓ | +| rendered_firebase_template | string | Your executed Firebase template (v1 only). | - | ✓ | +| rendered_message | string | Your payload (v2 for all providers and v1 for Huawei/Xiaomi) | - | ✓ | diff --git a/docs/push/registering_push_devices.md b/docs/push/registering_push_devices.md new file mode 100644 index 0000000..bb958ad --- /dev/null +++ b/docs/push/registering_push_devices.md @@ -0,0 +1,55 @@ +Once your app has enabled the push notifications, you can use the APIs to register user devices such as iPhones and Android phones. + +> [!NOTE] +> Each chat user has a limit of  **25**  unique devices. Once this limit is reached, the oldest device will be removed and replaced by the new device. + + +### Device Parameters + +| name | type | description | default | optional | +| ------------------ | ------- | -------------------------------------------------------------------------------- | ------- | -------- | +| user_id | string | The user ID for this device | - | | +| id | string | The device ID. | - | | +| push_provider | string | The push provider for this device. Either APN, Firebase, Huawei, or Xiaomi. | - | | +| disabled | boolean | Set if the device is disabled | - | ✓ | +| disabled_reason | string | Explanation if the device is disabled | - | ✓ | +| push_provider_name | string | The push provider name for this device if a multi-bundle configuration is added. | - | ✓ | + +### Register a Device + +Registering a device associates it with a user and tells the push provider to send new message notifications to the device. + +> [!NOTE] +> Register the user's device for remote push notifications once your user is successfully connected to Chat. + + +> [!NOTE] +> Multi-bundle configurations require that you specify a push_provider_name when registering a device that corresponds to the name of the push configuration that you've set up in the dashboard or via the API. + + +```python +client.add_device( + '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207', + 'apn', + '42' +) +``` + +### Unregister a Device + +Unregistering a device removes the device from the user and stops further new message notifications. + +```python +client.delete_device( + '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207', + '42' +) +``` + +### List Devices + +Provides a list of all devices associated with a user. + +```python +client.get_devices(user_id) +``` diff --git a/docs/quick_start/architecture_and_benchmark.md b/docs/quick_start/architecture_and_benchmark.md new file mode 100644 index 0000000..f45a564 --- /dev/null +++ b/docs/quick_start/architecture_and_benchmark.md @@ -0,0 +1,87 @@ +## Architecture Overview + +The chat API works for apps with hundreds of millions of users. A few key things about the architecture: + +- Edge network: We run an edge network of servers around the world +- Offline storage & optimistic UI updates: The SDKs handle offline storage, and update the UI optimistically, which makes everything fast. +- Excellent performance: Scale to 5m users online in a channel, with <40ms latency +- Highly redundant infra + +On the SDKs we provide both a low level client and UI components. This gives you the flexibility to build any type of chat or messaging UI. + +### Benchmark at a glance + +Stream Chat is regularly benchmarked to ensure its performance stays consistent. As you can see in the chart, we are able to connect 5 million users to the same channel. + +![Benchmark Results](@chat/_default/_assets/images/chat_bench_screenshot.png) + +API Latency stays consistent for the benchmark and there is no performance degradation. + +### The tech behind the Stream chat API + +Stream uses Golang as the backend language for chat. + +### Edge network + +When using Stream API, the end-user connects to the nearest API edge server, this is similar to how CDN works but it is more sophisticated than that. + +Edge servers support HTTP/3 and HTTP/2 when available. TLS termination is done at the edge, reducing the handshake time. Traffic between edges and origin servers is fully encrypted and we use long-lived HTTP/2 connections to ensure best latency. + +API authentication, rate-limiting and CORS requests are also performed at the edge. + +For increased resilience we also build circuit breakers, API request retry mechanism, consistent hash routing and locality-aware load balancing with fallback to other AZs if necessary to our edge infrastructure. + +### Websockets + +To get to 5m concurrent online users on a single channel we have a few optimizations in place. +First we use cuckoo filters to have a quick way to check if a user might be online on a given server. +There are also optimizations in place with skip lists and smart locking to prevent slowdowns at high traffic. + +Deflate compression is negotiated with API clients to minimize event payloads, vectored I/O using the [writev](https://linux.die.net/man/2/writev) syscall is leveraged to optimize the delivery of WS events to many clients. + +We extensively use sync.Pool to minimize GC pressure and implement our own reference-counted messages that allow us to serialize once and deliver to many clients via atomic ref counting (zero-copy cloning). + +Long-polling fallback mechanism: sometimes users connect from networks that do not handle websockets well, in that case we automatically fallback to the old-school HTTP long-polling connection mechanism to retrieve real-time events. + +### API performance + +The API often responds in <50ms. This is achieved using a combination of denormalization, caching and Redis client-side caching. + +Commonly accessed entities like users, channels and messages are heavily cached on the API memory directly as well as in Redis, using a cache-flow pattern. + +Redis [ZSET](https://redis.io/docs/latest/develop/data-types/sorted-sets/) and [HSET](https://redis.io/docs/latest/commands/hset/) are used to replicate some data structures and have a lock-free/atomic write path (eg. updating user.last_active_at). + +Redis cluster is used to store terabytes of data and scale horizontally. Lua scripting is used to have atomicity and consistency for data stored across multiple keys. + +Redis client-side caching is used to synchronize in-memory caches across different servers/processes. + +### Offline storage and Optimistic UI + +All SDKs come with offline storage and optimistic UI support. This allows the UI to always feel very responsive and snappy even if the user is on a very slow network. + +When the SDK starts, it instantly loads existing channels and messages from offline storage and runs a synchronization task in the background to fetch all deltas like new/updated/deleted messages, users and channels. +This guarantees great startup performance and makes it possible to perform optimistic UI updates without the risk of data-loss. + +Optimistic UI: when a user sends a message, the message shows in the UI instantly without waiting for network requests to complete. The SDK takes care of sending the request to the API and performs all the asynchronous work with the offline storage. + +The offline storage uses the best database tech available for each platform: Core Data on iOS, Room (Jetpack) on Android and SQLite for React Native and Flutter. + +### Members & Limits + +There are no limits on the number of concurrent users, the number of members, the number of messages or the number of channels. + +Performing read operations with stable performance requires careful data denormalization to avoid joining/ordering large amounts of data. + +### Infra & testing + +There is a large Go integration test suite, a set of smoketests in production and a JS QA suite. +Sometimes things can slip through, but in general this approach is very effective at preventing issues. + +The infra runs on AWS and is highly available. + +## Benchmark + +We built an extensive benchmark infrastructure to measure the scalability and performance of our API. Performing realistic benchmarks at large scale is a small challenge on its own that we solved by building our own distributed system with separate control-plane, workers and data plane to run large workloads and capture all relevant telemetry from probes. + +The benchmark result up to 5m users can be found here + diff --git a/docs/quick_start/backend_quickstart.md b/docs/quick_start/backend_quickstart.md new file mode 100644 index 0000000..133f0fc --- /dev/null +++ b/docs/quick_start/backend_quickstart.md @@ -0,0 +1,149 @@ +For the average Stream integration, the development work focuses on code that executes in the client. The React, React Native, Swift, Kotlin or Flutter SDKs connect to the chat API directly from the client. However, some tasks must be executed from the server for safety. + +- [Generating user tokens](/chat/docs/node/#generating-user-tokens/) + +- [Syncing users](/chat/docs/node/#syncing-users/) + +The chat API has some features that client side code *can*manage in specific cases but usually shouldn't. While these features _can_ be initiated with client side code. We recommend managing them server side instead unless you are certain that you need to manage them from the client side for your specific use case. + +- [Syncing channels](/chat/docs/node/#syncing-channels/) + +- [Adding & Removing members & moderators](/chat/docs/node/) + +- [Sending messages](/chat/docs/node/) + +- [Changing App Settings](/chat/docs/python/app_setting_overview/) + +The backend has full access to the chat API. + +This quick start covers the basics of a backend integration. If we don't have an SDK for your favorite language be sure to review the [REST documentation](https://getstream.github.io/protocol/). + +### Generating user tokens + +The backend creates a token for a user. You hand that token to the client side during login or registration. This token allows the client side to connect to the chat API for that user. Stream's permission system does the heavy work of determining which actions are valid for the user, so the backend just needs enough logic to provide a token to give the client side access to a specific user. + +The following code shows how to instantiate a client and create a token for a user on the server: + +```python +# pip install stream-chat +from stream_chat import StreamChat + +# instantiate your stream client using the API key and secret +# the secret is only used server side and gives you full access to the API +server_client = StreamChat(api_key="{{ api_key }}", api_secret="{{ api_secret }}") +token = server_client.create_token("john") + +# next, hand this token to the client in your in your login or registration response +``` + +You can also generate tokens that expire after a certain time. The tokens & authentication section explains this in detail. + +### Syncing users + +When a user starts a chat conversation with another user both users need to be present in Stream's user storage. So you'll want to make sure that users are synced in advance. The update users endpoint allows you to update 100 users at once, an example is shown below: + +```python +server_client.upsert_user({"id": user_id, "role": "admin", "mycustomfield": "123"}) +``` + +Note that user roles can only be changed server side. The role you assign to a user impacts their permissions and which actions they can take on a channel. + +### Syncing Channels + +You can create channels client side, but for many applications you'll want to restrict the creation of channels to the backend. Especially if a chat is related to a certain object in your database. One example is building a livestream chat like Twitch. You'll want to create a channel for each Stream and set the channel creator to the owner of the Stream. The example below shows how to create a channel and update it: + +```python +channel = server_client.channel("messaging", "kung-fu") +channel.create(user_id) +channel.update({"name": "my channel", "image": "image url", "mycustomfield": "123"}) +``` + +You can also add a message on the channel when updating it. It's quite common for chat apps to show messages like: "Jack invited John to the channel", or "Kate changed the color of the channel to green". + +### Adding Members or Moderators + +The backend SDKs also make it easy to add or remove members from a channel. The example below shows how to add and remove members: + +```python +channel.add_members(["thierry", "josh", "tommaso"]) +channel.remove_members(["tommaso"]) + +channel.add_moderators(["thierry"]) +channel.demote_moderators(["thierry"]) +``` + +### Sending Messages + +It's quite common that certain actions in your application trigger a message to be sent. If a new user joins an app some chat apps will notify you that your contact joined the app. The example below shows how to send a message from the backend. It's the same syntax as you use client side, but specifying which user is sending the message is required: + +```python +message = { + "text": "@Josh I told them I was pesca-pescatarian. Which is one who eats solely fish who eat other fish.", + "attachments": [ + { + "type": "image", + "asset_url": "https://bit.ly/2K74TaG", + "thumb_url": "https://bit.ly/2Uumxti", + "myCustomField": 123, + } + ], + "mentioned_users": [josh["id"]], + "anotherCustomField": 234, +} + +channel.send_message(message, "thierry") +``` + +### Changing Application Settings + +Some application settings should only be changed using the back end for security reasons: + +- [Webhook Configuration](/chat/docs/node/webhooks_overview/) + +- [SQS Configuration](/chat/docs/node/sqs/#configure-programmatically/) + +### API Changes + +A **breaking change** is a change that may require you to make changes to your application in order to avoid disruption to your integration. Stream will never introduce a breaking change without notifying its customers and giving them plenty of time to make the appropriate changes on their end. The following are a few examples of changes we consider breaking: + +- Changes to existing permission definitions. + +- Removal of an allowed parameter, request field or response field. + +- Addition of a required parameter or request field without default values. + +- Changes to the intended functionality of an endpoint. _For example, if a DELETE request previously used to soft delete the resource but now hard deletes the resource._ + +- Introduction of a new validation. + +A **non-breaking change** is a change that you can adapt to at your own discretion and pace without disruption. Ensure that your application is designed to be able to handle the following types of non-breaking changes without prior notice from Stream: + +- Addition of new endpoints. + +- Addition of new methods to existing endpoints. + +- Addition of new fields in the following scenarios: + +- New fields in responses. + +- New optional request fields or parameters. + +- New required request fields that have default values. + +- Addition of a new value returned for an existing text field. + +- Changes to the order of fields returned within a response. + +- Addition of an optional request header. + +- Removal of redundant request header. + +- Changes to the length of data returned within a field. + +- Changes to the overall response size. + +- Changes to error messages. We do not recommend parsing error messages to perform business logic. Instead, you should only rely on HTTP response codes and error codes. + +- Fixes to HTTP response codes and error codes from incorrect code to correct code. + +- Prefix your custom data properties, we could introduce new features that might clash with custom property names you are using, by prefixing them you avoid this problem. diff --git a/docs/quick_start/roadmap_and_changelog.md b/docs/quick_start/roadmap_and_changelog.md new file mode 100644 index 0000000..0b419bf --- /dev/null +++ b/docs/quick_start/roadmap_and_changelog.md @@ -0,0 +1,33 @@ +## Roadmap + +This section outlines the upcoming features and improvements planned for Stream Chat. + +- Design refresh for the SDKs +- New chat Demos on the website, showing slack style, live shopping and social use cases +- Permissions system ease of use improvements +- Documentation improvements +- Cache library standardization to improve performance +- Engagement stats +- Named query support (making it easier to change how you query channels across SDKs) + +## Changelog + +### January 2026 + +- Android: Added method for clearing cache and temporary files, plus a `team` property to `MessageReadEvent` +- iOS: Fixed a rare deadlock when reconnecting to the web-socket and a crash when querying Channel List by `.filterTags` + +### December 2025 + +- Moving the changelog docs here +- Reminders +- Live location sharing +- Campaign stats +- Restricted visibility messaging +- Team-level permissions +- Push V3 +- Multi-User device support +- Average response time API +- Message count per channel +- Delivery receipts +- Delete for me diff --git a/docs/webhooks/sns.md b/docs/webhooks/sns.md new file mode 100644 index 0000000..4991099 --- /dev/null +++ b/docs/webhooks/sns.md @@ -0,0 +1,83 @@ +Stream can send payloads of all events from your application to an [Amazon SNS](https://aws.amazon.com/sns/) topic you own. + +A chat application with a lot of users generates a lots of events. With a standard Webhook configuration, events are posted to your server and can overwhelm unprepared servers during high-use periods. While the server is out, it will not be able to receive Webhooks and will fail to process them. One way to avoid this issue is to use Stream Chat's support for sending webhooks to Amazon SNS. + +SNS removes the chance of losing data for Chat events by providing a large, scalable message exchange that delivers events generated by Stream Chat to as many consumers as you like. + +The complete list of supported events is identical to those sent through webhooks and can be found on the [Events](/chat/docs/python/webhook_events/) page. + +## Configuration + +You can configure your SNS topic through the [Stream Dashboard](https://getstream.io/dashboard/) or programmatically using the REST API or an SDK with Server Side Authorization. + +There are 2 ways to configure authentication on your SNS topic: + +1. By providing a key and secret + +2. Or by having Stream's AWS account assume a role on your SNS topic. With this option you omit the key and secret, but instead you set up a resource-based policy to grant Stream Publish permission on your SNS topic. The following policy needs to be attached to your topic (replace the value of Resource with the fully qualified ARN of your topic): + +```json +{ + "Sid": "AllowStreamProdAccount", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::185583345998:root" + }, + "Action": "SNS:Publish", + "Resource": "arn:aws:sns:us-west-2:1111111111:customer-sns-topic" +} +``` + +To configure an SNS topic, use the `event_hooks` array and Update App Settings method: + +```python +# Note: Any previously existing hooks not included in event_hooks array will be deleted. +# Get current settings first to preserve your existing configuration. + +# STEP 1: Get current app settings to preserve existing hooks +response = client.get_app_settings() +existing_hooks = response.get("event_hooks", []) +print("Current event hooks:", existing_hooks) + +# STEP 2: Add SNS hook while preserving existing hooks +new_sns_hook = { + "enabled": True, + "hook_type": "sns", + "sns_topic_arn": "arn:aws:sns:us-east-1:123456789012:sns-topic", + "sns_region": "us-east-1", + "sns_auth_type": "keys", # or "resource" for role-based auth + "sns_key": "yourkey", + "sns_secret": "yoursecret", + "event_types": [] # empty array = all events +} + +# STEP 3: Update with complete array including existing hooks +client.update_app_settings( + event_hooks=existing_hooks + [new_sns_hook] +) + +# Test the SNS connection +client.check_sns("yourkey", "yoursecret", "arn:aws:sns:us-east-1:123456789012:sns-topic") +``` + +## Configuration Options + +The following options are available when configuring an SNS event hook: + +| Option | Type | Description | Required | +| ------------- | ------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| id | string | Unique identifier for the event hook | No. If empty, it will generate an ID. | +| enabled | boolean | Boolean flag to enable/disable the hook | Yes | +| hook_type | string | Must be set to `"sns"` | Yes | +| sns_topic_arn | string | The AWS SNS topic ARN | Yes | +| sns_region | string | The AWS region where the SNS topic is located (e.g., "us-east-1") | Yes | +| sns_auth_type | string | Authentication type: `"keys"` for access key/secret or `"resource"` for role-based auth | Yes | +| sns_key | string | AWS access key ID (required if auth_type is "keys") | Yes if using key auth | +| sns_secret | string | AWS secret access key (required if auth_type is "keys") | Yes if using key auth | +| event_types | array | Array of event types this hook should handle | No. Not provided or empty array means subscribe to all existing and future events. | + +## SNS Best practices and Assumptions + +- Set the maximum message size set to 256 KB. + +Messages bigger than the maximum message size will be dropped. diff --git a/docs/webhooks/sqs.md b/docs/webhooks/sqs.md new file mode 100644 index 0000000..1999e73 --- /dev/null +++ b/docs/webhooks/sqs.md @@ -0,0 +1,178 @@ +Stream can send payloads of all events from your application to an [Amazon SQS](https://aws.amazon.com/sqs/) queue you own. + +A chat application with a lot of users generates a lots of events. With a standard Webhook configuration, events are posted to your server and can overwhelm unprepared servers during high-use periods. While the server is out, it will not be able to receive Webhooks and will fail to process them. One way to avoid this issue is to use Stream Chat's support for sending webhooks to Amazon SQS. + +SQS removes the chance of losing data for Chat events by providing a large, scalable bucket that holds events generated by Stream Chat in a queue for your server or other . + +The complete list of supported events is identical to those sent through webhooks and can be found on the [Events](/chat/docs/python/webhook_events/) page. + +## Configuration + +You can configure your SQS queue programmatically using the REST API or an SDK with Server Side Authorization. + +There are 2 ways to configure authentication on your SQS queue: + +1. By providing a key and secret + +2. Or by having Stream's AWS account assume a role on your SQS queue. With this option you omit the key and secret, but instead you set up a resource-based policy to grant Stream SendMessage permission on your SQS queue. The following policy needs to be attached to your queue (replace the value of Resource with the fully qualified ARN of your queue): + +```json +{ + "Sid": "AllowStreamProdAccount", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::185583345998:root" + }, + "Action": "SQS:SendMessage", + "Resource": "arn:aws:sqs:us-west-2:1111111111:customer-sqs-for-stream" +} +``` + +To configure an SQS queue, use the `event_hooks` array and Update App Settings method: + +```python +# Note: Any previously existing hooks not included in event_hooks array will be deleted. +# Get current settings first to preserve your existing configuration. + +# STEP 1: Get current app settings to preserve existing hooks +response = client.get_app_settings() +existing_hooks = response.get("event_hooks", []) +print("Current event hooks:", existing_hooks) + +# STEP 2: Add SQS hook while preserving existing hooks +new_sqs_hook = { + "enabled": True, + "hook_type": "sqs", + "sqs_queue_url": "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue", + "sqs_region": "us-east-1", + "sqs_auth_type": "keys", # or "resource" for role-based auth + "sqs_key": "yourkey", + "sqs_secret": "yoursecret", + "event_types": [] # empty array = all events +} + +# STEP 3: Update with complete array including existing hooks +client.update_app_settings( + event_hooks=existing_hooks + [new_sqs_hook] +) + +# Test the SQS connection +client.check_sqs("yourkey", "yoursecret", "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue") +``` + +## Configuration Options + +The following options are available when configuring an SQS event hook: + +| Option | Type | Description | Required | +| ------------- | ------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| id | string | Unique identifier for the event hook | No. If empty, it will generate an ID. | +| enabled | boolean | Boolean flag to enable/disable the hook | Yes | +| hook_type | string | Must be set to `"sqs"` | Yes | +| sqs_queue_url | string | The AWS SQS queue URL | Yes | +| sqs_region | string | The AWS region where the SQS queue is located (e.g., "us-east-1") | Yes | +| sqs_auth_type | string | Authentication type: `"keys"` for access key/secret or `"resource"` for role-based auth | Yes | +| sqs_key | string | AWS access key ID (required if auth_type is "keys") | Yes if using key auth | +| sqs_secret | string | AWS secret access key (required if auth_type is "keys") | Yes if using key auth | +| event_types | array | Array of event types this hook should handle | No. Not provided or empty array means subscribe to all existing and future events. | + +## SQS Permissions + +Stream needs the right permissions on your SQS queue to be able to send events to it. If updates are not showing up in your queue add the following permission policy to the queue: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt1459523779000", + "Effect": "Allow", + "Action": [ + "sqs:GetQueueUrl", + "sqs:SendMessage", + "sqs:SendMessageBatch", + "sqs:GetQueueAttributes" + ], + "Resource": ["arn:aws:sqs:region:acc_id:queue_name"] + } + ] +} +``` + +Here's an example list of messages read from your SQS queue: + +
+Response + +```json +{ + "type": "message.new", + "cid": "messaging:fun-d5f396e3-fbaf-469c-9b45-8837b4f75baa", + "message": { + "id": "8bffc454-e1da-4d91-8b88-a87853dfb41c", + "text": "Welcome to the Community!", + "html": "

Welcome to the Community!

\n", + "type": "regular", + "user": { + "id": "tommaso-52ec3a5f-e916-469f-bf54-b53b5247a4b0", + "role": "user", + "created_at": "2020-03-30T07:54:46.207332Z", + "updated_at": "2020-03-30T07:54:46.207719Z", + "banned": false, + "online": false + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": null, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-03-30T07:54:46.277381Z", + "updated_at": "2020-03-30T07:54:46.277382Z", + "mentioned_users": [] + }, + "user": { + "id": "tommaso-52ec3a5f-e916-469f-bf54-b53b5247a4b0", + "role": "user", + "created_at": "2020-03-30T07:54:46.207332Z", + "updated_at": "2020-03-30T07:54:46.207719Z", + "banned": false, + "online": false, + "channel_unread_count": 0, + "channel_last_read_at": "2020-03-30T07:54:46.270208768Z", + "total_unread_count": 0, + "unread_channels": 0, + "unread_count": 0 + }, + "created_at": "2020-03-30T07:54:46.295138Z", + "members": [ + { + "user_id": "thierry-735d0d44-8bf1-40df-81db-fa83363ac790", + "user": { + "id": "tommaso-52ec3a5f-e916-469f-bf54-b53b5247a4b0", + "role": "user", + "created_at": "2020-03-30T07:54:46.207332Z", + "updated_at": "2020-03-30T07:54:46.207719Z", + "banned": false, + "online": false + }, + "created_at": "2020-03-30T07:54:46.255628Z", + "updated_at": "2020-03-30T07:54:46.255628Z" + } + ], + "channel_type": "messaging", + "channel_id": "fun-d5f396e3-fbaf-469c-9b45-8837b4f75baa" +} +``` + +
+ +### SQS Best practices and Assumptions + +- Set the maximum message size set to 256 KB. + +Messages bigger than the maximum message size will be dropped. + +- Set up a dead-letter queue for your main queue. + +This queue will hold the messages that couldn't be processed successfully and is useful for debugging your application. diff --git a/docs/webhooks/webhook_events.md b/docs/webhooks/webhook_events.md new file mode 100644 index 0000000..a87ef06 --- /dev/null +++ b/docs/webhooks/webhook_events.md @@ -0,0 +1,1484 @@ +Below you can find the complete list of events that are sent via webhooks together with the description of the data payload. + +For message and channel events the webhook request body will also include the list of channel members and attach additional information about their read status. For performance reasons, such list is truncated up to 50 members. + +When applicable, the following attributes are included to the event user and to the event members: + +| total_unread_count | the total count of messages across all channels. | +| -------------------- | ------------------------------------------------------- | +| unread_channels | the count of channels with at least one unread message. | +| channel_last_read_at | the last time the channel was marked as read. | +| channel_unread_count | the count of unread messages on this channel | + +### Webhook Event Types + +| Event | Triggered | +| ---------------------------- | ------------------------------------------------------------------- | +| message.new | when a new message is added. | +| message.read | when a user calls mark as read. | +| message.updated | when a message is updated. | +| message.deleted | when a message is deleted. | +| reaction.new | when a message reaction is added. | +| reaction.deleted | when a message reaction deleted | +| reaction.updated | when a reaction is updated. | +| member.added | when a member is added to a channel. | +| member.updated | when a member is updated. | +| member.removed | when a member is removed from a channel. | +| channel.created | when a channel is created. | +| channel.updated | when a channel is updated. | +| channel.muted | when a channel is muted. | +| channel.unmuted | when a channel is unmuted. | +| channel.truncated | when a channel is truncated. | +| channel.deleted | when a channel is deleted. | +| channel.hidden | when a channel is hidden. | +| user.deactivated | when a user is deactivated | +| user.deleted | when a user is deleted | +| user.reactivated | when a user is reactivated | +| user.updated | when a user is updated. | +| user.muted | when a user is muted. | +| user.unmuted | when a user is unmuted. | +| user.banned | when a user is banned. | +| user.messages.deleted | when a user's messages are deleted. | +| user.unbanned | when a user is unbanned. | +| user.flagged | when a user is flagged. | +| user.unread_message_reminder | when the user has at least 1 unread message (see reminders section) | +| export.users.success | when an async users export is successful. | +| export.users.error | when an async users export fails. | +| export.channels.success | when an async channels export is successful. | +| export.channels.success | when an async channels export fails. | +| reminder.created | when a message reminder is created. | +| reminder.updated | when a message reminder is updated. | +| reminder.deleted | when a message reminder is deleted. | +| notification.reminder_due | when a message reminder's due time is reached. | + +### `message.new` + +> [!NOTE] +> Custom message metadata will be included in the _message.new_ webhook event but custom channel data will not be. + + +```json +{ + "type": "message.new", + "cid": "messaging:fun-d5f396e3-fbaf-469c-9b45-8837b4f75baa", + "message": { + "id": "8bffc454-e1da-4d91-8b88-a87853dfb41c", + "text": "67d61fa4-74b6-4e1a-bfe7-589d84b8215b", + "html": "

67d61fa4-74b6-4e1a-bfe7-589d84b8215b

\n", + "type": "regular", + "user": { + "id": "tommaso-52ec3a5f-e916-469f-bf54-b53b5247a4b0", + "role": "user", + "created_at": "2020-03-30T07:54:46.207332Z", + "updated_at": "2020-03-30T07:54:46.207719Z", + "banned": false, + "online": false + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": null, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-03-30T07:54:46.277381Z", + "updated_at": "2020-03-30T07:54:46.277382Z", + "mentioned_users": [] + }, + "user": { + "id": "tommaso-52ec3a5f-e916-469f-bf54-b53b5247a4b0", + "role": "user", + "created_at": "2020-03-30T07:54:46.207332Z", + "updated_at": "2020-03-30T07:54:46.207719Z", + "banned": false, + "online": false, + "channel_unread_count": 0, + "channel_last_read_at": "2020-03-30T07:54:46.270208768Z", + "total_unread_count": 0, + "unread_channels": 0, + "unread_count": 0 + }, + "created_at": "2020-03-30T07:54:46.295138Z", + "members": [ + { + "user_id": "thierry-735d0d44-8bf1-40df-81db-fa83363ac790", + "user": { + "id": "tommaso-52ec3a5f-e916-469f-bf54-b53b5247a4b0", + "role": "user", + "created_at": "2020-03-30T07:54:46.207332Z", + "updated_at": "2020-03-30T07:54:46.207719Z", + "banned": false, + "online": false + }, + "created_at": "2020-03-30T07:54:46.255628Z", + "updated_at": "2020-03-30T07:54:46.255628Z", + "notifications_muted": true //user muted notifications from this channel + } + ], + "channel_type": "messaging", + "channel_id": "fun-d5f396e3-fbaf-469c-9b45-8837b4f75baa", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `message.read` + +```json +{ + "cid": "messaging:fun", + "type": "message.read", + "user": { + "id": "a6e21b36-798b-408a-9cd1-0cf6c372fc7f", + "role": "user", + "created_at": "2019-04-24T08:49:58.170034Z", + "updated_at": "2019-04-24T08:49:59.345304Z", + "last_active": "2019-04-24T08:49:59.344201Z", + "online": true, + "total_unread_count": 0, + "unread_channels": 0, + "unread_count": 0, + "channel_unread_count": 0, + "channel_last_read_at": "2019-04-24T08:49:59.365498Z" + }, + "created_at": "2019-04-24T08:49:59.365489Z", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `message.updated` + +```json +{ + "cid": "messaging:fun", + "type": "message.updated", + "message": { + "id": "93163f53-4174-4be8-90cd-e59bef78da00", + "text": "new stuff", + "html": "

new stuff

\n", + "type": "regular", + "user": { + "id": "75af03a7-fe83-4a2a-a447-9ed4fac2ea36", + "role": "user", + "created_at": "2019-04-24T08:51:26.846395Z", + "updated_at": "2019-04-24T08:51:27.973941Z", + "last_active": "2019-04-24T08:51:27.972713Z", + "online": false + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": null, + "reply_count": 0, + "created_at": "2019-04-24T08:51:28.005691Z", + "updated_at": "2019-04-24T08:51:28.138422Z", + "mentioned_users": [] + "message_text_updated_at": "2024-02-26T10:06:55.302552Z" + }, + "user": { + "id": "75af03a7-fe83-4a2a-a447-9ed4fac2ea36", + "role": "user", + "created_at": "2019-04-24T08:51:26.846395Z", + "updated_at": "2019-04-24T08:51:27.973941Z", + "last_active": "2019-04-24T08:51:27.972713Z", + "online": true, + "channel_unread_count": 1, + "channel_last_read_at": "2019-04-24T08:51:27.994245Z", + "total_unread_count": 2, + "unread_channels": 2, + "unread_count": 2 + }, + "message_update": { + "old_text": "old text", + "change_set": { + "custom": false, + "text": true, + "html": false, + "attachments": false, + "mentioned_user_ids": false, + "quoted_message_id": false, + "silent": false, + "pin": false + } + }, + "created_at": "2019-04-24T10:51:28.142291+02:00", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `message.deleted` + +```json +{ + "cid": "messaging:fun", + "type": "message.deleted", + "message": { + "id": "268d121f-82e0-4de1-8c8b-ef1201efd7a3", + "text": "new stuff", + "html": "

new stuff

\n", + "type": "regular", + "user": { + "id": "76cd8430-2f91-4059-90e5-02dffb910297", + "role": "user", + "created_at": "2019-04-24T09:44:21.390868Z", + "updated_at": "2019-04-24T09:44:22.537305Z", + "last_active": "2019-04-24T09:44:22.535872Z", + "online": false + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reply_count": 0, + "created_at": "2019-04-24T09:44:22.57073Z", + "updated_at": "2019-04-24T09:44:22.717078Z", + "deleted_at": "2019-04-24T09:44:22.730524Z", + "mentioned_users": [] + }, + "created_at": "2019-04-24T09:44:22.733305Z", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `reaction.new` + +```json +{ + "cid": "messaging:fun", + "type": "reaction.new", + "message": { + "id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6", + "text": "new stuff", + "html": "

new stuff

\n", + "type": "regular", + "user": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T09:49:48.300566Z", + "online": false + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6", + "user": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T09:49:48.300566Z", + "online": true + }, + "type": "lol", + "created_at": "2019-04-24T09:49:48.481994Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "lol": 1 + }, + "reply_count": 0, + "created_at": "2019-04-24T09:49:48.334808Z", + "updated_at": "2019-04-24T09:49:48.483028Z", + "mentioned_users": [] + }, + "reaction": { + "message_id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6", + "user": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T09:49:48.300566Z", + "online": true + }, + "type": "lol", + "created_at": "2019-04-24T09:49:48.481994Z" + }, + "user": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T09:49:48.300566Z", + "online": true, + "unread_channels": 2, + "unread_count": 2, + "channel_unread_count": 1, + "channel_last_read_at": "2019-04-24T09:49:48.321138Z", + "total_unread_count": 2 + }, + "created_at": "2019-04-24T09:49:48.488497Z" +} +``` + +### `reaction.deleted` + +```json +{ + "cid": "messaging:fun", + "type": "reaction.deleted", + "message": { + "id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6", + "text": "new stuff", + "html": "

new stuff

\n", + "type": "regular", + "user": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T09:49:48.300566Z", + "online": false + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reply_count": 0, + "created_at": "2019-04-24T09:49:48.334808Z", + "updated_at": "2019-04-24T09:49:48.511631Z", + "mentioned_users": [] + }, + "reaction": { + "message_id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6", + "user": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T11:49:48.497656+02:00", + "online": true + }, + "type": "lol", + "created_at": "2019-04-24T09:49:48.481994Z" + }, + "user": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T11:49:48.497656+02:00", + "online": true, + "total_unread_count": 2, + "unread_channels": 2, + "unread_count": 2, + "channel_unread_count": 1, + "channel_last_read_at": "2019-04-24T09:49:48.321138Z" + }, + "created_at": "2019-04-24T09:49:48.511082Z" +} +``` + +### `member.added` + +```json +{ + "cid": "messaging:fun", + "type": "member.added", + "member": { + "user_id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec", + "user": { + "id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec", + "role": "user", + "created_at": "2019-04-24T09:49:47.149933Z", + "updated_at": "2019-04-24T09:49:47.151159Z", + "online": false + }, + "created_at": "2019-04-24T09:49:48.534412Z", + "updated_at": "2019-04-24T09:49:48.534412Z" + }, + "user": { + "id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec", + "role": "user", + "created_at": "2019-04-24T09:49:47.149933Z", + "updated_at": "2019-04-24T09:49:47.151159Z", + "online": false, + "channel_last_read_at": "2019-04-24T09:49:48.537084Z", + "total_unread_count": 0, + "unread_channels": 0, + "unread_count": 0, + "channel_unread_count": 0 + }, + "created_at": "2019-04-24T09:49:48.537082Z", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `member.updated` + +```json +{ + "cid": "messaging:fun", + "type": "member.updated", + "member": { + "user_id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec", + "user": { + "id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec", + "role": "user", + "created_at": "2019-04-24T09:49:47.149933Z", + "updated_at": "2019-04-24T09:49:47.151159Z", + "online": false + }, + "is_moderator": true, + "created_at": "2019-04-24T09:49:48.534412Z", + "updated_at": "2019-04-24T09:49:48.547034Z" + }, + "user": { + "id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec", + "role": "user", + "created_at": "2019-04-24T09:49:47.149933Z", + "updated_at": "2019-04-24T09:49:47.151159Z", + "online": false, + "total_unread_count": 0, + "unread_channels": 0, + "unread_count": 0, + "channel_unread_count": 0, + "channel_last_read_at": "2019-04-24T09:49:48.549211Z" + }, + "created_at": "2019-04-24T09:49:48.54921Z", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `member.removed` + +```json +{ + "cid": "messaging:fun", + "type": "member.removed", + "user": { + "id": "6585dbbb-3d46-4943-9b14-a645aca11df4", + "role": "user", + "created_at": "2019-03-22T14:22:04.581208Z", + "online": false + }, + "created_at": "2019-03-22T14:22:07.040496Z", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `channel.created` + +```json +{ + "type": "channel.created", + "cid": "messaging:custom-channel-7-channel-chat-1", + "channel_id": "custom-channel-7-channel-chat-1", + "channel_type": "messaging", + "channel": { + "id": "custom-channel-7-channel-chat-1", + "type": "messaging", + "cid": "messaging:custom-channel-7-channel-chat-1", + "created_at": "2024-10-15T10:12:00.378448Z", + "updated_at": "2024-10-15T10:12:00.378448Z", + "created_by": { + "id": "harshini-demo-ajgsteuyrgfhadbfk3858495-5608sdfhcvnmx-krjsqdgffz-hsdgfhsdfgruytghrfdgdfakytfjsdhgvsjdhfsdfgdmv", + "role": "user", + "created_at": "2024-09-09T09:42:59.197933Z", + "updated_at": "2024-09-09T09:42:59.197933Z", + "banned": false, + "online": false + }, + "frozen": false, + "disabled": false, + "members": [ + { + "user_id": "harshini-demo", + "user": { + "id": "harshini-demo", + "role": "admin", + "created_at": "2023-10-23T06:34:21.940631Z", + "updated_at": "2024-09-06T12:10:00.798961Z", + "last_active": "2023-10-23T06:34:23.218561Z", + "banned": false, + "online": false, + "language": "hi", + "last_name": "Jayabalan", + "first_name": "Harshini", + "staff_user": true, + "dashboard_user": true, + "name": "Harshini", + "image": "https://getstream.io/random_png/?name=Harshini" + }, + "status": "member", + "created_at": "2024-10-15T10:12:00.386198Z", + "updated_at": "2024-10-15T10:12:00.386198Z", + "banned": false, + "shadow_banned": false, + "role": "admin", + "channel_role": "channel_member", + "notifications_muted": false, + "Custom": null + } + ], + "member_count": 1, + "config": { + "created_at": "2024-02-07T08:29:49.879703Z", + "updated_at": "2024-09-02T06:04:24.918939Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "quotes": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "custom_events": true, + "push_notifications": true, + "reminders": false, + "mark_messages_pending": false, + "polls": false, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "blocklist_behavior": "flag", + "blocklists": [ + { + "blocklist": "profanity_en_2020_v1", + "behavior": "block" + } + ], + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + }, + { + "name": "mute", + "description": "Mute a user", + "args": "[@username]", + "set": "moderation_set" + }, + { + "name": "ban", + "description": "Ban a user", + "args": "[@username] [text]", + "set": "moderation_set" + }, + { + "name": "unmute", + "description": "Unmute a user", + "args": "[@username]", + "set": "moderation_set" + }, + { + "name": "unban", + "description": "Unban a user", + "args": "[@username]", + "set": "moderation_set" + } + ] + }, + "custom_data2": "test" + }, + "user": { + "id": "harshini-demo-ajgsteuyrgfhadbfk3858495-5608sdfhcvnmx-krjsqdgffz-hsdgfhsdfgruytghrfdgdfakytfjsdhgvsjdhfsdfgdmv", + "role": "user", + "created_at": "2024-09-09T09:42:59.197933Z", + "updated_at": "2024-09-09T09:42:59.197933Z", + "banned": false, + "online": false + }, + "created_at": "2024-10-15T10:12:00.402858186Z", + "members": [ + { + "user_id": "harshini-demo", + "user": { + "id": "harshini-demo", + "role": "admin", + "created_at": "2023-10-23T06:34:21.940631Z", + "updated_at": "2024-09-06T12:10:00.798961Z", + "last_active": "2023-10-23T06:34:23.218561Z", + "banned": false, + "online": false, + "language": "hi", + "dashboard_user": true, + "channel_unread_count": 0, + "unread_channels": 0, + "unread_count": 0, + "unread_threads": 0, + "unread_thread_messages": 0, + "first_name": "Harshini", + "image": "https://getstream.io/random_png/?name=Harshini", + "last_name": "Jayabalan", + "staff_user": true, + "channel_last_read_at": "2024-10-15T10:12:00.395195904Z", + "total_unread_count": 0, + "name": "Harshini" + }, + "status": "member", + "created_at": "2024-10-15T10:12:00.386198Z", + "updated_at": "2024-10-15T10:12:00.386198Z", + "banned": false, + "shadow_banned": false, + "role": "admin", + "channel_role": "channel_member", + "notifications_muted": false, + "Custom": null + } + ], + "request_info": { + "type": "server", + "ip": "117.201.40.78", + "user_agent": "axios/1.6.8", + "sdk": "stream-chat-javascript-client-node-8.31.0" + } +} +``` + +### `channel.updated` + +```json +{ + "cid": "messaging:fun", + "type": "channel.updated", + "channel": { + "cid": "messaging:fun", + "id": "fun", + "type": "messaging", + "last_message_at": "2019-04-24T09:49:48.576202Z", + "created_by": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T09:49:48.497656Z", + "online": true + }, + "created_at": "2019-04-24T09:49:48.180908Z", + "updated_at": "2019-04-24T09:49:48.180908Z", + "frozen": false, + "config": { + "created_at": "2016-08-18T16:42:30.586808Z", + "updated_at": "2016-08-18T16:42:30.586808Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "commands": ["giphy", "flag", "ban", "unban", "mute", "unmute"] + }, + "awesome": "yes" + }, + "created_at": "2019-04-24T09:49:48.594316Z", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `channel.hidden` + +```json +{ + "cid": "messaging:fun", + "type": "channel.hidden", + "channel": { + "cid": "messaging:fun", + "id": "fun", + "type": "messaging", + "last_message_at": "2019-04-24T09:49:48.576202Z", + "created_by": { + "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", + "role": "user", + "created_at": "2019-04-24T09:49:47.158005Z", + "updated_at": "2019-04-24T09:49:48.301933Z", + "last_active": "2019-04-24T09:49:48.497656Z", + "online": true + }, + "created_at": "2019-04-24T09:49:48.180908Z", + "updated_at": "2019-04-24T09:49:48.180908Z", + "frozen": false, + "config": { + "created_at": "2016-08-18T16:42:30.586808Z", + "updated_at": "2016-08-18T16:42:30.586808Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "commands": ["giphy", "flag", "ban", "unban", "mute", "unmute"] + }, + "awesome": "yes" + }, + "created_at": "2019-04-24T09:49:48.594316Z", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `channel.deleted` + +```json +{ + "cid": "messaging:fun", + "type": "channel.deleted", + "channel": { + "cid": "messaging:fun", + "id": "fun", + "type": "messaging", + "created_at": "2019-04-24T09:49:48.180908Z", + "updated_at": "2019-04-24T09:49:48.180908Z", + "deleted_at": "2019-04-24T09:49:48.626704Z", + "frozen": false, + "config": { + "created_at": "2016-08-18T18:42:30.586808+02:00", + "updated_at": "2016-08-18T18:42:30.586808+02:00", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "commands": ["giphy", "flag", "ban", "unban", "mute", "unmute"] + } + }, + "created_at": "2019-04-24T09:49:48.630913Z", + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### `channel.muted` + +```json +{ + "type": "channel.muted", + "user": { + "id": "jose", + "role": "user", + "created_at": "2021-08-20T08:16:15.591073Z", + "updated_at": "2024-02-14T14:12:42.275493Z", + "last_active": "2024-02-26T13:00:34.618485977Z", + "banned": false, + "online": true, + "name": "joselito" + }, + "created_at": "2024-02-26T13:07:04.881181537Z", + "mute": { + "user": { + "id": "jose", + "role": "user", + "created_at": "2021-08-20T08:16:15.591073Z", + "updated_at": "2024-02-14T14:12:42.275493Z", + "banned": false, + "online": true, + "name": "joselito" + }, + "channel": { + "id": "TeamBlue", + "type": "messaging", + "cid": "messaging:TeamBlue", + "last_message_at": "2024-02-22T14:02:39.746554Z", + "created_at": "2022-04-29T12:48:21.589157Z", + "updated_at": "2022-04-29T12:48:21.589157Z", + "created_by": { + "id": "jose", + "role": "user", + "created_at": "2021-08-20T08:16:15.591073Z", + "updated_at": "2024-02-14T14:12:42.275493Z", + "last_active": "2024-02-26T13:00:34.618485977Z", + "banned": false, + "online": true, + "name": "joselito" + }, + "frozen": false, + "disabled": false, + "member_count": 2, + "config": { + "created_at": "2021-10-05T16:07:41.544996Z", + "updated_at": "2024-02-14T16:38:03.417269Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "quotes": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "custom_events": true, + "push_notifications": true, + "reminders": true, + "mark_messages_pending": false, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "blocklist": "profanity_en_2020_v1", + "blocklist_behavior": "block", + "automod_thresholds": {}, + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + }, + { + "name": "ban", + "description": "Ban a user", + "args": "[@username] [text]", + "set": "moderation_set" + }, + { + "name": "unban", + "description": "Unban a user", + "args": "[@username]", + "set": "moderation_set" + }, + { + "name": "mute", + "description": "Mute a user", + "args": "[@username]", + "set": "moderation_set" + }, + { + "name": "unmute", + "description": "Unmute a user", + "args": "[@username]", + "set": "moderation_set" + } + ] + }, + "muted": true, + "team": "blue" + }, + "created_at": "2024-02-26T13:07:04.856723Z", + "updated_at": "2024-02-26T13:07:04.856723Z" + } +} +``` + +### `user.banned` + +```json +{ + "type": "user.banned", + "user": { + "id": "2a653a76-ae41-4608-8092-5ce9adf5e608", + "role": "user", + "created_at": "2020-06-24T14:01:56.934997Z", + "updated_at": "2020-06-24T14:01:56.935431Z", + "banned": false, + "online": false + }, + "reason": "testy mctestify", + "created_by": { + "id": "thierry", + "role": "user", + "created_at": "2020-06-24T14:01:56.184699Z", + "updated_at": "2020-06-24T14:01:56.621791Z", + "banned": false, + "online": true, + "awesome": true + }, + "created_at": "2020-06-24T14:01:56.940165Z", + "expiration": "2020-06-24T16:01:56.93919Z" +} +``` + +### `user.messages.deleted` + +```json +{ + "type": "user.messages.deleted", + "created_at": "2025-05-22T15:04:28.288564946Z", + "cid": "messaging:055e05e7-d0ed-4563-962e-db6b21ce2a5c", + "channel_type": "messaging", + "channel_id": "055e05e7-d0ed-4563-962e-db6b21ce2a5c", + "user": { + "id": "myuserid", + "language": "", + "role": "user", + "teams": [], + "created_at": "2025-05-22T14:52:12.286322Z", + "updated_at": "2025-05-22T14:52:12.286322Z", + "banned": false, + "online": false, + "blocked_user_ids": [], + "channel_last_read_at": "0001-01-01T00:00:00Z", + "total_unread_count": 0, + "unread_channels": 0, + "unread_count": 0, + "unread_threads": 0, + "unread_thread_messages": 0, + "channel_unread_count": 0 + }, + "soft_delete": true, + "hard_delete": false, + "channel_last_message_at": "2025-05-22T15:04:26.41134Z" +} +``` + +```json +{ + "type": "user.messages.deleted", + "created_at": "2025-05-22T15:11:49.761354607Z", + "user": { + "id": "myuserid", + "language": "", + "role": "user", + "teams": [], + "created_at": "2025-05-22T14:52:12.286322Z", + "updated_at": "2025-05-22T14:52:12.286322Z", + "banned": true, + "online": false, + "blocked_user_ids": [] + }, + "soft_delete": true, + "hard_delete": false +} +``` + +### `user.deactivated` + +```json +{ + "type": "user.deactivated", + "user": { + "id": "5f96e5dd-3998-4d0a-ae37-bd77cc67c2ce", + "role": "user", + "created_at": "2020-06-23T10:41:51.322897Z", + "updated_at": "2020-06-23T10:41:51.323291Z", + "banned": false, + "online": false + }, + "created_by_id": "thierry", + "created_at": "2020-06-23T10:41:51.33211Z" +} +``` + +### `user.deleted` + +```json +{ + "type": "user.deleted", + "user": { + "id": "bfdf5075-fe36-4b84-9732-39c09843dfd8", + "role": "user", + "created_at": "2020-06-23T10:48:37.391206Z", + "updated_at": "2020-06-23T10:48:37.39151Z", + "deleted_at": "2020-06-23T10:48:37.394938Z", + "banned": false, + "online": false + }, + "created_at": "2020-06-23T10:48:37.396179Z" +} +``` + +### `user.reactivated` + +```json +{ + "type": "user.reactivated", + "user": { + "id": "dad409c6-424c-4534-9d40-06620dbe47d8", + "role": "user", + "created_at": "2020-06-23T10:49:37.632951Z", + "updated_at": "2020-06-23T10:49:37.633248Z", + "banned": false, + "online": false + }, + "created_by": { + "id": "thierry-edb850ba-9dc2-4299-91f9-73e7066daff2", + "role": "user", + "created_at": "2020-06-23T10:49:36.952668Z", + "updated_at": "2020-06-23T10:49:37.337261Z", + "last_active": "2020-06-23T10:49:37.170582Z", + "banned": false, + "online": true, + "awesome": true + }, + "created_at": "2020-06-23T10:49:37.646512Z" +} +``` + +### `user.unbanned` + +```json +{ + "type": "user.unbanned", + "user": { + "id": "f867-46ed-b9ef-7dfb64f5dae2", + "role": "user", + "created_at": "2020-06-23T10:55:01.361163Z", + "updated_at": "2020-06-23T10:55:01.361762Z", + "banned": true, + "online": false + }, + "created_at": "2020-06-23T10:55:01.373079Z" +} +``` + +### `user.updated` + +```json +{ + "type": "user.updated", + "user": { + "id": "thierry", + "role": "user", + "online": false, + "awesome": true + }, + "created_at": "2019-04-24T12:54:58.956621Z", + "members": [] +} +``` + +### `user.unread_message_reminder` + +```json +{ + "type": "user.unread_message_reminder", + "created_at": "2022-03-25T09:47:42.98920218Z", + "user": { + "id": "thierry", + "role": "user", + "created_at": "2022-03-25T09:47:31.683109Z", + "updated_at": "2022-03-25T09:47:31.683109Z", + "banned": false, + "online": false + }, + "channels": { + "messaging:fun": { + "channel": { + "id": "be5dd20e-c65c-4b2b-b101-bc989753fff5", + "type": "messaging", + "cid": "messaging:fun", + "last_message_at": "2022-03-25T09:47:42.328548Z", + "created_at": "2022-03-25T09:47:31.615993Z", + "updated_at": "2022-03-25T09:47:31.615993Z", + "created_by": { + "id": "tommaso-9564c144-05cc-4cbe-9548-2cc55e948311", + "role": "user", + "created_at": "2022-03-25T09:47:31.614033Z", + "updated_at": "2022-03-25T09:47:31.614033Z", + "banned": false, + "online": false + }, + "frozen": false, + "disabled": false, + "members": [ + { + "user_id": "thierry", + "user": { + "id": "thierry", + "role": "user", + "created_at": "2022-03-25T09:47:31.683109Z", + "updated_at": "2022-03-25T09:47:31.683109Z", + "banned": false, + "online": false + }, + "created_at": "2022-03-25T09:47:31.748936Z", + "updated_at": "2022-03-25T09:47:31.748936Z", + "banned": false, + "shadow_banned": false, + "role": "member", + "channel_role": "channel_member" + }, + { + "user_id": "tommaso", + "user": { + "id": "tommaso", + "role": "user", + "created_at": "2022-03-25T09:47:31.614033Z", + "updated_at": "2022-03-25T09:47:31.614033Z", + "banned": false, + "online": false + }, + "created_at": "2022-03-25T09:47:31.748936Z", + "updated_at": "2022-03-25T09:47:31.748936Z", + "banned": false, + "shadow_banned": false, + "role": "owner", + "channel_role": "channel_member" + } + ], + "member_count": 2, + "config": { + "created_at": "2022-02-02T09:07:58.934909Z", + "updated_at": "2022-03-25T09:47:22.348561Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "quotes": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "custom_events": true, + "push_notifications": true, + "reminders": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "blocklist": "forbidden-words", + "blocklist_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + }, + { + "name": "mute", + "description": "Mute a user", + "args": "[@username]", + "set": "moderation_set" + }, + { + "name": "unmute", + "description": "Unmute a user", + "args": "[@username]", + "set": "moderation_set" + } + ] + } + }, + "messages": [ + { + "id": "aeea313d-3b89-41e3-a2dd-48508164bdf2", + "text": "d594c5d5-b75f-488d-bf9a-129dafd90786", + "html": "

d594c5d5-b75f-488d-bf9a-129dafd90786

\n", + "type": "regular", + "user": { + "id": "tommaso", + "role": "user", + "created_at": "2022-03-25T09:47:31.614033Z", + "updated_at": "2022-03-25T09:47:31.614033Z", + "banned": false, + "online": false + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": null, + "reaction_scores": null, + "reply_count": 0, + "cid": "messaging:fun", + "created_at": "2022-03-25T09:47:42.328548Z", + "updated_at": "2022-03-25T09:47:42.328548Z", + "shadowed": false, + "mentioned_users": [], + "silent": false, + "pinned": false, + "pinned_at": null, + "pinned_by": null, + "pin_expires": null + } + ] + } + } +} +``` + +### `export.users.success` + +```json +{ + "type": "export.users.success", + "created_at": "2025-02-23T21:16:37.747470731Z", + "url": "https://example.com/signed-s3-url", + "task_id": "55e445f2-0987-42a5-ab1f-4c76e896173c", + "started_at": "2025-02-23T21:16:37.625385788Z", + "finished_at": "2025-02-23T21:16:37.747469255Z" +} +``` + +### `export.users.error` + +```json +{ + "type": "export.users.error", + "created_at": "2025-02-23T21:16:37.747470731Z", + "error": "error message", + "task_id": "55e445f2-0987-42a5-ab1f-4c76e896173c", + "started_at": "2025-02-23T21:16:37.625385788Z", + "finished_at": "2025-02-23T21:16:37.747469255Z" +} +``` + +### `export.channels.success` + +```json +{ + "type": "export.channels.success", + "created_at": "2025-02-23T21:16:37.747470731Z", + "url": "https://example.com/signed-s3-url", + "task_id": "55e445f2-0987-42a5-ab1f-4c76e896173c", + "started_at": "2025-02-23T21:16:37.625385788Z", + "finished_at": "2025-02-23T21:16:37.747469255Z" +} +``` + +### `export.channels.error` + +```json +{ + "type": "export.channels.error", + "created_at": "2025-02-23T21:16:37.747470731Z", + "error": "error message", + "task_id": "55e445f2-0987-42a5-ab1f-4c76e896173c", + "started_at": "2025-02-23T21:16:37.625385788Z", + "finished_at": "2025-02-23T21:16:37.747469255Z" +} +``` + +### `reminder.created` + +```json +{ + "type": "reminder.created", + "created_at": "2024-01-15T10:30:00.123456Z", + "received_at": "2024-01-15T10:30:00.124000Z", + "message_id": "msg_12345", + "user_id": "user_67890", + "cid": "messaging:channel_abc123", + "parent_id": "parent_msg_456", + "reminder": { + "remind_at": "2024-01-16T09:00:00.000000Z", + "channel_cid": "messaging:channel_abc123", + "channel": { + "id": "channel_abc123", + "type": "messaging", + "cid": "messaging:channel_abc123", + "created_at": "2024-01-10T08:00:00.000000Z", + "updated_at": "2024-01-15T10:30:00.000000Z" + }, + "message_id": "msg_12345", + "message": { + "id": "msg_12345", + "text": "Don't forget about the meeting tomorrow", + "created_at": "2024-01-15T10:25:00.000000Z", + "updated_at": "2024-01-15T10:25:00.000000Z", + "user": { + "id": "user_67890", + "name": "John Doe", + "created_at": "2024-01-01T00:00:00.000000Z" + } + }, + "user_id": "user_67890", + "user": { + "id": "user_67890", + "name": "John Doe", + "created_at": "2024-01-01T00:00:00.000000Z" + }, + "created_at": "2024-01-15T10:30:00.000000Z", + "updated_at": "2024-01-15T10:30:00.000000Z" + } +} +``` + +### `reminder.updated` + +```json +{ + "type": "reminder.updated", + "created_at": "2024-01-15T11:45:00.123456Z", + "received_at": "2024-01-15T11:45:00.124000Z", + "message_id": "msg_12345", + "user_id": "user_67890", + "cid": "messaging:channel_abc123", + "parent_id": "parent_msg_456", + "reminder": { + "remind_at": "2024-01-16T10:00:00.000000Z", + "channel_cid": "messaging:channel_abc123", + "channel": { + "id": "channel_abc123", + "type": "messaging", + "cid": "messaging:channel_abc123", + "created_at": "2024-01-10T08:00:00.000000Z", + "updated_at": "2024-01-15T11:45:00.000000Z" + }, + "message_id": "msg_12345", + "message": { + "id": "msg_12345", + "text": "Don't forget about the meeting tomorrow", + "created_at": "2024-01-15T10:25:00.000000Z", + "updated_at": "2024-01-15T10:25:00.000000Z", + "user": { + "id": "user_67890", + "name": "John Doe", + "created_at": "2024-01-01T00:00:00.000000Z" + } + }, + "user_id": "user_67890", + "user": { + "id": "user_67890", + "name": "John Doe", + "created_at": "2024-01-01T00:00:00.000000Z" + }, + "created_at": "2024-01-15T10:30:00.000000Z", + "updated_at": "2024-01-15T11:45:00.000000Z" + } +} +``` + +### `reminder.deleted` + +```json +{ + "type": "reminder.deleted", + "created_at": "2024-01-15T12:15:00.123456Z", + "received_at": "2024-01-15T12:15:00.124000Z", + "message_id": "msg_12345", + "user_id": "user_67890", + "cid": "messaging:channel_abc123", + "parent_id": "parent_msg_456", + "reminder": { + "remind_at": "2024-01-16T10:00:00.000000Z", + "channel_cid": "messaging:channel_abc123", + "channel": { + "id": "channel_abc123", + "type": "messaging", + "cid": "messaging:channel_abc123", + "created_at": "2024-01-10T08:00:00.000000Z", + "updated_at": "2024-01-15T11:45:00.000000Z" + }, + "message_id": "msg_12345", + "message": { + "id": "msg_12345", + "text": "Don't forget about the meeting tomorrow", + "created_at": "2024-01-15T10:25:00.000000Z", + "updated_at": "2024-01-15T10:25:00.000000Z", + "user": { + "id": "user_67890", + "name": "John Doe", + "created_at": "2024-01-01T00:00:00.000000Z" + } + }, + "user_id": "user_67890", + "user": { + "id": "user_67890", + "name": "John Doe", + "created_at": "2024-01-01T00:00:00.000000Z" + }, + "created_at": "2024-01-15T10:30:00.000000Z", + "updated_at": "2024-01-15T11:45:00.000000Z" + } +} +``` + +### `notification.reminder_due` + +```json +{ + "type": "notification.reminder_due", + "created_at": "2024-01-16T10:00:00.123456Z", + "received_at": "2024-01-16T10:00:00.124000Z", + "message_id": "msg_12345", + "user_id": "user_67890", + "cid": "messaging:channel_abc123", + "parent_id": "parent_msg_456", + "reminder": { + "remind_at": "2024-01-16T10:00:00.000000Z", + "channel_cid": "messaging:channel_abc123", + "channel": { + "id": "channel_abc123", + "type": "messaging", + "cid": "messaging:channel_abc123", + "created_at": "2024-01-10T08:00:00.000000Z", + "updated_at": "2024-01-15T11:45:00.000000Z" + }, + "message_id": "msg_12345", + "message": { + "id": "msg_12345", + "text": "Don't forget about the meeting tomorrow", + "created_at": "2024-01-15T10:25:00.000000Z", + "updated_at": "2024-01-15T10:25:00.000000Z", + "user": { + "id": "user_67890", + "name": "John Doe", + "created_at": "2024-01-01T00:00:00.000000Z" + } + }, + "user_id": "user_67890", + "user": { + "id": "user_67890", + "name": "John Doe", + "created_at": "2024-01-01T00:00:00.000000Z" + }, + "created_at": "2024-01-15T10:30:00.000000Z", + "updated_at": "2024-01-15T11:45:00.000000Z" + } +} +``` diff --git a/docs/webhooks/webhooks_overview/before_message_send_webhook.md b/docs/webhooks/webhooks_overview/before_message_send_webhook.md new file mode 100644 index 0000000..a8df5ff --- /dev/null +++ b/docs/webhooks/webhooks_overview/before_message_send_webhook.md @@ -0,0 +1,164 @@ +Sometimes you want to have more control over what users are allowed to post and either discard or edit their messages when they do not meet your content guidelines. + +This webhook gets called before the message reaches the API and therefore before it is saved to the channel and observable by other users. This allows you to intercept a message and make sure that inappropriate content will never be displayed to other users. + +## Configuration + +To enable the Before Message Send webhook, configure the `before_message_send_hook_url` in your app settings: + +```python +client.update_app_settings( + before_message_send_hook_url="https://example.com/webhooks/stream/before-message-send", +) +``` + +## Use-cases + +You can use this webhook to enforce any of these rules: + +- Scrub PII from messages (ie. social security numbers, credit cards, etc.) + +- Restrict contact information sharing (ie. discard phone numbers, emails) + +- Validate messages do not include complex words that are best matched using a regular expression (regex) + +## Request format + +Your endpoint will receive a POST request with a JSON encoded body containing the message, user, and channel objects: + +```json +{ + "message": { + "id": "", + "text": "hello, here's my CC information 1234 1234 1234 1234", + "html": "", + "type": "regular", + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": null, + "reaction_scores": null, + "reply_count": 0, + "mentioned_users": [], + "silent": false + }, + "user": { + "id": "17f8ab2c-c7e7-4564-922b-e5450dbe4fe7", + "role": "user", + "banned": false, + "online": false + }, + "channel": { + "cid": "messaging:fun-01dce6d9-c6c8-4b59-8e3c-c31631e1f7c8", + "id": "fun-01dce6d9-c6c8-4b59-8e3c-c31631e1f7c8", + "type": "messaging", + "last_message_at": "2020-06-18T14:10:30.823058Z", + "created_at": "2020-06-18T14:10:29.457799Z", + "updated_at": "2020-06-18T14:10:29.457799Z", + "frozen": false, + "config": { + "created_at": "2020-06-18T14:10:29.494022Z", + "updated_at": "2020-06-18T14:10:29.504722Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [] + } + }, + "request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1" + } +} +``` + +### Request info + +The `request_info` object holds information about the client that performed the request. You can use this as an additional signal for what to do with the message. For example, you may block the message from being sent based on IP. + +When configuring the SDK, you may also set an additional `x-stream-ext` header to be sent with each request. The value of this header is passed along as an `ext` field in the `request_info` . You can use this to pass along information that may be useful, such as device information. Refer to the SDK-specific docs on how to set this header. + +```json +"request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1", + "ext": "device-id=123" +} +``` + +For example, in Javascript, you can set the value like this: + + +The format of the `ext` header is up to you and you may leave it blank if you don't need it. The value is passed as-is, so you can use a simple value, comma-separated key-values, or more structured data, such as JSON. Binary data must be encoded as a string, for example using base64 or hex encoding. + +### Response format + +If you intend to make any change to the message, you should return a JSON encoded response with the same message structure. Please note that not all message fields can be changed, the full list of fields that can be modified is available in the [rewriting messages](/chat/docs/python/before_message_send_webhook/#rewriting-messages/) section. + +### Discarding messages + +Your endpoint can decide to reject the message and return a user message. To do that the endpoint must return a regular message with type set to error. Please note that the HTTP response code should be still 200. + +```json +{ + "message": { + "type": "error", + "text": "this message did not meet our content guidelines" + } +} +``` + +### Rewriting messages + +You can also decide to modify the message, in this case, you should return the updated version of the message and it will overwrite the user input. + +```json +{ + "message": { + "text": "hello, here's my CC information " + } +} +``` + +## Rewritable message fields + +Not all message fields can be rewritten by your hook handler, fields such as created_at or updated_at, for instance, are reserved and can only be set by Stream Chat APIs. Any non-custom field that is not listed here will be ignored and not updated on the final message. + +1. text + +2. i18n + +3. show_in_channel + +4. silent + +5. type + +6. attachments + +7. _any custom field_ + +## Performance considerations + +Your webhook endpoint will be part of the send message transaction, so you should avoid performing any remote calls or potentially slow operations while processing the request. Stream Chat will give your endpoint 1 second of time to reply. If your endpoint is not available (ie. returns a response with status codes 4xx or 5xx) or takes too long, Stream Chat will continue with the execution and save the message as usual. + +To make sure that an outage on the hook does not impact your application, Stream will pause your webhook once it is considered unreachable and it will automatically resume once the webhook is found to be healthy again. + +### Example implementation + +An example of how to configure this webhook can be found in [this repo](https://github.com/GetStream/messagehook). diff --git a/docs/webhooks/webhooks_overview/custom_commands_webhook.md b/docs/webhooks/webhooks_overview/custom_commands_webhook.md new file mode 100644 index 0000000..123c906 --- /dev/null +++ b/docs/webhooks/webhooks_overview/custom_commands_webhook.md @@ -0,0 +1,232 @@ +Stream supports several slash commands out of the box: + +- **/giphy**  query +- **/ban**  @userid reason +- **/unban**  @userid +- **/mute**  @userid +- **/unmute**  @userid + +Additionally, it’s possible to add your own commands. + +By using Custom Commands, you can receive all messages sent using a specific slash command, eg. `/ticket` , in your application. When configured, every slash command message happening in a Stream Chat application will propagate to an endpoint via an HTTP POST request. + +```python +channel.send_event({"text": "/ticket suspicious transaction with id 1234"}, user["id"]) +``` + +Setting up your Custom Command consists of the following steps: + +1. Registering your Custom Command + +2. Configure a Channel Type + +3. Configuring a Custom Action Handler URL + +4. Implement a handler for your Custom Command + +## Registering Custom Commands + +The API provides methods to create, list, get, update, and delete Custom Command definitions. These determine which commands are allowed to be used and how they're presented to the user by providing a description of the command. + +### Command Fields + +| name | type | description | default | optional | +| ----------- | ------ | ------------------------------------------------------ | ------- | -------- | +| name | string | name of the command | - | ✓ | +| description | string | description, shown in commands auto-completion | - | ✓ | +| args | string | arguments help text, shown in commands auto-completion | - | ✓ | +| set | string | set name used for grouping commands | - | ✓ | + +### Creating a Command + +```python +client.create_command(dict( + name="ticket", + description="Create a support ticket", + args="[description]", + set="support_commands_set", +)) +``` + +### List Commands + +You can retrieve the list of all commands defined for your application. + +```python +client.list_commands() +``` + +### Get a Command + +You can retrieve a custom command definition. + +```python +client.get_command("ticket") +``` + +### Edit a Command + +Custom command _description_, _args_ & _set_ can be changed. Only the fields that must change need to be provided, fields that are not provided to this API will remain unchanged. + +```python +client.update_command("ticket", description="Create customer support tickets") +``` + +### Remove a Command + +You can remove a custom command definition. + +```python +client.delete_command("ticket") +``` + +> [!NOTE] +> You cannot delete a custom command if there are any active channel configurations referencing it explicitly. + + +## Configure a Channel Type + +In order to be able to use this command in a channel, we’ll need to create, or update an existing, channel type to include the `ticket` command. + +```python +client.create_channel_type({ + "name": "support-channel-type", + "commands": ["ticket"] +}) +``` + +## Configure a Custom Action URL + +In order to use the defined custom commands, you will first have to set an endpoint URL in the App Settings. + +```python +client.update_app_settings(custom_action_handler_url="https://example.com/webhooks/stream/custom-commands?type={type}") +``` + +> [!NOTE] +> You can use a `{type}` variable substitution in the URL to pass on the name of the command that was triggered. See [Webhooks Overview](/chat/docs/python/webhooks_overview/) for more information on URL configuration + + +## Request format + +Your endpoint will receive a POST request with a JSON encoded body containing: message, user and form_data objects. The form_data object will contain values of the interactions initiated by Attachment. + +```json +{ + "message": { + "id": "xyz", + "text": "/ticket suspicious transaction with id 1234", + "command": "ticket", + "args": "suspicious transaction with id 1234", + "html": "", + "type": "regular", + "cid": "messaging:xyz", + "created_at": "2021-11-16T12:56:59.854Z", + "updated_at": "2021-11-16T12:56:59.854Z", + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": null, + "reaction_scores": null, + "reply_count": 0, + "mentioned_users": [], + "silent": false + }, + "user": { + "id": "17f8ab2c-c7e7-4564-922b-e5450dbe4fe7", + "Custom": { + "name": "jdoe" + }, + "role": "user", + "banned": false, + "online": false + }, + "form_data": { + "action": "submit", + "name": "John Doe", + "email": "john@doe.com" + } +} +``` + +## Response format + +If you intend to make any change to the message, you should return a JSON encoded response with the same message structure. Please note that not all message fields can be changed, the full list of fields that can be modified is available in the **rewriting messages** section. + +## Discarding messages + +Your endpoint can decide to reject the command and return a user message. To do that the endpoint must return a regular message with type set to error. + +```json +{ + "message": { + "type": "error", + "text": "invalid arguments for command /ticket" + } +} +``` + +## Rewriting messages + +You can also decide to modify the message, in that case you return the updated version of the message and it will overwrite the user input. + +```json +{ + "message": { + "text": "Ticket #85736 has been created" + } +} +``` + +Interactions can be initiated either using Attachment actions: + +```json +{ + "message": { + "text": "Ticket #85736 has been created", + "attachments": [ + { + "type": "text", + "actions": [ + { + "name": "action", + "text": "Send", + "style": "primary", + "type": "button", + "value": "submit" + }, + { + "name": "action", + "text": "Cancel", + "style": "default", + "type": "button", + "value": "cancel" + } + } + ] + } +} +``` + +> [!NOTE] +> You can find more information on message rewrite in [Before Message Send Webhook](/chat/docs/python/before_message_send_webhook/) page + + +## Performance considerations + +Your webhook endpoint will be part of the send message transaction, +so you should avoid performing any remote calls or potentially slow +operations while processing the request. Stream Chat will give your +endpoint 1 second of time to reply. If your endpoint is not available +(ie. returns a response with status codes 4xx or 5xx) or takes too long, +Stream Chat will continue with the execution and save the message as +usual. + +To make sure that an outage on the hook does not impact your +application, Stream will pause your webhook once it is considered +unreachable and it will automatically resume once the webhook is found +to be healthy again. + +## Example code + +An example of how to handle incoming Custom Command requests can be found in [this repo](https://github.com/GetStream/customcommand). diff --git a/docs/webhooks/webhooks_overview/webhooks_overview.md b/docs/webhooks/webhooks_overview/webhooks_overview.md new file mode 100644 index 0000000..e9cff35 --- /dev/null +++ b/docs/webhooks/webhooks_overview/webhooks_overview.md @@ -0,0 +1,258 @@ +You can listen to events using webhooks, SQS or SNS. +When setting up a webhook you can specify the exact events you want to receive, or select to receive all events. + +To ensure that a webhook is triggered by Stream you can verify it's signature. +Webhook retries are in place. If you want to ensure an outage in your API never loses an event, it's better to use SQS or SNS for reliability. + +## Quick Start + +Here's how to quickly set up webhooks using the `event_hooks` configuration: + +### Subscribe to Specific Events + +```python +# Subscribe to message.new and message.updated events only +client.update_app_settings( + event_hooks=[ + { + "enabled": True, + "hook_type": "webhook", + "webhook_url": "https://example.com/webhooks/stream/messages", + "event_types": ["message.new", "message.updated"] + } + ] +) +``` + +### Subscribe to All Events + +Use an empty `event_types` array to receive all existing and future events: + +```python +# Subscribe to all events (empty list = all events) +client.update_app_settings( + event_hooks=[ + { + "enabled": True, + "hook_type": "webhook", + "webhook_url": "https://example.com/webhooks/stream/all", + "event_types": [] # empty list = all events + } + ] +) +``` + +> [!NOTE] +> For reliable event delivery, you can also configure [SQS](/chat/docs/python/sqs/) or [SNS](/chat/docs/python/sns/) instead of webhooks. + + +### Debugging webhook requests with NGROK + +The easiest way to debug webhooks is with NGROK. + +1. Start NGROK + +```bash +brew install ngrok +ngrok http 8000 +``` + +2. Update your webhook URL to the NGROK url + +3. Trigger a webhook + +4. Open up the ngrok inspector + + + +### Handling the webhook + +A few guidelines for the webhook handling + +- Webhooks should accept HTTP POST requests with JSON payloads +- Response code should be 2xx +- Webhook should be ready to accept the same call multiple times: in case of network or remote server failure Stream Chat could retry the request +- It's important to validate the signature, so you know the request originated from Stream +- Support HTTP Keep-Alive +- Use HTTPS + +The example below shows how to log the message new and verify the request + +```python +import stream_chat + +client = StreamChat(api_key="STREAM_KEY", api_secret="STREAM_SECRET") + +# Django request +valid = client.verify_webhook(request.body, request.META['HTTP_X_SIGNATURE']) + +# Flask request +valid = client.verify_webhook(request.data, request.headers['X-SIGNATURE']) +``` + +All webhook requests contain these headers: + +| Name | Description | Example | +| ----------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| X-Webhook-Id | Unique ID of the webhook call. This value is consistent between retries and could be used to deduplicate retry calls | 123e4567-e89b-12d3-a456-426614174000 | +| X-Webhook-Attempt | Number of webhook request attempt starting from 1 | 1 | +| X-Api-Key | Your application’s API key. Should be used to validate request signature | a1b23cdefgh4 | +| X-Signature | HMAC signature of the request body. See Signature section | ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb | + +## Webhook types + +In addition to the above there are 3 special webhooks. + +| Type | Description | +| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| Push | Push webhook is useful for triggering push notifications on your end | +| [Before Message Send](/chat/docs/python/before_message_send_webhook/) | Allows you to modify or moderate message content before sending it to the chat for everyone to see | +| [Custom Commands](/chat/docs/python/custom_commands_webhook/) | Reacts to custom /slash commands | + +## Configuration + +### Before Message Send and Custom Commands + +These webhooks continue to use the original configuration method and are **NOT** part of the multi-event hooks system: + +- **Before Message Send**: `before_message_send_hook_url` +- **Custom Commands**: `custom_action_handler_url` + +```python +client.update_app_settings( + before_message_send_hook_url="https://example.com/webhooks/stream/before-message-send", # sets Before Message Send webhook address + custom_action_handler_url="https://example.com/webhooks/stream/custom-commands?type={type}", # sets Custom Commands webhook address +) +``` + +### Push webhook + +The example below shows how to use the push webhooks + +```python +# Note: Any previously existing hooks not included in event_hooks array will be deleted. +# Get current settings first to preserve your existing configuration. + +# STEP 1: Get current app settings to preserve existing hooks +response = client.get_app_settings() +existing_hooks = response.get("event_hooks", []) +print("Current event hooks:", existing_hooks) + +# STEP 2: Add webhook hook while preserving existing hooks +new_webhook_hook = { + "enabled": True, + "hook_type": "webhook", + "webhook_url": "https://example.com/webhooks/stream/push", + "event_types": [] # empty array = all events +} + +# STEP 3: Update with complete array including existing hooks +client.update_app_settings( + event_hooks=existing_hooks + [new_webhook_hook] +) + +# Test the webhook connection +client.check_push("https://example.com/webhooks/stream/push") +``` + +You can also configure specific event types by providing an array of event names instead of an empty array: + +```python +# Configure webhook for specific events only +new_webhook_hook = { + "enabled": True, + "hook_type": "webhook", + "webhook_url": "https://example.com/webhooks/stream/messages", + "event_types": ["message.new", "message.updated", "message.deleted"] # specific events +} +``` + +## Request info + +Some webhooks contain a field `request_info` , which holds information about the client that issued the request. This info is intended as an additional signal that you can use for moderation, fraud detection, or other similar purposes. + +When configuring the SDK, you may also set an additional `x-stream-ext` header to be sent with each request. The value of this header is passed along as an `ext` field in the `request_info` . You can use this to pass along information that may be useful, such as device information. Refer to the SDK-specific docs on how to set this header. + +```json +"request_info": { + "type": "client", + "ip": "86.84.2.2", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0", + "sdk": "stream-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1", + "ext": "device-id=123" +} +``` + +For example, in Javascript, you can set the value like this: + + +The format of the `ext` header is up to you and you may leave it blank if you don't need it. The value is passed as-is, so you can use a simple value, comma-separated key-values, or more structured data, such as JSON. Binary data must be encoded as a string, for example using base64 or hex encoding. + +## Pending Message Options + +You can configure pending message hooks to handle messages that require approval before being sent. The following options are available: + +| Option | Type | Description | Required | +| ------------- | ------ | -------------------------------------------------------------------------- | ------------------------------------ | +| webhook_url | string | The URL where pending message events will be sent | Yes, except for `CALLBACK_MODE_NONE` | +| timeout_ms | number | How long messages should stay pending before being deleted in milliseconds | Yes | +| callback.mode | string | Callback mode ("CALLBACK_MODE_NONE", "CALLBACK_MODE_REST") | Yes | + +You may set up to two pending message hooks per application. Only the first commit to a pending message will succeed; any subsequent commit attempts will return an error, as the message is no longer pending. If multiple hooks specify a `timeout_ms`, the system will use the longest timeout value. + +For more information on configuring pending messages, please refer to the [Pending Messages](/chat/docs/python/pending_messages/) documentation. + +## Restricting access to webhook + +If necessary, you can only expose your webhook service to Stream. This is possible by configuring your network (eg. iptables rules) to drop all incoming traffic that is not coming from our API infrastructure. + +Below you can find the complete list of egress IP addresses that our webhook infrastructure uses. Such list is static and is not changing over time. + +| US-East | ZONE ID | eip | +| ---------- | -------- | ---------------- | +| Primary | use1-az2 | 34.225.10.29/32 | +| Secondary | use1-az4 | 34.198.125.61/32 | +| Tertiary | use1-az3 | 52.22.78.160/32 | +| Quaternary | use1-az6 | 3.215.161.238/32 | + +| EU-west | ZONE ID | eip | +| --------- | -------- | ----------------- | +| Primary | euw1-az3 | 52.212.14.212/32 | +| Secondary | euw1-az1 | 52.17.43.232/32 | +| Tertiary | euw1-az2 | 34.241.110.177/32 | + +| Sydney | ZONE ID | eip | +| --------- | --------- | ----------------- | +| Primary | apse2-az3 | 54.252.193.245/32 | +| Secondary | apse2-az2 | 13.55.254.141/32 | +| Tertiary | apse2-az1 | 3.24.48.104/32 | + +| mumbai | ZONE ID | eip | +| --------- | -------- | ---------------- | +| Primary | aps1-az1 | 65.1.48.87/32 | +| Secondary | aps1-az3 | 15.206.221.25/32 | +| Tertiary | aps1-az2 | 13.233.48.78/32 | + +| Singapore | ZONE ID | eip | +| --------- | --------- | ---------------- | +| Primary | apse1-az2 | 13.229.11.158/32 | +| Secondary | apse1-az1 | 52.74.225.150/32 | +| Tertiary | apse1-az3 | 52.76.180.70/32 | + +| OHIO | ZONE ID | EIP | +| --------- | -------- | ---------------- | +| Primary | use2-az1 | 3.14.163.216/32 | +| Secondary | use2-az2 | 3.15.245.3/32 | +| Tertiary | use2-az3 | 3.141.116.179/32 | + +| CANADA | ZONE ID | EIP | +| --------- | -------- | ---------------- | +| Primary | cac1-az1 | 35.183.141.98/32 | +| Secondary | cac1-az2 | 52.60.71.231/32 | +| Tertiary | cac1-az4 | 3.97.253.35/32 | + +| OREGON | ZONE ID | EIP | +| --------- | -------- | --------------- | +| Primary | usw2-az1 | 52.25.165.25/32 | +| Secondary | usw2-az2 | 44.237.58.11/32 | +| Tertiary | usw2-az3 | 52.10.213.81/32 |