From b7239defcf864647ddbd937fee2c3813d33d7963 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Wed, 10 Jun 2026 17:33:02 +0330 Subject: [PATCH] add clients and chat core. --- .../chat/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 154 bytes apps/chat/__pycache__/admin.cpython-313.pyc | Bin 0 -> 198 bytes apps/chat/__pycache__/apps.cpython-313.pyc | Bin 0 -> 519 bytes apps/chat/__pycache__/urls.cpython-313.pyc | Bin 0 -> 307 bytes apps/chat/api/__init__.py | 0 .../api/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 158 bytes .../chat/api/__pycache__/urls.cpython-313.pyc | Bin 0 -> 1046 bytes apps/chat/api/urls.py | 16 ++ apps/chat/api/views/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 164 bytes .../__pycache__/conversations.cpython-313.pyc | Bin 0 -> 3628 bytes .../__pycache__/messages.cpython-313.pyc | Bin 0 -> 2442 bytes .../__pycache__/read_state.cpython-313.pyc | Bin 0 -> 2277 bytes apps/chat/api/views/conversations.py | 72 +++++++ apps/chat/api/views/messages.py | 41 ++++ apps/chat/api/views/read_state.py | 46 ++++ apps/chat/apps.py | 1 + apps/chat/events/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 161 bytes .../events/__pycache__/event.cpython-313.pyc | Bin 0 -> 664 bytes apps/chat/events/base.py | 0 apps/chat/events/event.py | 11 + apps/chat/events/publishers/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 172 bytes .../__pycache__/longpoll.cpython-313.pyc | Bin 0 -> 1461 bytes .../__pycache__/push.cpython-313.pyc | Bin 0 -> 644 bytes .../__pycache__/websocket.cpython-313.pyc | Bin 0 -> 1228 bytes apps/chat/events/publishers/longpoll.py | 23 ++ apps/chat/events/publishers/push.py | 7 + apps/chat/events/publishers/websocket.py | 20 ++ apps/chat/integrations/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 167 bytes apps/chat/integrations/mattermost/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 178 bytes .../__pycache__/client.cpython-313.pyc | Bin 0 -> 8951 bytes .../__pycache__/exceptions.cpython-313.pyc | Bin 0 -> 487 bytes apps/chat/integrations/mattermost/client.py | 201 ++++++++++++++++++ .../integrations/mattermost/exceptions.py | 2 + apps/chat/migrations/0001_initial.py | 68 ++++++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 165 bytes apps/chat/models.py | 3 - apps/chat/models/__init__.py | 12 ++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 533 bytes .../__pycache__/conversation.cpython-313.pyc | Bin 0 -> 1901 bytes .../__pycache__/mapping.cpython-313.pyc | Bin 0 -> 1456 bytes .../__pycache__/participant.cpython-313.pyc | Bin 0 -> 1770 bytes .../__pycache__/read_state.cpython-313.pyc | Bin 0 -> 1897 bytes apps/chat/models/conversation.py | 30 +++ apps/chat/models/mapping.py | 18 ++ apps/chat/models/participant.py | 31 +++ apps/chat/models/read_state.py | 32 +++ apps/chat/serializers/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 166 bytes .../__pycache__/conversations.cpython-313.pyc | Bin 0 -> 2159 bytes .../__pycache__/messages.cpython-313.pyc | Bin 0 -> 948 bytes .../__pycache__/read_state.cpython-313.pyc | Bin 0 -> 714 bytes apps/chat/serializers/conversations.py | 27 +++ apps/chat/serializers/messages.py | 10 + apps/chat/serializers/read_state.py | 7 + apps/chat/services/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 163 bytes .../__pycache__/account.cpython-313.pyc | Bin 0 -> 630 bytes .../__pycache__/conversation.cpython-313.pyc | Bin 0 -> 3513 bytes .../__pycache__/message.cpython-313.pyc | Bin 0 -> 5439 bytes .../__pycache__/read_state.cpython-313.pyc | Bin 0 -> 2426 bytes .../__pycache__/storage.cpython-313.pyc | Bin 0 -> 2290 bytes apps/chat/services/account.py | 7 + apps/chat/services/conversation.py | 65 ++++++ apps/chat/services/message.py | 118 ++++++++++ apps/chat/services/read_state.py | 48 +++++ apps/chat/services/realtime.py | 12 ++ apps/chat/services/storage.py | 42 ++++ apps/chat/tests.py | 3 - apps/chat/tests/__init__.py | 0 apps/chat/tests/test_conversations.py | 0 apps/chat/tests/test_messages.py | 0 apps/chat/tests/test_read_state.py | 0 apps/chat/urls.py | 5 + apps/chat/views.py | 3 - apps/chat/websocket/__init__.py | 0 apps/chat/websocket/consumers.py | 0 .../core/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 154 bytes apps/core/__pycache__/admin.cpython-313.pyc | Bin 0 -> 696 bytes apps/core/__pycache__/apps.cpython-313.pyc | Bin 0 -> 519 bytes apps/core/__pycache__/models.cpython-313.pyc | Bin 0 -> 5062 bytes .../__pycache__/0001_initial.cpython-313.pyc | Bin 0 -> 1713 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 165 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 163 bytes .../__pycache__/admin.cpython-313.pyc | Bin 0 -> 425 bytes .../__pycache__/apps.cpython-313.pyc | Bin 0 -> 544 bytes .../__pycache__/models.cpython-313.pyc | Bin 0 -> 3096 bytes .../__pycache__/0001_initial.cpython-313.pyc | Bin 0 -> 5915 bytes .../__pycache__/0002_initial.cpython-313.pyc | Bin 0 -> 4655 bytes ...n_authorization_grant_type.cpython-313.pyc | Bin 0 -> 1121 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 174 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 155 bytes apps/users/__pycache__/admin.cpython-313.pyc | Bin 0 -> 330 bytes apps/users/__pycache__/apps.cpython-313.pyc | Bin 0 -> 522 bytes apps/users/__pycache__/models.cpython-313.pyc | Bin 0 -> 3994 bytes .../__pycache__/0001_initial.cpython-313.pyc | Bin 0 -> 3753 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 166 bytes env.sample | 12 +- main/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 149 bytes main/__pycache__/settings.cpython-313.pyc | Bin 0 -> 7201 bytes main/__pycache__/urls.cpython-313.pyc | Bin 0 -> 2090 bytes main/asgi.py | 21 +- .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 164 bytes .../__pycache__/logging.cpython-313.pyc | Bin 0 -> 2149 bytes main/settings.py | 25 +++ main/urls.py | 1 + requirements.in | 5 +- utils/__pycache__/exceptions.cpython-313.pyc | Bin 0 -> 3903 bytes 112 files changed, 1029 insertions(+), 16 deletions(-) create mode 100644 apps/chat/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/__pycache__/admin.cpython-313.pyc create mode 100644 apps/chat/__pycache__/apps.cpython-313.pyc create mode 100644 apps/chat/__pycache__/urls.cpython-313.pyc create mode 100644 apps/chat/api/__init__.py create mode 100644 apps/chat/api/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/api/__pycache__/urls.cpython-313.pyc create mode 100644 apps/chat/api/urls.py create mode 100644 apps/chat/api/views/__init__.py create mode 100644 apps/chat/api/views/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/api/views/__pycache__/conversations.cpython-313.pyc create mode 100644 apps/chat/api/views/__pycache__/messages.cpython-313.pyc create mode 100644 apps/chat/api/views/__pycache__/read_state.cpython-313.pyc create mode 100644 apps/chat/api/views/conversations.py create mode 100644 apps/chat/api/views/messages.py create mode 100644 apps/chat/api/views/read_state.py create mode 100644 apps/chat/events/__init__.py create mode 100644 apps/chat/events/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/events/__pycache__/event.cpython-313.pyc create mode 100644 apps/chat/events/base.py create mode 100644 apps/chat/events/event.py create mode 100644 apps/chat/events/publishers/__init__.py create mode 100644 apps/chat/events/publishers/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/events/publishers/__pycache__/longpoll.cpython-313.pyc create mode 100644 apps/chat/events/publishers/__pycache__/push.cpython-313.pyc create mode 100644 apps/chat/events/publishers/__pycache__/websocket.cpython-313.pyc create mode 100644 apps/chat/events/publishers/longpoll.py create mode 100644 apps/chat/events/publishers/push.py create mode 100644 apps/chat/events/publishers/websocket.py create mode 100644 apps/chat/integrations/__init__.py create mode 100644 apps/chat/integrations/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/integrations/mattermost/__init__.py create mode 100644 apps/chat/integrations/mattermost/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/integrations/mattermost/__pycache__/client.cpython-313.pyc create mode 100644 apps/chat/integrations/mattermost/__pycache__/exceptions.cpython-313.pyc create mode 100644 apps/chat/integrations/mattermost/client.py create mode 100644 apps/chat/integrations/mattermost/exceptions.py create mode 100644 apps/chat/migrations/0001_initial.py create mode 100644 apps/chat/migrations/__pycache__/__init__.cpython-313.pyc delete mode 100644 apps/chat/models.py create mode 100644 apps/chat/models/__init__.py create mode 100644 apps/chat/models/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/models/__pycache__/conversation.cpython-313.pyc create mode 100644 apps/chat/models/__pycache__/mapping.cpython-313.pyc create mode 100644 apps/chat/models/__pycache__/participant.cpython-313.pyc create mode 100644 apps/chat/models/__pycache__/read_state.cpython-313.pyc create mode 100644 apps/chat/models/conversation.py create mode 100644 apps/chat/models/mapping.py create mode 100644 apps/chat/models/participant.py create mode 100644 apps/chat/models/read_state.py create mode 100644 apps/chat/serializers/__init__.py create mode 100644 apps/chat/serializers/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/serializers/__pycache__/conversations.cpython-313.pyc create mode 100644 apps/chat/serializers/__pycache__/messages.cpython-313.pyc create mode 100644 apps/chat/serializers/__pycache__/read_state.cpython-313.pyc create mode 100644 apps/chat/serializers/conversations.py create mode 100644 apps/chat/serializers/messages.py create mode 100644 apps/chat/serializers/read_state.py create mode 100644 apps/chat/services/__init__.py create mode 100644 apps/chat/services/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/chat/services/__pycache__/account.cpython-313.pyc create mode 100644 apps/chat/services/__pycache__/conversation.cpython-313.pyc create mode 100644 apps/chat/services/__pycache__/message.cpython-313.pyc create mode 100644 apps/chat/services/__pycache__/read_state.cpython-313.pyc create mode 100644 apps/chat/services/__pycache__/storage.cpython-313.pyc create mode 100644 apps/chat/services/account.py create mode 100644 apps/chat/services/conversation.py create mode 100644 apps/chat/services/message.py create mode 100644 apps/chat/services/read_state.py create mode 100644 apps/chat/services/realtime.py create mode 100644 apps/chat/services/storage.py delete mode 100644 apps/chat/tests.py create mode 100644 apps/chat/tests/__init__.py create mode 100644 apps/chat/tests/test_conversations.py create mode 100644 apps/chat/tests/test_messages.py create mode 100644 apps/chat/tests/test_read_state.py create mode 100644 apps/chat/urls.py delete mode 100644 apps/chat/views.py create mode 100644 apps/chat/websocket/__init__.py create mode 100644 apps/chat/websocket/consumers.py create mode 100644 apps/core/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/core/__pycache__/admin.cpython-313.pyc create mode 100644 apps/core/__pycache__/apps.cpython-313.pyc create mode 100644 apps/core/__pycache__/models.cpython-313.pyc create mode 100644 apps/core/migrations/__pycache__/0001_initial.cpython-313.pyc create mode 100644 apps/core/migrations/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/gooyal_oauth2/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/gooyal_oauth2/__pycache__/admin.cpython-313.pyc create mode 100644 apps/gooyal_oauth2/__pycache__/apps.cpython-313.pyc create mode 100644 apps/gooyal_oauth2/__pycache__/models.cpython-313.pyc create mode 100644 apps/gooyal_oauth2/migrations/__pycache__/0001_initial.cpython-313.pyc create mode 100644 apps/gooyal_oauth2/migrations/__pycache__/0002_initial.cpython-313.pyc create mode 100644 apps/gooyal_oauth2/migrations/__pycache__/0003_alter_application_authorization_grant_type.cpython-313.pyc create mode 100644 apps/gooyal_oauth2/migrations/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/users/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/users/__pycache__/admin.cpython-313.pyc create mode 100644 apps/users/__pycache__/apps.cpython-313.pyc create mode 100644 apps/users/__pycache__/models.cpython-313.pyc create mode 100644 apps/users/migrations/__pycache__/0001_initial.cpython-313.pyc create mode 100644 apps/users/migrations/__pycache__/__init__.cpython-313.pyc create mode 100644 main/__pycache__/__init__.cpython-313.pyc create mode 100644 main/__pycache__/settings.cpython-313.pyc create mode 100644 main/__pycache__/urls.cpython-313.pyc create mode 100644 main/other_settings/__pycache__/__init__.cpython-313.pyc create mode 100644 main/other_settings/__pycache__/logging.cpython-313.pyc create mode 100644 utils/__pycache__/exceptions.cpython-313.pyc diff --git a/apps/chat/__pycache__/__init__.cpython-313.pyc b/apps/chat/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c11b9024ddf8ea2abe5e8893d5578a6afde19c2 GIT binary patch literal 154 zcmey&%ge<81by?=vOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iQerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIPMgcTp3nU`4-AFo$Xd5gm)H$SB` ZC)KWq6=)L3reYA|BQql-V-Yiu1pqaNB#SA6lGRRIHzoSe%iPn4=$1 zl%JKFTvDtbo|#vipRb>skyxUiSWr+5Wr57pE2zB1VUwGmQks)$SHuA{h7pL1L5vT~ OjEs!;7!-=wfE)m2Ju!p; literal 0 HcmV?d00001 diff --git a/apps/chat/__pycache__/apps.cpython-313.pyc b/apps/chat/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f270aaf6023ac3e51b179b5e9994c578440544e0 GIT binary patch literal 519 zcmYLFJxjzu5Zz63AD$kHsHiBYg_0(z4irT20}F*iSj0wH)=MszBbUV4O`(;QwHE#e ze~*nUSXpT$oc7LMPTXSV&AvCoycsr|4T7)!xz&Hf{BDZBR~duhCW0rz2&1RugaTb7 zi)4c^Ynd?nlD6zp2Ug^`l@C+6R8Q>Z`C*oJVSwHM3>0iXD%rf6B?i&mL$jfiQ zoOu+CN7=-1PhKY%?v7rUwqKkDnEvD2uB|7>zRegghcDaB5~Xkkk*@EScK2WwOLKEM XhvCbnl)jPm@2W+29+rLxrcu`~#b}2P literal 0 HcmV?d00001 diff --git a/apps/chat/__pycache__/urls.cpython-313.pyc b/apps/chat/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dee0d90f72713f365e38964e3572a8c6826b9f65 GIT binary patch literal 307 zcmey&%ge<81Z}~ZSy@2(F^B^LY*5B$6Ch(MLoh=yqc?*WV-celQxTH_Lky!5Loibe z6Od#!WrV0TWrEQ9j77}pESjt@L0nCyTPy{MB^kHaGxL&jN>fs+SP}~|^{a#u3kr($ zk~0!Z^niT5(xRMVKTVcf+$mX!dFlCJei1WJe-R6ixWx^W2bxlnT9jA3lHoJR;9I8p zp~b01#rheE#ThAyIr;%b`B|ySCB^#TnR&(e`T8KU^+C2kSRfnp3My}L*yQG?l;)(` z6@feta(b~MkodsN$jEq~LG>dW11o2T*hKLQlDd}#^)9pMe_&?fXKLgw;sB}w0Q;~_ Ao&W#< literal 0 HcmV?d00001 diff --git a/apps/chat/api/__init__.py b/apps/chat/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/api/__pycache__/__init__.cpython-313.pyc b/apps/chat/api/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a8d32fd6cc4544699d91fafe2f730380011d6ea GIT binary patch literal 158 zcmey&%ge<81S{+`vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iYerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIPMjFqV$AD@|*SrQ+wS5SG2!zMRB br8Fniu80+A8pyU{5aS~=BO_xGGmr%UyzwRx literal 0 HcmV?d00001 diff --git a/apps/chat/api/__pycache__/urls.cpython-313.pyc b/apps/chat/api/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c46a4d27433942442f5059131669c8993066531c GIT binary patch literal 1046 zcmaiz&ui0A9Kc_i*ECty&QXz}Wz2!Lp|oMh4%5<2hEP%!x8im%P~Mu*rnE_UFRd65 z@#e{!caQTw@VKKFp+64AfOzoaZ8F5G@4c>dheF>W@B8EP{rSj6JiY|z!6#btYXpFw zOqm|;p~^){0N?|tfQnEChJb|)CP&H({i^ zUu+6Z2w?~L4J{Jh*!SF?jS2D{&)vs1^6ewXei^PkCpNz7d+ZROd1$Qq#3lsQIY?6^ z_8Z85)U#cm@X+|7jjW1)nJJKJ)J^n2Wp}Q|ORxr9)V70LU0R0dg>Kie?lZaSSVjFZ zBEbwf9@e7OgY}7h0yCMz?MXAVLY$*hJ-9g+r}Zs8P&mruAT<}W`8?CEC)!hOIaIAC za_e5ci(3SXbeG|6)aj5sOOi(&C*PwxqUx2c5)3JaqYO^XgQp#1i6QIr5Y}mxu$|%< zOC~cihDeo_3su?;eNXCi$tQ3@eIVS?*$WcAfk?wbE&Unxns$vo#IfTN&*MARS*fYy z=xnBZ=T#^d+Md;I*+rbBndmPC$b0%Z7X?B14pM)iC`A6qK-m5PO1~u#Ro<=jliE1B zHB4@elJ`F62C(!E%6;W-KeIQ^l!lqoC}Z>w&;XuryLS3uoGlKs#Zh+e?9BikbL*Xt tugB@aFkKj>i)Swe@PJ#>r|V;Fcc|@-w1;2f19)_/chats/", UserConversationListView.as_view(), name="user-chat-list"), + path("chats//messages/", MessageView.as_view(), name="chat-messages"), + path("chats//read/", ReadStateView.as_view(), name="chat-read"), + path("chats//events/", ChatEventsView.as_view(), name="chat-events"), +] diff --git a/apps/chat/api/views/__init__.py b/apps/chat/api/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/api/views/__pycache__/__init__.cpython-313.pyc b/apps/chat/api/views/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8241c33827ad5f1d7620663bb6a1e9894cc35f6 GIT binary patch literal 164 zcmey&%ge<81S{+`vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~ieerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIPMjFqWhmYG^!tREkrnU`4-AFo$X hd5gm)H$SB`C)KWq6=){N&SDVbBQql-V-Yiu1pwFND0Bb- literal 0 HcmV?d00001 diff --git a/apps/chat/api/views/__pycache__/conversations.cpython-313.pyc b/apps/chat/api/views/__pycache__/conversations.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6750f1cfe310b1e70db21ca09b4cd771f4e74e22 GIT binary patch literal 3628 zcmbUkTWlLy^^Rx!%=nSmT|0@>IFB}U8sa>fELln+w2d0O%~qb6F7isFsXa;Eb;jO1 z?fZQVj5tsytGn9!SZb?|;EM*Ox zO>l85wHi2=u*L1vZs690BkrV51Ggny@iy8P=P7Tj?Fo0>Lp=uWNON9X>qCMV0 zJ0c`R`ibD`B0}4{_9?b*3-udR9;ogXYGBzD^}a-rD5GMhJra38}`<_ho zH(Gg3Iw}< zm}*EVmj5bmIU>)d&CnrJhCV`)_6E{2y)!V?I9uV;5PbEKZZokIXIgNQjcMMLOL|PK zIScHJWspR<1>IRvWh#z~xoiWS0F-hd8g)?giOyvuO`>?LZp#=>q&8Sk%nXWOqWi8b zF5VO;#>U0V$*Z%ASFeEYIaSUt>2@l=Uy@agcA&5yh51ZNSgE)_f&}Q&K_Y}ZeTBBp9CBTAY^bGJ*HtOD;e@MsSL%meB*0S32< z;q)W&+Tygj>N3dwKeH%=_tNT2On4)Y(1Z-eh&eDMj68}1Y|W$ep;XFZMy)w3E%^ZX zhOu>QBoCv%$q*R9V&=!8Ca#%T|AQu0;J%>A+RQUEM-^MzZJvKfv!~Is#yF%oQnn=C z!m;trg_PAS)QU4k6j#dWC)a*Yh|-qE)G$UmU}DKPOsqL4JDa$PrP`7h0mp`5onev~ z9><1YGrg7UJ;s8iU>6*MbLQ}l6#g_x*^_-=lsx&=6l;1?al=kW^30drDOiLyfuCU& zPs($yu}OmJkn#vd^?VzOyy8u{l0#osBUorEYS{wR;g)3sP4Ovr$}6}>54{s1H~v6K z8;OwgS4{bI%*0YI@RMi8k{mNB&Dr$tAn7CNev@t|_%+bNU5dMxEPSv;zRvcNCFVpK z_)?HrGi9RQ1t@CJA}KZm-HE8ELbrX1!2&JR+gbySQhv7pH6#laL$@0RK9^ktIc_O0 zX>%{JaYZc@y`wvdQ2tRN)F=xy0Zv;l%DSsw=nVyYvDy1o-39eoEGf_jvO1raR83r| zw{KLxA}Q<9&U)Kmq3Y{wPSJE8S^!;#L#VW(>g=+t8Qp|pHc@O-x;qagMO-RS5slS3 zTv7*e?1kG#7tn1>xx6M*I&K(Upjj~c_PXA-D&?WUh{g`gK;3!uyBWENZS_Hx>;0LGr9x6-@xoE>au~j zTkil4qq17cYdVKydI5FR&8z!NT~_Hd5LF)l?0>DJUeUjXM~44X@f`&>Vgjo_fZK#N z=IQw9=B}GP~>T*m$$%AFB9=YX0*T|M^FATeDUFbk+N2ncEEnH|@31NF_8< z3yoDmV_V&iZ&yRJ)xfvP&KLf(RsXrNW7j`W^`C*cBU0_?FWaAoBDK(HB{cd-eH^WZ zu9V%++x=zRp8|d5E4$sjyS|~CFIMrzYQE`;Z~BS$+qGY}T!a54$(N^JsCSd%JyN-_C_SmJzYIaBKytMrU*1wIXa5_~*R?YXq!+H-L3 z!JY6xEj(Tck8cfrI`qlV6Xtib+u`x;@WO`k&#|v=u0Lx3+4mo3H~2jVar-~G@#7oM zyG}nCx<9lNjy#yZKflv|_7}0A$94wCcKSyEzH-~!JO0Kx-JVxx$f^E=w}>zF$I!&q z^b@WUx>^gxE1`JRo7i?F{&5f`fu4QBpn3ftx%ODV{&m1Nh=V)510V579Q>y`2p93| z!SEbo`3)1D^IM)W{G8AF)XO0B^K<>yr+o}SqiMryfa1;32!cKYSS2XlHyuE5thHke zr3jF&Vp?Epk6N!Y_1=!Gh3F}rD`B`|r`0_WeUMO)aPnG#zE^*y1&{`9&;Z-EHbLG>E|;2Ig`89DVi>3c>- wo{@8(lM8>hu#DxkgD~F&&eSvV)`88%xHfMc5SR|ShMDWk=Gtq5vvCdo2AP8=`2YX_ literal 0 HcmV?d00001 diff --git a/apps/chat/api/views/__pycache__/messages.cpython-313.pyc b/apps/chat/api/views/__pycache__/messages.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a330376ee960e853d27ee20cdad6e0de9515090 GIT binary patch literal 2442 zcmb7G-%r~{96!fD;sl5ZHYBAC(spgI=#tTiR!CL54yXontiq&BN+rt;hPn+-dgs8H zsI;dleHxQmZF?HhDviGNu@CzrYGsn6Q`yVfhrXfFzVFV719Vgqr{wGJ-F<)E_r9O6 zz1z_d1!P>lrQW&51MoMU1cS`|v3rX=9s><%%rux{Ad{keXgV~-Le}TmX>N*#yw7vf z!c-WBeV(5dry?-o^TKp=stvZKKoXn;TDTWz;?3{~+i(Hfmm+HP4V|bA=5o2o@uv(H zkYhL%L{`LE6O~IgG6^5Pa((iKWv&u7{*{T4v1Do{w2Y$loe9ZeY;SSJDwu_ahX!Su zlid_y9)p_zG8%v(je)Ed>IIO~SVDP?gTfN83b?Ili~4)hMsy0zfPFh6p>)3cfY^Nu z@_}`bV`f9X!~vo)G@~O;DVsf9YBoswK3y%Ou_H~3Hg^O)w&jf{TEae)P4-Z4TuV#H zM*^LZK!c{?y|H)ld`GaA3k6n!XO7KrIX)+7{3vgS^HLz0j|W^X9LOG)zkF99H>(8P z-cu7sLmvY*JcA?1v=>aMTMM{-xltgxvsN}S=a}C*m|L`prpjZfP_kD{hz!Rn*+dw| zh%rc`f}NDms31p(+6L(@V{XB449qT>4(6{;UYpP$O%CR$JIo>nLYiPms{+zX#GPNx z&0W{eeLAd<%}!jIn;2I`DqUQ}VQAi~n8?B5!fnI(fO9NB6IF^1q83xlfyc5rM54~# zHqh+_qnN!8OLxqIgR=9MjY_2~HJCNZWfX8$c7;?g$~IGhGUYYQl}p5V=f3LB(3N)L z2t6U>d+<_8{+#%=@0Y&KyY8`bHRZEvq@L*iY5m7_FOhZ=>8;pvp_aH@ZLdo`Kde7o z_oM+=8rT$SQo72~wfhh6ds51kQky+BX|T$@iVxJ{C%pI>H-6@2U+RgxIsaQD~sm`9dfFjWpx8}KA0rzkm1$Q)_q+_GV>sZq>Ywu9RVV&EFlhQ>02RKJ10j-rJe zJ*aLUsxU!!_s}SlsVc%_bhVd~HetRmDq^@<=hV7m1tFb8*3tH@VVk9FL}u^H#t_5#$SlzFhyd03TD(W7Iob=mQ7v9 zQC(jyEmVq>Z`bvE6{FbPk#v30g2*XawrQ7i9rhFTMkcy-oxY{5f^Iktv~E=#6A^(= zr~q}+NE)p;ND2KT+Grnue?l?-8|GcAgP&j;qI7dc?Z8~6LfWz4b7Q0hI{G5Ha&xr= z?>72s;^1P2O#P1o|HiTM-mc&MAVmi5r+qO)J7k9R(B>xt$^^IVw{F~fK!|=qv?Tn3 z^Uv|#0kL?ok-AH8lsKZVctneMhh-RM8+5-0C$_=aZ7}#6T-*jDZ-stlg4syF1N7-1 F{$DtI@s0oh literal 0 HcmV?d00001 diff --git a/apps/chat/api/views/__pycache__/read_state.cpython-313.pyc b/apps/chat/api/views/__pycache__/read_state.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9abd17f682a1bc04122066b076716508bf31a49e GIT binary patch literal 2277 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 literal 0 HcmV?d00001 diff --git a/apps/chat/events/publishers/__pycache__/longpoll.cpython-313.pyc b/apps/chat/events/publishers/__pycache__/longpoll.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c86df23edcaf9138fc33d4e6c83a6569a665433 GIT binary patch literal 1461 zcmZWp&2Jk;6rcU_+VKY_CZT+UF2zMSYHivkl%N)g4@;Ie)@0oX()GNl@#LdrGij7()iV5O#2$|Z; z$V|+_X@Y2?EtT}A0xpy!{z97sn39&waMN>L(hO|Rh5dY;FlM!hN!*}%6_|ZQLZ~U+ zBKkL-H(_TJT|`te5tU7eDyDoKJfA_`AX67;CM&>W>Z43+|}z&H2?a@a)6z(X>n*%5YSQ*KC0|MeQ#^l zWjU2a>UClES>KgC|dW8=S*ltxB@OHsn=R=jFYs%07+{fX@0uvid>s4;Qy57?S-dC0mdn z#T$4nP)y+gJOb&Rfb^exJqx{Mkc8J2 zdQzY-z4Ji&GI$oq@B~%?hBCs4q(?4Zw5C|J7gcErjn^#VLe1hXLTy{D=lRe~XCnxG zdiKoR!jk%$e}lwxfzj0!j_B4fe>!!q(sxFqNsG)BmsyoAuvzKcn)H2Ho2E(gBC~0> z*_b}-OijAE?S)fQ%r>9hXt;Ancbc6I*AaV$1Yh9Sc(SJikoQm1jg_jiwN}av!dc&drgxT}NcgH;WjCU~WC+|b=-bvoIapSXofEUOX{2$DH Uf}Jmf_zh2QB45ORfuQsG2RAyL^#A|> literal 0 HcmV?d00001 diff --git a/apps/chat/events/publishers/__pycache__/websocket.cpython-313.pyc b/apps/chat/events/publishers/__pycache__/websocket.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89ddeee6bdf54583588d2fb00a749e3f697d9ba1 GIT binary patch literal 1228 zcmZWo%}*0S6rb(x_KOx!L#=#-4N;Ot*^MAK@Wurcjhlu8lF7Cm%HpzHXSSMp(u?6J z2O^x%lkw)Cg3$!B0b^o3c&ifQ)%W&8L?`Kczj<%wz2Elt=A>B66IffzmGx&SLcZW8 zSxO8}W&msxlbF&1nU}bvAxG?kHFONdb0k|8i&X>xlg!c z5}q<8E}NEjf57~^?V4kYHO9YXpg^E9>=2|m z^V{5AtjD~=ZdtH0{7ycB*(S?U7zHyO0~gv>rF(zhi8H-qD&@+{YI38Rx|dGysh^H3 zj*%*mhf3J-pS9eR1R!5X@TpH~K(_ZZnjllw70C1_`%^zBB^qg@Nh-=>pn?!SeNsTD zZ$uhYltA%2Ef!>2p75#N2y%kC4aN}`;zJZqRB{}p2{K_7yTS3F3-V3wwObVLaHJQ= z!somX5Jyf>*6KK()xbW2IjcN}(c+nrNQ$@?^urz+tl*YW6^sjG%@S)3%Q5b9Z=KbB zVJz9M@H_))7*?w#A{5Fjj8>w)Fdnm-2q!{sb$~~d2nPQj6g|$NIHCsTDLE=vK9sL+ z77s5D?^O3zc2{=p>6nZklZmfn>Q}lb<$n+`A+6t6N+vV_ literal 0 HcmV?d00001 diff --git a/apps/chat/events/publishers/longpoll.py b/apps/chat/events/publishers/longpoll.py new file mode 100644 index 0000000..101487f --- /dev/null +++ b/apps/chat/events/publishers/longpoll.py @@ -0,0 +1,23 @@ +import json + +from django_redis import get_redis_connection + +from apps.chat.events.event import MessageSentEvent + +_KEY_TTL = 300 + + +class LongPollPublisher: + def publish(self, event: MessageSentEvent) -> None: + redis = get_redis_connection("default") + key = f"chat_events:{event.chat_id}" + payload = json.dumps( + { + "post_id": event.post_id, + "sender_id": str(event.sender_id), + "message_type": event.message_type, + **{k: v for k, v in event.payload.items() if v is not None}, + } + ) + redis.rpush(key, payload) + redis.expire(key, _KEY_TTL) diff --git a/apps/chat/events/publishers/push.py b/apps/chat/events/publishers/push.py new file mode 100644 index 0000000..fbba604 --- /dev/null +++ b/apps/chat/events/publishers/push.py @@ -0,0 +1,7 @@ +from apps.chat.events.event import MessageSentEvent + + +class PushPublisher: + # Future: FCM / APNS integration + def publish(self, event: MessageSentEvent) -> None: + pass diff --git a/apps/chat/events/publishers/websocket.py b/apps/chat/events/publishers/websocket.py new file mode 100644 index 0000000..22e2f0f --- /dev/null +++ b/apps/chat/events/publishers/websocket.py @@ -0,0 +1,20 @@ +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer + +from apps.chat.events.event import MessageSentEvent + + +class WebSocketPublisher: + def publish(self, event: MessageSentEvent) -> None: + channel_layer = get_channel_layer() + group_name = f"chat_{event.chat_id}" + async_to_sync(channel_layer.group_send)( + group_name, + { + "type": "chat.message", + "post_id": event.post_id, + "sender_id": str(event.sender_id), + "message_type": event.message_type, + **event.payload, + }, + ) diff --git a/apps/chat/integrations/__init__.py b/apps/chat/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/integrations/__pycache__/__init__.cpython-313.pyc b/apps/chat/integrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..067b0fd5281282c7ff9365e6f7bb7878933eb589 GIT binary patch literal 167 zcmey&%ge<81S{+`vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iGerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIPMgq4|BlA2zWSdy8aSF9f&pP83g m5+AQuP>P{wB#AY&>+I)f&o-%5reCLr%KNa|LgerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIPMgq4|BlA2zWSdy8aSFE3#SW=Q& xl$&2%q8}fhnU`4-AFo$Xd5gm)H$SB`C)KWq6=(y<1;rr7M`lJw#v*1Q3jo1fE`b05 literal 0 HcmV?d00001 diff --git a/apps/chat/integrations/mattermost/__pycache__/client.cpython-313.pyc b/apps/chat/integrations/mattermost/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b0176a121276e9ed3e82a5ab439e04fa03c8db4 GIT binary patch literal 8951 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 literal 0 HcmV?d00001 diff --git a/apps/chat/integrations/mattermost/__pycache__/exceptions.cpython-313.pyc b/apps/chat/integrations/mattermost/__pycache__/exceptions.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efcff159104ca7f4ef3fdb483bf62cd912f7782a GIT binary patch literal 487 zcmYLFu};G<5VeyQO4X`jK&m=LY++~?x*;S61_r8%2vtIrP9}95S4kYsuAqDXGfO{$ zUy9h6*pLD{cGJQQzIS);o&8+4+f8EPZtO3{CV$vs7xgVDdj_mYmmHIj^hii2WZ+ga zq;to2qn*NLx-H7C0c$cLNJD~7NYM=)tc5Pt=QY1xbgr4!98;Q0zm7$v_6V3xK$ig5Hl4|SrUv;E_kF>Fcz7T zGKgZP1D5BiLL$?Aj%K0ERFM7|4ER&T^G&y(uh4$p=CGEPz3hd&*G{pu>koaWIJn$t mD!!#vjJ82V str: + return f"chat-{UUID(str(user_id)).hex}" + + def _email_for(self, user_id: UUID) -> str: + return f"chat-{UUID(str(user_id)).hex}@mm.internal" + + @staticmethod + def _random_password() -> str: + alphabet = string.ascii_letters + string.digits + "!@#$%" + return "".join(secrets.choice(alphabet) for _ in range(32)) + + # ------------------------------------------------------------------ + # Public API — return only opaque strings, never raw Mattermost dicts + # with identifying fields surfaced beyond this layer. + # ------------------------------------------------------------------ + + def get_or_create_user(self, user_id: UUID) -> str: + """ + Ensure a Mattermost account exists for the given external UUID. + Returns the opaque mattermost_user_id string. + """ + username = self._username_for(user_id) + + try: + user = self._driver.users.get_user_by_username(username) + return user["id"] + except ResourceNotFound: + pass + except Exception as exc: + raise MattermostError(f"User lookup failed: {exc}") from exc + + try: + user = self._driver.users.create_user( + options={ + "username": username, + "email": self._email_for(user_id), + "password": self._random_password(), + } + ) + return user["id"] + except Exception as exc: + # Guard against a concurrent request having created the account + # between our lookup and our create attempt. + try: + user = self._driver.users.get_user_by_username(username) + return user["id"] + except Exception: + pass + raise MattermostError(f"User creation failed: {exc}") from exc + + def create_private_channel(self, mm_user_ids: list[str]) -> str: + """ + Create a new private Mattermost channel and add all members. + Each call always creates a brand-new channel. + Returns the opaque mattermost_channel_id string. + """ + if not self._team_id: + raise MattermostError("MATTERMOST_TEAM_ID is not configured") + + channel_name = f"conv-{secrets.token_hex(8)}" + + try: + channel = self._driver.channels.create_channel( + options={ + "team_id": self._team_id, + "name": channel_name, + "display_name": "Conversation", + "type": "P", # P = private channel (not a DM) + } + ) + except Exception as exc: + raise MattermostError(f"Channel creation failed: {exc}") from exc + + channel_id: str = channel["id"] + + for mm_user_id in mm_user_ids: + try: + self._driver.channels.add_user( + channel_id, options={"user_id": mm_user_id} + ) + except Exception as exc: + raise MattermostError( + f"Adding user {mm_user_id} to channel failed: {exc}" + ) from exc + + return channel_id + + def post_message(self, channel_id: str, message: str) -> str: + """ + Post a message to the given channel using the admin token. + Returns the opaque mattermost_post_id string. + """ + try: + post = self._driver.posts.create_post( + options={ + "channel_id": channel_id, + "message": message, + } + ) + return post["id"] + except Exception as exc: + raise MattermostError(f"Post creation failed: {exc}") from exc + + def get_posts( + self, + channel_id: str, + page: int = 0, + per_page: int = 20, + since: int | None = None, + ) -> list[dict]: + """ + Fetch posts from a channel and return them in chronological order. + + `since` is a Unix timestamp in **milliseconds**. When provided, + `page` is ignored and all posts after that timestamp are returned. + """ + params: dict = {"per_page": per_page} + if since is not None: + params["since"] = since + else: + params["page"] = page + + try: + data = self._driver.posts.get_posts_for_channel(channel_id, params=params) + except Exception as exc: + raise MattermostError(f"Fetching posts failed: {exc}") from exc + + order: list[str] = data.get("order", []) + posts_map: dict = data.get("posts", {}) + + # Mattermost returns newest-first; reverse to get chronological order. + return [posts_map[pid] for pid in reversed(order) if pid in posts_map] + + def get_latest_post_id(self, channel_id: str) -> str | None: + """ + Return the ID of the most recent post in the channel, or None if empty. + Used to derive `has_unread` against a stored last_read_mattermost_post_id. + """ + try: + data = self._driver.posts.get_posts_for_channel( + channel_id, params={"page": 0, "per_page": 1} + ) + except Exception as exc: + raise MattermostError(f"Fetching latest post failed: {exc}") from exc + + order: list[str] = data.get("order", []) + return order[0] if order else None diff --git a/apps/chat/integrations/mattermost/exceptions.py b/apps/chat/integrations/mattermost/exceptions.py new file mode 100644 index 0000000..2237877 --- /dev/null +++ b/apps/chat/integrations/mattermost/exceptions.py @@ -0,0 +1,2 @@ +class MattermostError(Exception): + """Raised when any Mattermost API call fails.""" diff --git a/apps/chat/migrations/0001_initial.py b/apps/chat/migrations/0001_initial.py new file mode 100644 index 0000000..e7cbb68 --- /dev/null +++ b/apps/chat/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# Generated by Django 5.2.13 on 2026-06-10 12:09 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Conversation', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('mattermost_channel_id', models.CharField(db_index=True, max_length=255, unique=True)), + ('type', models.CharField(choices=[('direct', 'Direct'), ('group', 'Group')], default='direct', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'indexes': [models.Index(fields=['type'], name='chat_conver_type_857bd7_idx'), models.Index(fields=['created_at'], name='chat_conver_created_656b50_idx')], + }, + ), + migrations.CreateModel( + name='MattermostAccountMapping', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('user_id', models.UUIDField(db_index=True, unique=True)), + ('mattermost_user_id', models.CharField(db_index=True, max_length=255, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'indexes': [models.Index(fields=['mattermost_user_id'], name='chat_matter_matterm_e23c84_idx')], + }, + ), + migrations.CreateModel( + name='ConversationParticipant', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('user_id', models.UUIDField()), + ('joined_at', models.DateTimeField(auto_now_add=True)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='chat.conversation')), + ], + options={ + 'indexes': [models.Index(fields=['user_id'], name='chat_conver_user_id_697713_idx'), models.Index(fields=['conversation', 'user_id'], name='chat_conver_convers_59b4dd_idx')], + 'constraints': [models.UniqueConstraint(fields=('conversation', 'user_id'), name='unique_participant_per_conversation')], + }, + ), + migrations.CreateModel( + name='ConversationReadState', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('user_id', models.UUIDField()), + ('last_read_mattermost_post_id', models.CharField(blank=True, max_length=255, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_states', to='chat.conversation')), + ], + options={ + 'indexes': [models.Index(fields=['user_id'], name='chat_conver_user_id_e7609f_idx'), models.Index(fields=['conversation', 'user_id'], name='chat_conver_convers_9555aa_idx')], + 'constraints': [models.UniqueConstraint(fields=('conversation', 'user_id'), name='unique_read_state_per_user')], + }, + ), + ] diff --git a/apps/chat/migrations/__pycache__/__init__.cpython-313.pyc b/apps/chat/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d079a82c4a8f073ca3617d589c72295cdeb4c4d GIT binary patch literal 165 zcmey&%ge<81by?=vOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~i0erR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIPMgq53_UX)mpnV(mzA0MBYmst`Y kuUAlci^C>2KczG$)vkyYXeh|iVi4maGb1Bo5i^hl06xPhUH||9 literal 0 HcmV?d00001 diff --git a/apps/chat/models.py b/apps/chat/models.py deleted file mode 100644 index 71a8362..0000000 --- a/apps/chat/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/apps/chat/models/__init__.py b/apps/chat/models/__init__.py new file mode 100644 index 0000000..d9080a4 --- /dev/null +++ b/apps/chat/models/__init__.py @@ -0,0 +1,12 @@ +from apps.chat.models.conversation import Conversation, ConversationType +from apps.chat.models.mapping import MattermostAccountMapping +from apps.chat.models.participant import ConversationParticipant +from apps.chat.models.read_state import ConversationReadState + +__all__ = [ + "Conversation", + "ConversationType", + "ConversationParticipant", + "MattermostAccountMapping", + "ConversationReadState", +] diff --git a/apps/chat/models/__pycache__/__init__.cpython-313.pyc b/apps/chat/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c62aefcc917780bd4ca2549f3b4f6c70b96e8de GIT binary patch literal 533 zcmZWm%Sr<=6iwz?tCeoVf(qTvruhMt5jQeJr8D0JyZxby@3Bm>2T zD|cdlz?-Skfo5?yH|OTulVsKDv=OZLFqpk!gg#C2zpOv93W2OsA1`Ef4XTUY! z16&7feFDyMkWQHz%=tJgfH&f8j_Tf4<9r>e_S45(a~7>;Zh+7~>CgwM1u26axM@-mdlRi+vD(0c&)d{r~^~ literal 0 HcmV?d00001 diff --git a/apps/chat/models/__pycache__/conversation.cpython-313.pyc b/apps/chat/models/__pycache__/conversation.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09d1e62ad403aae7d565efc7d5c1cd1b91f62d9b GIT binary patch literal 1901 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 {self.mattermost_user_id}" diff --git a/apps/chat/models/participant.py b/apps/chat/models/participant.py new file mode 100644 index 0000000..528c30d --- /dev/null +++ b/apps/chat/models/participant.py @@ -0,0 +1,31 @@ +import uuid + +from django.db import models + +from apps.chat.models.conversation import Conversation + + +class ConversationParticipant(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + conversation = models.ForeignKey( + Conversation, + related_name="participants", + on_delete=models.CASCADE, + ) + user_id = models.UUIDField() + joined_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["conversation", "user_id"], + name="unique_participant_per_conversation", + ) + ] + indexes = [ + models.Index(fields=["user_id"]), + models.Index(fields=["conversation", "user_id"]), + ] + + def __str__(self): + return f"{self.user_id} in {self.conversation_id}" diff --git a/apps/chat/models/read_state.py b/apps/chat/models/read_state.py new file mode 100644 index 0000000..259c75b --- /dev/null +++ b/apps/chat/models/read_state.py @@ -0,0 +1,32 @@ +import uuid + +from django.db import models + +from apps.chat.models.conversation import Conversation + + +class ConversationReadState(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + conversation = models.ForeignKey( + Conversation, + related_name="read_states", + on_delete=models.CASCADE, + ) + user_id = models.UUIDField() + last_read_mattermost_post_id = models.CharField(max_length=255, null=True, blank=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["conversation", "user_id"], + name="unique_read_state_per_user", + ) + ] + indexes = [ + models.Index(fields=["user_id"]), + models.Index(fields=["conversation", "user_id"]), + ] + + def __str__(self): + return f"{self.user_id} read state for {self.conversation_id}" diff --git a/apps/chat/serializers/__init__.py b/apps/chat/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/serializers/__pycache__/__init__.cpython-313.pyc b/apps/chat/serializers/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..830c4325b828d40c1a7513c718bdfdbd3253d972 GIT binary patch literal 166 zcmey&%ge<81S{+`vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iWerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIPMgauTanV6GV1=129pP83g5+AQu iP!p>dO_mQLyzgLe?oHV725}+QK}qzsJNvvQ7=98yzBvK=te||9^jE8@HjK5saxiE24zZs%lGWc z6IkkwX;=7pzqY7T$1Ivvz2X%8cEzsJS%k$7StV3Z2@R+M4X%o3xjaUux>veR2f*pz zRKVM$5mg)^G^&QEv=LS!e8}&FdhG2%81SbY=^7xN44m{jLPJ@I>{kK}t3evs5S0+W zc(&n*hlZmcNMJ(|dB7}j(b=vVJXWNJ?ieLaceq?Vi3R(OEGSVP)wGJfWoR0YY1)=u zs#&;}HSKXtx4I|&nzn9I=2&LMsMwlDF?2ixXv5;+be=*wVGov zfF=Rjfgj=d>@wtxWy?A%mvk$;NbOCd=&-+*@hpY%Nxnt!RRXAeB z(GkoAyw@|9g+jDv3shB@5eo4$_5)MhZxs?}oCZ}<4b2>5J|;?d0Vv+@f3^k*l{Rh2 zfSbuk4dMfPOMnyCE!_daiI&M6iZ#k3_L>`8%#P(4lwyQ<)TSl4wYJUUPjssW`N11d zJt8x#bsmIE3?q@r&`Tf5`)jb`T0>RG4>`g{Cs8i!i#}tD|?BFPDD(`-V#xYdAURyEhsUX zf|;H}kw!6q0#nKdPbv(!QMOBSNK4v}03-svhT=R5wB=dFS5nNbE3h!x6%atB^k_SM zvzflxN>A74{}ltFyZ;an@1mhl*CV`ks*I1D<9~3{TgD&iC(v=uXnnqp69ojmyJz?i zF+dFU9D_PWKGeGj;Kw+GEhR=QgQV%fAxOLIWKm~L72Q>{|7EFPuC(tYc ze>{Y*m7pkLvq>DnWJqPjOiQ3 znH(Ai+Y|*pju{|;L()K78f{9WEh$r<-H#8oP9Q|X+7_< z?RqnP{l)j~Y_6HjwX$=K^q2K{7p62*N-H()gZ-X+In_wt|6$%^F@ASJk$4|9z|D0E z?fj`tA9;0=#Ev?Fy>-Iv*UW9+UYF>9h#mYSHEtE~m~X><*XerU(L3JX#9ev^K8$^h zeFoxfP!NPa$tQo4*~73b#CBKOmv1*N-##Ru9FB&CyzuMA_SIXBtGC_})LiHP2Z=-J A1ONa4 literal 0 HcmV?d00001 diff --git a/apps/chat/serializers/__pycache__/messages.cpython-313.pyc b/apps/chat/serializers/__pycache__/messages.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e9a52699dcfbed8c51ccc483dea151db814f8f8 GIT binary patch literal 948 zcmah|%}*0S6rbsqe(ciHra}s$5J(NnfxUVP#{pS7ly|<^CjEdy>`byvVj1l@7 z8s}%Aube)R$^kNwiC57I20V$1XbPFJF=Q%Rcq4uexO~qmkq*jsTpu_I4IK-OQE)8R zc}Ia&FOw0vsPtWW56~6@Y$Ax67?iE}#u>w_=}1itNGqV3iLo4KGDhn7=*WF;o z%>fxtUmK5^2{XA61NFZdn#?F(!y&pOW2PXxoz_*6e@+>*wyAm1i<7W{o3cNu<+|P; zY5J}!;+($a(wDSp&GKEoq{kpB^F#{huI~V~KcRJfNTdj9S`A8wPzh;xw(o}eAR)WH z<@O^(gwz~h+;y6?=@A0qbVPcwZo!g6U0Wnu9^=HZMY>X1E=RsJqfMJa=pAfClnLK& zQAmYg#Rpep$`+@aPJ>1WwF*=^Y+`XSObFu^cdF9h;Jo%ZWwNj)v8VDu;;B)Q+rW&v z#p<@@8ZW@xp;gX|mrj#;o>8q^+^||L7HL;D$LOK9G{{bO zv(w+QrS`L4?q-lHb#tYo8=c&}_HwUqJ1ER_3o}O}ox=V0T2C7Zw8^eEd00N4?`Tij z<(@hms1sdv;_&WqsiQt_FZS}|L4LNIpFOg_R66<9_bV`ZUU*%Pi!4ytzZ%Fv?0N7e z>aTh^;fQAV@eyuJY9jS88rLJZB~AJBAvP|>F9l=#1Ks(ZNZ>X6Q45N5o#Nad6k2EK Ezel*{JOBUy literal 0 HcmV?d00001 diff --git a/apps/chat/serializers/__pycache__/read_state.cpython-313.pyc b/apps/chat/serializers/__pycache__/read_state.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..643a26c446e28987c073cdaa8157cf9f0eaba85d GIT binary patch literal 714 zcmah`O=}ZD7@o0UxO%UH<8OHXuw=RxCSddc z!3Bv(M4yv=3iKAaPVSJXR3nkoqK)!YFuQl10)ul@4i+vg3245e1xTLAGXc6i3nrvA zA)_+{7o(o6E5rmB2l6f^xpm5Il|y zHzNAm>!ls=3pof@@+HPnej*slUB*sSoFx{oGIpBrWb$X7u|o;kBr+ALVhpSo`Es-Q z?8#Fpk~m*)tMnaOz>QQXlW|_j&=?C=A1jpzo)&R#hijIlfF0%67}MC5Y%^{Q$Tyi0 z8dFy=^$0h~_rfM7(IIL&F;BvmppHe`=Yn*9Wzo MueF`u1j$(Y2b(pky#N3J literal 0 HcmV?d00001 diff --git a/apps/chat/serializers/conversations.py b/apps/chat/serializers/conversations.py new file mode 100644 index 0000000..8b3f322 --- /dev/null +++ b/apps/chat/serializers/conversations.py @@ -0,0 +1,27 @@ +from rest_framework import serializers + +from apps.chat.models import Conversation, ConversationParticipant + + +class ConversationSerializer(serializers.ModelSerializer): + participants = serializers.SerializerMethodField() + + class Meta: + model = Conversation + fields = ["id", "type", "created_at", "participants"] + + def get_participants(self, obj): + return list( + ConversationParticipant.objects.filter(conversation_id=obj.id) + .order_by("joined_at") + .values_list("user_id", flat=True) + ) + + +class ConversationListSerializer(serializers.Serializer): + id = serializers.UUIDField() + type = serializers.CharField() + created_at = serializers.DateTimeField() + participants = serializers.ListField(child=serializers.UUIDField()) + has_unread = serializers.BooleanField() + last_message = serializers.DictField(allow_null=True, required=False) diff --git a/apps/chat/serializers/messages.py b/apps/chat/serializers/messages.py new file mode 100644 index 0000000..833568b --- /dev/null +++ b/apps/chat/serializers/messages.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + + +class MessageSerializer(serializers.Serializer): + post_id = serializers.CharField() + sender_id = serializers.UUIDField(allow_null=True) + message_type = serializers.CharField(default="text") + text = serializers.CharField(allow_null=True, required=False) + url = serializers.CharField(allow_null=True, required=False) + created_at = serializers.DateTimeField(allow_null=True, required=False) diff --git a/apps/chat/serializers/read_state.py b/apps/chat/serializers/read_state.py new file mode 100644 index 0000000..23a5c17 --- /dev/null +++ b/apps/chat/serializers/read_state.py @@ -0,0 +1,7 @@ +from rest_framework import serializers + + +class ReadStateSerializer(serializers.Serializer): + conversation_id = serializers.UUIDField() + user_id = serializers.UUIDField() + has_unread = serializers.BooleanField() diff --git a/apps/chat/services/__init__.py b/apps/chat/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/services/__pycache__/__init__.cpython-313.pyc b/apps/chat/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2d6c02f5d370698dd3aa87de5e8e63dc3fa3177 GIT binary patch literal 163 zcmey&%ge<81S{+`vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~i8erR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIPMgauSvmYJMdtREkrnU`4-AFo$X hd5gm)H$SB`C)KWq6=)>L%3=`XBQql-V-Yiu1pu-qC_n%J literal 0 HcmV?d00001 diff --git a/apps/chat/services/__pycache__/account.cpython-313.pyc b/apps/chat/services/__pycache__/account.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1830d01be743e0a35cab77bc8fdc3bf64c96643 GIT binary patch literal 630 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 literal 0 HcmV?d00001 diff --git a/apps/chat/services/__pycache__/storage.cpython-313.pyc b/apps/chat/services/__pycache__/storage.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cde293b6f118e28bbe043514809767ad62b0776 GIT binary patch literal 2290 zcmZuy&2JM&6rc6(`YRjfi{iuy!33ykwb-IbB%l!lQlsXBgnF}7m62AP^~BlW?7Fk- zDoHOrRF%_G1rDgKidy&7aLbWD!!!r1A#f=@a0{do*S^`!+JG|B&b)cw`{w;-_S@T| z2*&UE)ba%pp?}!MAHo5#KLf-)BqN!dM#Nh=!ofZ;9hl+?pJY6KT9^`v=+eS;a4JMX zNz{vmkSr#U92^aJ-blE}r9v;*W;Vr9u~IP#BS3_0>^O#5v>Ef7VH%e18F+Iumyck7 z58Xs8J_*Pi;pIRA5kcl}in4H191&Q&Wq}0cAPE&isW6SnjzzR0mNEI-(D921xX`&b zyfX$5-~K7!-b4AOEPs$Rf?t=>DAR#qatil>N1{R-LU|~hH{`(Rfi30^^}@?S(@P*9 z^6iwvwZ&G&dtGNyAQ>mXp*ibI~YP2rf{`(~z?h zN-leqc6!^(6BAikR;ROH`W#TmW#<_u#nY&!>)5u{TX>B|ZLAaQxO5O`@JhQpFISX_ zX|S32?D9Mn^d-%q;*#T(Z7Q&3oxhODq(U@Q#E#}Tgs>{n=nbt@!C68qLRcwiNYzUQ zHXR@qD*7$#(1^OA*;uWRQc9qLjZ2F(jLkyXGE9ebGMk{SFX0tzGml9a^m0bAF|jjC zn!QxeN}21#TE@C#XTC5@+p;n&xr|mW+a3kFtSRk`?KNq-yhg*S3IK3amB!z0-Lx-Y zptY+oeT#N`68F#5PmgXpJ3V97@UxEiZuda_*rlEB@wznrysP(qZ|&mt=uX%9YH&9` z(2$Po#s}}O)>EH6oZpFGd27??>3bOwd!tq1w)CHbdXF=`v4@L0@hoFK8yLD9-wIZP zje((G&eeu%tF`5N-`T40Q*0k{K{UK*l(4C-U>byTst{8@fDDtOFnlh|zIz8|3MX^^ z{m^VgsIJ20-n4g}AT)Zg&H)Tfz6Cyo_rYfh`DU}`Sk!mM5K@{ztw6(%GvBB}3O7!!k3L?qKJP?SN#3 zav8XgB|xzQz)+pFGETMA5Hz=oqf-!&@^I!fvSO@YCbns`SYe1|8^X?y@=JJ?#2FPW zR~*$^V1OVjZ5l8v8oILPV0&_o^Z_RoCJ81GSSyheE?LkV4bW$-WAZ*@a4K!{oT#kX zY<9#9du)(nOrxz*E?HVZWlm_~9T2@CW1;K^FaeGRPuB`tS04`!KN=jaI-6@7Yq!67 zc%&-qc68qv`Eji(JWrf_oJjwjNN=s~B+hMgRHbJf{f++7>df=L!QbM4^#9ubOMC5e z7Zpd(R@)k3X;a#e9*2)V3LkGsotrZoGYu)eE5$aaewe!B>_~vd&PLzqy*3m{zC^)D zbT5W_;yI39PbxTXwqvcNQ)wEf)zeleEG4bQQWT!aJZJpdhY~4Qj+Fd bool: + # Future: call external Account Service HTTP API + return True diff --git a/apps/chat/services/conversation.py b/apps/chat/services/conversation.py new file mode 100644 index 0000000..8a0fe75 --- /dev/null +++ b/apps/chat/services/conversation.py @@ -0,0 +1,65 @@ +from uuid import UUID + +from django.db import transaction + +from apps.chat.integrations.mattermost.client import MattermostClient +from apps.chat.models import ( + Conversation, + ConversationParticipant, + MattermostAccountMapping, +) +from apps.chat.services.account import AccountService + + +class ConversationService: + def __init__( + self, + account_service: AccountService | None = None, + mattermost_client: MattermostClient | None = None, + ): + self._account = account_service or AccountService() + self._mm = mattermost_client or MattermostClient() + + def create(self, user_1_id: UUID, user_2_id: UUID) -> Conversation: + if not self._account.validate_user(user_1_id): + raise ValueError(f"User {user_1_id} is not valid") + if not self._account.validate_user(user_2_id): + raise ValueError(f"User {user_2_id} is not valid") + + mm_user_1 = self._mm.get_or_create_user(user_1_id) + mm_user_2 = self._mm.get_or_create_user(user_2_id) + + MattermostAccountMapping.objects.get_or_create( + user_id=user_1_id, + defaults={"mattermost_user_id": mm_user_1}, + ) + MattermostAccountMapping.objects.get_or_create( + user_id=user_2_id, + defaults={"mattermost_user_id": mm_user_2}, + ) + + channel_id = self._mm.create_private_channel([mm_user_1, mm_user_2]) + + with transaction.atomic(): + conversation = Conversation.objects.create(mattermost_channel_id=channel_id) + ConversationParticipant.objects.create( + conversation=conversation, user_id=user_1_id + ) + ConversationParticipant.objects.create( + conversation=conversation, user_id=user_2_id + ) + + return conversation + + def list_for_user( + self, user_id: UUID, page: int, page_size: int + ) -> list[Conversation]: + offset = page * page_size + conversation_ids = ConversationParticipant.objects.filter( + user_id=user_id + ).values_list("conversation_id", flat=True) + return list( + Conversation.objects.filter(id__in=conversation_ids).order_by( + "-created_at" + )[offset : offset + page_size] + ) diff --git a/apps/chat/services/message.py b/apps/chat/services/message.py new file mode 100644 index 0000000..e62fce2 --- /dev/null +++ b/apps/chat/services/message.py @@ -0,0 +1,118 @@ +from datetime import datetime, timezone +from uuid import UUID + +from apps.chat.events.event import MessageSentEvent +from apps.chat.events.publishers.longpoll import LongPollPublisher +from apps.chat.events.publishers.push import PushPublisher +from apps.chat.events.publishers.websocket import WebSocketPublisher +from apps.chat.integrations.mattermost.client import MattermostClient +from apps.chat.models import Conversation, MattermostAccountMapping +from apps.chat.services.account import AccountService +from apps.chat.services.storage import StorageService + + +class MessageService: + def __init__( + self, + account_service: AccountService | None = None, + storage_service: StorageService | None = None, + mattermost_client: MattermostClient | None = None, + publishers: list | None = None, + ): + self._account = account_service or AccountService() + self._storage = storage_service or StorageService() + self._mm = mattermost_client or MattermostClient() + self._publishers = ( + publishers + if publishers is not None + else [WebSocketPublisher(), PushPublisher(), LongPollPublisher()] + ) + + def send( + self, + conversation_id, + sender_id: UUID, + message_type: str, + text: str | None = None, + file=None, + ) -> dict: + if not self._account.validate_user(sender_id): + raise ValueError(f"Sender {sender_id} is not valid") + + file_url: str | None = None + if file is not None: + filename = getattr(file, "name", f"{sender_id}") + file_url = self._storage.upload_file(file, filename) + + if message_type == "text": + mm_message = text or "" + else: + mm_message = file_url or text or "" + + conversation = Conversation.objects.get(id=conversation_id) + post_id = self._mm.post_message(conversation.mattermost_channel_id, mm_message) + + event = MessageSentEvent( + chat_id=conversation_id, + post_id=post_id, + sender_id=sender_id, + message_type=message_type, + payload={"text": text, "file_url": file_url}, + ) + for publisher in self._publishers: + publisher.publish(event) + + return { + "post_id": post_id, + "sender_id": sender_id, + "message_type": message_type, + "text": text, + "url": file_url, + "created_at": None, + } + + def list_messages( + self, + conversation_id, + page: int, + per_page: int, + since: int | None = None, + ) -> list[dict]: + conversation = Conversation.objects.get(id=conversation_id) + raw_posts = self._mm.get_posts( + conversation.mattermost_channel_id, + page=page, + per_page=per_page, + since=since, + ) + + mm_user_ids = {p["user_id"] for p in raw_posts if p.get("user_id")} + mappings = { + m.mattermost_user_id: m.user_id + for m in MattermostAccountMapping.objects.filter( + mattermost_user_id__in=mm_user_ids + ) + } if mm_user_ids else {} + + return [self._normalize_post(p, mappings) for p in raw_posts] + + @staticmethod + def _normalize_post(post: dict, mappings: dict) -> dict: + mm_uid = post.get("user_id") + sender_id = mappings.get(mm_uid) + msg = post.get("message", "") + is_url = msg.startswith("http") + created_ms = post.get("create_at") + created_at = ( + datetime.fromtimestamp(created_ms / 1000, tz=timezone.utc) + if created_ms + else None + ) + return { + "post_id": post.get("id"), + "sender_id": sender_id, + "message_type": "file" if is_url else "text", + "text": None if is_url else msg, + "url": msg if is_url else None, + "created_at": created_at, + } diff --git a/apps/chat/services/read_state.py b/apps/chat/services/read_state.py new file mode 100644 index 0000000..679b9c9 --- /dev/null +++ b/apps/chat/services/read_state.py @@ -0,0 +1,48 @@ +from uuid import UUID + +from django.core.cache import cache + +from apps.chat.integrations.mattermost.client import MattermostClient +from apps.chat.models import Conversation, ConversationReadState + +_LATEST_POST_CACHE_TTL = 60 + + +class ReadStateService: + def __init__(self, mattermost_client: MattermostClient | None = None): + self._mm = mattermost_client or MattermostClient() + + def mark_read( + self, conversation_id, user_id: UUID, post_id: str + ) -> ConversationReadState: + read_state, _ = ConversationReadState.objects.update_or_create( + conversation_id=conversation_id, + user_id=user_id, + defaults={"last_read_mattermost_post_id": post_id}, + ) + return read_state + + def has_unread(self, conversation_id, user_id: UUID) -> bool: + try: + read_state = ConversationReadState.objects.get( + conversation_id=conversation_id, user_id=user_id + ) + except ConversationReadState.DoesNotExist: + return True + + last_read = read_state.last_read_mattermost_post_id + + cache_key = f"latest_post:{conversation_id}" + latest_post_id = cache.get(cache_key) + + if latest_post_id is None: + conversation = Conversation.objects.get(id=conversation_id) + latest_post_id = self._mm.get_latest_post_id( + conversation.mattermost_channel_id + ) + cache.set(cache_key, latest_post_id or "", _LATEST_POST_CACHE_TTL) + + if not latest_post_id: + return False + + return latest_post_id != last_read diff --git a/apps/chat/services/realtime.py b/apps/chat/services/realtime.py new file mode 100644 index 0000000..190fe10 --- /dev/null +++ b/apps/chat/services/realtime.py @@ -0,0 +1,12 @@ +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer + + +class RealtimeService: + def publish_message(self, chat_id, payload: dict) -> None: + channel_layer = get_channel_layer() + group_name = f"chat_{chat_id}" + async_to_sync(channel_layer.group_send)( + group_name, + {"type": "chat.message", **payload}, + ) diff --git a/apps/chat/services/storage.py b/apps/chat/services/storage.py new file mode 100644 index 0000000..eff1738 --- /dev/null +++ b/apps/chat/services/storage.py @@ -0,0 +1,42 @@ +import io +import mimetypes +from uuid import uuid4 + +from django.conf import settings +from minio import Minio + + +class StorageService: + def __init__(self): + endpoint = getattr(settings, "MINIO_ENDPOINT", None) + if not endpoint: + raise ValueError("MINIO_ENDPOINT is not configured") + + secure = getattr(settings, "MINIO_SECURE", False) + self._client = Minio( + endpoint, + access_key=getattr(settings, "MINIO_ACCESS_KEY", None), + secret_key=getattr(settings, "MINIO_SECRET_KEY", None), + secure=secure, + ) + self._bucket = getattr(settings, "MINIO_BUCKET_CHAT", "chat") + scheme = "https" if secure else "http" + self._base_url = f"{scheme}://{endpoint}" + + def upload_file(self, file, filename: str) -> str: + data = file.read() if hasattr(file, "read") else file + size = len(data) + + content_type, _ = mimetypes.guess_type(filename) + if not content_type: + content_type = "application/octet-stream" + + object_name = f"{uuid4().hex}/{filename}" + self._client.put_object( + self._bucket, + object_name, + io.BytesIO(data), + size, + content_type=content_type, + ) + return f"{self._base_url}/{self._bucket}/{object_name}" diff --git a/apps/chat/tests.py b/apps/chat/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/apps/chat/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/chat/tests/__init__.py b/apps/chat/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/tests/test_conversations.py b/apps/chat/tests/test_conversations.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/tests/test_messages.py b/apps/chat/tests/test_messages.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/tests/test_read_state.py b/apps/chat/tests/test_read_state.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/urls.py b/apps/chat/urls.py new file mode 100644 index 0000000..f332565 --- /dev/null +++ b/apps/chat/urls.py @@ -0,0 +1,5 @@ +from django.urls import path, include + +urlpatterns = [ + path("api/", include("apps.chat.api.urls")), +] diff --git a/apps/chat/views.py b/apps/chat/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/apps/chat/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/apps/chat/websocket/__init__.py b/apps/chat/websocket/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chat/websocket/consumers.py b/apps/chat/websocket/consumers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/core/__pycache__/__init__.cpython-313.pyc b/apps/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..461e6fe948f9f694112658e22a255effe501c69f GIT binary patch literal 154 zcmey&%ge<81by?=vOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iQerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIQ%esX?Ms(yTYW?p7Ve7s&kMLI^14(mlnK2qJiBv}hMmAux=))6K-q&iZB|$sG`;*RAfFajhoU?V2zMfTyZ2sUF6 z{jAyd%|@+%QuTCx`+@O3@VW6nv3$l&SS70GB{$bHu4%o!k#d!wD7BDDxHgh7t)n;p)^f}DswhBHz-w_X^~K- zHH!B$%@w8S#vO!hsvPv&=6X6|MJD$WS-9)b{htY}7oID0QWL&#q?%gQ&oq_L z+_aHr)(FSyt##8|?tJ@EJ&DJ7*HZU)YGA{qM+iBG?m6sU1%$jK$M?>5dZ#sk3~H_?%?hMVUn3>;etc(g9}a&p^-}#ns%mlc9wB^tnLZU zR4^0`OwIi_hSFekk(scXTfMVZ@ajEYRZ&&bUavziE^jv{*O>o1@gA)?nQkJvA&fA( zPySG#-^e=oL74ZMFn>q~ep3ft7KE)QCqinMwySDam7_dM8oNO{7t=3DZpe@TWduA% z!Dk)>%!d{Wpq;hCw(aaHkj;#z0L#z5rV~+SDoT%|6P3zBM>~18T^sc~mqmKEB&teB z2{zr4gK)tva$brP$+_)tjtzC;_y^8UYf;RdWzI)AXj9~+EEU@QRE|Vl7%p%wdN|ky zuGp?}u8lBx!i6!AAJ;}|&S3#Jo4Tv)AU@I(bUYS%oQfho1T~R~(edBB)Jnz4SeV%D z7`q*3EUNy2d-Dw3jk;QSbp~pLf>}hFK}bPJ-%soOEiTak=eyx g&AD69N9lg9R}h|kO6eo{{?hj7r>l)uf@#+E22zEG9smFU literal 0 HcmV?d00001 diff --git a/apps/core/__pycache__/models.cpython-313.pyc b/apps/core/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b367e7a9934ceb7101ed63ce873b32eff4d6d542 GIT binary patch literal 5062 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 literal 0 HcmV?d00001 diff --git a/apps/gooyal_oauth2/__pycache__/__init__.cpython-313.pyc b/apps/gooyal_oauth2/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09157ec7115e103a983584655eeacc4e611cc1ac GIT binary patch literal 163 zcmey&%ge<81by?=vOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~i8erR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIQ%etLd>WnxZzeqw1!hLL`Jd}dx| mNqoFsLFFwDo80`A(wtPgB37W0AS;VOjE~HWjEqIhKo$Vriztl% literal 0 HcmV?d00001 diff --git a/apps/gooyal_oauth2/__pycache__/admin.cpython-313.pyc b/apps/gooyal_oauth2/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be781c94da33110dd6c4758d8b0a7ac043494a86 GIT binary patch literal 425 zcmY+Aze@u#6vvah>kld?E-p@S5eoJK#Zi<(rxuD>q~j5D>0Rpe5?)fF{t<4j?*2;$ zPJ-a36nArXI{1di_x<4WeaS8id&tG(^T}#~5qhhNzwmy=?$n7VG)I8uc61 z+MoK90D@Wv)7GR7ZGzHPzx^=ojaRHl6=BI*7KE!}Dbo!wD^)?33W&`FNtN2Nd;r2& zklYRoS6UdN8NXwxhvzgd94Cs$ zak-z=H$=|e8Qg;JaOKheW>m8yBdstHR11fSZEQA-HK==;2xvB-5UO! zXvR#Iu$*3jS_y6qUC6>HMR~?7Wm+5BJTY#NwDHp0vU|Q($tD*U5IUawcw`P;`t&iz VU!I49?=Hf}ujuR@4PnnsegLcUeXjrj literal 0 HcmV?d00001 diff --git a/apps/gooyal_oauth2/__pycache__/apps.cpython-313.pyc b/apps/gooyal_oauth2/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12ea8e5d7ce176e4eee3fe76a391770509779c32 GIT binary patch literal 544 zcmY*WK}#D!6rS1LM2!v6prtLAracII*#$xHAc1JHP$4Z0(o5hm$!<0q)}8gu>_M-B zM{j~Bp~wCkk22t?CvSz+duOuo&^f&Kz4_kv=DjzwX0t)exxU+;{xtr#4PH)d0cP6< z+z>_>Jtki%(hhk;J`v`;Bg`GpUAM|ZC-MB+lMQ^TS9(QpDDrWdj4HaabP>$947ec! zf|L<-7)6&k=rI>-%)@$8^Xq#3ixB5L`^HN(sY*S0Ms@K%n({mmK{O0bMHFW;I7pLT zsl-tlXVFEQ7ljOxxfX;K@BAWte@QO`JNEE~WI5eEeH?Xy>il!u~RS8QGQH4}RNuiEn8WOQ2RVA0z+B>lqmR<99 z-G-bvRgo(7P$}0c>IrT+wx_12UV#f}q@HrAZeKsjr4cSCFw$G*nn?yEMj_gWHXUV^__<_S{2cwaR>d)>~jM z&+JLw0n-img}v?!dkoC4heEx3?A=L0RKsnBsI@yge_`XfRF**2C5Y%UMD++LWpyTf zSUhZ_kD|I#j?ZMf$8aR7tK%xfQTKfJxX_L2vGE8bQKIiyiMX($jYlDkG8d1PN#Mw( z?h&Dx99N)M_&kL^Pv3}ya*)+CaIO}~_wdy112?wF3DdL1HI=6tU@w}mY%H*4t})9F z%!2HXVnn6~}ZJTK}M_e3T~_%_oM#+*(iv zasuQKo+g_<1Ap`(BFZE@*T9IgfE|c*#70IPdUpB|x?`h9L9Hoy^=bCdUPkY+z-;xR zLUB!+UW;+fFkEwy83s=p2Buikp?2CZmYSy19_clV$2RzZW4p}ta9A_j8OYx=3_rjO ztQuwzfL&<@%r^`epk?7J@mmGjj$f#oe%&&i!UOQ;SvBwrU)rwkd4+1-3<_qW;TLM2 zw`@9wXEuZS^+K5Kqm5;*(2{xACTI^kafI)r?Ogx&8hn6l_>h2R&!2}uYEufp5W)Kh zJYriwb;}bEAKslT+RU-I(rnrmR|&^>qNLxS4*8gc6Wb0?Ow>&XsYKPWnHv}=pDcR7 z?3z1;nGk<4bAR^piCLa*k9*&`48p|>)7-S8aDkVpMdy-D;`ky>1t+|J~-GDDl0p%-JT!A53mrMR8#+sY1aW`|cN zURtk4HnMkC?r!G>wsIqzxsey;UmpE7_v_sH%%ko@f#E zPPjSV{#R;Iji1TG>9(cNxgVps6P??kLK>&`YmtNr#L;x%SEBMg;=tkwYuL2+!{AN_*|~ P+_i(wGw%%zh*|#yhP-rr literal 0 HcmV?d00001 diff --git a/apps/gooyal_oauth2/migrations/__pycache__/0001_initial.cpython-313.pyc b/apps/gooyal_oauth2/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35bb56fc898b5386d1682db8a3a9c52f7feea9d8 GIT binary patch literal 5915 zcmcgwOHdoh86J%uNW46f0fWGJ5ExswK^SANVRya8VllABU=7&LE|u{}Ge~2PX0$yc zFv`nSsmd{xOR6}fIBss_6prMSJ*7t4gQ(s^T$K|yHcl$X{rLLt z|Ns6+_vlSiQ#}KJzx_l1bFqzK{y{qXf8I*t&0}c%o)H+q^%;X)^vPD9ZsdkO`~r6?DpfBBeDav_6I5Tqb`urP)iCR8Y!ab6ht#coB>4mBuxZ% zsX5Vod`93vI{5@geV{`K*7L_vz#t3%&W^HxX#q^62BsBIZG@9pKsYCGLoT6x)Lqjn z9jD0xU-TW{CDDI;ObElSoueMmtqXKJ(aV51FLYD=FPwtmg&rY7ecD^o%0A&@j1~HY zOH|tBTDTY^L?5{ha93)?R`djWfa@C8UpWDNRcIBije5cQ0np{zaRl|=AdQb$Ep36H zR$+)*biJk&PH?3B5Rp1uTN7aJ1~A79Bf?GU)mwnAiJ4EblVYH^+ zB=*NBgYQxgKCER^RXzG~YQ{%3En>k39}9PAK74Wt>aH*j8NmzpD*lR}g5@F39ElUP z7yurfiuk@BZFmQdekx3Y9w*k!Q!3_tVTwlPyLkG?@#6b(<$DLNrs*kqa_w&wW@v@^ ztQMQ#RdUWepwaY0RFGNN>BLhPc6sG|j*9GKm|sq6-z+%DjT z@l;AN%?0CyqGPW`AI`RCQT-3HKf8rKl--+R9*GURS@& zE4WU1nNyKsVy~Gpa!S;Ny-7{dU*NhFQY1@}VWd&YTZX6`tD+>!xGtZQD--_x3GB(| zRe3V%LyfR0u9KBzDX&@BugIz;B{c;%CvUEf_=-XqRA$%Gjh(Y zu373~W>J-Y8oL9hn_S!S%Qh8n0f#lQEF$SF?m<72G{tX_3*KWSXl6k_+b( zifQB#uxG653gY)C29lCVh%AI0(*_{oDh8Q_P@H<7uNnhn6NIR)$^)6TBvR!<>l}2a zCqNNh2j}t5Fm9?)Wh$`RDvV4?W(J&T=)54UD1&_5$gT0J#Y05%>N0O-RFjAQ60a+( z;9;_8FzR(2K;qen7UBhmxKIWJibpnyx*U*0^kJmpkoiK*iAT7^0ZB^(?^Y%obweCo z2Em0h4-I~9dLqu7`CQIG7BJ{d2*WpT6+#JNoL>brFHDUO4D&EXCa37+*{Rv__*=8P zlr&65q-CQq zp)5lQ%uqadl4{lHRMTZCt7>ceN>d56_|zd~1*$HTM_P>j2rrZ!*e_CnqKNB75nd?s zngZ=cQT#G5X=E$toe{-l6`7W%>WXf_K%J_qP7=!sgfnD7N`(|9udKKsPK)p?c{fEK zG7?yTQZq@)96n=^uo@O~$XJ0&fN+Q!5U$fkaZb zM{r<9n0r`3#4{3dn)PV~@>Q}7Wa3bz4es18%Q)!ds6aEuS|20w}|!f|27NYcrY7Mn-Lb4YeG_5|YB zFk%p1R!qv}%vjnm)+9}IXo(%Z+{cE7hOWaINNFC-tsx)CrC;PQHU9?r@*C!LbLiFm zJ&&u!_v*o(p9!2T`@2j2?ww%w>tOqOe>r@m6u$CJ_?pcP>;!u@J}LK(7JEm_y$_1L z5A4UE+iBHCD>k%yS8eX)PB6SaS?(Gtbq#&fHEeS?b`FJXwiLs+Z0>_9%$8j2ird^o z^-y22>l2&1yAwRO-d662mO7%FBik*14gV!CqR zPOxMBR=M+Psq^Y)-_}C0^PbJccYD^HrmNfUM)XD7(Uv+N_H#f1dbLI5rJ zjs9|ExD*-QYO$xY#fWZm#!m3sSIgzWJEg%p+Y7d^2n&<01X&mp=)wh9*uCK^^Ft+m z$R7LLUdq{{Ujpn`u5kkExkq7ffL;CSae3h5(!j_6WSIM|ryc+)p0o!*gBj8Zsve?5 ztQ@&rjNC3qW{Qy+`_ZEPOt$r3{D)x-*EEqx9<=w&={+MGXl&9A4+DN_vLiFl>NQC} zXt{TS7dKYQm&Qt$#Pi=0p8h5jJY1-yys$F}Uw-4CfTmaK1 zTvVgytVR@DV+%yh3qPYY{2XYAY)qE>MoN7nTO(hG0P)oIkRUG3QN%nTzE||@oj(Hs z5vdA@i>(k4*Qx@d{dJW{VY{W+F==!6tE}6P_X`_s<(})Mp6lQA+_1Tk_eg-Aja%j3 z8>QYGTYX=%_LI+xyj+620HRq1}As{U;PX=66`;ZE?t zyHFJJw(z9b^VH^kQf0@fCGDU%ZbB-2XmcNZU&>EbTVS8KLcU+mAad2G1wQJ>;kS;- z!NXGxk)N4y+uH!=r(oxR49L$VJ#7riNqQpFL)bY#$O+|aq!20okDLSl4pO1(f{%F# szV_TMm+Os>aYepm&VS1^{GGY{FW*mHF5mC!%UoZP>-#rDdQ{N=02`2m(f|Me literal 0 HcmV?d00001 diff --git a/apps/gooyal_oauth2/migrations/__pycache__/0002_initial.cpython-313.pyc b/apps/gooyal_oauth2/migrations/__pycache__/0002_initial.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cedc8f414ff3564d006e0a34c5e52f3f03711b9d GIT binary patch literal 4655 zcmdT|Pi)gx7`L5&PMXk?6k1>^rIga3DPc%xtAr3)Ynry9>0+k^sp82^UQ!p=&Yqp2 zGO;0T+Hu-t*PY?C9e3PL+fF0h!PS$dA??JisGD}(d$vXfMP3NgK^iz^&{QNabrrWJGq3K`be`Id0a6e8aTQH|L)XpulVp z1&5hIW`q&_CmAuY94H)V0zEG;`p%_#8dY z=8*~CzASsal{|!KKe(FRJl^ZA=Ii0C1@C? zeg|qeT2d9LnLxj*L6r34z=|qscd%cpt11rb8X)NvG$H0pJ?SH<7frW-Ca*&>g+2on z1*&E2uNx5E8#p&E*J?nOSD>0Sz`5~~DjP=9NCr@hFhexoLnMi0a1=pRHeuO@LNx0l zv}!d~DcOk~4v-8_Gn1S>+a6E)ocb|}VJ`?NWyc0yDFfFU$Fyb(!3u)LsxuaITkc@L zq1RCfg1x=R-6mny3iXI2p(im9Y`Os82mnNhx(Z1>3c!bTS*2=_Rxbb*1sSHQXi(Eh zN63{O`Kgip#$95jyh2u%VGU|!sFgMl&3SQerYL1WQ4|W`Mt(}j;ppxR1sa&ol*`wt z)`(`^IC5P_P^oHja03U+YqD0=F;`x}yzMWN8{^PIAukD&5)Msf#L3K*fY0RV7MB1S zSe%(6`(zdtax;?|X(m7K?itH#P}0f6-U<)BxMF4sNclOTIh_{;3CtIBIXv>>%*^DZ zAc}itpM7yoLAYKJ#O$6)5xoYR+5Is943iv)5|B+3DJyjo8UUcvM1URg&)^TzMRM+p z^r~#EmSr`)fb=z3GL7_-q8YlLF0IODnh0*B_XI@x`8S9(35`q5&`8xbP>A5#4;o9x z=frDYFi(!eA53otnec@zK5p^nH~I4qd29SyWBl50d}ce~>kmH2ZAY1K!s1VD@~5^2 zMz@Zh-ii-e@w1!pvkynVlYUtI>DG_8ETPyCik5J@A>6ivN<*kTzGM8uFe*F6J=-l3 z0{1NB4@b5mdl3f?gt*NxAF+9k6cV@05pao9DY#oC*8c>87E|Kv*D^MBl zvqj3wEfR!Xn~#R~@H&9kyIM*RTWIcs_>x{fWjkhUGd}iE`riL@@YnD!VJmyHk-ceU z414BxX7~?g{IB2~%Lc!SSbd|7zR|xK+Okpp1>Rh4 A^8f$< literal 0 HcmV?d00001 diff --git a/apps/gooyal_oauth2/migrations/__pycache__/0003_alter_application_authorization_grant_type.cpython-313.pyc b/apps/gooyal_oauth2/migrations/__pycache__/0003_alter_application_authorization_grant_type.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae77f9216d952418e4ecf66c86e026c22c198ce0 GIT binary patch literal 1121 zcmZuv&ubGw6rSBpHrZ{0HMLm8VhBZ8ZMvo6p+O3zMQN*5yNH*-Fxj0ZQ#QMCW@1ea zA_yMEORwI&`LEbZ%U(V8mKw#YZ#E50!G)cD-}|-ioA(WiMH|8M`Rm<-?>a(16&OF+ z3uCwj#u4(6r|qC^jceOF*UM-QRgkCOLY`6A_RMk7<@cd6vboXBIN37=96hnytjURF zaU>G69S1ZN{$&k9)&&?gz&Jv6#5E6b-P5??>8{2z_3U0@vS4JxD2A7*=k})m7ju!9 zeW`h-ms{1n{Hg(cEYIS0({`p3>)CjsGsOaCEH>k~M?xHvu57I&h2`bt70ecEPbnmGDkJ8spF#}GB8g?MLmku6`=u+@(zbIYRZ;l08(4W5oyyTlb+dqM#JD6+J$Z8aT>umu`!0s zZb-o{VEm>_LX{2Z8pivK3mLMAMzIP96b6CWeMSXWx=ZZ!P*VO_HRc)Rur^v`gcBQB zj5Romv5>Gm9}_7#Yjhu8N!Pa>An(_3%5l?D5D&hhjtT87b$VPWF+G$ap1|=54NPR+8yNZx^TX4DjjWlI z{QPl#{`BU;>Gk`krMZ(*<+xP&h0x;r%mAUC?6d4SLNmMB^IXQV2ZmWPKW_6os3 literal 0 HcmV?d00001 diff --git a/apps/gooyal_oauth2/migrations/__pycache__/__init__.cpython-313.pyc b/apps/gooyal_oauth2/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2cf833faac6f16a17dbfb9b751fd3e602a072261 GIT binary patch literal 174 zcmey&%ge<81by?=vOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iaerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIQ%etLd>WnxZzeqw1!hLL`5W_nR# xNoIatv3`7fW?p7Ve7s&k5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iAerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIQ%ekoX8e0*kJW=VX!UP0w84x8Nk bl+v73yCPPgQ6Q^|L5z>gjEsy$%s>_ZYg8qA literal 0 HcmV?d00001 diff --git a/apps/users/__pycache__/admin.cpython-313.pyc b/apps/users/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad9d3ce1e9b52458b1e5ad958ef547fabd9d2929 GIT binary patch literal 330 zcmey&%ge<81by?=vciD$V-N=hn4yf%K|sb-hG2$ZMsEf$#v(=qhG3>(W^X1h<|1YV zh8V^m4wx1M9mI`b!&ouQri>7_0z*2BChJQON0aduYhp@nX5LGX9FWHnTAW(sr^$AU zFC{B6FFju`IX|zYC^M-Dx%nxnImJb+AP!4$W=ZNTj-u4`%;J*NqLmDvfuanz z%=JM=6zgXs7H6a+=I93$=j$hDB$ntW78Dfgmx9%St=B84yv1Py zG`%z@)vkyaXb#9d#qvPn12ZEd<6Q=Yy9^Q^*cceu+PNCJt}_T!~;msS~Ox|iWkc!9G&gfayFWpR|G68uL23~ox|Bxz$>L0xmh|vH5 literal 0 HcmV?d00001 diff --git a/apps/users/__pycache__/models.cpython-313.pyc b/apps/users/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1616f7faf69c106a348a6d2bb21d458e3d685eae GIT binary patch literal 3994 zcmaJEO>h(Ebyr$RE3NFcB>xCwU?E^Cek3Ru=~MK7C{SsE_y*#QkIbQGo~y z7sdjkK@wzmU@SB$5|QD-vG8bwL>L|#lSZQ?%5ZV4Wwe#Z4Clw%NEJ(;!5sd9mF*?P(NrEZx8-7Vu@uml?xDiU3@WGs|z z12q)k0OP)^S^C2Kly@@^`f4xRX+R!uIgaouM|>(z{Hl)#s;COx+$>K5YM`4VK{W_4 zq=txC^rym9YP#rF3^YuRPt`S|Vsw%Of7^co@PNy56Y$)-z-WGz&+_N^tZ$gV#M}Ol z8#*S0z1$oaxvxgd`mLIoIWFs;fDqZc;6xTeyXmTLfLB1`IF}N}sc-H!m?%(jzysfK zsUQt78xj$OsNXK%qY_xx&}Pe~Z4{{B=ywh3*9!TR0566$O<+=iU^Y~l9{EAqLA6IJ=#qd~xygck?j`D*ZQgMYuW zBs>+98)Edc@bAJ8g|EcFic8@Y@hC_wN!y@++uwr(cEE$a2_AWNFO^q)eKntfUH9jK zcxvL|CK^88(FKtZP=%dH0RIHXsc1TyUKZBFi_Pia+0Z zZ+#MYDqeaiC)Qhqv<|qyUf#pOySQ0GC{|pGY3PAn1 zVO^E)tKkNp6($;S?(rg2eX767qWkj*dwA+)b4Ly6sh_0!Z}Ix>TLkKR>=ye;3w4ft zq=WT6kme?WO^4P{@VNL<6ER#}^Jy&KOG3yldDBx*DsTQmQzvk~b9a zG&v0NljI1%g=1kuXY%9hMLdt|Fa6I)xY4sv5;6>bVppp{^dJ(YLL%Tuj)uXSi7G#Z3rak+F zU}IMSY3C0B7P)`PT`%H2)k8yT@hjEnm6uInIs8;S-yD{Z=6x@vu660)igfVd=$dqL z*EsR|vh z1tHM>->49{{3aR=Kr3TH$iJ%*A_3R-mn!a#ro zK^802o^_1Ng|XPx(?boap#dL>XckG!>babMeDS4P@>^q z%YaB)Krh;0&#K+2wJxE`Ov0zO!S9gYDidqS;2H4&KWTd_f1=Kxi%np<>{)6%*dmc@ zZ?Z*)+5v+{r`k!nie0HB6)<16G9WlPk_u31j+iB#%xkxec`6zO)7A5qK|{5UK!X+A zyi+l#Sja=L6^#2VH@6l+9$J5-q~F&p!!Eir1h2F8X8;Q$6ULp2NsK~TIp23qfewu$ z7&LLEOq6Nko8fa46gPzQK(=O+MA{iGPw{#EJVvhbwxUEzn>?`NyO{-io%tw^QC;SITC zT|TlZA9>6_I`wtSx9_aTR~FxUcHsQlfeTfkv$6e53@@Gfv}Gd!q{Op$qS~HYi+{4w z-djzad>L*3La!#TuXPT;^81pJS3yorY_!Dw(;>A57vFo;%_R=3$9q@fy^rrKA6h%v z|J}783$;7MON(P0v99&liPhMN$NF;g>+-jgKME_c4;Du^;>q1&$sc;Y?^%hDFOL1Y z^}wg4#hY&gK5*9i{rJ;UT~s8Htp+3z4C}MHT`Z@k%JwwD7^R1HDA+*K5dAahJbY%c znVMiE((e6uyeXVu_oj#UV`wgwwhHAE4MKp!mkABxjW1oKA^e80%)Hm~nCEv#0~H6o?t6v&1{7hoI2bvv z{5;P;=j2UJ+2jT`xeJ@z^-Zp4lRLV}U3$(*&$*WR|JK*yL4KD1JoB2v=j*{Fe|Bl` K4F?Zq_5T6(lcNIw literal 0 HcmV?d00001 diff --git a/apps/users/migrations/__pycache__/0001_initial.cpython-313.pyc b/apps/users/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1eb08b14ba40211a0ec5218f4172a057a16c478 GIT binary patch literal 3753 zcmaJEOHdohbu=>)lK4sZ3kWf`@keVjf;HZa{7J2C>|MZi)@oqA3FX<5rX|`Q&B#3? zW0Xqbs#NVUmCL5cCGj;kAIT}?v_4A8K~(#&Ta^TI`im>pG$dA;Dq>YTYnX-smggd~U9@X9 zN~4-|TskQsWk^EmQdfRpN9gW)NeT8eRt{ytTO7ETky*S#%GC6U>v$|uaV>0n#a$*z z8tMRVec=C2S`rpf8HK%$y^kyIR)07+fNBumC+b{r_q_53Up^bKqsSfC?oRv(fg>tmyh;q z$zM1Cr|-w1^XP(z-8jgf*ncjp)ln8rph+~2J{GyS_yO7}L3;^(63}M*fb&zx<4<%2 z<(3BXQ~Pz~69?cv-H+Re@stcvzMNB`t5WC+9VuVUTw}4jJ3YF^$HzRht!mc%Wp;XE zV)A3dw0%sCng>56LOtJb%nCLt<}<^_P8rD51vq0p#525F6;n=myZ+@g_DI<=ee7xe zvgyMQ@icy>Et{TZ+O`I?72QAT;g-2nZK!(|HVI z>YY23j~W)U&5oyau@&?zj4LLwxBkoJ^oqS^S{B8g$D-VQ-z=5txjmk*EMqX9at{LE zi>9O5ZdoIa?*<_;tqO58kN8-x_Xr6c_gD->nnj;H#dUn3%$SE6efjAjOm_v67ySZ%RkxLwG)diph0K}2!ArKcSQ@2+2>gGJuR3p3MV zO9weqn0M?Hi&&3Lr|hz>qU-pS6lfF@dbwzLCZp?{T6o$oApjZ(&kA82cp9@ zpV%l8f%x)n`tCXfA? z9DQ-LePXJ0VrqS1TatbjGG$n*@(tyUB%Qpe0FazkHj^W3Puru})@b&nwc$0s#?8@E zBT?Q=rq)XBwBAbVFXuOkjR(bM+G-^5X7c#jnfB26*3kKvV;dJ63s!RoHxi}IJ zdt{OHM9+n8b9D=HQ3iEsivvk4cf?M%(v$0Bf{gN$>>bYIIguH3wE5TRYkw z%(MnGFQ+yRe?Rp1p~m7b8l=)#{WXw(A>ZLrhwcig_du#pKHeG`UqAHfa&u&{k@#v4 z^5yiabB+1m0OxGzI_J47FK|r-1@pyLs(e{he#MtL^}hNazI?5M4!#KJVE{c0AQC_% zfNq2Z<&7{TeombZgOpkI7GLJW5BXWz;6U=5@MbdoBGT5TTH4gQ^@=tgEHxfKYig^F zM7@)Vi>=X%>th@9jR#h9G>Ggj%i`ab{<_q@mTz9mx34WWuPrt(ZE9X4;cq4f*FI?v zPqv09*HzFp6SBFZzm#urpMI`g}oNcGZo2l`3D%VWq{*k)UNL+tassxw~$2q9Yw5(ct<{e+L63);Y5ZVlIhM6!aAD*1!hDg+#9g zWACT*0e~U+JpNSQRzjiB+lUkzd@Y@REu~&d-9Jd@{uLPuMSdS|C&rqIv42Yd2uc3~ DU~T`2 literal 0 HcmV?d00001 diff --git a/apps/users/migrations/__pycache__/__init__.cpython-313.pyc b/apps/users/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0cbd8bbecf75c84cc7ec80d9dc30d70195877d2 GIT binary patch literal 166 zcmey&%ge<81by?=vOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iWerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6VnIQ%ekoX8Zf1H>Vo7FxUa@|Bd}dx| mNqoFsLFFwDo80`A(wtPgB37WOAX|$;jE~HWjEqIhKo$Uc>?!d8 literal 0 HcmV?d00001 diff --git a/env.sample b/env.sample index 3a63c12..1dd3033 100644 --- a/env.sample +++ b/env.sample @@ -13,4 +13,14 @@ REDIS_BASE_URL=redis://127.0.0.1:6379/1 MATTERMOST_TOKEN=3fktq5yzw7yp7m4ty7d3nx8gph MATTERMOST_TOKEN_ID=h5h7unnmeigxdjk4yhtywxjkow -MATTERMOST_BASE_PUBLICE_URL=https://chat.addwin.ir \ No newline at end of file +MATTERMOST_BASE_PUBLICE_URL=https://chat.addwin.ir +MATTERMOST_BASE_PUBLIC_URL=https://chat.addwin.ir +MATTERMOST_TEAM_ID= +MATTERMOST_SERVICE_USERNAME=chat-service +MATTERMOST_SERVICE_EMAIL=chat-service@local.invalid +CHAT_LONG_POLL_TIMEOUT_SECONDS=25 +CHAT_LONG_POLL_INTERVAL_SECONDS=1 +MINIO_ENDPOINT=localhost:9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin +MINIO_BUCKET_CHAT=chat diff --git a/main/__pycache__/__init__.cpython-313.pyc b/main/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbaf534d8b18124c1ef92c2ae44483a2e0482c98 GIT binary patch literal 149 zcmey&%ge<81by?=vOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~h}erR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6ZenJhetdjpUS>&ryk0@&Ee@O9{FKt1 YRJ$Tppdlbjib0Hz%#4hTMa)1J0ABbb1ONa4 literal 0 HcmV?d00001 diff --git a/main/__pycache__/settings.cpython-313.pyc b/main/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e0345afa3c8e93723c97367f34cbcddd286be9b GIT binary patch literal 7201 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 literal 0 HcmV?d00001 diff --git a/main/asgi.py b/main/asgi.py index 04b4e6d..31b565c 100644 --- a/main/asgi.py +++ b/main/asgi.py @@ -1,16 +1,27 @@ """ -ASGI config for main project. +ASGI config for chat project. It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application +from apps.chat.websocket import websocket_application + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings') -application = get_asgi_application() +django_asgi_application = get_asgi_application() + + +async def application(scope, receive, send): + if scope["type"] == "http": + await django_asgi_application(scope, receive, send) + return + + if scope["type"] == "websocket": + await websocket_application(scope, receive, send) + return + + raise ValueError(f"Unsupported ASGI scope type: {scope['type']}") diff --git a/main/other_settings/__pycache__/__init__.cpython-313.pyc b/main/other_settings/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b79ae16b09a404bc0fa80aab9597a5094400564e GIT binary patch literal 164 zcmey&%ge<81by?=vOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~ieerR!OQL%nT zVsS=FVvc@5QGQlxa!Ij%cxGO4e!hNkMq-J6ZenJhett9e0*kJ nW=VX!UP0w84x8Nkl+v73yCPPgnIJohL5z>gjEsy$%s>_Z8vH3V literal 0 HcmV?d00001 diff --git a/main/other_settings/__pycache__/logging.cpython-313.pyc b/main/other_settings/__pycache__/logging.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a67518bf6a7bd7312d7f7e6102d78c590cc0ae07 GIT binary patch literal 2149 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 Z!z^=rwR@L9w;N>{{@xE>0?%8X{0A!-aFzf7 literal 0 HcmV?d00001