Skip to main content

A small, lightweight REST endpoint for direct messaging between users.

Project description

drf-directmessages

PyPI License Python Django

A small, lightweight Django REST Framework library that adds direct messaging between your users. Drop it into any DRF project, run migrations, and you have a fully functional private messaging API.

Features

  • Send and receive direct messages between users
  • List conversation partners with cursor-based pagination
  • Retrieve all messages in a conversation (inbound messages are auto-marked as read)
  • Total unread message count and per-conversation unread counts
  • Soft-delete messages per user (the other participant still sees them)
  • Django signals for message_sent and message_read events
  • OpenAPI schema support via drf-spectacular
  • Configurable recipient restrictions via DIRECTMESSAGES_ALLOWED_RECIPIENTS
  • Self-messaging prevention (enforced at both model and service layer)

Requirements

Package Version
Python >= 3.10
Django >= 4.2
Django REST Framework >= 3.14

Installation

Install with pip:

pip install drf-directmessages

Or with uv:

uv add drf-directmessages

Add the app to your Django project's INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "rest_framework",
    "directmessages",
]

Run migrations:

python manage.py migrate

Mount the URLs in your project's urls.py:

from django.urls import include, path

urlpatterns = [
    # ...
    path("api/dm/", include("directmessages.urls")),
]

Configuration

DIRECTMESSAGES_ALLOWED_RECIPIENTS

An optional list of user IDs that act as a messaging whitelist. When set:

  • Users in the list can message anyone.
  • Users not in the list can only message users in the list.
  • If the setting is None or an empty list (the default), there are no restrictions.
# settings.py
DIRECTMESSAGES_ALLOWED_RECIPIENTS = [1, 2, 3]  # Only these users can be messaged by anyone

Default: None (no restrictions)

API Endpoints

All endpoints require authentication. The table below assumes you mounted the URLs at api/dm/.

Method Path Description
GET api/dm/unread/ Get your user ID and total unread message count
GET api/dm/conversations/ List your conversation partners (paginated)
GET api/dm/conversations/unread/ Get unread counts per conversation partner
GET api/dm/conversations/<id>/ List messages with user <id> (paginated, auto-read)
POST api/dm/conversations/<id>/ Send a message to user <id>
DELETE api/dm/messages/<id>/ Soft-delete a message for yourself
POST api/dm/send/<id>/ Send a message to user <id>

All list endpoints use cursor-based pagination with a page size of 50.

How-To Guides

How to send a message

You can send a message in two ways: via the dedicated send endpoint or from within a conversation.

Using the send endpoint:

curl -X POST http://localhost:8000/api/dm/send/2/ \
  -H "Authorization: Token your-token-here" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hello, world!"}'
import requests

response = requests.post(
    "http://localhost:8000/api/dm/send/2/",
    headers={"Authorization": "Token your-token-here"},
    json={"content": "Hello, world!"},
)
print(response.status_code)  # 201
print(response.json())

Response (201 Created):

{
  "id": 1,
  "sender": 1,
  "recipient": 2,
  "direction": "out",
  "sent_at": "2025-06-06T12:00:00Z",
  "read_at": null,
  "content": "Hello, world!"
}

From within a conversation:

curl -X POST http://localhost:8000/api/dm/conversations/2/ \
  -H "Authorization: Token your-token-here" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hello again!"}'

This returns the full updated conversation (201 Created) rather than a single message.

How to list your conversations

curl http://localhost:8000/api/dm/conversations/ \
  -H "Authorization: Token your-token-here"
import requests

response = requests.get(
    "http://localhost:8000/api/dm/conversations/",
    headers={"Authorization": "Token your-token-here"},
)
print(response.json())

Response:

{
  "next": "http://localhost:8000/api/dm/conversations/?cursor=cj0xJnA9Mg%3D%3D",
  "previous": null,
  "results": [
    {
      "id": 2,
      "username": "jane",
      "first_name": "Jane",
      "last_name": "Doe"
    }
  ]
}

How to get messages in a conversation

Retrieving messages automatically marks inbound messages as read.

curl http://localhost:8000/api/dm/conversations/2/ \
  -H "Authorization: Token your-token-here"
import requests

response = requests.get(
    "http://localhost:8000/api/dm/conversations/2/",
    headers={"Authorization": "Token your-token-here"},
)
print(response.json())

Response:

{
  "next": null,
  "previous": null,
  "results": [
    {
      "id": 1,
      "sender": 2,
      "recipient": 1,
      "direction": "in",
      "sent_at": "2025-06-06T12:00:00Z",
      "read_at": "2025-06-06T12:01:00Z",
      "content": "Hey there!"
    },
    {
      "id": 2,
      "sender": 1,
      "recipient": 2,
      "direction": "out",
      "sent_at": "2025-06-06T12:02:00Z",
      "read_at": null,
      "content": "Hello, world!"
    }
  ]
}

The direction field is relative to the authenticated user: "in" for messages you received, "out" for messages you sent.

How to check unread message counts

Total unread count:

curl http://localhost:8000/api/dm/unread/ \
  -H "Authorization: Token your-token-here"
import requests

response = requests.get(
    "http://localhost:8000/api/dm/unread/",
    headers={"Authorization": "Token your-token-here"},
)
print(response.json())

Response:

{
  "id": 1,
  "count": 5
}

Per-conversation unread counts:

curl http://localhost:8000/api/dm/conversations/unread/ \
  -H "Authorization: Token your-token-here"
import requests

response = requests.get(
    "http://localhost:8000/api/dm/conversations/unread/",
    headers={"Authorization": "Token your-token-here"},
)
print(response.json())

Response:

[
  {
    "partner_id": 2,
    "partner_username": "jane",
    "unread_count": 3
  },
  {
    "partner_id": 5,
    "partner_username": "bob",
    "unread_count": 1
  }
]

How to delete a message

Deleting a message is a soft-delete that only hides it for the requesting user. The other participant still sees the message.

curl -X DELETE http://localhost:8000/api/dm/messages/42/ \
  -H "Authorization: Token your-token-here"
import requests

response = requests.delete(
    "http://localhost:8000/api/dm/messages/42/",
    headers={"Authorization": "Token your-token-here"},
)
print(response.status_code)  # 204

A successful deletion returns 204 No Content with an empty body. If the message doesn't exist or doesn't belong to you, you'll get a 404.

How to use signals

drf-directmessages fires two Django signals that you can connect to for notifications, analytics, real-time updates, or any other side effects.

message_sent — fired when a new message is created.

message_read — fired when an unread message is marked as read.

Both signals provide from_user (the sender of the message) and to (the recipient of the message). The signal sender is the Message instance.

# your_app/signals.py
from django.dispatch import receiver
from directmessages.signals import message_sent, message_read


@receiver(message_sent)
def on_message_sent(sender, **kwargs):
    from_user = kwargs["from_user"]
    to = kwargs["to"]
    # sender is the Message instance
    # e.g. send a push notification, update a feed, log analytics
    print(f"Message {sender.id} sent from {from_user} to {to}")


@receiver(message_read)
def on_message_read(sender, **kwargs):
    from_user = kwargs["from_user"]
    to = kwargs["to"]
    # e.g. notify the sender that their message was read
    print(f"Message {sender.id} from {from_user} read by {to}")

How to restrict who can message whom

Use DIRECTMESSAGES_ALLOWED_RECIPIENTS in your Django settings to control which users can receive messages from unrestricted users.

# settings.py

# Only users 1, 2, and 3 can receive messages from anyone.
# All other users can only message users in this list.
DIRECTMESSAGES_ALLOWED_RECIPIENTS = [1, 2, 3]

Common use cases:

  • Support staff: list support agent IDs so customers can only message agents.
  • Admin-only: restrict to admin users for a moderated messaging system.
  • Unset (default): anyone can message anyone.

How to integrate with drf-spectacular

All views include @extend_schema decorators. To serve interactive API docs, add drf-spectacular to your project:

pip install drf-spectacular
# settings.py
INSTALLED_APPS = [
    # ...
    "drf_spectacular",
]

REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

SPECTACULAR_SETTINGS = {
    "TITLE": "My API",
    "VERSION": "1.0.0",
}
# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

urlpatterns = [
    # ...
    path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
    path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="docs"),
]

Signals Reference

Signal Sender Keyword Arguments Fired When
message_sent Message instance from_user (User), to (User) A message is created
message_read Message instance from_user (User), to (User) An unread message is marked as read

Running Tests

Clone the repository and run the test suite:

git clone https://github.com/garrethcain/drf-directmessages.git
cd drf-directmessages
uv sync --group dev
uv run pytest

License

This project is licensed under the GNU Lesser General Public License v3.0 or later.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

drf_directmessages-0.10.0.tar.gz (59.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drf_directmessages-0.10.0-py3-none-any.whl (15.5 kB view details)

Uploaded Python 3

File details

Details for the file drf_directmessages-0.10.0.tar.gz.

File metadata

  • Download URL: drf_directmessages-0.10.0.tar.gz
  • Upload date:
  • Size: 59.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for drf_directmessages-0.10.0.tar.gz
Algorithm Hash digest
SHA256 58ac266f133126b9a570f7660beb4f8c23da844632e5b73f715257739d716e08
MD5 cf9cfb04faabdec46c80a63b62ecc44f
BLAKE2b-256 38f1b18a225b395e6af4a3084e8e42c4f5aacffe5e559293e835b6f6889068ab

See more details on using hashes here.

File details

Details for the file drf_directmessages-0.10.0-py3-none-any.whl.

File metadata

  • Download URL: drf_directmessages-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 15.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for drf_directmessages-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 44846006c09b5fec4fb1dba296cd093d6c9654ba6ef16bab6f34d4ffa21a949d
MD5 30bc740eab0e90046f02b1f9fb262b60
BLAKE2b-256 5f2bce6abdde10b368ab281752cb4b4c41ddd89e4d9ebe9bcf466ed6bf021f27

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page