]> git.mar77i.info Git - chat/commitdiff
more frontend, better websocket server.
authormar77i <mar77i@protonmail.ch>
Tue, 15 Oct 2024 01:16:35 +0000 (03:16 +0200)
committermar77i <mar77i@protonmail.ch>
Tue, 15 Oct 2024 01:16:35 +0000 (03:16 +0200)
12 files changed:
.gitignore
chat/migrations/0001_initial.py
chat/rest_views.py
chat/serializers.py
chat/static/chat/chat.css
chat/static/chat/chat.html [new file with mode: 0644]
chat/static/chat/chat.js
chat/static/chat/underscore.js [new file with mode: 0644]
chat/tests.py
chat/triggers.py
chat/views.py
chat/websocket.py

index a2e02209b34fe4c2241c1af8aab1c61feb094ff1..72858697cbf6da37b9af34af3517f290dfc1d623 100644 (file)
@@ -1,5 +1,6 @@
+chat/settings_local.py
+.coverage
 .idea/
 __pycache__/
-chat/settings_local.py
 staticfiles
 venv/
index e1ddeea4869dcf2345617cbd7c54918df52e60b8..73207ba665f517731e60ac153f501e0aabdd9ceb 100644 (file)
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.1 on 2024-09-30 15:56
+# Generated by Django 5.1.1 on 2024-10-04 21:43
 
 import django.contrib.auth.models
 import django.contrib.auth.validators
@@ -82,7 +82,7 @@ class Migration(migrations.Migration):
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='user',
-            trigger=pgtrigger.compiler.Trigger(name='pg_notify_user', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","old":{"id":\' || OLD."id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","new":{"id":\' || NEW."id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='2f48ed2d25af6836e62e7506ebb833638d21f225', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_user_152e9', table='chat_user', when='AFTER')),
+            trigger=pgtrigger.compiler.Trigger(name='pg_notify_user', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || OLD."id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || NEW."id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='7f6e8eba01193825febb722ab5558a49773b1c1a', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_user_152e9', table='chat_user', when='AFTER')),
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='user',
@@ -90,7 +90,7 @@ class Migration(migrations.Migration):
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='channelmessage',
-            trigger=pgtrigger.compiler.Trigger(name='pg_notify_channelmessage', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","old":{"id":\' || OLD."id" || \',"user_id":\' || OLD."user_id" || \',"channel_id":\' || OLD."channel_id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","new":{"id":\' || NEW."id" || \',"user_id":\' || NEW."user_id" || \',"channel_id":\' || NEW."channel_id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='8839d39c99595353dad9ba1de4805be117880a3b', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_channelmessage_d2b2e', table='chat_channelmessage', when='AFTER')),
+            trigger=pgtrigger.compiler.Trigger(name='pg_notify_channelmessage', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || OLD."id" || \',"user_id":\' || OLD."user_id" || \',"channel_id":\' || OLD."channel_id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || NEW."id" || \',"user_id":\' || NEW."user_id" || \',"channel_id":\' || NEW."channel_id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='b6e672dd9e454c168ef544260779943b1e40fe6d', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_channelmessage_d2b2e', table='chat_channelmessage', when='AFTER')),
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='channelmessage',
@@ -98,7 +98,7 @@ class Migration(migrations.Migration):
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='channeluser',
-            trigger=pgtrigger.compiler.Trigger(name='pg_notify_channeluser', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","old":{"id":\' || OLD."id" || \',"user_id":\' || OLD."user_id" || \',"channel_id":\' || OLD."channel_id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","new":{"id":\' || NEW."id" || \',"user_id":\' || NEW."user_id" || \',"channel_id":\' || NEW."channel_id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='e919ee13216581d3895b820bfef92e17397b7c1a', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_channeluser_f01cc', table='chat_channeluser', when='AFTER')),
+            trigger=pgtrigger.compiler.Trigger(name='pg_notify_channeluser', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || OLD."id" || \',"user_id":\' || OLD."user_id" || \',"channel_id":\' || OLD."channel_id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || NEW."id" || \',"user_id":\' || NEW."user_id" || \',"channel_id":\' || NEW."channel_id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='6d4422d5ba8d654603affe34f0cabebe53fe23b0', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_channeluser_f01cc', table='chat_channeluser', when='AFTER')),
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='channeluser',
@@ -106,7 +106,7 @@ class Migration(migrations.Migration):
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='channel',
-            trigger=pgtrigger.compiler.Trigger(name='pg_notify_channel', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","old":{"id":\' || OLD."id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","new":{"id":\' || NEW."id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='07e8b07cb0612a98869139e71cda37635e0d4fda', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_channel_fab0c', table='chat_channel', when='AFTER')),
+            trigger=pgtrigger.compiler.Trigger(name='pg_notify_channel', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || OLD."id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || NEW."id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='32baa6dfc09e87012fc270e79ff8a074b5c9ffcd', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_channel_fab0c', table='chat_channel', when='AFTER')),
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='channel',
@@ -114,7 +114,7 @@ class Migration(migrations.Migration):
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='privatemessage',
-            trigger=pgtrigger.compiler.Trigger(name='pg_notify_privatemessage', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","old":{"id":\' || OLD."id" || \',"sender_id":\' || OLD."sender_id" || \',"recipient_id":\' || OLD."recipient_id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","new":{"id":\' || NEW."id" || \',"sender_id":\' || NEW."sender_id" || \',"recipient_id":\' || NEW."recipient_id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='5b1db355e28bdcbe3440d36e8da1709c7cba8156', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_privatemessage_92534', table='chat_privatemessage', when='AFTER')),
+            trigger=pgtrigger.compiler.Trigger(name='pg_notify_privatemessage', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n        IF TG_OP = \'DELETE\' THEN\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || OLD."id" || \',"sender_id":\' || OLD."sender_id" || \',"recipient_id":\' || OLD."recipient_id" || \'}}\'\n            );\n        ELSE\n            PERFORM pg_notify(\n                \'pg_notify\',\n                \'{"op":"\' || TG_OP || \'","table":"\' || TG_TABLE_NAME ||\n                \'","obj":{"id":\' || NEW."id" || \',"sender_id":\' || NEW."sender_id" || \',"recipient_id":\' || NEW."recipient_id" || \'}}\'\n            );\n        END IF;\n        RETURN NULL;\n        ', hash='03ce1756ef8739ddbaac8c46c954bb7f29cfb352', operation='DELETE OR INSERT OR UPDATE', pgid='pgtrigger_pg_notify_privatemessage_92534', table='chat_privatemessage', when='AFTER')),
         ),
         pgtrigger.migrations.AddTrigger(
             model_name='privatemessage',
index f891f77c8457f6c7ce494522c59b4b324914dd3d..258680292374b87559d5ce722b596a4809f4b4ba 100644 (file)
@@ -183,11 +183,11 @@ class PrivateMessageRestView(ModelRestView):
 
     def get_queryset(self):
         queryset = super().get_queryset()
-        if "other" in self.request.GET:
+        recipient_id = self.request.GET.get("recipient_id")
+        if recipient_id is not None:
             queryset = queryset.filter(
-                Q(sender=self.request.user, recipient_id=self.request.GET["other"]) | Q(
-                    sender_id=self.request.GET["other"], recipient=self.request.user
-                )
+                Q(sender=self.request.user, recipient_id=recipient_id)
+                | Q(sender_id=recipient_id, recipient=self.request.user)
             )
         else:
             queryset = queryset.filter(
@@ -217,6 +217,6 @@ class ChannelMessageRestView(ModelRestView):
         queryset = super().get_queryset().filter(
             channel__users=self.request.user.pk
         )
-        if "channel" in self.request.GET:
-            queryset = queryset.filter(channel_id=self.request.GET["channel"])
+        if "channel_id" in self.request.GET:
+            queryset = queryset.filter(channel_id=self.request.GET["channel_id"])
         return queryset.order_by("ts")
index 1b1a1bc20ded13670196b55babcbcde62d6d11f8..744f279acc4baaaffeeaf995b0278bc32e338f4c 100644 (file)
@@ -31,8 +31,6 @@ class ModelSerializer:
         value = getattr(instance, field_name)
         if isinstance(field, DateTimeField):
             value = value.isoformat(" ")
-        elif isinstance(field, ForeignKey):
-            value = value.pk
         elif isinstance(field, (ManyToManyField, ManyToManyRel)):
             value = [v.pk for v in value.all()]
         return value
@@ -51,8 +49,11 @@ class ModelSerializer:
                 m2m[field_name] = value
             elif isinstance(field, ForeignKey):
                 # disallow reassigning FKs on update
-                if instance.pk is None:
-                    setattr(instance, f"{field_name}_id", value)
+                if instance.pk is not None:
+                    raise ValueError(f"Not allowed to update: {field_name}")
+                if not isinstance(value, int):
+                    raise ValueError(f"Please just add the _id field: {field_name}")
+                setattr(instance, field_name, value)
             else:
                 setattr(instance, field_name, value)
         return instance, m2m
@@ -90,19 +91,18 @@ class UserSerializer(ModelSerializer):
 
 class PrivateMessageSerializer(ModelSerializer):
     model = PrivateMessage
-    fields = ["id", "url", "sender", "recipient", "ts", "text"]
+    fields = ["id", "url", "sender_id", "recipient_id", "ts", "text"]
 
     def from_json(self, data, instance=None):
         if instance is None:
             data.update(
                 {
-                    "sender": self.request.user.pk,
+                    "sender_id": self.request.user.pk,
                     "ts": now().isoformat(" ")
                 }
             )
         else:
-            for key in ("sender", "recipient", "ts"):
-                data.pop(key, None)
+            data.pop("ts", None)
         return super().from_json(data, instance)
 
 
@@ -127,7 +127,7 @@ class ChannelSerializer(ModelSerializer):
 
 class ChannelMessageSerializer(ModelSerializer):
     model = ChannelMessage
-    fields = ["id", "url", "user", "channel", "ts", "text"]
+    fields = ["id", "url", "user_id", "channel_id", "ts", "text"]
 
     def from_json(self, data, instance=None):
         instance, m2m = super().from_json(data, instance)
index 271f979606391fb400d50783ece3d3558057ee0e..6eda701ab608f99d0e2c016170415cb6b9d1ee67 100644 (file)
@@ -11,7 +11,7 @@ nav {
     overflow: auto;
 }
 
-main, .messages, .input > textarea {
+main, .messages, .messages_footer > textarea {
     width: 100%;
 }
 
@@ -19,8 +19,8 @@ nav, main {
     flex-direction: column;
 }
 
-nav, .input {
-    display:flex;
+nav, .messages_footer {
+    display: flex;
 }
 
 .user, .users, .channels {
@@ -33,10 +33,27 @@ nav, .input {
 
 nav > div {
     width: calc(100% - 1px);
-    border-bottom: 1px solid;
     border-right: 1px solid;
 }
 
-.input > textarea {
+.messages_footer > textarea {
     resize: none;
 }
+
+.messages_header, nav > div {
+    border-bottom: 1px solid;
+}
+
+.messages_header > ul {
+    position: fixed;
+    width: calc(100% - 10em);
+    border: 1px solid;
+}
+
+.messages {
+    overflow-y: auto;
+}
+
+ul {
+    list-style-position: inside;
+}
diff --git a/chat/static/chat/chat.html b/chat/static/chat/chat.html
new file mode 100644 (file)
index 0000000..c546f24
--- /dev/null
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <title>Chat</title>
+        <link rel="stylesheet" href="static/chat/style.css">
+        <link rel="stylesheet" href="static/chat/chat.css">
+        <script src="static/chat/underscore.js"></script>
+        <script src="static/chat/chat.js"></script>
+    </head>
+    <body>
+        <nav>
+            <div class="user">
+            </div>
+            <div class="channels">
+                Channels
+                <ul>
+                </ul>
+            </div>
+            <div class="users">
+                Users
+                <ul>
+                </ul>
+            </div>
+        </nav>
+        <main>
+            <div class="messages_header">
+                <h1></h1>
+                <ul>
+                </ul>
+            </div>
+            <div class="messages">
+            </div>
+            <div class="messages_footer">
+                <textarea></textarea>
+                <button>Send</button>
+            </div>
+        </main>
+    </body>
+</html>
index 5d9172049260fd4602f84c25d5be8d29b575b422..1dacf6c5db988b3f2fd9f6951f9fbe52e66185cb 100644 (file)
 (function () {
-    var current_channel = {};
-    var users_per_id = null;
-    var channels_per_id = null;
+    var current_channel = null, private_manager = null, channel_manager = null;
 
-    function clear_children(element) {
-        while (element.firstChild) {
-            element.removeChild(element.firstChild);
-        }
+    var bottom_callback, no_bottom_callback;
+    bottom_callback = no_bottom_callback = function () {
+        bottom_callback = no_bottom_callback;
+    };
+
+    function xhr(method, url, callback, data) {
+        var cb = callback
+            ? function () { callback(JSON.parse(this.responseText)); }
+            : null;
+        return _.xhr(
+            method,
+            url,
+            cb,
+            function (msg) { add_msg(null, "xhr_error", msg); },
+            function (msg) { add_msg(null, "xhr_abort", msg); },
+            data,
+        );
     }
 
     function get_data_id(element) {
         var attr = element.getAttribute("data-id");
-        if (attr === null)
-            return null;
-        return Number(attr);
+        return attr !== null ? Number(attr) : null;
     }
 
     function get_current_user_id() {
-        return get_data_id(document.getElementsByClassName("user")[0].children[0]);
+        return get_data_id(_.dgEBCN0("user").children[0]);
     }
 
     function set_msg(p, sender, msg) {
-        clear_children(p);
+        _.clear_children(p);
         if (typeof sender === 'number') {
-            sender = users_per_id[sender].username;
+            sender = private_manager.items_per_id[sender].username;
         }
-        p.appendChild(document.createTextNode(sender));
-        p.appendChild(document.createTextNode(": "));
-        p.appendChild(document.createTextNode(msg));
-    }
-
-    function add_msg(msg_id, sender, msg) {
-        var p = document.createElement("P");
-        p.setAttribute("data-id", msg_id);
-        set_msg(p, sender, msg);
-        document.getElementsByClassName("messages")[0].appendChild(p);
+        p.appendChild(_.dcTN(sender));
+        p.appendChild(_.dcTN(": "));
+        p.appendChild(_.dcTN(msg));
+        return p;
     }
 
-    function find_existing_message(msg_id, callback) {
-        var i, messages = document.getElementsByClassName("messages")[0];
-        for (i = 0; i < messages.children.length; i++) {
-            if (get_data_id(messages.children[i]) === msg_id) {
-                fetch_data(current_channel.url + msg_id + "/", callback);
-                return;
-            }
+    function make_tag_with_data_id(tag_name, data_id) {
+        var i, tag = document.createElement(tag_name);
+        if (data_id !== null) {
+            tag.setAttribute("data-id", data_id);
+        }
+        for (i = 2; i < arguments.length; i += 1) {
+            tag.appendChild(arguments[i]);
         }
+        return tag;
     }
 
-    function delete_existing_message(msg_id) {
-        var i, messages = document.getElementsByClassName("messages")[0];
-        for (i = 0; i < messages.children.length; i++) {
-            if (get_data_id(messages.children[i]) === msg_id) {
-                messages.removeChild(messages.children[i]);
-                return;
-            }
+    function make_callback_a(callback) {
+        var a = document.createElement("a"), i;
+        a.setAttribute("href", "javascript:void(0)");
+        a.addEventListener("click", callback);
+        for (i = 1; i < arguments.length; i += 1) {
+            a.appendChild(arguments[i]);
         }
+        return a;
     }
 
-    function ws_receive(msg) {
-        var data = JSON.parse(msg.data);
-        add_msg(null, "ws", msg.data);
-        if (
-            current_channel.url === "/api/privatemessage/"
-            && data.table === "chat_privatemessage"
-        ) {
-            if (
-                (
-                    data.new.sender_id === get_current_user_id()
-                    && data.new.recipient_id === current_channel.data.recipient
-                ) || (
-                    data.new.sender_id === current_channel.data.recipient
-                    && data.new.recipient_id === get_current_user_id()
-                )
-            ) {
-                if (data.op === "INSERT") {
-                    fetch_data(
-                        current_channel.url + data.new.id + "/", add_privatemessage
-                    );
-                } else if (data.op === "UPDATE") {
-                    find_existing_message(data.new.id, update_privatemessage);
-                } else if (data.op === "DELETE") {
-                    delete_existing_message(data.old.id);
-                } else if (data.op === "TRUNCATE") {
-                    clear_children(document.getElementsByClassName("messages")[0]);
-                }
-            }
-        } else if (
-            current_channel.url === "/api/channelmessage/"
-            && data.table === "chat_channelmessage"
-        ) {
-            if (data.new.channel_id === current_channel.data.channel) {
-                if (data.op === "INSERT") {
-                    fetch_data(
-                        current_channel.url + data.new.id + "/", add_channelmessage
-                    );
-                } else if (data.op === "UPDATE") {
-                    find_existing_message(data.new.id, update_channelmessage);
-                } else if (data.op === "DELETE") {
-                    delete_existing_message(data.old.id);
-                } else if (data.op === "TRUNCATE") {
-                    clear_children(document.getElementsByClassName("messages")[0]);
+    function find_existing_msg(msg_id, callback) {
+        var i, messages = _.dgEBCN0("messages");
+        return _.foreach_arr(
+            messages.children,
+            function (item) {
+                if (get_data_id(item) === msg_id) {
+                    callback(item);
+                    return true;
                 }
             }
-        } else if (data.table === "chat_user") {
-            reload_users();
-        } else if (data.table === "chat_channel") {
-            reload_channels();
-        }
-    }
-
-    function reload_users() {
-        clear_children(document.getElementsByClassName("users")[0].children[0]);
-        fetch_data("/api/user/", setup_user_callback);
+        );
     }
 
-    function reload_channels() {
-        clear_children(document.getElementsByClassName("channels")[0].children[0]);
-        fetch_data("/api/channel/", setup_channel_callback);
+    function add_msg(id, user, text) {
+        _.dgEBCN0("messages").appendChild(
+            set_msg(make_tag_with_data_id("p", id), user, text)
+        );
     }
 
-    function xhr_error(msg) {
-        console.log(msg);
-        add_msg(null, "xhr_error", msg);
+    function add_msg_callback(item) {
+        add_msg(item.id, item[current_channel.msg_user_field], item.text);
     }
 
-    function xhr_abort(msg) {
-        console.log(msg);
-        add_msg(null, "xhr_abort", msg);
+    function clear_messages_header() {
+        var messages_header = _.dgEBCN0("messages_header");
+        _.clear_children(messages_header.children[0]);
+        _.clear_children(messages_header.children[1]);
+        _.clear_after(messages_header.children[1]);
     }
 
-    function fetch_data(url, callback) {
-        var xhr = new XMLHttpRequest();
-        xhr.addEventListener("load", callback);
-        xhr.addEventListener("error", xhr_error);
-        xhr.open("get", url);
-        xhr.send();
+    function clear_channel() {
+        _.clear_children(_.dgEBCN0("messages"));
+        clear_messages_header();
+        _.dgEBCN0("messages_header").style.display = "none";
+        _.dgEBCN0("messages_footer").children[0].disabled = true;
     }
 
-    function post_data(url, data, callback) {
-        var xhr = new XMLHttpRequest();
-        if (callback !== null) {
-            xhr.addEventListener("load", callback);
+    function set_channel(field_value) {
+        clear_channel();
+        if (this === window) {
+            current_channel = null;
+            _.dgEBCN0("messages").appendChild(_.dcTN("No channel selected!"));
+            return;
         }
-        xhr.addEventListener("error", xhr_error);
-        xhr.addEventListener("abort", xhr_abort);
-        xhr.open("post", url);
-        xhr.send(data);
-    }
-
-    function add_privatemessage() {
-        var msg = JSON.parse(this.responseText);
-        add_msg(msg.id, msg.sender, msg.text);
-    }
-
-    function update_privatemessage() {
-        var msg = JSON.parse(this.responseText);
-        var i, messages = document.getElementsByClassName("messages")[0];
-        for (i = 0; i < messages.children.length; i++) {
-            if (get_data_id(messages.children[i]) === msg.id) {
-                set_msg(messages.children[i], msg.sender, msg.text);
-                return;
+        current_channel = this;
+        this.msg_data[this.msg_dest_field] = field_value;
+        _.dgEBCN0("messages_footer").children[0].disabled = false;
+        current_channel.setup_channel_header();
+        xhr(
+            "get",
+            this.msg_url + "?" + this.msg_dest_field + "=" + field_value.toString(),
+            function (msgs) {
+                var messages = _.dgEBCN0("messages");
+                // add a "load previous" button here
+                _.foreach_arr(msgs.result, add_msg_callback);
+                messages.scrollTop = messages.scrollHeight - messages.clientHeight;
             }
-        }
+        );
     }
 
-    function add_privatemessages() {
-        var msgs = JSON.parse(this.responseText), i;
-        for (i = 0; i < msgs.result.length; i += 1) {
-            add_msg(msgs.result[i].id, msgs.result[i].sender, msgs.result[i].text);
+    function update_channel_header() {
+        if (current_channel !== this) {
+            return;
+        }
+        if (this.items_per_id[this.msg_data.channel_id]) {
+            current_channel.setup_channel_header();
+        } else {
+            set_channel.call(window);
         }
     }
 
-    function set_privatemessage_channel(user_id) {
-        var messages = document.getElementsByClassName("messages")[0];
-        current_channel = {
-            "url": "/api/privatemessage/",
-            "data": {"recipient": user_id},
-        }
-        clear_children(messages);
-        fetch_data(
-            "/api/privatemessage/?other=" + user_id.toString(), add_privatemessages
-        );
-        document.getElementsByClassName("input")[0].children[0].removeAttribute(
-            "disabled"
+    function items_reload_callback(item) {
+        var t = this;
+        this.items_per_id[item.id] = item;
+        _.dgEBCN0(this.items_classname).children[0].appendChild(
+            make_tag_with_data_id(
+                "li",
+                item.id,
+                make_callback_a(
+                    function () { t.set_channel(item.id); },
+                    _.dcTN(this.item_to_str(item)),
+                ),
+            ),
         );
     }
 
-    function setup_user_callback() {
-        var data = JSON.parse(this.responseText), ul, li, a, p;
-        ul = document.getElementsByClassName("users")[0].children[0];
-        users_per_id = {};
-        for (i = 0; i < data.result.length; i += 1) {
-            users_per_id[data.result[i].id] = data.result[i];
-            if (data.result[i].is_authenticated) {
-                p = document.createElement("P");
-                p.setAttribute("data-id", data.result[i].id);
-                p.appendChild(document.createTextNode(data.result[i].username));
-                document.getElementsByClassName("user")[0].appendChild(p);
-            }
-            li = document.createElement("li");
-            li.setAttribute("data-id", data.result[i].id);
-            a = document.createElement("a");
-            a.setAttribute("href", "#");
-            a.appendChild(
-                document.createTextNode(
-                    data.result[i].username + "\xa0(" + data.result[i].email + ")"
-                )
+    function PrivateMessageManager() {
+        var t = this;
+        this.msg_url = "/api/privatemessage/";
+        this.msg_table = "chat_privatemessage";
+        this.msg_user_field = "sender_id";
+        this.msg_dest_field = "recipient_id";
+        this.msg_data = {};
+        this.set_channel = set_channel.bind(this);
+        this.setup_channel_header = function () {
+            var messages_header = _.dgEBCN0("messages_header");
+            clear_messages_header();
+            messages_header.children[0].appendChild(
+                _.dcTN(this.item_to_str(this.items_per_id[this.msg_data.recipient_id]))
             );
-            a.addEventListener(
-                "click",
-                (function (user_id) {
-                    return function () { set_privatemessage_channel(user_id); };
-                }(data.result[i].id))
+            messages_header.style.display = "block";
+        };
+        this.items_url = "/api/user/";
+        this.items_classname = "users";
+        this.items_per_id = {};
+        this.items_reload = function () {
+            console.log("a");
+            _.clear_children(_.dgEBCN0(this.items_classname).children[0]);
+            _.clear_attributes(this.items_per_id);
+            console.log("b");
+            xhr(
+                "get",
+                this.items_url,
+                function (data) {
+                    console.log("items_reload callback", data);
+                    _.foreach_arr(
+                        data.result,
+                        function (item) {
+                            items_reload_callback.call(t, item);
+                            if (!item.is_authenticated) {
+                                return;
+                            }
+                            _.clear_children(_.dgEBCN0("user"));
+                            _.dgEBCN0("user").appendChild(
+                                make_tag_with_data_id(
+                                    "p", item.id, _.dcTN(item.username)
+                                ),
+                            );
+                        }
+                    );
+                    update_channel_header.call(t);
+                },
             );
-            li.appendChild(a);
-            ul.appendChild(li);
+            console.log("c");
         }
+        this.item_to_str = function (item) {
+            return item.username +  "\xa0(" + item.email + ")";
+        };
+        this.match_channel = function (data) {
+            var user_id, recipient_id;
+            if (data.table !== this.msg_table) {
+                return false;
+            }
+            user_id = get_current_user_id();
+            other_id = current_channel.msg_data.recipient_id;
+            return (
+                data.obj.sender_id === user_id && data.obj.recipient_id === other_id
+            ) || (
+                data.obj.sender_id === other_id && data.obj.recipient_id === user_id
+            );
+        };
+        this.items_reload();
     }
 
-    function add_channelmessage() {
-        var msg = JSON.parse(this.responseText);
-        add_msg(msg.id, msg.user, msg.text);
+    function ChannelManager() {
+        var t = this;
+        this.msg_url = "/api/channelmessage/";
+        this.msg_table = "chat_channelmessage";
+        this.msg_user_field = "user_id";
+        this.msg_dest_field = "channel_id";
+        this.msg_data = {};
+        this.set_channel = set_channel.bind(this);
+        this.setup_channel_header = function () {
+            var messages_header = _.dgEBCN0("messages_header");
+            var btn = document.createElement("button");
+            clear_messages_header();
+            messages_header.children[0].appendChild(
+                _.dcTN(this.item_to_str(this.items_per_id[this.msg_data.channel_id]))
+            );
+            _.foreach_arr(
+                this.items_per_id[this.msg_data.channel_id].users,
+                function (user_id) {
+                    var item = private_manager.items_per_id[user_id];
+                    messages_header.children[1].appendChild(
+                        make_tag_with_data_id(
+                            "li",
+                            item.id,
+                            make_callback_a(
+                                function () { private_manager.set_channel(item.id); },
+                                _.dcTN(private_manager.item_to_str(item)),
+                            ),
+                        ),
+                    );
+                },
+            );
+            btn.appendChild(_.dcTN("Users"));
+            messages_header.addEventListener("click", function () {
+                messages_header.children[1].style.display = "none";
+            });
+            btn.addEventListener("click", function (event) {
+                event = event || window.event;
+                if (messages_header.children[1].style.display === "none") {
+                    messages_header.children[1].style.display = "block";
+                    messages_header.children[1].style.top = (btn.offsetTop + btn.offsetHeight).toString() + "px";
+                } else {
+                    messages_header.children[1].style.display = "none";
+                }
+                event.preventDefault();
+                event.stopPropagation();
+            });
+            messages_header.appendChild(btn);
+            messages_header.children[1].style.display = "none";
+            messages_header.style.display = "block";
+        };
+        this.items_url = "/api/channel/";
+        this.items_classname = "channels";
+        this.items_per_id = {};
+        this.items_reload = function () {
+            _.clear_children(_.dgEBCN0(this.items_classname).children[0]);
+            _.clear_attributes(this.items_per_id);
+            xhr(
+                "get",
+                this.items_url,
+                function (data) {
+                    _.foreach_arr(data.result, items_reload_callback.bind(t));
+                    update_channel_header.call(t);
+                },
+            );
+        }
+        this.item_to_str = function (item) { return item.name; };
+        this.match_channel = function (data) {
+            var obj;
+            if (data.table !== this.msg_table) {
+                return false;
+            }
+            return data.obj.channel_id === current_channel.msg_data.channel_id;
+        };
+        this.items_reload();
     }
 
-    function update_channelmessage() {
-        var msg = JSON.parse(this.responseText);
-        var i, messages = document.getElementsByClassName("messages")[0];
-        for (i = 0; i < messages.children.length; i++) {
-            if (get_data_id(messages.children[i]) === msg.id) {
-                set_msg(messages.children[i], msg.user, msg.text);
-                return;
+    function stick_to_bottom(element) {
+        if (element.clientHeight + element.scrollTop === element.scrollHeight) {
+            bottom_callback = function () {
+                element.scrollTop = element.scrollHeight - element.clientHeight;
+                no_bottom_callback();
             }
+        } else {
+            no_bottom_callback();
         }
     }
 
-    function add_channelmessages() {
-        var msgs = JSON.parse(this.responseText), i;
-        for (i = 0; i < msgs.result.length; i += 1) {
-            add_msg(msgs.result[i].id, msgs.result[i].user, msgs.result[i].text);
+    function ws_receive_msg(data) {
+        var messages = _.dgEBCN0("messages");
+        if (data.op === "INSERT") {
+            stick_to_bottom(messages);
+            xhr(
+                "get",
+                current_channel.msg_url + data.obj.id + "/",
+                function (item) {
+                    add_msg_callback(item);
+                    bottom_callback();
+                }
+            );
+        } else if (data.op === "UPDATE") {
+            stick_to_bottom(messages);
+            find_existing_msg(
+                data.obj.id,
+                function (tag) {
+                    stick_to_bottom(messages);
+                    xhr(
+                        "get",
+                        current_channel.msg_url + get_data_id(tag).toString() + "/",
+                        function (msg) {
+                            set_msg(
+                                tag, msg[current_channel.msg_user_field], msg.text
+                            );
+                            bottom_callback();
+                        }
+                    );
+                },
+            );
+        } else if (data.op === "DELETE") {
+            find_existing_msg(
+                data.obj.id, function (tag) { tag.parentElement.removeChild(tag); }
+            );
+        } else if (data.op === "TRUNCATE") {
+            _.clear_children(messages);
         }
     }
 
-    function set_channel(channel_id) {
-        var messages = document.getElementsByClassName("messages")[0];
-        current_channel = {
-            "url": "/api/channelmessage/",
-            "data": {"channel": channel_id},
+    function ws_receive(msg) {
+        var data = JSON.parse(msg.data);
+        // here we could add indication for the pm or channel that is being modified
+        if (data.table === "chat_channel" || data.table === "chat_channeluser") {
+            channel_manager.items_reload();
+        } else if (data.table === "chat_user") {
+            private_manager.items_reload();
         }
-        clear_children(messages);
-        fetch_data(
-            "/api/channelmessage/?channel=" + channel_id.toString(), add_channelmessages
-        );
-        document.getElementsByClassName("input")[0].children[0].removeAttribute(
-            "disabled"
-        );
-    }
-
-    function setup_channel_callback() {
-        var data = JSON.parse(this.responseText), ul, li, a;
-        ul = document.getElementsByClassName("channels")[0].children[0];
-        channels_per_id = {};
-        for (i = 0; i < data.result.length; i += 1) {
-            channels_per_id[data.result[i].id] = data.result[i];
-            li = document.createElement("li");
-            li.setAttribute("data-id", data.result[i].id);
-            a = document.createElement("a");
-            a.setAttribute("href", "#");
-            a.appendChild(document.createTextNode(data.result[i].name));
-            a.addEventListener(
-                "click",
-                (function (channel_id) {
-                    return function () { set_channel(channel_id); };
-                }(data.result[i].id))
-            );
-            li.appendChild(a);
-            ul.appendChild(li);
+        if (current_channel !== null && current_channel.match_channel(data)) {
+            ws_receive_msg(data);
         }
     }
 
-    function send() {
-        var ta = document.getElementsByClassName("input")[0].children[0], data, name;
+    function send_msg() {
+        var ta = _.dgEBCN0("messages_footer").children[0], data, name;
         data = {"text": ta.value};
-        for (name in current_channel.data) {
-            if (current_channel.data.hasOwnProperty(name)) {
-                data[name] = current_channel.data[name];
-            }
-        }
-        if (current_channel.url) {
-            post_data(current_channel.url, JSON.stringify(data), null);
+        _.foreach_obj(
+            current_channel.msg_data, function (name, value) { data[name] = value; }
+        );
+        if (current_channel) {
+            xhr("post", current_channel.msg_url, null, JSON.stringify(data));
             ta.value = "";
         }
     }
         keycode = event.code || event.key;
         if (keycode === "Enter" || keycode === "NumpadEnter") {
             // todo: make it possible to add newlines using ctrl+enter
-            send();
+            send_msg();
             event.preventDefault();
             event.stopPropagation();
         }
     window.addEventListener(
         "load",
         function (event) {
-            var ws, schema, input;
+            var ws, schema, messages_footer;
+            messages_footer = _.dgEBCN0("messages_footer");
+            set_channel.call(window);
             event = event || window.event;
-            input = document.getElementsByClassName("input")[0];
-            if (!current_channel.url) {
-                input.children[0].setAttribute("disabled", "");
-            }
             if (event.target.readyState !== "complete") {
                 return;
             }
+            private_manager = new PrivateMessageManager();
+            channel_manager = new ChannelManager();
             schema = {"http:": "ws:", "https:": "wss:"}[window.location.protocol];
             ws = new WebSocket(schema + "//" + window.location.host + "/");
             ws.addEventListener("message", ws_receive);
-            input.children[0].addEventListener("keydown", input_onkeydown);
-            input.children[0].value = "";
-            input.children[1].addEventListener("click", send);
-            reload_users();
-            reload_channels();
+            messages_footer.children[0].addEventListener("keydown", input_onkeydown);
+            messages_footer.children[0].value = "";
+            messages_footer.children[1].addEventListener("click", send_msg);
         }
     );
 }());
diff --git a/chat/static/chat/underscore.js b/chat/static/chat/underscore.js
new file mode 100644 (file)
index 0000000..f97bc82
--- /dev/null
@@ -0,0 +1,70 @@
+(function () {
+    function foreach_arr(arr, callback) {
+        var i;
+        for (i = arguments.length === 3 ? arguments[2] : 0; i < arr.length; i += 1) {
+            if (callback(arr[i]) === false) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    function foreach_obj(obj, callback) {
+        var name;
+        for (name in obj) {
+            if (obj.hasOwnProperty(name) && callback(name, obj[name]) === true) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    function clear_children(element) {
+        while (element.firstChild) {
+            element.removeChild(element.firstChild);
+        }
+    }
+
+    function clear_after(element) {
+        while (element.nextSibling) {
+            element.parentElement.removeChild(element.nextSibling);
+        }
+    }
+
+    function clear_attributes(obj) {
+        foreach_obj(obj, function (name) { delete obj[name]; })
+    }
+
+    function xhr(method, url, callback, error_callback, abort_callback, data) {
+        var xhr = new XMLHttpRequest();
+        if (!error_callback) {
+            error_callback = function (msg) { console.log("xhr_error", msg) };
+        }
+        if (!abort_callback) {
+            abort_callback = function (msg) { console.log("xhr_abort", msg) };
+        }
+        xhr.addEventListener("error", error_callback);
+        xhr.addEventListener("abort", abort_callback);
+        if (callback) {
+            xhr.addEventListener("load", callback);
+        }
+        xhr.open(method, url);
+        if (method.toLowerCase() === "put" || method.toLowerCase() === "post") {
+            xhr.send(data);
+        } else {
+            xhr.send();
+        }
+        return xhr;
+    }
+
+    window._ = {
+        "dcTN": function (t) { return document.createTextNode(t); },
+        "dgEBCN0": function (cn) { return document.getElementsByClassName(cn)[0]; },
+        "foreach_arr": foreach_arr,
+        "foreach_obj": foreach_obj,
+        "clear_children": clear_children,
+        "clear_after": clear_after,
+        "clear_attributes": clear_attributes,
+        "xhr": xhr,
+    };
+}());
index f5ec2ab15f19216867ea25a093767dde0a022bca..bf731230b264e1cae7d5f0a17571adacd2204ce8 100644 (file)
@@ -449,9 +449,6 @@ class ChatTest(ChatTestMixin, TestCase):
         response = self.client.get(user2_url)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.headers["Location"], f"/login/?next={user2_url}")
-
-        user2.is_
-
         cookies = self.login_user(user2, user2_password)
         response = self.client.get(user2_url, headers={"cookie": cookies})
         data = json.loads(response.content)
@@ -651,7 +648,7 @@ class ChatTest(ChatTestMixin, TestCase):
             json.dumps(
                 {
                     "id": None,
-                    "recipient": user1.pk,
+                    "recipient_id": user1.pk,
                     "text": "hello pms world",
                 }
             ),
@@ -666,8 +663,8 @@ class ChatTest(ChatTestMixin, TestCase):
                 "id": data["id"],
                 "url": f"http://testserver{privatemessage_url}",
                 "ts": data["ts"],
-                "sender": user2.pk,
-                "recipient": user1.pk,
+                "sender_id": user2.pk,
+                "recipient_id": user1.pk,
                 "text": "hello pms world",
             }
         )
@@ -681,8 +678,8 @@ class ChatTest(ChatTestMixin, TestCase):
             data["url"],
             json.dumps(
                 {
-                    "sender": user3.pk,
-                    "recipient": user3.pk,
+                    #"sender_id": user3.pk,
+                    #"recipient_id": user3.pk,
                     "ts": "2022-02-22 22:22:22.363636+00:00",
                     "text": "hello updated pms world",
                 }
@@ -697,8 +694,8 @@ class ChatTest(ChatTestMixin, TestCase):
                 "id": data["id"],
                 "url": f"http://testserver{privatemessage_url}",
                 "ts": data["ts"],
-                "sender": user2.pk,
-                "recipient": user1.pk,
+                "sender_id": user2.pk,
+                "recipient_id": user1.pk,
                 "text": "hello updated pms world",
             }
         )
@@ -710,8 +707,8 @@ class ChatTest(ChatTestMixin, TestCase):
         self.assertTrue(
             all(
                 (
-                    (m["sender"] == user2.pk and m["recipient"] == user1.pk)
-                    or (m["sender"] == user1.pk and m["recipient"] == user2.pk)
+                    (m["sender_id"] == user2.pk and m["recipient_id"] == user1.pk)
+                    or (m["sender_id"] == user1.pk and m["recipient_id"] == user2.pk)
                 )
                 for m in data["result"]
             )
@@ -725,8 +722,8 @@ class ChatTest(ChatTestMixin, TestCase):
                         "id": data["result"][0]["id"],
                         "url": f"http://testserver{privatemessage_url}",
                         "ts": data["result"][0]["ts"],
-                        "sender": user2.pk,
-                        "recipient": user1.pk,
+                        "sender_id": user2.pk,
+                        "recipient_id": user1.pk,
                         "text": "hello updated pms world",
                     },
                 ]
@@ -741,7 +738,7 @@ class ChatTest(ChatTestMixin, TestCase):
             reverse("chat-channelmessage-list"),
             json.dumps(
                 {
-                    "channel": channel.pk,
+                    "channel_id": channel.pk,
                     "text": "hello world",
                 }
             ),
@@ -756,9 +753,9 @@ class ChatTest(ChatTestMixin, TestCase):
                 "id": data["id"],
                 "url": f"http://testserver{channelmessage_url}",
                 "ts": data["ts"],
-                "channel": channel.pk,
+                "channel_id": channel.pk,
                 "text": "hello world",
-                "user": user2.pk,
+                "user_id": user2.pk,
             }
         )
         for x in range(10):
index fe90c83d44e3f89328d7e93c0b7438a65f45c6bd..ed56437d821373e9b1f27cdf7a7191e6100b0e34 100644 (file)
@@ -17,13 +17,13 @@ def triggers_for_table(channel_name, model_name, fields):
             PERFORM pg_notify(
                 '{channel_name}',
                 '{{"op":"' || TG_OP || '","table":"' || TG_TABLE_NAME ||
-                '","old":{{{fields_to_json('OLD', fields)}}}}}'
+                '","obj":{{{fields_to_json('OLD', fields)}}}}}'
             );
         ELSE
             PERFORM pg_notify(
                 '{channel_name}',
                 '{{"op":"' || TG_OP || '","table":"' || TG_TABLE_NAME ||
-                '","new":{{{fields_to_json('NEW', fields)}}}}}'
+                '","obj":{{{fields_to_json('NEW', fields)}}}}}'
             );
         END IF;
         RETURN NULL;
index e76a7a6be32e53ec3b6b041dd21868b9bd095501..a31fb11fcb1530a08ff68d7f1ddc51fe6c161c69 100644 (file)
@@ -1,19 +1,21 @@
+from asgiref.sync import sync_to_async
+
 from django.conf import settings
 from django.contrib.auth.forms import SetPasswordForm
-from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.auth.models import update_last_login
 from django.contrib.auth.tokens import default_token_generator
 from django.contrib.auth.views import (
     LoginView as DjangoLoginView,
     LogoutView as DjangoLogoutView,
 )
+from django.contrib.staticfiles import finders
 from django.core.mail import mail_admins
 from django.core.signing import Signer
-from django.http import Http404
+from django.http import Http404, FileResponse
 from django.urls import reverse, reverse_lazy
 from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
 from django.utils.safestring import mark_safe
-from django.views.generic import TemplateView
+from django.views.generic import TemplateView, View
 from django.views.generic.detail import SingleObjectTemplateResponseMixin
 from django.views.generic.edit import BaseCreateView, FormView, UpdateView
 
@@ -21,8 +23,11 @@ from .forms import AuthenticationForm, RegisterForm, PasswordResetForm
 from .models import User
 
 
-class ChatView(LoginRequiredMixin, TemplateView):
-    template_name = "chat/chat.html"
+class ChatView(View):
+    async def get(self, request, *args, **kwargs):
+        path = finders.find("chat/chat.html")
+        with open(path) as fh:
+            return FileResponse(fh.read(), filename=path, content_type="text/html")
 
 
 class LoginView(DjangoLoginView):
index a658916d3259c9e5381ff4c375dd809b309144b7..23b0c15542966139ca625a153d1a31bc06bb0efd 100644 (file)
@@ -1,5 +1,8 @@
+import json
 import sys
-from asyncio import CancelledError, ensure_future
+from asyncio import (
+    CancelledError, ensure_future, get_running_loop, run_coroutine_threadsafe
+)
 from contextlib import contextmanager
 from functools import partial
 from io import BytesIO
@@ -21,6 +24,7 @@ async def process_ws(receive, send):
         elif event['type'] == 'websocket.disconnect':
             return
         elif event['type'] == 'websocket.receive':
+            # ...maybe make it possible to request data through the ws?
             if event['text'] == 'ping':
                 await send({
                     'type': 'websocket.send',
@@ -29,20 +33,89 @@ async def process_ws(receive, send):
 
 
 @contextmanager
-def listen_notify_handler(conn, callback, loop):
-    loop.add_reader(conn.fileno(), partial(conn.execute, "SELECT 1"))
-    conn.add_notify_handler(callback)
-    conn.execute(f"LISTEN {settings.PG_NOTIFY_CHANNEL}")
+def listen_notify_handler(connection, callback):
+    loop = get_running_loop()
+    loop.add_reader(connection.fileno(), partial(connection.execute, "SELECT 1"))
+    connection.add_notify_handler(callback)
+    connection.execute(f"LISTEN {settings.PG_NOTIFY_CHANNEL}")
     try:
         yield
     finally:
-        conn.execute(f"UNLISTEN {settings.PG_NOTIFY_CHANNEL}")
-        conn.remove_notify_handler(callback)
-        loop.remove_reader(conn.fileno())
+        connection.execute(f"UNLISTEN {settings.PG_NOTIFY_CHANNEL}")
+        connection.remove_notify_handler(callback)
+        loop.remove_reader(connection.fileno())
 
 
-def process_triggers(send, notification):
-    ensure_future(send({"type": "websocket.send", "text": notification.payload}))
+def filter_trigger_always(coro, data, user, user_channels):
+    ensure_future(coro)
+
+
+def filter_trigger_channelmessage(coro, data, user, user_channels):
+    if data["obj"]["channel_id"] in user_channels:
+        ensure_future(coro)
+    else:
+        coro.close()
+
+
+def get_user_channels(user):
+    return set(user.channels.all().values_list("pk", flat=True))
+
+
+def update_user_channels(coro, loop, user, user_channels):
+    new_channels = get_user_channels(user)
+    old_channels = user_channels - new_channels
+    if not old_channels and not new_channels - user_channels:
+        if coro is not None:
+            coro.close()
+        return
+    user_channels -= old_channels
+    user_channels.update(new_channels)
+    if coro is not None:
+        ensure_future(coro, loop=loop)
+
+
+def filter_trigger_channeluser(coro, data, user, user_channels):
+    op = data["op"]
+    if op == "TRUNCATE":
+        if user_channels:
+            user_channels.clear()
+            ensure_future(coro)
+        else:
+            coro.close()
+        return
+    if data["obj"]["user_id"] != user.pk:
+        filter_trigger_channelmessage(coro, data, user, user_channels)
+        return
+    loop = get_running_loop()
+    run_coroutine_threadsafe(
+        sync_to_async(update_user_channels)(coro, loop, user, user_channels), loop
+    )
+
+
+def filter_trigger_privatemessage(coro, data, user, user_channels):
+    if user.pk in (data["obj"]["sender_id"], data["obj"]["recipient_id"]):
+        ensure_future(coro)
+    else:
+        coro.close()
+
+
+filter_triggers = {
+    "chat_user": filter_trigger_always,
+    "chat_channel": filter_trigger_always,
+    "chat_channelmessage": filter_trigger_channelmessage,
+    "chat_channeluser": filter_trigger_channeluser,
+    "chat_privatemessage": filter_trigger_privatemessage,
+}
+
+
+def process_triggers(send, user, user_channels, notification):
+    data = json.loads(notification.payload)
+    filter_triggers[data["table"]](
+        send({"type": "websocket.send", "text": notification.payload}),
+        data,
+        user,
+        user_channels,
+    )
 
 
 async def handle_websocket(scope, receive, send):
@@ -58,8 +131,10 @@ async def handle_websocket(scope, receive, send):
         return
 
     await sync_to_async(connection.connect)()
+    user_channels = await sync_to_async(get_user_channels)(request.user)
     with listen_notify_handler(
-        connection.connection, partial(process_triggers, send), receive.__self__.loop
+        connection.connection,
+        partial(process_triggers, send, request.user, user_channels),
     ):
         try:
             await process_ws(receive, send)