minimost.presence

minimost.presence

Real-time presence tracking, typing indicators, read receipts, message reactions, and private channel membership — all backed by the shared presence.db SQLite database.

This module manages all transient shared state in MiniMost. Because MiniMost stores per-user message history in individual SQLite files, a separate shared database is needed for data that all users must see simultaneously: who is typing, who is online, who has read a message, what reactions a message has received, and which users belong to each private channel.

``presence.db`` tables:

Table

Purpose

presence

One row per user: last_seen (epoch) and state (active/idle/ hidden/offline). Updated on every presence heartbeat.

typing

One row per (user, channel) pair. Timestamp is updated on each keystroke. Rows older than 5 s are considered stale.

read_receipts

Permanent record of (channel, msg_ts, reader) triples written when a user calls /mark_read/<channel>.

message_reactions

One row per (channel, msg_ts, emoji, reactor) combination. Toggled atomically by /react/<msg_id>.

private_channels

One row per private channel: name, created_by, and created_ts. The auto-increment id forms the channel identifier used throughout the app ("private:<id>").

private_channel_members

One row per (channel_id, username) pair. Records joined_ts and history_start_ts (the timestamp from which a member can see messages; NULL means from the beginning of the channel).

The tables are created at module import time by _init_tables().

Module-level attributes

presence_bpflask.Blueprint

The Flask Blueprint for presence routes. Registered in minimost.create_app().

PRESENCE_DBstr

Absolute path to the shared presence.db SQLite file.

_VALID_STATESset of str

Allowed presence state values: {"active", "idle", "hidden", "offline"}.

minimost.presence._init_tables()[source]

Create all required tables in presence.db if they do not exist.

Called unconditionally at module import time. Uses CREATE TABLE IF NOT EXISTS throughout, so repeated calls are safe.

Tables created:

  • presence — tracks each user’s last-seen timestamp and state.

  • typing — records when a user was last observed typing in a channel.

  • read_receipts — permanent log of which users have read which messages.

  • message_reactions — stores each (channel, message, emoji, user) reaction tuple.

  • private_channels — one row per private channel with name, creator, and creation timestamp.

  • private_channel_members — one row per (channel_id, username) pair recording membership and join timestamp.

Returns:

None

minimost.presence.typing_start(channel)[source]

Record that the current user is typing in a channel.

Route: POST /typing/<channel>

Does not require the @login_required decorator — if the session is missing, the request is silently dropped (204 No Content) rather than redirecting to the login page. This avoids a redirect loop when the client sends typing notifications for a brief period after a session expiry.

The timestamp is written to the typing table using INSERT OR REPLACE, so the row for (user, channel) is updated in place on each call.

Parameters:

channel (str) – The channel name or DM identifier.

Returns:

Empty body with HTTP 204 No Content.

Return type:

flask.Response

minimost.presence.typing_get(channel)[source]

Return the list of users currently typing in a channel.

Route: GET /typing/<channel>

A user is considered to be “currently typing” if their ts in the typing table is within the last 5 seconds. The current user is excluded from the result so they never see their own typing indicator.

The client polls this endpoint every second and displays a "<user> is typing…" banner in the chat area.

Parameters:

channel (str) – The channel name or DM identifier.

Returns:

JSON array of usernames who are currently typing, e.g. ["alice", "bob"]. Returns [] if the session is missing or no one is typing.

Return type:

flask.Response (application/json)

minimost.presence.presence()[source]

Update the current user’s presence state.

Route: POST /presence

Accepts a JSON body with a state key. Valid values are defined by _VALID_STATES: "active", "idle", "hidden", "offline".

The client sends this request:

  • Immediately when the page’s visibilitychange event fires ("hidden" when the tab goes to the background, "active" when it returns).

  • After detecting 5 minutes of keyboard/mouse inactivity ("idle").

  • On a 30-second heartbeat to keep last_seen fresh.

Silently returns 204 if the session is missing or the state is invalid.

Request body (JSON):

state (str): One of "active", "idle", "hidden", "offline".

Returns:

Empty body with HTTP 204 No Content.

Return type:

flask.Response

minimost.presence.presence_override()[source]

Set or clear the current user’s manual presence override.

Route: POST /presence/override

The account modal lets a user pin how they appear to others regardless of their real activity. Accepts a JSON body with an override key:

  • "active" / "idle" / "offline" — appear Online / Away / Offline.

  • null, "" or "auto" — clear the override (Automatic), so the live activity-derived state is shown again.

Silently returns 204 if the session is missing or the value is invalid.

Returns:

Empty body with HTTP 204 No Content.

Return type:

flask.Response

minimost.presence.presence_override_get()[source]

Return the current user’s manual presence override.

Route: GET /presence/override

Lets the account modal pre-select the active choice after a reload, since overrides persist server-side in presence.db.

Returns:

JSON object {"override": <state-or-null>} where the value is "active", "idle", "offline" or null (Automatic).

Return type:

flask.Response (application/json)

minimost.presence.reset_all_offline() None[source]

Reset every user’s presence to "offline" and clear manual overrides.

Called once at application startup so that stale presence records from a previous server run (e.g. users who were "active" or "hidden" when the server was stopped) do not mislead other users. Any manual presence overrides are cleared too, so a stale “appear online” override can’t keep a logged-out user looking active across a restart.

Returns:

None

minimost.presence.update_presence(user: str, state) None[source]

Write a presence record directly to presence.db.

This function is the shared implementation used by both the /presence HTTP route and the minimost.auth.logout() view (which sets the user’s state to "offline" before clearing the session).

If state is not in _VALID_STATES the function returns immediately without writing anything.

Parameters:
  • user (str) – The username whose presence should be updated.

  • state (str) – New presence state — one of "active", "idle", "hidden", "offline".

Returns:

None

minimost.presence.set_override(user: str, override) None[source]

Set or clear a user’s manual presence override in presence.db.

Used by the /presence/override route and by minimost.auth.logout() (which clears the override on sign-out so a logged-out user can’t keep an “appear online” override).

Unlike update_presence(), this only touches the override column (and last_seen); the live activity-derived state is left intact so that clearing the override falls straight back to the real state.

If override is neither None nor a member of _VALID_OVERRIDES the function returns immediately without writing anything.

Parameters:
  • user (str) – The username whose override should be updated.

  • override"active", "idle", "offline" or None to clear the override (Automatic).

Returns:

None

Route Summary

Method

Path

Handler

POST

/typing/<channel>

minimost.presence.typing_start()

GET

/typing/<channel>

minimost.presence.typing_get()

POST

/presence

minimost.presence.presence()

presence.db Schema

Table

Description

presence

user (PK), last_seen (INTEGER epoch), state (TEXT). One row per user.

typing

user + channel (PK), ts (INTEGER epoch). Rows older than 5 s are considered stale.

read_receipts

channel + msg_ts + reader (PK). Permanent record of who has read which message.

message_reactions

channel + msg_ts + emoji + reactor (PK). One row per user-emoji-message combination.