Manually Moderating Amazon IVS Chat Messages

recursivecodes

Todd Sharp

Posted on October 21, 2022

Manually Moderating Amazon IVS Chat Messages

In my last post, we learned how to use an AWS Lambda function to moderate Amazon Interactive Video Service (Amazon IVS) chat messages. In that post, we looked at the pros and cons of both automated and manual message moderation, so we won't repeat that in this post. Instead, we'll focus on implementing manual chat moderation which requires a delegated end user (from here on referred to as a chat moderator) with a token granting them specific capabilities that we'll outline below. This chat moderator must manually identify offensive or insensitive messages and/or users that should be disconnected. Our application must also invoke the proper API or SDK methods which result in certain events being published to our connected clients that we can use to replace or remove the offensive content.

In this post, we'll look at two different approaches to chat moderation. The first approach is to delete messages and disconnect users by publishing a message request through the chat WebSocket connection, as outlined in the Chat Messaging API (docs). You can use this approach when the chat moderator for your application is also connected to the chat room. This method requires the chat moderator who sends the request has a token with the DELETE_MESSAGE and/or DISCONNECT_USER capabilities assigned (depending on the action being invoked).

The second approach is to perform the delete operation with one of the AWS SDKs. We'll use the ivschat client (docs) from the AWS SDK for JavaScript v3 in this post, but the operation is available in all of the Amazon IVS Chat SDKs. Feel free to refer to the documentation for your favorite language if you're not a JavaScript user. This approach isn't necessarily more difficult than using the Chat Messaging API, but it does require an authenticated SDK client running on a server (or available in a serverless environment). The AWS SDK approach can be used to moderate messages and disconnect users from a chat moderator who is not currently connected to the chat room via a WebSocket client.

Moderating with the Chat Messaging API

Let's look at the Chat Messaging API first.

Generating a Token

As you may recall from my post a few weeks ago (https://dev.to/aws/adding-chat-to-your-amazon-ivs-live-stream-43i6), in order to establish a connection to an Amazon IVS chat room via WebSockets, each user must have a token that is passed to the call to new WebSocket() along with the chat room endpoint. You can obtain a token via the Amazon IVS console, or via the CLI, but in a production app you'll be using the Amazon IVS SDK's CreateChatTokenCommand (docs) method. That method expects a CreateChatTokenCommandInput object (docs) and returns a CreateChatTokenCommandOutput object (docs). Here's an example of creating a chat token for a regular chat user:

async createToken(chatArn, userId, username) {
  let capabilities = ['SEND_MESSAGE'];
  const params = {
    roomIdentifier: chatArn,
    userId: userId,
    attributes: {},
    capabilities,
    sessionDurationInMinutes: 60,
  };
  const response = await IvsChat.createChatToken(params).promise();
  return response.token;
}
Enter fullscreen mode Exit fullscreen mode

Regular chat users require the SEND_MESSAGE capability, but if we look at the documentation for ChatTokenCapability, we can see two other capabilities that can be assigned to a user: DELETE_MESSAGE and DISCONNECT_USER. These are aptly named - they allow a user to do exactly what you'd expect them to do. Let's modify the createToken() function above to accommodate the creation of tokens for admin users:

async createToken(chatArn, userId, username, isAdmin) {
  let capabilities = ['SEND_MESSAGE'];
  if (isAdmin) {
    capabilities = [
      ...capabilities,
      'DISCONNECT_USER',
      'DELETE_MESSAGE',
    ];
  }
  const params = {
    roomIdentifier: chatArn,
    userId: userId,
    attributes: {},
    capabilities,
    sessionDurationInMinutes: 60,
  };
  const response = await IvsChat.createChatToken(params).promise();
  return response.token;
}
Enter fullscreen mode Exit fullscreen mode

In this updated function, we've added an isAdmin parameter, and if we pass true as that parameter, we'll grant the user the additional capabilities they need in order to moderate chat messages. Your front end will likely use some logic based on the logged in user to determine whether or not the user is an admin (and you should secure the method appropriately to make sure that only the proper user is granted an admin token). Additionally, you'll probably want to use that logic to present a button for the admin user to quickly and easily remove a message.

Deleting an Existing Message

When an end user has the DELETE_MESSAGE capability, they are able to publish a specially formatted message via their chat WebSocket connection to delete a message. Here's the required format:

{
  "Action": "DELETE_MESSAGE",
  "Id": "string",
  "Reason": "string",
  "RequestId": "string"
}
Enter fullscreen mode Exit fullscreen mode

The Id parameter comes from the Id in the offending message when it was originally received, so you'll want to store this somehow. One way to do that is by saving it via a data-* attribute (docs) on the message that you append to your chat container when a new message is received. That might look similar to this:

<div>
  <i class="trash-icon" role="button"></i> 
  <b>Todd</b>: 
  <span class="msg" data-msg-id="I4fme01f7HYM">
    you are not very nice, admin person!
  </span>
</div>
Enter fullscreen mode Exit fullscreen mode

When the admin clicks the trash-icon, the click handler can call a deleteMessage(id) function.

document.getElementById('chat').addEventListener('click', (e) => {
  if (e.target.classList.contains('trash-icon')) {
    const icon = e.target;
    const msgEl = icon.parentElement.querySelector('[data-msg-id]');
    deleteMessage(msgEl.dataset.msgId);
  }
});

const deleteMessage = (id) => {
  const payload = {
    'Action': 'DELETE_MESSAGE',
    'Id': id,
    'Reason': '[removed by moderator]'
  };
  window.chatConnection.send(JSON.stringify(payload));
};
Enter fullscreen mode Exit fullscreen mode

The value passed as the 'Reason' is available to connected clients in the incoming event message (as shown below). This might allow your moderator to pass a custom message to display to the offending user or to replace the existing chat message contents. You can also choose to delete the existing message altogether - it's up to you and your business needs.

After publishing the request to delete the message, a new message will be received with a Type of EVENT and an EventName of aws:DELETE_MESSAGE. This might look like this.

{
  "Type": "EVENT",
  "Id": "1OjMY6p8h4uv",
  "RequestId": "",
  "EventName": "aws:DELETE_MESSAGE",
  "Attributes": {
    "MessageID": "I4fme01f7HYM",
    "Reason": "[removed by moderator]"
  },
  "SendTime": "2022-10-14T20:08:11.258800067Z"
}
Enter fullscreen mode Exit fullscreen mode

Finally, we can modify our onmessage handler on the WebSocket connection to handle this new event and take any necessary action.

window.chatConnection.onmessage = (event) => {
  const data = JSON.parse(event.data);
  const chatEl = document.getElementById('chat');
  if (data.Type === 'MESSAGE') {
    // removed for brevity...
  }
  if (data.Type === 'EVENT') {
    switch (data.EventName) {
      case 'aws:DELETE_MESSAGE':
        // find the existing msg in chat
        const msg = chatEl.querySelector(`[data-msg-id="${data.Attributes.MessageID}"]`);
        // replace the msg contents with the removed reason
        // (could also delete, if necessary)
        msg.innerHTML = data.Attributes.Reason;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Here's how this looks in action:

Manual chat moderation demo

Disconnecting Users

Sometimes we need to take the additional step of banning users who repeatedly post offensive messages in chat rooms. To do this, we can use the DISCONNECT_USER method of the Chat Messaging API. This is very similar to the DELETE_MESSAGE method, and looks like this:

const disconnectUser = (id, reason) => {
  const payload = {
    'Action': 'DISCONNECT_USER',
    'UserId': id,
    'Reason': reason,
  };
  window.chatConnection.send(JSON.stringify(payload));
};
Enter fullscreen mode Exit fullscreen mode

We can modify our onmessage handler to listen for this new event and display a message (if desired) to the user:

if (data.Type === 'EVENT') {
  switch (data.EventName) {
    case 'aws:DELETE_MESSAGE':
      // [removed for brevity]
    case 'aws:DISCONNECT_USER':
      alert(`You have been disconnected by the chat moderator. Message from mod: ${data.Attributes.Reason}.`);
  }
}
Enter fullscreen mode Exit fullscreen mode

The DISONNECT_USER event looks similar to the DELETE_MESSAGE event.

{
  "Type": "EVENT",
  "Id": "b4aYjMZyU8bo",
  "RequestId": "",
  "EventName": "aws:DISCONNECT_USER",
  "Attributes": {
    "Reason": "Repeated offensive messages. Be nice!",
    "UserId": "42fb390e-2fb0-4e4e-b25f-c97f661da5fa"
  },
  "SendTime": "2022-10-17T12:53:12.922376139Z"
}
Enter fullscreen mode Exit fullscreen mode

When the DISCONNECT_USER message is received on a WebSocket, the user is automatically disconnected and is unable to publish new messages. It's up to your application to determine the length of any such ban and perform and implement the necessary logic on the server side to prevent further reconnections by the user.

Moderating with the AWS SDK for JavaScript v3

As mentioned above, we can also use the AWS SDK for JavaScript to handle manual chat moderation. The user performing the SDK operation must have the ivschat:DeleteMessage and ivschat:DisconnectUser IAM permissions. Here are a few functions that can delete existing chat messages with DeleteMessageCommand (docs) and disconnect users with DisconnectUserRequest (docs).

const ivsChat = new AWS.Ivschat({ apiVersion: '2020-07-14', region: 'us-east-1' });

deleteChat = async (msgId, chatArn) => {
  const result = await ivsChat.deleteMessage({
    roomIdentifier: chatArn,
    id: msgId,
    reason: '[removed by moderator]',
  }).promise();
  return result;
};

disconnectUser = async (userId, chatArn, reason) => {
  const result = await ivsChat.disconnectUser({
    roomIdentifier: chatArn,
    userId: msgId,
    reason: reason,
  }).promise();
  return result;
};
Enter fullscreen mode Exit fullscreen mode

Invoking the deleteChat function from the server side via the AWS SDK performs the exact same operation as when it is invoked via WebSockets with the Chat Messaging API, and the message that is published to all connected clients will look identical:

{
  "Type": "EVENT",
  "Id": "1OjMY6p8h4uv",
  "RequestId": "",
  "EventName": "aws:DELETE_MESSAGE",
  "Attributes": {
    "MessageID": "I4fme01f7HYM",
    "Reason": "[removed by moderator]"
  },
  "SendTime": "2022-10-14T20:08:11.258800067Z"
}
Enter fullscreen mode Exit fullscreen mode

The DELETE_MESSAGE event can be handled in the same manner as above. You can replace the offending message or delete it.

Invoking the disconnectUser function via the AWS SDK also performs the same operation as when it's invoked via the Chat Messaging API and results in an event being published to all connected clients and the offending user being disconnected just as with the Chat Messaging API that we saw above. Again, it's left to your application to prevent further connections by the user (if necessary).

{
  "Type": "EVENT",
  "Id": "b4aYjMZyU8bo",
  "RequestId": "",
  "EventName": "aws:DISCONNECT_USER",
  "Attributes": {
    "Reason": "repeated offensive messages. be nice!",
    "UserId": "42fb390e-2fb0-4e4e-b25f-c97f661da5fa"
  },
  "SendTime": "2022-10-17T12:53:12.922376139Z"
}
Enter fullscreen mode Exit fullscreen mode

Summary

In this post we learned how to manually moderate chat messages and disconnect users via the Chat Messaging API and the AWS SDK for JavaScript. If you have any questions, leave a comment or reach out to me on Twitter.

💖 💪 🙅 🚩
recursivecodes
Todd Sharp

Posted on October 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related