django-huddle 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. django_huddle-0.1.0.dist-info/METADATA +278 -0
  2. django_huddle-0.1.0.dist-info/RECORD +55 -0
  3. django_huddle-0.1.0.dist-info/WHEEL +4 -0
  4. django_huddle-0.1.0.dist-info/licenses/LICENSE +21 -0
  5. huddle/__init__.py +6 -0
  6. huddle/api/__init__.py +4 -0
  7. huddle/api/serializers.py +50 -0
  8. huddle/api/urls.py +16 -0
  9. huddle/api/views.py +255 -0
  10. huddle/apps.py +17 -0
  11. huddle/conf.py +54 -0
  12. huddle/consumers.py +179 -0
  13. huddle/exceptions.py +54 -0
  14. huddle/forms.py +89 -0
  15. huddle/markup.py +93 -0
  16. huddle/migrations/0001_initial.py +112 -0
  17. huddle/migrations/__init__.py +0 -0
  18. huddle/models.py +194 -0
  19. huddle/payloads.py +87 -0
  20. huddle/policy.py +121 -0
  21. huddle/py.typed +0 -0
  22. huddle/querysets.py +137 -0
  23. huddle/realtime.py +58 -0
  24. huddle/rendering.py +117 -0
  25. huddle/routing.py +11 -0
  26. huddle/services.py +248 -0
  27. huddle/signals.py +17 -0
  28. huddle/static/huddle/huddle.css +254 -0
  29. huddle/static/huddle/huddle.js +134 -0
  30. huddle/templates/huddle/base.html +13 -0
  31. huddle/templates/huddle/browse_page.html +2 -0
  32. huddle/templates/huddle/channel_page.html +2 -0
  33. huddle/templates/huddle/components/browse_list.html +19 -0
  34. huddle/templates/huddle/components/channel_pane.html +19 -0
  35. huddle/templates/huddle/components/composer.html +8 -0
  36. huddle/templates/huddle/components/composer_fields.html +6 -0
  37. huddle/templates/huddle/components/edit_form.html +7 -0
  38. huddle/templates/huddle/components/mention_results.html +6 -0
  39. huddle/templates/huddle/components/message.html +27 -0
  40. huddle/templates/huddle/components/posted.html +2 -0
  41. huddle/templates/huddle/components/replied.html +2 -0
  42. huddle/templates/huddle/components/reply_composer.html +8 -0
  43. huddle/templates/huddle/components/sidebar.html +22 -0
  44. huddle/templates/huddle/components/sidebar_entry.html +8 -0
  45. huddle/templates/huddle/components/thread.html +12 -0
  46. huddle/templates/huddle/components/thread_summary.html +8 -0
  47. huddle/templates/huddle/components/timeline_page.html +9 -0
  48. huddle/templates/huddle/components/ws_message.html +8 -0
  49. huddle/templates/huddle/components/ws_sidebar_entry.html +1 -0
  50. huddle/templates/huddle/create_channel_page.html +12 -0
  51. huddle/templates/huddle/home_page.html +12 -0
  52. huddle/templates/huddle/layout.html +11 -0
  53. huddle/templates/huddle/new_direct_page.html +12 -0
  54. huddle/urls.py +27 -0
  55. huddle/views.py +419 -0
@@ -0,0 +1,278 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-huddle
3
+ Version: 0.1.0
4
+ Summary: Slack-style team chat for Django: channels, direct messages, threads and @mentions over Django Channels.
5
+ Project-URL: bugs, https://github.com/Streamlined-Analytics/django-huddle/issues
6
+ Project-URL: changelog, https://github.com/Streamlined-Analytics/django-huddle/releases
7
+ Project-URL: homepage, https://github.com/Streamlined-Analytics/django-huddle
8
+ Author-email: Ben Atkinson <ben@streamlinedanalytics.co.uk>
9
+ Maintainer-email: Ben Atkinson <ben@streamlinedanalytics.co.uk>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 6.0
16
+ Classifier: Framework :: Django :: 6.1
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Communications :: Chat
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: channels>=4.2
27
+ Requires-Dist: django>=6
28
+ Provides-Extra: api
29
+ Requires-Dist: djangorestframework>=3.16; extra == 'api'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # django-huddle
33
+
34
+ Slack-style team chat for Django: **channels, direct messages, threads and
35
+ @mentions**, live over [Django Channels](https://channels.readthedocs.io/).
36
+
37
+ - **Channels and DMs.** Public and private named channels, plus one-to-one and
38
+ group direct messages (one conversation per group of people).
39
+ - **Threads.** Reply to any message in a side thread, as in Slack. Threads
40
+ are one level deep and track reply counts, followers and read state.
41
+ - **@mentions.** Autocomplete offers only people who can read the channel.
42
+ Mentions are stored by user id, so renaming someone never breaks old messages.
43
+ - **Live.** New messages, edits, deletions, thread replies and unread badges
44
+ arrive over a websocket.
45
+ - **Your permissions.** One policy class decides every visibility and action
46
+ check. Attach channels to your own objects (projects, teams) and say who can
47
+ see them.
48
+ - **Bring your own frontend, or don't.** It ships a working htmx UI whose every
49
+ piece is an overridable template. There's also a REST API, and a JSON
50
+ websocket mode for React, mobile or anything else.
51
+
52
+ > **Status:** alpha. The public API may change before 1.0.
53
+
54
+ ## Requirements
55
+
56
+ - Python 3.12+
57
+ - Django 6.0+
58
+ - Django Channels 4.2+ with a channel layer (Redis in production)
59
+ - Optional: Django REST Framework 3.16+ for the REST API
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install django-huddle # HTML UI + websocket
65
+ pip install "django-huddle[api]" # plus the REST API
66
+ ```
67
+
68
+ ```python
69
+ # settings.py
70
+ INSTALLED_APPS = [
71
+ # ...
72
+ "channels",
73
+ "rest_framework", # only for the REST API
74
+ "huddle",
75
+ ]
76
+ ASGI_APPLICATION = "myproject.asgi.application"
77
+ CHANNEL_LAYERS = {
78
+ "default": {
79
+ "BACKEND": "channels_redis.core.RedisChannelLayer",
80
+ "CONFIG": {"hosts": ["redis://localhost:6379/1"]},
81
+ },
82
+ }
83
+ HUDDLE_BASE_TEMPLATE = "base.html" # must define {% block content %} and load htmx + htmx-ext-ws
84
+ ```
85
+
86
+ ```python
87
+ # urls.py
88
+ urlpatterns = [
89
+ path("chat/", include("huddle.urls")),
90
+ path("api/chat/", include("huddle.api.urls")), # optional
91
+ ]
92
+ ```
93
+
94
+ ```python
95
+ # asgi.py
96
+ from channels.auth import AuthMiddlewareStack
97
+ from channels.routing import ProtocolTypeRouter, URLRouter
98
+ from channels.security.websocket import AllowedHostsOriginValidator
99
+ from huddle.routing import websocket_urlpatterns
100
+
101
+ application = ProtocolTypeRouter(
102
+ {
103
+ "http": django_asgi_app,
104
+ "websocket": AllowedHostsOriginValidator(AuthMiddlewareStack(URLRouter(websocket_urlpatterns))),
105
+ }
106
+ )
107
+ ```
108
+
109
+ Then run `python manage.py migrate`.
110
+
111
+ ## Permissions: the chat policy
112
+
113
+ Every decision goes through one `huddle.policy.ChatPolicy`. The default allows:
114
+
115
+ - public unscoped channels for every signed-in user;
116
+ - private channels and DMs for their members;
117
+ - **scoped channels for nobody**, until you say otherwise.
118
+
119
+ To attach channels to your own objects, subclass the policy:
120
+
121
+ ```python
122
+ from django.db.models import Q
123
+ from huddle.policy import ChatPolicy
124
+
125
+
126
+ class ProjectChatPolicy(ChatPolicy):
127
+ def scope_q(self, user):
128
+ projects = Project.objects.visible_to(user).values("pk")
129
+ return Q(scope_content_type__isnull=True) | self.scopes_q(Project, projects)
130
+
131
+ def can_create_channel(self, user, scope):
132
+ return scope is not None and user.has_perm("projects.manage", scope)
133
+
134
+ def mentionable_users(self, channel):
135
+ if channel.is_private or channel.scope is None:
136
+ return super().mentionable_users(channel)
137
+ return channel.scope.members.filter(is_active=True)
138
+
139
+ def direct_message_candidates(self, user):
140
+ return User.objects.filter(projects__in=Project.objects.visible_to(user)).exclude(pk=user.pk).distinct()
141
+ ```
142
+
143
+ ```python
144
+ HUDDLE_POLICY = "myapp.chat.ProjectChatPolicy"
145
+ ```
146
+
147
+ Every lookup (HTML, API and websocket) goes through
148
+ `Channel.objects.visible_to(user)`, so a channel someone can't see returns a
149
+ **404**, never a 403 that would confirm it exists. Every websocket event is
150
+ re-checked for each viewer before it is sent, so losing access takes effect
151
+ immediately.
152
+
153
+ Overridable methods: `scope_q`, `filter_visible`, `can_view`, `can_post`,
154
+ `can_create_channel`, `can_manage_channel`, `can_edit_message`,
155
+ `can_delete_message`, `mentionable_users`, `direct_message_candidates`.
156
+
157
+ ## Writing from your own code
158
+
159
+ ```python
160
+ from huddle.services import ChatService
161
+
162
+ chat = ChatService()
163
+ channel = chat.create_channel(user, "site-visits", scope=project)
164
+ message = chat.post_message(user, channel, "Booked for Tuesday @Ann Lee", mentioned=[ann])
165
+ chat.post_message(ann, channel, "Thanks!", parent=message) # a thread reply
166
+ dm = chat.open_direct(user, [ann, bob])
167
+ ```
168
+
169
+ The service enforces the policy and raises
170
+ `huddle.exceptions.ChatPermissionDeniedError` (and friends). Each exception
171
+ has a `.context` dict for structured logging.
172
+
173
+ ## Signals: notifications and integrations
174
+
175
+ Signals are sent **after the transaction commits**:
176
+
177
+ | Signal | Arguments |
178
+ |---|---|
179
+ | `huddle.signals.message_posted` | `message`, `channel`, `author` |
180
+ | `huddle.signals.user_mentioned` | `message`, `user` (once per newly mentioned user, including on edit) |
181
+ | `huddle.signals.thread_replied` | `message`, `thread`, `recipients` (the thread's followers, minus the author) |
182
+
183
+ ```python
184
+ @receiver(user_mentioned)
185
+ def notify_mention(sender, message, user, **kwargs):
186
+ notify([user], "chat.mention", target=message)
187
+ ```
188
+
189
+ ## Customising the UI
190
+
191
+ Every fragment is its own template under `huddle/components/`, and pages
192
+ extend `huddle/layout.html`. To restyle one piece, override one file in your
193
+ project's templates directory:
194
+
195
+ | Template | What it draws |
196
+ |---|---|
197
+ | `huddle/components/message.html` | One message, with its actions and thread summary |
198
+ | `huddle/components/composer.html`, `composer_fields.html` | The message box and @mention autocomplete |
199
+ | `huddle/components/sidebar.html`, `sidebar_entry.html` | Channel and DM list, unread badges |
200
+ | `huddle/components/thread.html` | The thread side panel |
201
+ | `huddle/components/channel_pane.html` | Channel header, timeline and composer |
202
+ | `huddle/layout.html` | The three-column chat layout |
203
+
204
+ Each message template receives a `message_view` with `message`,
205
+ `author_name`, `html` (safe, escaped), `can_edit`, `can_delete` and `is_own`.
206
+
207
+ The default stylesheet (`huddle/huddle.css`) is plain CSS with custom
208
+ properties (`--huddle-accent`, `--huddle-border`, …) for theming.
209
+
210
+ ## REST API (`django-huddle[api]`)
211
+
212
+ | Method and path | Purpose |
213
+ |---|---|
214
+ | `GET /channels/` | Your channels and DMs (`?browse=true`: every visible named channel) |
215
+ | `POST /channels/` | Create a channel `{name, topic, is_private}` |
216
+ | `GET /channels/{id}/` | One channel, with `is_member` and `unread_count` |
217
+ | `POST /channels/direct/` | Open a DM `{user_ids: [...]}` |
218
+ | `POST /channels/{id}/join/` · `leave/` · `read/` | Membership and read state |
219
+ | `GET /channels/{id}/messages/?before=&limit=` | A page of top-level messages, oldest first |
220
+ | `POST /channels/{id}/messages/` | Post `{text, mention_ids, parent_id}` |
221
+ | `GET /channels/{id}/mentionable/?q=` | People who can be @mentioned |
222
+ | `GET`, `PATCH`, `DELETE /messages/{id}/` | Read, edit `{text, mention_ids}`, delete |
223
+ | `GET /messages/{id}/replies/` · `POST read/` | A thread, and marking it read |
224
+
225
+ Messages come back with `body` (stored tokens), `text` (editable, with
226
+ `@Name`), `html` (rendered and safe), `mention_ids`, `author`, the timestamps,
227
+ `reply_count`, and per-viewer `can_edit`, `can_delete` and `is_own`. To
228
+ attach API-created channels to your own objects, subclass
229
+ `huddle.api.views.ChannelViewSet` and override `get_scope()`.
230
+
231
+ ## Websocket
232
+
233
+ Connect to `/ws/huddle/` (`HUDDLE_WEBSOCKET_URL`) and send
234
+ `{"subscribe": <channel id>}` / `{"unsubscribe": <channel id>}`.
235
+
236
+ - **HTML mode** (the default) sends htmx out-of-band swaps; the bundled UI
237
+ needs no client-side rendering.
238
+ - **JSON mode** (`/ws/huddle/?format=json`) sends
239
+ `{"type": "message.created" | "message.updated" | "message.deleted", "message": {...}}`
240
+ and `{"type": "channel.updated", "channel": {...}}`, in the same shapes as
241
+ the REST API.
242
+
243
+ If you already run your own consumer (for example, one socket per page that
244
+ also carries notifications), mix in `huddle.consumers.HuddleConsumerMixin`,
245
+ then call `huddle_connect()`, `huddle_receive(content)` and
246
+ `huddle_disconnect()`.
247
+
248
+ ## Message format
249
+
250
+ Bodies are stored as plain text with tokens: `<@user-pk>` for mentions, and
251
+ `<#kind:id>` reserved for future references to your own objects. Rendering
252
+ escapes everything else, so there's no HTML sanitiser to keep patched.
253
+
254
+ ## Settings
255
+
256
+ | Setting | Default |
257
+ |---|---|
258
+ | `HUDDLE_POLICY` | `"huddle.policy.ChatPolicy"` |
259
+ | `HUDDLE_BASE_TEMPLATE` | `"huddle/base.html"` (loads htmx from unpkg; replace it in production) |
260
+ | `HUDDLE_PAGE_SIZE` | `50` |
261
+ | `HUDDLE_MAX_MESSAGE_LENGTH` | `10000` |
262
+ | `HUDDLE_WEBSOCKET_URL` | `"/ws/huddle/"` |
263
+
264
+ ## Development
265
+
266
+ ```bash
267
+ uv sync
268
+ uv run pytest
269
+ uv run mypy src/ tests/ example/
270
+ uv run ruff check . && uv run ruff format --check .
271
+ uv run python manage.py migrate && uv run daphne example.asgi:application # the example project
272
+ ```
273
+
274
+ Design decisions are recorded in [`docs/adr/`](docs/adr/).
275
+
276
+ ## License
277
+
278
+ MIT
@@ -0,0 +1,55 @@
1
+ huddle/__init__.py,sha256=0HLLBdNuEpKpJJsHORyk6eeva8MLf-xrhJ6Gn6PQhrE,175
2
+ huddle/apps.py,sha256=awGuyjNHR8aPFwZslIOQ06sL_H3Dvd553C63TVUGqUo,501
3
+ huddle/conf.py,sha256=7WxuOCoNuUBuRF_SJQurxsxdsOon1obl-5pxVkZ1fT4,1724
4
+ huddle/consumers.py,sha256=-MP0IMMIaBcaSxETRxC1x0UiXCwRgtzevvCQIdOQlXw,7501
5
+ huddle/exceptions.py,sha256=naM--Tjh7WiRzLITdCKmUAKdrUh69pb-6YLNvWgk5Ik,1757
6
+ huddle/forms.py,sha256=0VicA3Eh3W-8isM93FNYpgjX_qAiBFEEU0j5WAHH4Us,3216
7
+ huddle/markup.py,sha256=oA7Jj7u8DaoIiDOPiS-guy5y2suEy-9u9fiBhnaVxM4,3959
8
+ huddle/models.py,sha256=ZNAsfZD4lJLNLmhT0stv0taeUZ9QtSbqilw0Jc6ElvE,6999
9
+ huddle/payloads.py,sha256=Ee5JGf0yibkuMbH4AJorjcf7sF4LEZFGi_6-CT2LeuI,3431
10
+ huddle/policy.py,sha256=Q1xkMAJ6HIsTkdaAYhJuFSNjjcg-Hc1HtCR1bpXKvfE,5528
11
+ huddle/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ huddle/querysets.py,sha256=D1B2YfuhZDyPB-KQyQ1fyC4Du-nAEtu7cwyBXfh5cek,4869
13
+ huddle/realtime.py,sha256=G28KUaLsniXujBl7oxNEevmpU4lxVS5x5iPwBkQgQGg,2084
14
+ huddle/rendering.py,sha256=_1JwZFOJI-mNdTIQDvdg2-KafC1tDRtbSr_i9WXqFp0,4433
15
+ huddle/routing.py,sha256=DTXcszSpl4I2ra1BhU8fvd1jQEHxsYZ1A87LEAl_MsQ,282
16
+ huddle/services.py,sha256=EGVp582ZojyBu4z7wcQXI-g07m6AZuIhQMdD-X1A-rs,11695
17
+ huddle/signals.py,sha256=5IxESwrRm9Zi-JQHWq2RrW_e_vT6jYhGh0myN7wTlas,600
18
+ huddle/urls.py,sha256=7YHjLyenX8Z6lRs3Ni6YgA7FdduisMLECQaIeZWi26U,1439
19
+ huddle/views.py,sha256=mzFeoyaTeMDfEt7QHZdaOAKhZxZe6KptHfWvVW-35wE,17427
20
+ huddle/api/__init__.py,sha256=52Bz6ZQrzIV41nwSLK_Itq1ZbmRxe1K2IQF549Ti9tA,157
21
+ huddle/api/serializers.py,sha256=91WUN3kuuQ94H1u5b4MWA2MDXkY_wYX3mjDfbE07yVY,2025
22
+ huddle/api/urls.py,sha256=LJ-X9ry0qdhc0eKcR3KWeS0ebd0jvR6MDA0s-PYWQWo,466
23
+ huddle/api/views.py,sha256=ZKIzHIUTMjN_F7QiY27cNYmZcL87hqxNMDDT-eA_c0o,11319
24
+ huddle/migrations/0001_initial.py,sha256=LxqwISpxUvbLo2pBuxw_9NdJuB3JiWdZ-ju0psub46M,6684
25
+ huddle/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ huddle/static/huddle/huddle.css,sha256=MNG1kl9-bwzjXc4BHPEyURZxzqL2s_PNfEJ8PDvesZw,4312
27
+ huddle/static/huddle/huddle.js,sha256=9tl9hCSz7hdRagV4xdUbcSs-VZbAjvGr67hHELsLFWU,4103
28
+ huddle/templates/huddle/base.html,sha256=5hY4_i8zw_lwiQqJH3nQAszlnUHeawce1185yeuOoes,404
29
+ huddle/templates/huddle/browse_page.html,sha256=gfigG55XgzkwtG3YtrVNuaPsyXCE_AhIxkSl9l4_DiM,123
30
+ huddle/templates/huddle/channel_page.html,sha256=3FhkrA_8pdJFQetfuIM27Ftsdz3y-tMePb0nIDLyw6s,124
31
+ huddle/templates/huddle/create_channel_page.html,sha256=f5iJGeFev07oO9VeXAqCJB7VogOQKoePVqJLjT2E64o,363
32
+ huddle/templates/huddle/home_page.html,sha256=tdGm31Sy-mufBGDf0CesWMM6KpraUVrQ9QdQ1fStzHE,481
33
+ huddle/templates/huddle/layout.html,sha256=vAAij05sxmHApYZSz-m9jgGCvmqv9ejeimL83S3kHnw,567
34
+ huddle/templates/huddle/new_direct_page.html,sha256=-cgHHSumn7Oedk5381CL5H34Z-PTUs7JTmd-t-CD_L0,373
35
+ huddle/templates/huddle/components/browse_list.html,sha256=5QrEVoDe9SZK05OV8rxeOk_geNC_RcSDS7JE6XDRmpg,830
36
+ huddle/templates/huddle/components/channel_pane.html,sha256=gaeFqfYzcqrbcoBP7W6rzSCflU094Ou3G7xrxGR4B2A,1000
37
+ huddle/templates/huddle/components/composer.html,sha256=7rq855PbzHmwAiIYGZIyIQabYlMO7gXLoeaZpYSwvE0,325
38
+ huddle/templates/huddle/components/composer_fields.html,sha256=ifK5pDkK72Dag993v5k9Bkzda8D3GdKMdVfZYuNgsrY,266
39
+ huddle/templates/huddle/components/edit_form.html,sha256=7ujbf3huH-xoXuCPmVAgVBlG-vVtEfpLb1W1Co9jQuw,565
40
+ huddle/templates/huddle/components/mention_results.html,sha256=MGWu5ANa3Wl2vUFSgmPIe_4rt37kIfEYDxO08TSS9D8,316
41
+ huddle/templates/huddle/components/message.html,sha256=QmCLe-oZiArB-mruH_wwTaiP8NpuDzAeLxhvZFbQQPw,1996
42
+ huddle/templates/huddle/components/posted.html,sha256=vAf2MnjgyhP6Qhl8i9Z8eWPUdB7TC33m6Zq5wcwC1M8,164
43
+ huddle/templates/huddle/components/replied.html,sha256=E2-17SnWbjcouqXgqL03yGVKEN5Lx7CUR3RLH3qmn-w,175
44
+ huddle/templates/huddle/components/reply_composer.html,sha256=rHCyZa_di3KpaZNkh-7QTH1SY9VdcYfrcjMMJ8WnX4U,378
45
+ huddle/templates/huddle/components/sidebar.html,sha256=lbTq273pwLozUF9bcDroUIgWRrUAFDQugfz2i2IaQns,1089
46
+ huddle/templates/huddle/components/sidebar_entry.html,sha256=px_Pxf8P-nEeF6PAz8qLYfZxsSFF-m595mq78qz4E_Q,537
47
+ huddle/templates/huddle/components/thread.html,sha256=XmB_y1WYYwUfyX4gkMHkfJqD0ycvY2GYQzM1vwj6ums,711
48
+ huddle/templates/huddle/components/thread_summary.html,sha256=u-BZ2f8DzZmFp-VVvHnZYGWKiSGoPj1XYie48Kd6B-k,461
49
+ huddle/templates/huddle/components/timeline_page.html,sha256=w8rqev1OGWBs2UJ8bpAiqehYBM-ddX_BfuvY17ylQ44,453
50
+ huddle/templates/huddle/components/ws_message.html,sha256=VwaC_42RkuSjlw_EGahPBLA4k-yAFLm1UVajLySk3Vc,661
51
+ huddle/templates/huddle/components/ws_sidebar_entry.html,sha256=WmcA-rtVu3jbNNlNwCbiW7Ro3j4cXkgaFrGz1PDsd9Q,180
52
+ django_huddle-0.1.0.dist-info/METADATA,sha256=nW05LGW-g36qyoZy-yiJT8vfgQsHWy1C1ywjAahS05s,10504
53
+ django_huddle-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
54
+ django_huddle-0.1.0.dist-info/licenses/LICENSE,sha256=p7fI6PkctPIJ79gN-RSAMlxnM4r3vtf3-r6dOEjXyA8,1070
55
+ django_huddle-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026, Ben Atkinson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
huddle/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Slack-style team chat for Django: channels, direct messages, threads and @mentions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["__version__"]
huddle/api/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Optional REST API for custom frontends. Needs ``pip install django-huddle[api]``.
2
+
3
+ Include it with ``path("api/chat/", include("huddle.api.urls"))``.
4
+ """
@@ -0,0 +1,50 @@
1
+ """Input serializers for the REST API.
2
+
3
+ Responses are built by :class:`~huddle.payloads.ChatPayloads` (shared with the
4
+ websocket JSON mode); these serializers only validate what clients send.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from rest_framework import serializers
12
+
13
+ from huddle.conf import get_setting
14
+ from huddle.forms import ChannelForm
15
+
16
+
17
+ class MessageInputSerializer(serializers.Serializer[Any]):
18
+ """A new or edited message. ``mention_ids`` are the users the client chose to @mention."""
19
+
20
+ text = serializers.CharField(trim_whitespace=True)
21
+ mention_ids = serializers.ListField(child=serializers.CharField(), required=False, default=list)
22
+ parent_id = serializers.IntegerField(required=False, allow_null=True, default=None)
23
+
24
+ def validate_text(self, value: str) -> str:
25
+ """Enforce ``HUDDLE_MAX_MESSAGE_LENGTH``."""
26
+ limit = get_setting("HUDDLE_MAX_MESSAGE_LENGTH")
27
+ if len(value) > limit:
28
+ raise serializers.ValidationError(f"Ensure this field has no more than {limit} characters.")
29
+ return value
30
+
31
+
32
+ class ChannelInputSerializer(serializers.Serializer[Any]):
33
+ """A new named channel. Names are normalised the same way as in the HTML form."""
34
+
35
+ name = serializers.CharField(max_length=80)
36
+ topic = serializers.CharField(max_length=250, required=False, allow_blank=True, default="")
37
+ is_private = serializers.BooleanField(required=False, default=False)
38
+
39
+ def validate_name(self, value: str) -> str:
40
+ """Normalise to Slack style via :class:`~huddle.forms.ChannelForm`."""
41
+ form = ChannelForm({"name": value, "topic": ""})
42
+ if not form.is_valid() and "name" in form.errors:
43
+ raise serializers.ValidationError([str(error) for error in form.errors["name"]])
44
+ return str(form.cleaned_data["name"])
45
+
46
+
47
+ class DirectInputSerializer(serializers.Serializer[Any]):
48
+ """The other people in a direct message."""
49
+
50
+ user_ids = serializers.ListField(child=serializers.CharField(), min_length=1)
huddle/api/urls.py ADDED
@@ -0,0 +1,16 @@
1
+ """REST API routes. Include with ``path("api/chat/", include("huddle.api.urls"))``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rest_framework.routers import SimpleRouter
6
+
7
+ from huddle.api.views import ChannelViewSet
8
+ from huddle.api.views import MessageViewSet
9
+
10
+ app_name = "huddle-api"
11
+
12
+ router = SimpleRouter()
13
+ router.register("channels", ChannelViewSet, basename="channel")
14
+ router.register("messages", MessageViewSet, basename="message")
15
+
16
+ urlpatterns = router.urls