drf-directmessages 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- drf_directmessages-0.0.1/PKG-INFO +20 -0
- drf_directmessages-0.0.1/README.md +4 -0
- drf_directmessages-0.0.1/directmessages/__init__.py +11 -0
- drf_directmessages-0.0.1/directmessages/admin.py +8 -0
- drf_directmessages-0.0.1/directmessages/apps.py +14 -0
- drf_directmessages-0.0.1/directmessages/migrations/0001_initial.py +58 -0
- drf_directmessages-0.0.1/directmessages/migrations/__init__.py +0 -0
- drf_directmessages-0.0.1/directmessages/models.py +44 -0
- drf_directmessages-0.0.1/directmessages/serializers.py +85 -0
- drf_directmessages-0.0.1/directmessages/services.py +152 -0
- drf_directmessages-0.0.1/directmessages/signals.py +4 -0
- drf_directmessages-0.0.1/directmessages/urls.py +15 -0
- drf_directmessages-0.0.1/directmessages/views.py +85 -0
- drf_directmessages-0.0.1/pyproject.toml +21 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: drf-directmessages
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: An API allowing users to directly message each other.
|
|
5
|
+
Author: Garreth Cain
|
|
6
|
+
Author-email: garrethccain@gmail.com
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Requires-Dist: django (>=4.2.1,<5.0.0)
|
|
13
|
+
Requires-Dist: djangorestframework (>=3.14.0,<4.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# DirectMessages
|
|
17
|
+
|
|
18
|
+
A small, lightweight and easy to use Rest endpoint to add messaging between
|
|
19
|
+
your users.
|
|
20
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
__version__ = '0.9.7'
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from django import VERSION as DJANGO_VERSION
|
|
5
|
+
if DJANGO_VERSION >= (1, 7):
|
|
6
|
+
default_app_config = 'directmessages.apps.DirectmessagesConfig'
|
|
7
|
+
else:
|
|
8
|
+
from directmessages.apps import populateInbox
|
|
9
|
+
populateInbox()
|
|
10
|
+
except ImportError:
|
|
11
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
Inbox = None
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AppConfig(AppConfig):
|
|
7
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
8
|
+
name = "directmessages"
|
|
9
|
+
|
|
10
|
+
def ready(self):
|
|
11
|
+
# For convenience
|
|
12
|
+
from .services import MessagingService
|
|
13
|
+
global Inbox
|
|
14
|
+
Inbox = MessagingService()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Generated by Django 4.2.1 on 2023-05-13 08:24
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name="Message",
|
|
19
|
+
fields=[
|
|
20
|
+
(
|
|
21
|
+
"id",
|
|
22
|
+
models.BigAutoField(
|
|
23
|
+
auto_created=True,
|
|
24
|
+
primary_key=True,
|
|
25
|
+
serialize=False,
|
|
26
|
+
verbose_name="ID",
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
("content", models.TextField(verbose_name="Content")),
|
|
30
|
+
(
|
|
31
|
+
"sent_at",
|
|
32
|
+
models.DateTimeField(auto_now=True, verbose_name="sent at"),
|
|
33
|
+
),
|
|
34
|
+
(
|
|
35
|
+
"read_at",
|
|
36
|
+
models.DateTimeField(blank=True, null=True, verbose_name="read at"),
|
|
37
|
+
),
|
|
38
|
+
(
|
|
39
|
+
"recipient",
|
|
40
|
+
models.ForeignKey(
|
|
41
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
42
|
+
related_name="received_dm",
|
|
43
|
+
to=settings.AUTH_USER_MODEL,
|
|
44
|
+
verbose_name="Recipient",
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
(
|
|
48
|
+
"sender",
|
|
49
|
+
models.ForeignKey(
|
|
50
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
51
|
+
related_name="sent_dm",
|
|
52
|
+
to=settings.AUTH_USER_MODEL,
|
|
53
|
+
verbose_name="Sender",
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
],
|
|
57
|
+
),
|
|
58
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
from django.contrib.auth import get_user_model
|
|
4
|
+
from django.core.exceptions import ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# from django.contrib.auth.models import User
|
|
8
|
+
|
|
9
|
+
User = get_user_model()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Message(models.Model):
|
|
13
|
+
"""
|
|
14
|
+
A private directmessage
|
|
15
|
+
"""
|
|
16
|
+
content = models.TextField('Content')
|
|
17
|
+
sender = models.ForeignKey(User,
|
|
18
|
+
related_name='sent_dm',
|
|
19
|
+
verbose_name="Sender",
|
|
20
|
+
on_delete=models.CASCADE)
|
|
21
|
+
recipient = models.ForeignKey(User,
|
|
22
|
+
related_name='received_dm',
|
|
23
|
+
verbose_name="Recipient",
|
|
24
|
+
on_delete=models.CASCADE)
|
|
25
|
+
sent_at = models.DateTimeField("sent at", auto_now=True)
|
|
26
|
+
read_at = models.DateTimeField("read at", null=True, blank=True)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def unread(self):
|
|
30
|
+
"""
|
|
31
|
+
returns whether the message was read or not
|
|
32
|
+
"""
|
|
33
|
+
return self.read_at is None
|
|
34
|
+
|
|
35
|
+
def __str__(self):
|
|
36
|
+
return f"{self.id}/{self.sender}/{self.recipient}/{self.content}"
|
|
37
|
+
|
|
38
|
+
def save(self, **kwargs):
|
|
39
|
+
if self.sender == self.recipient:
|
|
40
|
+
raise ValidationError("You can't send messages to yourself")
|
|
41
|
+
|
|
42
|
+
if not self.id:
|
|
43
|
+
self.sent_at = timezone.now()
|
|
44
|
+
super(Message, self).save(**kwargs)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from django.contrib.auth import get_user_model
|
|
2
|
+
|
|
3
|
+
from rest_framework import serializers
|
|
4
|
+
|
|
5
|
+
from .models import Message
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
User = get_user_model()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UnreadMessageSerializer(serializers.ModelSerializer):
|
|
12
|
+
count = serializers.SerializerMethodField()
|
|
13
|
+
|
|
14
|
+
def get_count(self, obj):
|
|
15
|
+
breakpoint()
|
|
16
|
+
return Message.objects.filter(read_at=None, recipient=obj).count()
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
model = User
|
|
20
|
+
fields = (
|
|
21
|
+
"id",
|
|
22
|
+
"count",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConversationSerializer(serializers.ModelSerializer):
|
|
27
|
+
|
|
28
|
+
class Meta:
|
|
29
|
+
model = User
|
|
30
|
+
fields = (
|
|
31
|
+
"id",
|
|
32
|
+
"email",
|
|
33
|
+
"first_name",
|
|
34
|
+
"last_name",
|
|
35
|
+
"is_active",
|
|
36
|
+
"date_joined",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MessageSerializer(serializers.ModelSerializer):
|
|
41
|
+
direction = serializers.SerializerMethodField()
|
|
42
|
+
|
|
43
|
+
def get_direction(self, obj):
|
|
44
|
+
request = self.context.get("request")
|
|
45
|
+
user_id = request.user if request else None
|
|
46
|
+
if type(obj) != Message:
|
|
47
|
+
return ""
|
|
48
|
+
return (
|
|
49
|
+
"in" if user_id == obj.recipient
|
|
50
|
+
else
|
|
51
|
+
"out"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
class Meta:
|
|
55
|
+
model = Message
|
|
56
|
+
fields = (
|
|
57
|
+
"id",
|
|
58
|
+
"sender",
|
|
59
|
+
"recipient",
|
|
60
|
+
"direction",
|
|
61
|
+
"sent_at",
|
|
62
|
+
"read_at",
|
|
63
|
+
"content",
|
|
64
|
+
)
|
|
65
|
+
read_only_fields = (
|
|
66
|
+
"sender",
|
|
67
|
+
"recipient",
|
|
68
|
+
"sent_at",
|
|
69
|
+
"read_at",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MessageSendSerializer(serializers.ModelSerializer):
|
|
74
|
+
|
|
75
|
+
class Meta:
|
|
76
|
+
model = Message
|
|
77
|
+
fields = (
|
|
78
|
+
"id",
|
|
79
|
+
"content",
|
|
80
|
+
)
|
|
81
|
+
read_only_fields = (
|
|
82
|
+
" sender",
|
|
83
|
+
"sent_at",
|
|
84
|
+
"read_at",
|
|
85
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import unicode_literals
|
|
2
|
+
|
|
3
|
+
from .models import Message
|
|
4
|
+
from .signals import message_read, message_sent
|
|
5
|
+
from django.core.exceptions import ValidationError
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
from django.db.models import Q
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MessagingService(object):
|
|
11
|
+
"""
|
|
12
|
+
A object to manage all messages and conversations
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Message creation
|
|
16
|
+
|
|
17
|
+
def send_message(self, sender, recipient, message):
|
|
18
|
+
"""
|
|
19
|
+
Send a new message
|
|
20
|
+
:param sender: user
|
|
21
|
+
:param recipient: user
|
|
22
|
+
:param message: String
|
|
23
|
+
:return: Message and status code
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if sender == recipient:
|
|
27
|
+
raise ValidationError("You can't send messages to yourself.")
|
|
28
|
+
|
|
29
|
+
message = Message(
|
|
30
|
+
sender=sender,
|
|
31
|
+
recipient=recipient,
|
|
32
|
+
content=str(message))
|
|
33
|
+
message.save()
|
|
34
|
+
|
|
35
|
+
message_sent.send(sender=message,
|
|
36
|
+
from_user=message.sender,
|
|
37
|
+
to=message.recipient)
|
|
38
|
+
|
|
39
|
+
# The second value acts as a status value
|
|
40
|
+
return message, 200
|
|
41
|
+
|
|
42
|
+
# Message reading
|
|
43
|
+
def get_unread_messages(self, user):
|
|
44
|
+
"""
|
|
45
|
+
List of unread messages for a specific user
|
|
46
|
+
:param user: user
|
|
47
|
+
:return: messages
|
|
48
|
+
"""
|
|
49
|
+
return Message.objects.all().filter(recipient=user,
|
|
50
|
+
read_at=None).distinct()
|
|
51
|
+
|
|
52
|
+
# Message reading
|
|
53
|
+
def get_unread_message_count(self, user):
|
|
54
|
+
"""
|
|
55
|
+
Count of unread messages for a specific user
|
|
56
|
+
:param user: user
|
|
57
|
+
:return: messages
|
|
58
|
+
"""
|
|
59
|
+
return Message.objects.all().filter(recipient=user,
|
|
60
|
+
read_at=None).count()
|
|
61
|
+
|
|
62
|
+
def read_message(self, message_id):
|
|
63
|
+
"""
|
|
64
|
+
Read specific message
|
|
65
|
+
:param message_id: Integer
|
|
66
|
+
:return: Message Text
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
message = Message.objects.get(id=message_id)
|
|
70
|
+
self.mark_as_read(message)
|
|
71
|
+
return message.content
|
|
72
|
+
except Message.DoesNotExist:
|
|
73
|
+
return ""
|
|
74
|
+
|
|
75
|
+
def read_message_formatted(self, message_id):
|
|
76
|
+
"""
|
|
77
|
+
Read a message in the format <User>: <Message>
|
|
78
|
+
:param message_id: Id
|
|
79
|
+
:return: Formatted Message Text
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
message = Message.objects.get(id=message_id)
|
|
83
|
+
self.mark_as_read(message)
|
|
84
|
+
return f"{message.sender.email}: {message.content}"
|
|
85
|
+
except Message.DoesNotExist:
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
######
|
|
89
|
+
# Conversation management
|
|
90
|
+
######
|
|
91
|
+
def get_conversations(self, user):
|
|
92
|
+
"""
|
|
93
|
+
Lists all conversation-partners for a specific user
|
|
94
|
+
:param user: User
|
|
95
|
+
:return: Conversation list
|
|
96
|
+
"""
|
|
97
|
+
all_conversations = Message.objects.all().filter(
|
|
98
|
+
Q(sender=user) | Q(recipient=user)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
contacts = []
|
|
102
|
+
for conversation in all_conversations:
|
|
103
|
+
if conversation.sender != user:
|
|
104
|
+
contacts.append(conversation.sender)
|
|
105
|
+
elif conversation.recipient != user:
|
|
106
|
+
contacts.append(conversation.recipient)
|
|
107
|
+
|
|
108
|
+
# To abolish duplicates
|
|
109
|
+
return list(set(contacts))
|
|
110
|
+
|
|
111
|
+
def get_conversation(self, user1, user2, limit=None,
|
|
112
|
+
reversed=False, mark_read=False):
|
|
113
|
+
"""
|
|
114
|
+
List of messages between two users
|
|
115
|
+
:param user1: User
|
|
116
|
+
:param user2: User
|
|
117
|
+
:param limit: int
|
|
118
|
+
:param reversed: Boolean - Makes the newest message be at index 0
|
|
119
|
+
:return: messages
|
|
120
|
+
"""
|
|
121
|
+
users = [user1, user2]
|
|
122
|
+
|
|
123
|
+
# Newest message first if it's reversed (index 0)
|
|
124
|
+
order = "-pk" if reversed else "pk"
|
|
125
|
+
conversation = Message.objects.all().filter(
|
|
126
|
+
sender__in=users,
|
|
127
|
+
recipient__in=users).order_by(order)
|
|
128
|
+
|
|
129
|
+
if limit:
|
|
130
|
+
# Limit number of messages to the x newest
|
|
131
|
+
conversation = conversation[:limit]
|
|
132
|
+
|
|
133
|
+
if mark_read:
|
|
134
|
+
for message in conversation:
|
|
135
|
+
# Just to be sure, everything is read
|
|
136
|
+
self.mark_as_read(message)
|
|
137
|
+
|
|
138
|
+
return conversation
|
|
139
|
+
|
|
140
|
+
# Helper methods
|
|
141
|
+
def mark_as_read(self, message):
|
|
142
|
+
"""
|
|
143
|
+
Marks a message as read, if it hasn't been read before
|
|
144
|
+
:param message: Message
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
if message.read_at is None:
|
|
148
|
+
message.read_at = timezone.now()
|
|
149
|
+
message_read.send(sender=message,
|
|
150
|
+
from_user=message.sender,
|
|
151
|
+
to=message.recipient)
|
|
152
|
+
message.save()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
from .views import (
|
|
3
|
+
ConversationListView,
|
|
4
|
+
MessageListView,
|
|
5
|
+
UnreadMessagesView,
|
|
6
|
+
MessageSendView
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
urlpatterns = [
|
|
11
|
+
path("unread/", view=UnreadMessagesView.as_view()),
|
|
12
|
+
path("conversations/", view=ConversationListView.as_view()),
|
|
13
|
+
path("conversations/<int:pk>", view=MessageListView.as_view()),
|
|
14
|
+
path("send/<int:pk>/", view=MessageSendView.as_view()),
|
|
15
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
from django.core.exceptions import ValidationError
|
|
3
|
+
from django.contrib.auth import get_user_model
|
|
4
|
+
|
|
5
|
+
from rest_framework import (
|
|
6
|
+
generics,
|
|
7
|
+
status,
|
|
8
|
+
views)
|
|
9
|
+
from rest_framework.response import Response
|
|
10
|
+
from rest_framework.permissions import IsAuthenticated
|
|
11
|
+
|
|
12
|
+
from .apps import Inbox
|
|
13
|
+
from .models import Message
|
|
14
|
+
from .serializers import (
|
|
15
|
+
ConversationSerializer,
|
|
16
|
+
MessageSendSerializer,
|
|
17
|
+
MessageSerializer,
|
|
18
|
+
UnreadMessageSerializer)
|
|
19
|
+
from .services import MessagingService
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
User = get_user_model()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MessageViewBase:
|
|
26
|
+
permission_classes = [IsAuthenticated,]
|
|
27
|
+
|
|
28
|
+
def get_user(self):
|
|
29
|
+
return self.request.user
|
|
30
|
+
|
|
31
|
+
def get_recipient(self):
|
|
32
|
+
return User.objects.get(id=self.kwargs['pk'])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UnreadMessagesView(MessageViewBase, views.APIView):
|
|
36
|
+
serializer_class = UnreadMessageSerializer
|
|
37
|
+
|
|
38
|
+
def get(self, request):
|
|
39
|
+
user = self.get_user()
|
|
40
|
+
serializer = UnreadMessageSerializer(user)
|
|
41
|
+
return Response(data=serializer.data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConversationListView(MessageViewBase, generics.ListAPIView):
|
|
45
|
+
serializer_class = ConversationSerializer
|
|
46
|
+
|
|
47
|
+
def get_queryset(self):
|
|
48
|
+
return Inbox.get_conversations(self.get_user())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MessageListView(MessageViewBase, generics.ListCreateAPIView):
|
|
52
|
+
serializer_class = MessageSerializer
|
|
53
|
+
|
|
54
|
+
def get_queryset(self):
|
|
55
|
+
user1 = self.get_user()
|
|
56
|
+
user2 = self.kwargs['pk']
|
|
57
|
+
return Inbox.get_conversation(
|
|
58
|
+
user1=user1, user2=user2, mark_read=True).order_by('-sent_at')
|
|
59
|
+
|
|
60
|
+
def create(self, request, *args, **kwargs):
|
|
61
|
+
sender = self.get_user()
|
|
62
|
+
recipient = self.get_recipient()
|
|
63
|
+
content = request.data.get('content')
|
|
64
|
+
_ = Message.objects.create(
|
|
65
|
+
sender=sender, recipient=recipient, content=content)
|
|
66
|
+
return Response(MessageSerializer(
|
|
67
|
+
self.get_queryset(),
|
|
68
|
+
many=True).data, status=status.HTTP_201_CREATED)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MessageSendView(MessageViewBase, generics.CreateAPIView):
|
|
72
|
+
serializer_class = MessageSendSerializer
|
|
73
|
+
|
|
74
|
+
def create(self, request, *args, **kwargs):
|
|
75
|
+
sender = self.get_user()
|
|
76
|
+
recipient = self.get_recipient()
|
|
77
|
+
content = request.data.get('content')
|
|
78
|
+
|
|
79
|
+
ms = MessagingService()
|
|
80
|
+
try:
|
|
81
|
+
ms.send_message(
|
|
82
|
+
sender=sender, recipient=recipient, message=content)
|
|
83
|
+
except ValidationError as e:
|
|
84
|
+
return Response(e, status=status.HTTP_400_BAD_REQUEST)
|
|
85
|
+
return Response(status=status.HTTP_201_CREATED)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "drf-directmessages"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "An API allowing users to directly message each other."
|
|
5
|
+
authors = ["Garreth Cain <garrethccain@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "directmessages"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.9"
|
|
11
|
+
django = "^4.2.1"
|
|
12
|
+
djangorestframework = "^3.14.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[tool.poetry.group.test.dependencies]
|
|
16
|
+
pytest = "^7.3.1"
|
|
17
|
+
requests-mock = "^1.10.0"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["poetry-core"]
|
|
21
|
+
build-backend = "poetry.core.masonry.api"
|