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.
- django_huddle-0.1.0.dist-info/METADATA +278 -0
- django_huddle-0.1.0.dist-info/RECORD +55 -0
- django_huddle-0.1.0.dist-info/WHEEL +4 -0
- django_huddle-0.1.0.dist-info/licenses/LICENSE +21 -0
- huddle/__init__.py +6 -0
- huddle/api/__init__.py +4 -0
- huddle/api/serializers.py +50 -0
- huddle/api/urls.py +16 -0
- huddle/api/views.py +255 -0
- huddle/apps.py +17 -0
- huddle/conf.py +54 -0
- huddle/consumers.py +179 -0
- huddle/exceptions.py +54 -0
- huddle/forms.py +89 -0
- huddle/markup.py +93 -0
- huddle/migrations/0001_initial.py +112 -0
- huddle/migrations/__init__.py +0 -0
- huddle/models.py +194 -0
- huddle/payloads.py +87 -0
- huddle/policy.py +121 -0
- huddle/py.typed +0 -0
- huddle/querysets.py +137 -0
- huddle/realtime.py +58 -0
- huddle/rendering.py +117 -0
- huddle/routing.py +11 -0
- huddle/services.py +248 -0
- huddle/signals.py +17 -0
- huddle/static/huddle/huddle.css +254 -0
- huddle/static/huddle/huddle.js +134 -0
- huddle/templates/huddle/base.html +13 -0
- huddle/templates/huddle/browse_page.html +2 -0
- huddle/templates/huddle/channel_page.html +2 -0
- huddle/templates/huddle/components/browse_list.html +19 -0
- huddle/templates/huddle/components/channel_pane.html +19 -0
- huddle/templates/huddle/components/composer.html +8 -0
- huddle/templates/huddle/components/composer_fields.html +6 -0
- huddle/templates/huddle/components/edit_form.html +7 -0
- huddle/templates/huddle/components/mention_results.html +6 -0
- huddle/templates/huddle/components/message.html +27 -0
- huddle/templates/huddle/components/posted.html +2 -0
- huddle/templates/huddle/components/replied.html +2 -0
- huddle/templates/huddle/components/reply_composer.html +8 -0
- huddle/templates/huddle/components/sidebar.html +22 -0
- huddle/templates/huddle/components/sidebar_entry.html +8 -0
- huddle/templates/huddle/components/thread.html +12 -0
- huddle/templates/huddle/components/thread_summary.html +8 -0
- huddle/templates/huddle/components/timeline_page.html +9 -0
- huddle/templates/huddle/components/ws_message.html +8 -0
- huddle/templates/huddle/components/ws_sidebar_entry.html +1 -0
- huddle/templates/huddle/create_channel_page.html +12 -0
- huddle/templates/huddle/home_page.html +12 -0
- huddle/templates/huddle/layout.html +11 -0
- huddle/templates/huddle/new_direct_page.html +12 -0
- huddle/urls.py +27 -0
- 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,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
huddle/api/__init__.py
ADDED
|
@@ -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
|