Skip to content

Chat

The chat system is powered by a websocket server. Please read the entire page before implementing anything, as there are some important notes.

The websocket server is located at wss://hackclub.tv/api/chat/ws/:username, where :username is the channel you want to connect to.

You’ll need to provide authentication, which can be done by providing an auth_session cookie, just like the REST API.

Once connected, you must implement a subroutine in your code to send ping messages every about 5 seconds. This is because of Cloudflare limitations.

Messages are sent and received in JSON format. The following message types are supported:

  • session: sent by the server immediately upon connection.

    • received by client:

      {
      "type": "session",
      "viewer": {
      "id": "user_id",
      "username": "your_username"
      },
      "permissions": {
      "canModerate": false
      },
      "moderation": {
      "hasBlockedTerms": false,
      "slowModeSeconds": 0,
      "maxMessageLength": 400
      }
      }

      viewer is null for unauthenticated (grant-only) connections. canModerate is true for channel owners, managers, moderators, and platform admins.

  • chatAccess: sent by the server on connect (for authenticated non-bot users) and whenever a user’s restriction state changes.

    • received by client:

      {
      "type": "chatAccess",
      "canSend": true,
      "restriction": null
      }

      When the user is restricted, canSend is false and restriction contains:

      {
      "type": "timeout",
      "reason": "Timed out by moderator",
      "expiresAt": "2026-01-01T00:00:00.000Z"
      }

      type is either "timeout" or "ban". expiresAt is an ISO 8601 string for timeouts, or null for permanent bans.

  • ping: a ping message to keep the connection alive.

    • sent by client:

      {
      "type": "ping"
      }
    • received by client:

      {
      "type": "pong"
      }
  • message: a chat message.

    • sent by client:

      {
      "type": "message",
      "message": "Hello, world!"
      }
    • received by client (broadcast to all viewers of the channel):

      {
      "type": "message",
      "msgId": "uuid-v4",
      "user": {
      "id": "user_id",
      "username": "user_who_sent_message",
      "pfpUrl": "https://example.com/avatar.png",
      "displayName": "Display Name",
      "isBot": false,
      "isPlatformAdmin": false,
      "channelRole": null
      },
      "message": "Hello, world!"
      }

      channelRole is one of "owner", "manager", "chatModerator", "botModerator", or null. displayName may be undefined for regular users.

  • history: the recent chat history, sent upon connection.

    • received by client:

      {
      "type": "history",
      "messages": [
      {
      "type": "message",
      "msgId": "uuid-v4",
      "user": {
      "id": "user_id",
      "username": "user_who_sent_message",
      "pfpUrl": "https://example.com/avatar.png",
      "displayName": "Display Name",
      "isBot": false,
      "isPlatformAdmin": false,
      "channelRole": null
      },
      "message": "Hello, world!"
      }
      ]
      }

      Up to 100 messages are returned. Each message has the same shape as a received message event.

  • systemMsg: a system notification broadcast to all viewers, e.g. when a user is banned or unbanned.

    • received by client:

      {
      "type": "systemMsg",
      "message": "username was banned."
      }
  • moderationError: sent to the acting client when a message or moderation action is rejected.

    • received by client:

      {
      "type": "moderationError",
      "code": "RATE_LIMIT",
      "message": "You are sending messages too fast.",
      "restriction": null
      }

      restriction is only present (non-null) for TIMED_OUT and BANNED codes, and has the same shape as the restriction field in chatAccess. Possible codes:

    CodeTrigger
    FORBIDDENNot permitted to perform the action
    RATE_LIMITToo many messages in the rate limit window
    SLOW_MODESent before the slow mode cooldown expired
    TIMED_OUTUser is currently timed out
    BANNEDUser is permanently banned
    MESSAGE_TOO_LONGMessage exceeds maxMessageLength
    BLOCKED_TERMMessage contains a blocked term
    INVALID_TARGETModeration target is invalid or does not exist
    INVALID_REQUESTMalformed moderation command
    NOT_FOUNDTarget message not found (delete)

moderation commands are only available to authenticated users with the canModerate permission (owner, manager, chatModerator, botModerator, or platform admin). sending any of these without permission returns a moderationError with code FORBIDDEN.

obviously, role hierarchy is enforced: a chatModerator cannot moderate a manager or owner. Platform admins bypass hierarchy checks entirely.

  • mod:deleteMessage: delete a message from the chat history and broadcast its removal.

    • sent by client:

      {
      "type": "mod:deleteMessage",
      "msgId": "uuid-of-message-to-delete"
      }
    • received by all clients on success:

      {
      "type": "messageDeleted",
      "msgId": "uuid-of-message-to-delete"
      }
  • mod:timeoutUser: temporarily restrict a user from sending messages.

    • sent by client:

      {
      "type": "mod:timeoutUser",
      "targetUserId": "user_id",
      "durationSeconds": 300,
      "reason": "Optional reason"
      }

      durationSeconds is clamped between 10 and 86400 (24 hours). Defaults to 300 if omitted. On success, a systemMsg is broadcast and the target receives a chatAccess update.

  • mod:banUser: permanently ban a user from sending messages.

    • sent by client:

      {
      "type": "mod:banUser",
      "targetUserId": "user_id",
      "reason": "Optional reason"
      }

      On success, a systemMsg is broadcast and the target receives a chatAccess update.

  • mod:liftTimeout / mod:unbanUser: remove an active timeout or ban.

    • sent by client:

      {
      "type": "mod:liftTimeout",
      "targetUserId": "user_id"
      }

      Both types behave identically and remove any active restriction for the target user. On success, a systemMsg is broadcast and the target receives a chatAccess update with canSend: true.

diagram source: devin deepwiki

graph TB
    subgraph "Emoji Processing Pipeline"
        CHAT_MSG["Chat Message"]
        PATTERN_MATCH["Regex :emoji: Pattern"]
        EMOJI_REQUEST["emojiMsg WebSocket"]
        REDIS_LOOKUP["Redis HGET emojis"]
        FUZZY_SEARCH["uFuzzy"]
        EMOJI_RESPONSE["emojiMsgResponse"]
    end

    subgraph "Redis Storage"
        EMOJI_HASH["emojis hash key"]
        EMOJI_PREFIXED["emoji:{name} url"]
        EMOJIS_PREFIXED["emojis:{name} url"]
    end

    CHAT_MSG --> PATTERN_MATCH
    PATTERN_MATCH --":emojiname:"--> EMOJI_REQUEST
    EMOJI_REQUEST --> REDIS_LOOKUP

    REDIS_LOOKUP --> EMOJI_HASH
    REDIS_LOOKUP --> EMOJI_PREFIXED
    REDIS_LOOKUP --> EMOJIS_PREFIXED

    REDIS_LOOKUP --> EMOJI_RESPONSE

    FUZZY_SEARCH --> EMOJI_HASH
    FUZZY_SEARCH --"search results"--> EMOJI_RESPONSE

When a chat message is sent, the server looks for patterns in the format :emojiname: using regex. For each match, it sends a request to the emojiMsg WebSocket.
The server then checks Redis for the emoji URL and returns it.

When a user wants to look up an emoji (by typing :(partial name)), the server uses uFuzzy to find matching emojis in the Redis emojis hash key and returns the results.

Here’s what gets sent on the websocket:

  • emojiMsg: Looks up emojis

    • sent by client:

      {
      "type": "emojiMsg",
      "emojis": ["aga", "yapa", "heavysob", "yay", "yay-bounce"]
      }
    • received by client:

      {
      "type": "emojiMsgResponse",
      "emojis": {
      "aga": "https://emoji.slack-edge.com/aga.png",
      "yapa": "https://emoji.slack-edge.com/yapa.png",
      "heavysob": "https://emoji.slack-edge.com/heavysob.png",
      "yay": "https://emoji.slack-edge.com/yay.png",
      "yay-bounce": "https://emoji.slack-edge.com/yay-bounce.png"
      }
      }
  • emojiSearch: Searches for emojis

    • sent by client:

      {
      "type": "emojiSearch",
      "searchTerm": "aga"
      }
    • received by client:

      {
      "type": "emojiSearchResponse",
      "results": [
      "aga",
      "aga-brick-throw",
      "aga-dance",
      "aga-transparent",
      "a-aga",
      "a-aga-transparent",
      "agaban",
      "agaboing",
      "agabounce",
      "agabusiness"
      ]
      }