Flag, Mute & Ban

Overview

Stream provides built-in user actions for moderation that allow users and moderators to flag inappropriate content, mute disruptive users, and ban users who violate community guidelines. These actions integrate seamlessly with the moderation review queue and dashboard.

Flag

Flagging allows any user to report a message or user for review. Flagged content is automatically added to your moderation review queue on the Stream Dashboard, where moderators can take appropriate action.

Flag a Message

// Flag a message
const flag = await client.flagMessage(messageId);

// Flag with a reason and custom data
const flag = await client.flagMessage(messageId, {
  reason: "spam",
  custom: {
    user_comment: "This user is spamming the channel.",
  },
});

Flag a User

// Flag a user
const flag = await client.flagUser(userId);

// Flag with a reason
const flag = await client.flagUser(userId, {
  reason: "harassment",
});

Reasons & Custom Data

You can enhance flags by associating them with a specific reason and custom data. Use a slug or keyword as the reason for easy filtering and translation.

// Flag with a reason
let flag = await client.flagMessage(messageID, {
  reason: "spammy_user",
});

// Flag with a reason and additional custom data
flag = await client.flagMessage(messageID, {
  reason: "spammy_user",
  custom: {
    user_comment: "This user is spamming the homepage.",
    page: "homepage",
  },
});

Query Flagged Content

Query flagged messages using the QueryReviewQueue API endpoint to build your own in-app moderation dashboard.

const response = await client.moderation.queryReviewQueue(
  { entity_type: "stream:chat:v1:message" },
  [{ field: "created_at", direction: -1 }],
  { next: null },
);

for (const item of response.items) {
  console.log(item.message.id);
  console.log(item.message.text);
  console.log(item.created_at);
}

For more details on the review queue, see Query Review Queue.

Flag Content API

The Flag Content API allows you to programmatically flag content for review. Flagged items appear on the Stream dashboard, where moderators can take appropriate action.

await client.moderation.flag(
  "entity_type",
  "entity_id",
  "entity_creator_id",
  "reason for flag",
  {
    custom: {},
    moderation_payload: { text: ["this is shit"] },
    user_id: "user_id", // only for server side usage
  },
);

Request Params

keyrequiredtypedescription
entity_typetruestringThis is identifier for the type of content you are sending for moderation. This helps with categorizing content on dashboard. E.g., if you have multiple products then you can set unique entity_type for content coming from each product. It could be any string.
entity_idtruestringUnique identifier for entity
entity_creator_idtruestringUnique identifier for user who created this entity. Generally this is the user id of the app user.
reasontruestringReason for flagging. This is necessary for any moderator to know the intent
options.moderation_payloadfalseobjectContent to be manually reviewed. This is used to display the content on dashboard for review.
options.customtruestringAny custom properties you may want to attach with this flag
options.user_idfalsestringOnly needed if flag API is being used with server side usage.

Response

keytypedescription
item_idstringId for the Review Queue Item which got created as a result of flagging. You can request the entire review queue object using GetReviewQueueItem endpoint

Flag a User via API

When a user/moderator flags a user, it ends up in "Users" review list.

await client.moderation.flagUser("target_user_id", "spam");

// Internally this method simply calls the flag endpoint as following
// await client.moderation.flag(
//	"stream:user",
//	"target_user_id",
//	"",
//	"spam",
//)

Users Review Queue

Flag a Message via API

await client.moderation.flagMessage("message_id", "spam");

// Internally this method simply calls the flag endpoint as following
// await client.moderation.flag(
//	"stream:chat:v1:message",
//	"message_id",
//	"",
//	"spam",
//)

Text Content Review Queue

Mute

Any user is allowed to mute another user. Mutes are stored at the user level and returned with the rest of the user information when connectUser is called. A user will be muted until the user is unmuted or the mute is expired.

nametypedescriptiondefaultoptional
timeoutnumberThe timeout in minutes until the mute is expired.no limit
client.muteUser("user-id").enqueue { result ->
  if (result.isSuccess) {
    // User was muted
    val mute: Mute = result.data()
  } else {
    // Handle result.error()
  }
}

client.unmuteUser("user-id").enqueue { result ->
  if (result.isSuccess) {
    // User was unmuted
  } else {
    // Handle result.error()
  }
}

After muting a user messages will still be delivered via web-socket. Implementing business logic such as hiding messages from muted users or displaying them differently is left to the developer to implement.

Messages from muted users are not delivered via push (APN/Firebase)

Ban

Users can be banned from an app entirely or from a channel. When a user is banned, they will not be allowed to post messages until the ban is removed or expired but will be able to connect to Chat and to channels as before.

Channel watchers cannot be banned.

It is also possible to ban the user's last known IP address to prevent the creation of new "throw-away" accounts. This type of ban is only applicable on the app level. ISPs often rotate IP addresses, therefore in order to prevent innocent users to be banned, we automatically apply a 30 day timeout when applying an IP ban. This value can be overwritten, if a timeout is supplied in the request. The IP address will be unbanned either after reaching a timeout or with explicit user unban.

It is also possible to delete the user's messages when a ban is applied. If the user is banned from a channel, all their messages in that specific channel can be deleted. Similarly, if the user is banned from an app entirely, all their messages across the app can be deleted. Messages can be deleted in two ways: soft deletion or hard deletion. Soft deletion removes the messages from the client but retains them on the server, making them accessible via server-side export functions. Hard deletion, on the other hand, permanently removes the messages from both the client and the server, ensuring they are no longer retrievable.

In most cases, only admins or moderators are allowed to ban other users from a channel.

nametypedescriptiondefaultoptional
timeoutnumberThe timeout in minutes until the ban is automatically expired.no limit
reasonstringThe reason that the ban was created.
ip_banbooleanWhether or not to apply IP address banfalse
banned_by_idstringThe ID of the user who is performing the ban. This is required only when using API from the server-side
delete_messagesstringDelete messages of the banned user. Can be "soft" or "hard" which soft-deletes and hard-deletes the messages respectively

Banning a user from all channels can only be done using server-side auth.

// Ban user for 60 minutes from a channel
channelClient.banUser(targetId = "user-id", reason = "Bad words", timeout = 60).enqueue { result ->
  if (result.isSuccess) {
    // User was banned
  } else {
    // Handle result.error()
  }
}

channelClient.unBanUser(targetId = "user-id").enqueue { result ->
  if (result.isSuccess) {
    // User was unbanned
  } else {
    // Handle result.error()
  }
}

Query Banned Users

Banned users can be retrieved in different ways:

  1. Using the dedicated query bans endpoint

  2. User Search: you can add the banned: true condition to your search. Please note that this will only return users that were banned at the app level and not the ones that were banned only on channels.

// Retrieve the list of banned users
client.queryUsers(
  QueryUsersRequest(
    filter = Filters.eq("banned", true),
    offset = 0,
    limit = 10,
  )
).enqueue { result ->
  if (result.isSuccess) {
    val users: List<User> = result.data()
  } else {
    // Handle result.error()
  }
}

// Query for banned members from one channel
client.queryBannedUsers(filter = Filters.eq("channel_cid", "ChannelType:ChannelId")).enqueue { result ->
  if (result.isSuccess) {
    val bannedUsers: List<BannedUser> = result.data()
  } else {
    // Handle result.error()
  }
}

Query Bans Endpoint

The query bans endpoint allows you to list bans for your application. Similar to other query endpoints, you can filter bans by different fields and control the ordering of results.

Globally banned users can only be retrieved using server-side auth

Available fields

NameDescriptionExampleOperators
channel_cidThe channel CID for the ban. When this parameter is not provided, both global and channel bans will be returned.{ channel_cid :{$in:["livestream:1","livestream:2"]}}$eq, $in
user_idThe ID of the banned user{ user_id: "evil_user" }$eq, $in, $neq, $nin
created_atThe date (RFC339) of the ban creation{ created_at: {$gt: "2020-10-02T15:00:00Z"} }$eq, $gt, $gte, $lt, $lte
banned_by_idThe ID of the user that created the ban{ banned_by_id: "42"}$eq, $in, $neq, $nin

Pagination for bans can be done in two ways: using offset/limit or using the created_at field. Bans are returned in ascending order by default so to get the second page you need to request bans with created_at less than the created_at of the last ban on the first page. Ordering can be reversed using the sort option.

// Get the bans for channel livestream:123 in descending order
client.queryBannedUsers(
  filter = Filters.eq("channel_cid", "livestream:123"),
  sort = QuerySortByField.descByName("createdAt"),
).enqueue { result ->
  if (result.isSuccess) {
    val bannedUsers: List<BannedUser> = result.data()
  } else {
    // Handle result.error()
  }
}

// Get the page of bans which where created before or equal date for the same channel
client.queryBannedUsers(
  filter = Filters.eq("channel_cid", "livestream:123"),
  sort = QuerySortByField.descByName("createdAt"),
  createdAtBeforeOrEqual = Date(),
).enqueue { result ->
  if (result.isSuccess) {
    val bannedUsers: List<BannedUser> = result.data()
  } else {
    // Handle result.error()
  }
}

Shadow Ban

Instead of a default ban, you can shadow ban users from a channel, set of channels, or an entire App. When a user is shadow banned, they will still be allowed to post messages, but any message sent during the ban will only be visible to the author of the message and invisible to other users of the App.

Shadow banning can delay a persistent bad actor from attempting ban evasion techniques by obfuscating the fact that a ban has occurred for them. Since a shadow ban's effectiveness relies on it not being discovered, shadow bans work best in chats with a high volume of messages and fast velocity where the offending user's messages not receiving engagement appears plausible; think livestreams.

Messages from a shadow banned user will include the shadowed: true flag on the message object. This flag is only visible to users other than the shadow banned user, so the offender will not be able to recognize that they are shadow banned even if they use developer tools to inspect responses from the API. You will need to implement UI logic for how your application will handle shadowed messages. Having the client hide these messages for everybody other than the user sending them is a common approach.

nametypedescriptiondefaultoptional
timeoutnumberThe timeout in minutes until the ban is automatically expired.no-limit
reasonstringThe reason that the ban was created.-
ip_banbooleanWhether or not to apply IP address banfalse
banned_by_idstringThe ID of the user who is performing the ban. This is required only when using API from the server-side-
// Shadow ban user for 60 minutes from a channel
channelClient.shadowBanUser(targetId = "user-id", reason = "Bad words", timeout = 60).enqueue { result ->
   if (result.isSuccess) {
     // User was shadow banned
   } else {
     // Handle result.error()
   }
}

channelClient.removeShadowBan("user-id").enqueue { result ->
  if (result.isSuccess) {
    // Shadow ban was removed
  } else {
    // Handle result.error()
  }
}

Administrators can view shadow banned user status in queryChannels(), queryMembers() and queryUsers() .