├── README.md ├── drf_messaging ├── __init__.py ├── admin.py ├── apps.py ├── asgi.py ├── consumers.py ├── exceptions.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_usertechinfo_is_log.py │ ├── 0003_remove_usertechinfo_is_log.py │ ├── 0004_reportedmessages.py │ ├── 0005_auto_20180606_1107.py │ └── __init__.py ├── models.py ├── routing.py ├── serializers.py ├── signals.py ├── tests.py ├── token_auth.py ├── urls.py ├── utils.py ├── validators.py └── views.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # drf-messaging (in development) 2 | Simple async and sync messaging app for Django Rest Framework (Django 2 only) 3 | 4 | Features: 5 | + Facebook-style chat API 6 | + Websocket-based chat 7 | + Words blacklist 8 | + Files attachments 9 | + Firebase Messaging notifycations 10 | 11 | ### Installation: 12 | * Install redis-server 13 | * Install requirements.txt 14 | * Configure location /socket/ on your proxy settings 15 | * Configure your Django settings: 16 | * Rest framework 17 | 18 | ```python 19 | # REST 20 | REST_FRAMEWORK = { 21 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 22 | 'rest_framework.authentication.TokenAuthentication', 23 | ), 24 | 'DEFAULT_PERMISSION_CLASSES': [ 25 | 'rest_framework.permissions.IsAuthenticated', 26 | ], 27 | 'EXCEPTION_HANDLER': 'drf_messaging.exceptions.api_exception_handler', 28 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' 29 | } 30 | ``` 31 | * ASGI and Channels 32 | ```python 33 | # Channels 34 | CHANNEL_LAYERS = { 35 | "default": { 36 | "BACKEND": "channels_redis.core.RedisChannelLayer", 37 | "CONFIG": { 38 | "hosts": [("localhost", 6379)], 39 | }, 40 | }, 41 | } 42 | ASGI_APPLICATION = "drf_messaging.routing.application" 43 | ``` 44 | * Add drf_messaging to your installed apps 45 | ```python 46 | INSTALLED_APPS = [ 47 | ... 48 | 'channels', 49 | 'rest_framework', 50 | 'rest_framework.authtoken', 51 | 52 | ... 53 | 54 | 'drf_messaging' 55 | ] 56 | ``` 57 | -------------------------------------------------------------------------------- /drf_messaging/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'drf_messaging.apps.DRFMessagingConfig' -------------------------------------------------------------------------------- /drf_messaging/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /drf_messaging/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DRFMessagingConfig(AppConfig): 5 | name = 'drf_messaging' 6 | verbose_name = "Messages" 7 | 8 | def ready(self): 9 | from . import signals 10 | -------------------------------------------------------------------------------- /drf_messaging/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI entrypoint. Configures Django and then runs the application 3 | defined in the ASGI_APPLICATION setting. 4 | """ 5 | 6 | import os 7 | import django 8 | from channels.routing import get_default_application 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "yoga.settings") 11 | django.setup() 12 | application = get_default_application() -------------------------------------------------------------------------------- /drf_messaging/consumers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 4 | from channels.db import database_sync_to_async 5 | from django.core.exceptions import ValidationError 6 | from django.contrib.auth.models import User 7 | from .exceptions import ClientError 8 | from .models import Messages, UserTechInfo 9 | from asgiref.sync import AsyncToSync 10 | from .validators import blacklist_validator 11 | 12 | 13 | # import the logging library 14 | import logging 15 | 16 | # Get an instance of a logger 17 | logger = logging.getLogger(__name__) 18 | 19 | class SocketCostumer(AsyncJsonWebsocketConsumer): 20 | 21 | # WebSocket event handlers 22 | 23 | def __init__(self, *args, **kwargs): 24 | super().__init__(*args, **kwargs) 25 | self.chat = None 26 | 27 | async def connect(self): 28 | """ 29 | Called when the websocket is handshaking as part of initial connection. 30 | """ 31 | if self.scope["user"].is_anonymous: 32 | logger.debug("user anon") 33 | await self.close() 34 | else: 35 | logger.debug("user %s accepted" % self.scope["user"].username) 36 | await self.accept() 37 | await self.connect_user() 38 | 39 | async def receive_json(self, content): 40 | """ 41 | Called when we get a text frame. Channels will JSON-decode the payload 42 | for us and pass it as the first argument. 43 | """ 44 | # Messages will have a "command" key we can switch on 45 | command = content.get("command", None) 46 | try: 47 | if command == "join_chat": 48 | if content.get('user_id'): 49 | try: 50 | self.chat = User.objects.get(id=content.get('user_id')).id 51 | await self.send_json({"chat": self.chat}) 52 | except User.DoesNotExist: 53 | await self.send_json({"error": "USER_NOT_FOUND"}) 54 | except: 55 | await self.send_json({"error": "BAD_REQUEST"}) 56 | elif command == "leave_chat": 57 | self.chat = None 58 | await self.send_json({ 59 | "chat": self.chat 60 | }) 61 | elif content.get('message'): 62 | await self.send_message(content.get('message')) 63 | except ClientError as e: 64 | # Catch any errors and send it back 65 | await self.send_json({"error": e.code}) 66 | 67 | async def disconnect(self, code): 68 | """ 69 | Called when the WebSocket closes for any reason. 70 | """ 71 | await self.disconnect_user() 72 | 73 | async def connect_user(self): 74 | """ 75 | Called when user connected to app 76 | """ 77 | user = self.scope["user"] 78 | profile, created = UserTechInfo.objects.get_or_create(user=user) 79 | profile.current_channel = self.channel_name 80 | profile.online = True 81 | profile.save() 82 | await self.send_json( 83 | { 84 | "user": user.username, 85 | }, 86 | ) 87 | 88 | async def chat_message(self, event): 89 | # try: 90 | if event.get('source') == "signals": 91 | logger.debug(str(event)) 92 | if event.get("sender") == "you" or (event.get('receiver') == 'you' and self.chat == event.get('sender')): 93 | response = { 94 | "message": event.get("message"), 95 | "receiver": event.get("receiver"), 96 | "sender": event.get("sender"), 97 | } 98 | elif event.get('receiver') == "you": 99 | # refresh new messages if chat not joined 100 | new_messages = Messages.objects.get_unread(self.scope["user"].id) 101 | response = { 102 | "new_messages": [{ 103 | "sender": message.get('sender'), 104 | "count": message.get('count') 105 | } for message in new_messages] 106 | } 107 | else: 108 | return False 109 | await self.send_json(response) 110 | # except: 111 | # pass 112 | 113 | @database_sync_to_async 114 | def send_message(self, message): 115 | if not self.chat: 116 | AsyncToSync(self.send_json)( 117 | { 118 | 'error': 'NO_CHAT_JOINED' 119 | } 120 | ) 121 | else: 122 | try: 123 | blacklist_validator(message) 124 | Messages.objects.send_message(sender=self.scope['user'], receiver_id=self.chat, message=message) 125 | except ValidationError: 126 | AsyncToSync(self.send_json)( 127 | { 128 | 'error': 'BLACKLISTED_MESSAGE' 129 | } 130 | ) 131 | 132 | @database_sync_to_async 133 | def joined_chat(self): 134 | return self.chat 135 | 136 | @database_sync_to_async 137 | def disconnect_user(self): 138 | try: 139 | profile = UserTechInfo.objects.get(user=self.scope["user"]) 140 | profile.current_channel = "" 141 | profile.online = False 142 | profile.save() 143 | except: 144 | pass 145 | 146 | -------------------------------------------------------------------------------- /drf_messaging/exceptions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import exception_handler 2 | from rest_framework.response import Response 3 | from rest_framework import status 4 | 5 | 6 | class ClientError(Exception): 7 | """ 8 | Custom exception class that is caught by the websocket receive() 9 | handler and translated into a send back to the client. 10 | """ 11 | def __init__(self, code): 12 | super().__init__(code) 13 | self.code = code 14 | 15 | 16 | def api_exception_handler(exc, context): 17 | try: 18 | message = str(exc.args[0]) 19 | code = int(exc.args[1]) 20 | status_dict = { 21 | 400: status.HTTP_400_BAD_REQUEST, 22 | 401: status.HTTP_401_UNAUTHORIZED, 23 | 404: status.HTTP_404_NOT_FOUND, 24 | 500: status.HTTP_500_INTERNAL_SERVER_ERROR 25 | } 26 | response = Response({"error": {"code": code, "message": message}}, status=status_dict[code]) 27 | except: 28 | response = exception_handler(exc, context) 29 | 30 | return response 31 | -------------------------------------------------------------------------------- /drf_messaging/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-05-11 17:04 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='Attachment', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('file', models.FileField(upload_to='attachments/', verbose_name='File')), 22 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 23 | ], 24 | options={ 25 | 'verbose_name_plural': 'Attachments', 26 | 'verbose_name': 'Attachment', 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='BlackList', 31 | fields=[ 32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('word', models.CharField(max_length=200)), 34 | ('regex', models.BooleanField(verbose_name='Regular expression')), 35 | ('enabled', models.BooleanField(default=True)), 36 | ], 37 | options={ 38 | 'verbose_name_plural': 'Black lists', 39 | 'verbose_name': 'Black list', 40 | }, 41 | ), 42 | migrations.CreateModel( 43 | name='Messages', 44 | fields=[ 45 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 46 | ('message', models.TextField()), 47 | ('datetime', models.DateTimeField(auto_now=True)), 48 | ('read', models.BooleanField(default=False)), 49 | ('read_datetime', models.DateTimeField(blank=True, default=None, null=True)), 50 | ('attachments', models.ManyToManyField(blank=True, to='drf_messaging.Attachment')), 51 | ('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_receiver', to=settings.AUTH_USER_MODEL)), 52 | ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_sender', to=settings.AUTH_USER_MODEL)), 53 | ], 54 | options={ 55 | 'verbose_name_plural': 'Messages', 56 | 'verbose_name': 'Message', 57 | }, 58 | ), 59 | migrations.CreateModel( 60 | name='UserTechInfo', 61 | fields=[ 62 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 63 | ('current_channel', models.CharField(blank=True, default='', max_length=500)), 64 | ('online', models.BooleanField(default=False)), 65 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='info', to=settings.AUTH_USER_MODEL)), 66 | ], 67 | ), 68 | migrations.AddIndex( 69 | model_name='messages', 70 | index=models.Index(fields=['sender'], name='drf_messagi_sender__0d641e_idx'), 71 | ), 72 | migrations.AddIndex( 73 | model_name='messages', 74 | index=models.Index(fields=['receiver'], name='drf_messagi_receive_b0a878_idx'), 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /drf_messaging/migrations/0002_usertechinfo_is_log.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-05-11 17:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('drf_messaging', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='usertechinfo', 15 | name='is_log', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /drf_messaging/migrations/0003_remove_usertechinfo_is_log.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-05-11 17:17 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('drf_messaging', '0002_usertechinfo_is_log'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='usertechinfo', 15 | name='is_log', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /drf_messaging/migrations/0004_reportedmessages.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-06-05 18:41 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 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('drf_messaging', '0003_remove_usertechinfo_is_log'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ReportedMessages', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('datetime', models.DateTimeField(auto_now_add=True)), 21 | ('comment', models.CharField(blank=True, max_length=2000, null=True)), 22 | ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='drf_messaging.Messages')), 23 | ('reporter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /drf_messaging/migrations/0005_auto_20180606_1107.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-06-06 11:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('drf_messaging', '0004_reportedmessages'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='messages', 15 | name='message', 16 | field=models.TextField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /drf_messaging/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rluts/drf-messaging/1433a7367eba56e3ebb5a406ef496e0d5de6f865/drf_messaging/migrations/__init__.py -------------------------------------------------------------------------------- /drf_messaging/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Q, Count, Max, F, Case, When, CharField, Value, OuterRef, Subquery 3 | from django.contrib.auth.models import User 4 | from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank 5 | from django.utils import timezone 6 | from django.conf import settings 7 | 8 | 9 | # Create your models here. 10 | class MessagesManager(models.Manager): 11 | def search_message(self, query): 12 | search_query = SearchQuery(query) 13 | search_vector = SearchVector('message') 14 | return self.get_queryset().annotate(rank=SearchRank(search_vector, search_query)) \ 15 | .filter(rank__gte=0.1).order_by('-rank') 16 | 17 | def send_message(self, **kwargs): 18 | if (kwargs.get('sender_id') or kwargs.get('sender')) and (kwargs.get('receiver_id') or kwargs.get('receiver')): 19 | try: 20 | sender = kwargs.get('sender') or User.objects.get(id=kwargs['sender_id']) 21 | receiver = kwargs.get('receiver') or User.objects.get(id=kwargs['receiver_id']) 22 | message = kwargs.get('message') 23 | except User.DoesNotExist: 24 | raise ValueError("User does not exist", 400) 25 | except Exception: 26 | raise ValueError("Request error", 400) 27 | else: 28 | if sender == receiver: 29 | raise ValueError("You can't send message to yourself", 400) 30 | 31 | message = self.create(sender=sender, receiver=receiver, message=message) 32 | return message 33 | else: 34 | return False 35 | 36 | def get_inbox(self, user): 37 | messages = self.get_queryset().filter(receiver=user).order_by("-pk") 38 | return messages 39 | 40 | def get_outbox(self, user): 41 | messages = self.get_queryset().filter(sender=user) 42 | return messages 43 | 44 | def read_message(self, mes_id): 45 | """ 46 | Get message and set as read 47 | :param mes_id: 48 | :return: 49 | """ 50 | message = self.get_queryset().get(id=mes_id) 51 | if not message.read: 52 | message.read_datetime = timezone.now() 53 | message.save() 54 | return message 55 | 56 | def get_chats(self, user): 57 | # Must be rewrite. Order_by doesn't work 58 | qs = self.get_queryset().filter(Q(receiver=user) | Q(sender=user)).annotate( 59 | user=Case( 60 | When(receiver=user, then=F('sender')), 61 | When(sender=user, then=F('receiver')), 62 | output_field=CharField(), 63 | ), 64 | ).values('user').annotate( 65 | unread_messages=Count( 66 | 'pk', 67 | filter=Q(receiver=user, read=False), 68 | ), 69 | last_message_id=Max('pk') 70 | ).order_by('-last_message_id') 71 | 72 | return qs 73 | 74 | def get_chat(self, sender_id, receiver_id): 75 | """ 76 | Mark as read and return chat messages 77 | :param sender_id: 78 | :param receiver_id: 79 | :return: queryset 80 | """ 81 | self.set_read(sender_id, receiver_id) 82 | return self.get_queryset().filter(Q(sender_id=sender_id, receiver_id=receiver_id) | 83 | Q(sender_id=receiver_id, receiver_id=sender_id)).order_by("-pk") 84 | 85 | def get_unread(self, receiver_id): 86 | """ 87 | Get unread messages count and group by sender 88 | :param receiver_id: 89 | :return: queryset 90 | """ 91 | return self.get_queryset().filter(receiver_id=receiver_id, read=False).values("sender")\ 92 | .annotate(count=Count("pk")).order_by("-count") 93 | 94 | def set_read(self, receiver_id, sender_id): 95 | """ 96 | Set all messages in chat as read 97 | :param sender_id: 98 | :param receiver_id: 99 | :return: messages count 100 | """ 101 | return self.get_queryset().filter(sender_id=sender_id, receiver_id=receiver_id, read=False)\ 102 | .update(read_datetime=timezone.now(), read=True) 103 | 104 | 105 | class Messages(models.Model): 106 | class Meta: 107 | indexes = [ 108 | models.Index(fields=['sender']), 109 | models.Index(fields=['receiver']), 110 | ] 111 | verbose_name = 'Message' 112 | verbose_name_plural = 'Messages' 113 | 114 | sender = models.ForeignKey( 115 | User, 116 | related_name='user_sender', 117 | on_delete=models.CASCADE 118 | ) 119 | 120 | receiver = models.ForeignKey( 121 | User, 122 | related_name='user_receiver', 123 | on_delete=models.CASCADE 124 | ) 125 | 126 | message = models.TextField( 127 | blank=True, 128 | null=True 129 | ) 130 | 131 | datetime = models.DateTimeField( 132 | auto_now=True 133 | ) 134 | 135 | read = models.BooleanField( 136 | default=False 137 | ) 138 | 139 | read_datetime = models.DateTimeField( 140 | null=True, 141 | default=None, 142 | blank=True 143 | ) 144 | 145 | attachments = models.ManyToManyField( 146 | "Attachment", 147 | blank=True 148 | ) 149 | 150 | objects = MessagesManager() 151 | 152 | def save(self, force_insert=False, force_update=False, using=None, 153 | update_fields=None): 154 | if self.read_datetime: 155 | self.read = True 156 | else: 157 | self.read = False 158 | return super().save(force_insert, force_update, using, update_fields) 159 | 160 | def __str__(self): 161 | return "%s %s (%s) to %s %s (%s): %s" % (self.sender.first_name, self.sender.last_name, 162 | self.sender.email, 163 | self.receiver.first_name, self.receiver.last_name, 164 | self.receiver.email, 165 | self.message[:20]) 166 | 167 | 168 | class Attachment(models.Model): 169 | class Meta: 170 | verbose_name = "Attachment" 171 | verbose_name_plural = "Attachments" 172 | 173 | file = models.FileField( 174 | verbose_name="File", 175 | upload_to="attachments/" 176 | ) 177 | 178 | owner = models.ForeignKey( 179 | User, 180 | on_delete=models.CASCADE 181 | ) 182 | 183 | 184 | class UserTechInfo(models.Model): 185 | user = models.OneToOneField( 186 | User, 187 | related_name="info", 188 | on_delete=models.CASCADE 189 | ) 190 | 191 | current_channel = models.CharField( 192 | blank=True, 193 | default="", 194 | max_length=500 195 | ) 196 | 197 | online = models.BooleanField( 198 | default=False 199 | ) 200 | 201 | 202 | class BlackList(models.Model): 203 | class Meta: 204 | verbose_name = "Black list" 205 | verbose_name_plural = "Black lists" 206 | 207 | word = models.CharField( 208 | max_length=200 209 | ) 210 | 211 | regex = models.BooleanField( 212 | verbose_name='Regular expression' 213 | ) 214 | 215 | enabled = models.BooleanField( 216 | default=True 217 | ) 218 | 219 | def __str__(self): 220 | return "regex: /%s/" % self.word if self.regex else self.word 221 | 222 | 223 | class ReportedMessages(models.Model): 224 | message = models.ForeignKey( 225 | "Messages", 226 | on_delete=models.CASCADE 227 | ) 228 | 229 | reporter = models.ForeignKey( 230 | User, 231 | on_delete=models.CASCADE 232 | ) 233 | 234 | datetime = models.DateTimeField( 235 | auto_now_add=True 236 | ) 237 | 238 | comment = models.CharField( 239 | max_length=2000, 240 | null=True, 241 | blank=True 242 | ) 243 | 244 | def __str__(self): 245 | return self.message.message 246 | 247 | -------------------------------------------------------------------------------- /drf_messaging/routing.py: -------------------------------------------------------------------------------- 1 | from channels.routing import ProtocolTypeRouter, URLRouter 2 | from django.urls import path 3 | from .consumers import SocketCostumer 4 | from .token_auth import TokenAuthMiddleware 5 | 6 | application = ProtocolTypeRouter({ 7 | 8 | # Channels will do this for you automatically. It's included here as an example. 9 | # "http": AsgiHandler, 10 | 11 | # Route all WebSocket requests to our custom chat handler. 12 | # We actually don't need the URLRouter here, but we've put it in for 13 | # illustration. Also note the inclusion of the AuthMiddlewareStack to 14 | # add users and sessions - see http://channels.readthedocs.io/en/latest/topics/authentication.html 15 | "websocket": TokenAuthMiddleware( 16 | URLRouter([ 17 | path("socket/", SocketCostumer), 18 | ]), 19 | ), 20 | 21 | }) -------------------------------------------------------------------------------- /drf_messaging/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Messages, User, ReportedMessages, Attachment 3 | from .validators import blacklist_validator, ValidationError 4 | from .utils import get_user_info_from_instance 5 | 6 | 7 | class UploadAttachmentSerializer(serializers.ModelSerializer): 8 | id = serializers.IntegerField(read_only=True) 9 | 10 | class Meta: 11 | model = Attachment 12 | fields = ('id', 'file') 13 | 14 | def validate(self, attrs): 15 | attrs['owner'] = self.context['request'].user 16 | return super().validate(attrs) 17 | 18 | 19 | class MessageSerializer(serializers.Serializer): 20 | id = serializers.IntegerField(read_only=True) 21 | sender = serializers.PrimaryKeyRelatedField(read_only=True) 22 | receiver = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) 23 | message = serializers.CharField(validators=(blacklist_validator,), required=False) 24 | datetime = serializers.DateTimeField(read_only=True) 25 | read = serializers.BooleanField(read_only=True) 26 | attachments = serializers.PrimaryKeyRelatedField(many=True, queryset=Attachment.objects.all()) 27 | 28 | def to_representation(self, instance): 29 | data = super().to_representation(instance) 30 | data['sender'] = get_user_info_from_instance(instance.sender) 31 | data['receiver'] = get_user_info_from_instance(instance.receiver) 32 | data['attachments'] = [{ 33 | 'file': self.context['request'].build_absolute_uri(i.file.url) 34 | } for i in instance.attachments.all()] 35 | return data 36 | 37 | def create(self, validated_data): 38 | mes = Messages.objects.send_message(**validated_data, sender=self.context['request'].user) 39 | mes.attachments.set(validated_data['attachments']) 40 | mes.save() 41 | return mes 42 | 43 | 44 | class GetChatsSerializer(serializers.Serializer): 45 | user = serializers.IntegerField() 46 | unread_messages = serializers.IntegerField() 47 | last_message = serializers.SerializerMethodField() 48 | 49 | def to_representation(self, instance): 50 | data = super().to_representation(instance) 51 | data['user'] = get_user_info_from_instance(User.objects.get(id=data['user'])) 52 | return data 53 | 54 | def get_last_message(self, instance): 55 | message = Messages.objects.get(id=instance['last_message_id']) 56 | response = { 57 | 'message': message.message, 58 | 'datetime': message.datetime, 59 | 'sender': get_user_info_from_instance(message.sender), 60 | 'receiver': get_user_info_from_instance(message.receiver) 61 | } 62 | return response 63 | 64 | 65 | class ReportSerializer(serializers.ModelSerializer): 66 | class Meta: 67 | model = ReportedMessages 68 | fields = ('datetime', 'message', 'comment') 69 | 70 | def validate(self, attrs): 71 | if attrs['message'].receiver != self.context['request'].user: 72 | raise ValidationError("You are not receiver of this message") 73 | attrs['reporter'] = self.context['request'].user 74 | return super().validate(attrs) 75 | -------------------------------------------------------------------------------- /drf_messaging/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.dispatch import receiver 3 | 4 | from channels.layers import get_channel_layer 5 | from asgiref.sync import async_to_sync 6 | from .models import Messages 7 | from .utils import get_user_info_from_instance 8 | from fcm_django.models import FCMDevice 9 | 10 | 11 | @receiver(post_save, sender=Messages) 12 | def send_message_to_socket(sender, instance, created, **kwargs): 13 | if created and hasattr(instance, 'receiver'): 14 | channel_layer = get_channel_layer() 15 | message = instance.message 16 | if getattr(instance.receiver, "info", None) and instance.receiver.info.current_channel: 17 | channel_name = instance.receiver.info.current_channel 18 | async_to_sync(channel_layer.send)(channel_name, {"type": "chat.message", "source": "signals", 19 | "message": message, 20 | "receiver": "you", "sender": instance.sender.id}) 21 | if getattr(instance.sender, "info", None) and instance.sender.info.current_channel: 22 | channel_name = instance.sender.info.current_channel 23 | async_to_sync(channel_layer.send)(channel_name, {"type": "chat.message", "source": "signals", 24 | "message": message, 25 | "receiver": instance.receiver.id, "sender": "you"}) 26 | fcm_devices = FCMDevice.objects.filter(user=instance.receiver) 27 | if fcm_devices: 28 | data = { 29 | "message": { 30 | "sender": get_user_info_from_instance(instance.sender), 31 | "message": message 32 | } 33 | } 34 | new_messages = Messages.objects.get_unread(instance.receiver) 35 | data['new_messages'] = [{ 36 | "sender": get_user_info_from_instance(instance.sender), 37 | "count": message.get('count') 38 | } for message in new_messages] 39 | 40 | fcm_devices.send_message(data=data) 41 | -------------------------------------------------------------------------------- /drf_messaging/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /drf_messaging/token_auth.py: -------------------------------------------------------------------------------- 1 | from rest_framework.authtoken.models import Token 2 | 3 | 4 | class TokenAuthMiddleware: 5 | """ 6 | Token authorization middleware for Django Channels 2 7 | """ 8 | 9 | def __init__(self, inner): 10 | self.inner = inner 11 | 12 | def __call__(self, scope): 13 | query = dict((x.split('=') for x in scope['query_string'].decode().split("&"))) 14 | token = query['token'] 15 | token = Token.objects.get(key=token) 16 | scope['user'] = token.user 17 | return self.inner(scope) 18 | -------------------------------------------------------------------------------- /drf_messaging/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | GetChatView, InboxMessagesView, OutboxMessagesView, 4 | SendMessageView, GetChatsView, UploadAttachmentView, 5 | ReportMessageView 6 | ) 7 | from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet 8 | 9 | urlpatterns = [ 10 | path('chat/', GetChatView.as_view()), 11 | path('chats/', GetChatsView.as_view()), 12 | path('inbox/', InboxMessagesView.as_view()), 13 | path('outbox/', OutboxMessagesView.as_view()), 14 | path('send/', SendMessageView.as_view({'post': 'create'})), 15 | path('attachments/', UploadAttachmentView.as_view({'post': 'create'})), 16 | path('report/', ReportMessageView.as_view({'post': 'create'})), 17 | path('devices/', FCMDeviceAuthorizedViewSet.as_view({'post': 'create'}), name='create_fcm_device') 18 | ] -------------------------------------------------------------------------------- /drf_messaging/utils.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from collections import OrderedDict 3 | from .models import Attachment 4 | 5 | # import the logging library 6 | import logging 7 | 8 | # Get an instance of a logger 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class ExtRelatedField(serializers.RelatedField): 13 | """ 14 | Temporary fixing DRF bug. Please update DRF after bug fix 15 | https://github.com/encode/django-rest-framework/issues/5141 16 | """ 17 | def to_representation(self, obj): 18 | logger.debug("to rep: " + str(obj)) 19 | return { 20 | 'id': obj.pk, 21 | } 22 | 23 | def get_choices(self, cutoff=None): 24 | queryset = self.get_queryset() 25 | if queryset is None: 26 | return {} 27 | 28 | if cutoff is not None: 29 | queryset = queryset[:cutoff] 30 | 31 | return OrderedDict([ 32 | ( 33 | item.pk, 34 | self.display_value(item) 35 | ) 36 | for item in queryset 37 | ]) 38 | 39 | def to_internal_value(self, data): 40 | try: 41 | queryset = self.get_ 42 | except: 43 | raise ValueError("Unknown request", 400) 44 | 45 | 46 | def get_user_info_from_instance(instance): 47 | info = { 48 | "id": instance.id, 49 | "name": "{} {}".format(instance.first_name, instance.last_name), 50 | "email": instance.email 51 | } 52 | return info 53 | -------------------------------------------------------------------------------- /drf_messaging/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .models import BlackList 3 | from django.core.exceptions import ValidationError 4 | 5 | 6 | def blacklist_validator(value): 7 | blacklists = BlackList.objects.all() 8 | if not value: 9 | return None 10 | for blacklist in blacklists: 11 | if (blacklist.regex and re.search(blacklist.word, value)) or \ 12 | not blacklist.regex and blacklist.word in value: 13 | raise ValidationError("Blacklist error") 14 | -------------------------------------------------------------------------------- /drf_messaging/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListAPIView 2 | from rest_framework.viewsets import ModelViewSet 3 | from rest_framework.permissions import IsAuthenticated 4 | 5 | from .models import Messages 6 | from .serializers import MessageSerializer, GetChatsSerializer, UploadAttachmentSerializer, ReportSerializer 7 | 8 | 9 | class SendMessageView(ModelViewSet): 10 | serializer_class = MessageSerializer 11 | 12 | 13 | class MessagesView(ListAPIView): 14 | permission_classes = (IsAuthenticated, ) 15 | serializer_class = MessageSerializer 16 | 17 | 18 | class GetChatView(MessagesView): 19 | def get(self, request, *args, **kwargs): 20 | receiver = request.GET.get('receiver') 21 | self.queryset = Messages.objects.get_chat(request.user.id, receiver) 22 | return super().get(request, *args, **kwargs) 23 | 24 | 25 | class GetChatsView(ListAPIView): 26 | permission_classes = (IsAuthenticated, ) 27 | serializer_class = GetChatsSerializer 28 | 29 | def get(self, request, *args, **kwargs): 30 | self.queryset = Messages.objects.get_chats(request.user.id) 31 | return super().get(request, *args, **kwargs) 32 | 33 | 34 | class InboxMessagesView(MessagesView): 35 | def get(self, request, *args, **kwargs): 36 | self.queryset = Messages.objects.get_inbox(request.user) 37 | return super().get(request, *args, **kwargs) 38 | 39 | 40 | class OutboxMessagesView(MessagesView): 41 | def get(self, request, *args, **kwargs): 42 | self.queryset = Messages.objects.get_outbox(request.user) 43 | return super().get(request, *args, **kwargs) 44 | 45 | 46 | class UploadAttachmentView(ModelViewSet): 47 | serializer_class = UploadAttachmentSerializer 48 | 49 | 50 | class ReportMessageView(ModelViewSet): 51 | serializer_class = ReportSerializer 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=2.0.8 2 | channels 3 | redis 4 | channels_redis 5 | djangorestframework 6 | --------------------------------------------------------------------------------