MessageHandler

MessageHandler is a publish-subscribe queue for secure, live communication between users. It is used for chat, live commenting, and message queues via WebSockets.

MessageHandler example

MessageHandler configuration

The configuration is similar to FormHandler databases:

url:
  messagehandler/simple:
    pattern: /$YAMLURL/simple
    handler: MessageHandler
    kwargs:
      url: sqlite:///$YAMLPATH/messages.db
      table: simple
      columns:
        body: TEXT

This opens a WebSocket connection to /messages. You can send messages to it using:

var url = location.href.replace(/^http/, "ws").replace(/\/[^/]*$/, "/messages");
var ws = new WebSocket(url);
ws.send(JSON.stringify({ _method: "POST", body: "Hello" }));

This will insert a message into the simple table with these columns:

id user timestamp body
Xjs3k user@example.org 2023-01-02T03:04:05 Hello

id, user, timestamp are always present in the table. The rest are specified by columns: in the FormHandler columns syntax.

It also broadcasts the messages to all clients, which you can listen to on the websocket.

ws.onmessage = function (response) {
  const msg = JSON.parse(response.data);
  console.log(msg);
};

When opened, the websocket sends all past message. You can limit it with MessageHandler filters.

MessageHandler filters

By default, MessageHandlers listen to all messages. You can specify arguments like FormHandler filters to restrict the messages received.

For example:

These are only applied when receiving, not sending. To filter or validate sent messages, use MessageHandler prepare

MessageHandler open

To perform tasks when the websocket is opened, use open:. You can write any Python expression using:

To log the user who opened the websocket:

kwargs:
  open: logging.info('MessageHandler opened by %s', handler_current_user)

To ensure the websocket only receives messages where a recipient column has the user ID:

open: args.update(recipient=[handler.current_user.id])

To ensure the websocket only receives messages processed after 2023 Apr:

open: >
  args.update({"timestamp>~": ["2023-04"]})

Note that the values for handler.args must be arrays.

MessageHandler defaults

You can update the default values for messages using the message_default configuration. For example:

kwargs:
  message_default:
    POST:
      to: f'all@example.org'
      length: msg['message'].length
      session: handler.session['id']
    PUT:
      length: msg['message'].length

When a MessageHandler receives any message, it picks the section based on _method. (E.g. if _method is POST, it picks the POST section).

It updates the message based on the keys. Values can be expression that uses msg and handler.

By default, message defaults are set for POST (new messages) for:

message_default:
  POST:
    id: base64.urlsafe_b64encode(uuid.uuid4().bytes).strip(b"=").decode('utf-8')
    user: handler.current_user.get('id', None) if handler.current_user else None
    timestamp: datetime.datetime.utcnow().isoformat()

MessageHandler prepare

To validate modify each message before updating the database, use prepare:. Example:

prepare: >
  msg.update(message='MSG: ' + msg['message']) if msg['_method'] == 'POST' else ''

This prepare: expression updates POST methods with the “message” key by prefixing “MSG: ” in front of it. You can use

For example, to allow only certain users to post to certain topics, use prepare: mymodule.my_prepare(msg, handler) where mymodule.py has:

def my_prepare(msg, handler):
    allowed = valid_users.get(msg['topic'], [])
    if not handler.current_user or handler.current_user['id'] not in allowed:
        raise ValueError(f"You cannot write to {msg['topic']}")

MessageHandler modify

To modify each message after updating the database, use modify:. This changes the message object that is used in alerts and broadcasted.

modify: >
  msg.update(message='MSG: ' + msg['message']) if msg['_method'] == 'POST' else ''

This modify: expression updates POST methods with the “message” key by prefixing “MSG: ” in front of it. You can use

MessageHandler alert

To email users when a message is added, deleted, or modified, use alert:. This section has the same structure as Gramex alerts. It sends an email alert for each message. For example:

alert:
  to: "{{ to }}"
  subject: "Message: {{ body }}"
  html: >
    {% import pandas as pd %}
    {% import time %}
    <p>Message from {{ user }} at {{ pd.to_datetime(time.time() * 1E9).strftime('%d %b %Y %H:%M') }}.</p>
    <p>{{ body }}</p>

All keys in the message (e.g. id, user, timestamp, anything specified in columns) are available to use as Tornado templates.

By default, it sends alerts only for _method: POST. To send messages for other methods, use:

alert:
  methods: [POST, PUT, DELETE]