Messaging plugin¶
dhis2 messaging covers DHIS2's internal messaging surface
(/api/messageConversations). Pairs with the files plugin — a
MESSAGE_ATTACHMENT-domain fileResource uploaded via dhis2 files
resources upload --domain MESSAGE_ATTACHMENT ... can be referenced
from a message by UID.
MCP mirrors the full surface: messaging_list, messaging_get,
messaging_send, messaging_reply, messaging_mark_read,
messaging_mark_unread, messaging_delete.
Scope¶
- Private / direct conversations (user ↔ user, user ↔ group, user ↔ orgUnit). The common case.
- Tickets (feedback, priority, status, assignee) exist as an extra
endpoint family on the OpenAPI spec (
/assign,/priority,/status). Not wired into this plugin — they're workflow-plugin material. Usepost_rawdirectly if you need them.
Send + reply¶
# Send a direct message:
dhis2 messaging send "Pilot rollout" "Please review the attached plan." \
--user YzqyZKXzcxI --user aB3dEf5gH7i
# Attach a previously-uploaded fileResource (send-time only):
dhis2 files resources upload report.pdf --domain MESSAGE_ATTACHMENT # prints the FR uid
dhis2 messaging send "Report" "latest numbers" \
--user YzqyZKXzcxI \
--attachment <fr-uid>
# Reply to the thread:
dhis2 messaging reply <conversation-uid> "thanks — reviewed"
Reply attachment caveat: DHIS2's reply endpoint
(POST /api/messageConversations/{uid}) takes a text/plain body on
v42 — it stores whatever bytes arrive as the message text. Attachments
+ the internal-note flag only work on the initial send call. To
attach a second file after a thread exists, start a new conversation
referencing the earlier one in the subject.
List + inbox filtering¶
# Full inbox (rich table):
dhis2 messaging list
# Unread only (DHIS2 filter syntax):
dhis2 messaging list --filter "read:eq:false"
# Machine-readable:
dhis2 --json messaging list
The CLI table colors the read column (unread bold-yellow) and the
type column (TICKET / SYSTEM / VALIDATION_RESULT in magenta, PRIVATE /
DIRECT in blue).
Read-state + cleanup¶
dhis2 messaging mark-read <uid> [<uid> ...]
dhis2 messaging mark-unread <uid> [<uid> ...]
dhis2 messaging delete <uid> # soft-delete for the calling user only
delete is always soft from the caller's perspective: other participants
of the conversation keep their view. DHIS2 purges fully once every
participant has deleted.
Library API¶
from dhis2w_core.client_context import open_client
from dhis2w_core.profile import profile_from_env
async with open_client(profile_from_env()) as client:
me = await client.system.me()
assert me.id is not None
# Upload an attachment first.
fr = await client.files.upload_file_resource(
b"...",
filename="report.pdf",
domain="MESSAGE_ATTACHMENT",
)
# Send — returns a typed MessageConversation (BUGS.md #17 workaround).
conversation = await client.messaging.send(
subject="Report",
text="Latest numbers attached.",
users=[me.id],
attachments=[fr.id],
)
# Read / reply / mark / delete.
await client.messaging.reply(conversation.id, text="thanks")
await client.messaging.mark_read(conversation.id)
await client.messaging.delete_conversation(conversation.id)
BUGS.md #17 — the Location-header UID dance¶
POST /api/messageConversations returns 201 Created with the new UID
on the Location header, NOT in the JSON envelope. Every other DHIS2
create endpoint carries response.uid inside the body. The accessor
papers over this: send() extracts the UID from Location and GETs the
conversation back, so callers receive a typed MessageConversation
object the way they do from client.files.upload_document. See
BUGS.md #17 for the full repro + upstream-fix preference.
Related wire quirks the accessor handles so callers don't have to:
- Attachments must be
{id}reference objects onsend— bare UID strings produce a 500. Callers passlist[str]; the accessor wraps. - The reply endpoint (
POST /api/messageConversations/{uid}) takestext/plainbody on v42, not JSON — a JSON payload gets stored verbatim as the message text.reply()encodes itstextargument as plain UTF-8 bytes.