geek-cafe-saas-sdk 0.6.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.
Potentially problematic release.
This version of geek-cafe-saas-sdk might be problematic. Click here for more details.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
ChatChannel model for internal team messaging (Slack-like functionality).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
9
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
10
|
+
import datetime as dt
|
|
11
|
+
from typing import List, Optional, Dict, Any
|
|
12
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChatChannel(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
ChatChannel model for Slack-like team messaging.
|
|
18
|
+
|
|
19
|
+
Optimized for high-volume, real-time messaging with symmetric membership.
|
|
20
|
+
Messages are stored separately to avoid document size limits.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Public channels (visible to all team members)
|
|
24
|
+
- Private channels (invite-only)
|
|
25
|
+
- Direct messages (1-on-1)
|
|
26
|
+
- Member management
|
|
27
|
+
- Channel settings (announcements-only, auto-join, etc.)
|
|
28
|
+
- Archive/unarchive
|
|
29
|
+
- Activity tracking
|
|
30
|
+
|
|
31
|
+
Note: Messages are stored in separate ChatMessage documents.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self._name: str | None = None
|
|
37
|
+
self._description: str | None = None
|
|
38
|
+
self._channel_type: str = "public" # public, private, direct
|
|
39
|
+
|
|
40
|
+
# Membership tracking (members stored as adjacent records)
|
|
41
|
+
self._member_count: int = 0 # Cached count for display
|
|
42
|
+
self._created_by: str | None = None
|
|
43
|
+
|
|
44
|
+
# Activity tracking (no embedded messages)
|
|
45
|
+
self._last_message_id: str | None = None
|
|
46
|
+
self._last_message_at: float | None = None
|
|
47
|
+
self._message_count: int = 0
|
|
48
|
+
|
|
49
|
+
# Channel settings
|
|
50
|
+
self._is_archived: bool = False
|
|
51
|
+
self._is_default: bool = False # Auto-join for new users
|
|
52
|
+
self._is_announcement: bool = False # Only admins can post
|
|
53
|
+
|
|
54
|
+
# Metadata
|
|
55
|
+
self._topic: str | None = None # Channel topic/purpose
|
|
56
|
+
self._icon: str | None = None # Emoji or image URL
|
|
57
|
+
|
|
58
|
+
# Sharding configuration (optional, for high-traffic channels)
|
|
59
|
+
self._sharding_config: Dict[str, Any] | None = None
|
|
60
|
+
|
|
61
|
+
self._setup_indexes()
|
|
62
|
+
|
|
63
|
+
def _setup_indexes(self):
|
|
64
|
+
"""Setup DynamoDB indexes for efficient querying."""
|
|
65
|
+
|
|
66
|
+
# Primary index: channels by ID
|
|
67
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
68
|
+
primary.name = "primary"
|
|
69
|
+
primary.partition_key.attribute_name = "pk"
|
|
70
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("channel", self.id))
|
|
71
|
+
primary.sort_key.attribute_name = "sk"
|
|
72
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("channel", self.id))
|
|
73
|
+
self.indexes.add_primary(primary)
|
|
74
|
+
|
|
75
|
+
# GSI1: Query by tenant and type (list all public/private channels)
|
|
76
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
77
|
+
gsi.name = "gsi1"
|
|
78
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
79
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
80
|
+
("tenant", self.tenant_id),
|
|
81
|
+
("type", self.channel_type)
|
|
82
|
+
)
|
|
83
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
84
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
85
|
+
("ts", self.last_message_at or self.created_utc_ts)
|
|
86
|
+
)
|
|
87
|
+
self.indexes.add_secondary(gsi)
|
|
88
|
+
|
|
89
|
+
# GSI2: Query all channels by tenant (for admin views)
|
|
90
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
91
|
+
gsi.name = "gsi2"
|
|
92
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
93
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
94
|
+
("tenant", self.tenant_id)
|
|
95
|
+
)
|
|
96
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
97
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
98
|
+
("model", "channel"),
|
|
99
|
+
("name", self.name)
|
|
100
|
+
)
|
|
101
|
+
self.indexes.add_secondary(gsi)
|
|
102
|
+
|
|
103
|
+
# GSI3: Query default channels (auto-join for new users)
|
|
104
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
105
|
+
gsi.name = "gsi3"
|
|
106
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
107
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
108
|
+
("tenant", self.tenant_id),
|
|
109
|
+
("default", "1" if self.is_default else "0")
|
|
110
|
+
)
|
|
111
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
112
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
113
|
+
("ts", self.created_utc_ts)
|
|
114
|
+
)
|
|
115
|
+
self.indexes.add_secondary(gsi)
|
|
116
|
+
|
|
117
|
+
# GSI4: Query archived channels
|
|
118
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
119
|
+
gsi.name = "gsi4"
|
|
120
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
121
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
122
|
+
("tenant", self.tenant_id),
|
|
123
|
+
("archived", "1" if self.is_archived else "0")
|
|
124
|
+
)
|
|
125
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
126
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
127
|
+
("ts", self.last_message_at or self.created_utc_ts)
|
|
128
|
+
)
|
|
129
|
+
self.indexes.add_secondary(gsi)
|
|
130
|
+
|
|
131
|
+
# Name
|
|
132
|
+
@property
|
|
133
|
+
def name(self) -> str | None:
|
|
134
|
+
return self._name
|
|
135
|
+
|
|
136
|
+
@name.setter
|
|
137
|
+
def name(self, value: str | None):
|
|
138
|
+
self._name = value
|
|
139
|
+
|
|
140
|
+
# Description
|
|
141
|
+
@property
|
|
142
|
+
def description(self) -> str | None:
|
|
143
|
+
return self._description
|
|
144
|
+
|
|
145
|
+
@description.setter
|
|
146
|
+
def description(self, value: str | None):
|
|
147
|
+
self._description = value
|
|
148
|
+
|
|
149
|
+
# Channel Type
|
|
150
|
+
@property
|
|
151
|
+
def channel_type(self) -> str:
|
|
152
|
+
return self._channel_type
|
|
153
|
+
|
|
154
|
+
@channel_type.setter
|
|
155
|
+
def channel_type(self, value: str | None):
|
|
156
|
+
valid_types = ["public", "private", "direct"]
|
|
157
|
+
if value in valid_types:
|
|
158
|
+
self._channel_type = value
|
|
159
|
+
else:
|
|
160
|
+
self._channel_type = "public" # default
|
|
161
|
+
|
|
162
|
+
# Member Count (cached for display)
|
|
163
|
+
@property
|
|
164
|
+
def member_count(self) -> int:
|
|
165
|
+
return self._member_count
|
|
166
|
+
|
|
167
|
+
@member_count.setter
|
|
168
|
+
def member_count(self, value: int | None):
|
|
169
|
+
self._member_count = value if isinstance(value, int) else 0
|
|
170
|
+
|
|
171
|
+
# Created By
|
|
172
|
+
@property
|
|
173
|
+
def created_by(self) -> str | None:
|
|
174
|
+
return self._created_by
|
|
175
|
+
|
|
176
|
+
@created_by.setter
|
|
177
|
+
def created_by(self, value: str | None):
|
|
178
|
+
self._created_by = value
|
|
179
|
+
|
|
180
|
+
# Last Message ID
|
|
181
|
+
@property
|
|
182
|
+
def last_message_id(self) -> str | None:
|
|
183
|
+
return self._last_message_id
|
|
184
|
+
|
|
185
|
+
@last_message_id.setter
|
|
186
|
+
def last_message_id(self, value: str | None):
|
|
187
|
+
self._last_message_id = value
|
|
188
|
+
|
|
189
|
+
# Last Message At
|
|
190
|
+
@property
|
|
191
|
+
def last_message_at(self) -> float | None:
|
|
192
|
+
return self._last_message_at
|
|
193
|
+
|
|
194
|
+
@last_message_at.setter
|
|
195
|
+
def last_message_at(self, value: float | None):
|
|
196
|
+
self._last_message_at = value
|
|
197
|
+
|
|
198
|
+
# Message Count
|
|
199
|
+
@property
|
|
200
|
+
def message_count(self) -> int:
|
|
201
|
+
return self._message_count
|
|
202
|
+
|
|
203
|
+
@message_count.setter
|
|
204
|
+
def message_count(self, value: int | None):
|
|
205
|
+
self._message_count = value if value is not None else 0
|
|
206
|
+
|
|
207
|
+
# Is Archived
|
|
208
|
+
@property
|
|
209
|
+
def is_archived(self) -> bool:
|
|
210
|
+
return self._is_archived
|
|
211
|
+
|
|
212
|
+
@is_archived.setter
|
|
213
|
+
def is_archived(self, value: bool | None):
|
|
214
|
+
self._is_archived = value if value is not None else False
|
|
215
|
+
|
|
216
|
+
# Is Default
|
|
217
|
+
@property
|
|
218
|
+
def is_default(self) -> bool:
|
|
219
|
+
return self._is_default
|
|
220
|
+
|
|
221
|
+
@is_default.setter
|
|
222
|
+
def is_default(self, value: bool | None):
|
|
223
|
+
self._is_default = value if value is not None else False
|
|
224
|
+
|
|
225
|
+
# Is Announcement
|
|
226
|
+
@property
|
|
227
|
+
def is_announcement(self) -> bool:
|
|
228
|
+
return self._is_announcement
|
|
229
|
+
|
|
230
|
+
@is_announcement.setter
|
|
231
|
+
def is_announcement(self, value: bool | None):
|
|
232
|
+
self._is_announcement = value if value is not None else False
|
|
233
|
+
|
|
234
|
+
# Topic
|
|
235
|
+
@property
|
|
236
|
+
def topic(self) -> str | None:
|
|
237
|
+
return self._topic
|
|
238
|
+
|
|
239
|
+
@topic.setter
|
|
240
|
+
def topic(self, value: str | None):
|
|
241
|
+
self._topic = value
|
|
242
|
+
|
|
243
|
+
# Icon
|
|
244
|
+
@property
|
|
245
|
+
def icon(self) -> str | None:
|
|
246
|
+
return self._icon
|
|
247
|
+
|
|
248
|
+
@icon.setter
|
|
249
|
+
def icon(self, value: str | None):
|
|
250
|
+
self._icon = value
|
|
251
|
+
|
|
252
|
+
# Sharding Config
|
|
253
|
+
@property
|
|
254
|
+
def sharding_config(self) -> Dict[str, Any] | None:
|
|
255
|
+
"""
|
|
256
|
+
Sharding configuration for high-traffic channels.
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
{
|
|
260
|
+
"enabled": True,
|
|
261
|
+
"bucket_span": "day", # "day" or "hour"
|
|
262
|
+
"shard_count": 4, # 1, 2, 4, or 8
|
|
263
|
+
"enabled_at": 1729123200.0
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Sharding config dict or None if not sharded
|
|
268
|
+
"""
|
|
269
|
+
return self._sharding_config
|
|
270
|
+
|
|
271
|
+
@sharding_config.setter
|
|
272
|
+
def sharding_config(self, value: Dict[str, Any] | None):
|
|
273
|
+
if value is None:
|
|
274
|
+
self._sharding_config = None
|
|
275
|
+
elif isinstance(value, dict):
|
|
276
|
+
self._sharding_config = value
|
|
277
|
+
else:
|
|
278
|
+
self._sharding_config = None
|
|
279
|
+
|
|
280
|
+
# Helper Methods
|
|
281
|
+
|
|
282
|
+
def increment_member_count(self):
|
|
283
|
+
"""Increment the cached member count."""
|
|
284
|
+
self._member_count += 1
|
|
285
|
+
|
|
286
|
+
def decrement_member_count(self):
|
|
287
|
+
"""Decrement the cached member count."""
|
|
288
|
+
if self._member_count > 0:
|
|
289
|
+
self._member_count -= 1
|
|
290
|
+
|
|
291
|
+
def increment_message_count(self):
|
|
292
|
+
"""Increment the message count for this channel."""
|
|
293
|
+
self._message_count += 1
|
|
294
|
+
|
|
295
|
+
def update_last_message(self, message_id: str, timestamp: float):
|
|
296
|
+
"""
|
|
297
|
+
Update the last message tracking.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
message_id: ID of the last message
|
|
301
|
+
timestamp: Timestamp of the message
|
|
302
|
+
"""
|
|
303
|
+
self._last_message_id = message_id
|
|
304
|
+
self._last_message_at = timestamp
|
|
305
|
+
self.increment_message_count()
|
|
306
|
+
|
|
307
|
+
def is_sharded(self) -> bool:
|
|
308
|
+
"""
|
|
309
|
+
Check if this channel uses message sharding.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
True if sharding is enabled, False otherwise
|
|
313
|
+
"""
|
|
314
|
+
return (self._sharding_config is not None
|
|
315
|
+
and self._sharding_config.get("enabled", False))
|
|
316
|
+
|
|
317
|
+
def get_bucket_span(self) -> str | None:
|
|
318
|
+
"""
|
|
319
|
+
Get the time bucket span for sharded messages.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
"day" or "hour" if sharded, None otherwise
|
|
323
|
+
"""
|
|
324
|
+
if not self.is_sharded():
|
|
325
|
+
return None
|
|
326
|
+
return self._sharding_config.get("bucket_span", "day")
|
|
327
|
+
|
|
328
|
+
def get_shard_count(self) -> int:
|
|
329
|
+
"""
|
|
330
|
+
Get the number of shards per bucket.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Shard count (1-8) if sharded, 1 otherwise
|
|
334
|
+
"""
|
|
335
|
+
if not self.is_sharded():
|
|
336
|
+
return 1
|
|
337
|
+
return self._sharding_config.get("shard_count", 1)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
ChatChannelMember model for scalable channel membership using single-table design.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
9
|
+
import datetime as dt
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ChatChannelMember(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
ChatChannelMember model for scalable membership tracking.
|
|
17
|
+
|
|
18
|
+
Single-table design pattern:
|
|
19
|
+
- PK: channel#<channel_id>
|
|
20
|
+
- SK: member#<user_id>
|
|
21
|
+
- GSI1PK: user#<user_id> (for "what channels am I in?")
|
|
22
|
+
- GSI1SK: channel#<channel_id>#<joined_at>
|
|
23
|
+
|
|
24
|
+
Benefits:
|
|
25
|
+
- Unlimited members per channel
|
|
26
|
+
- Fast membership checks (single GetItem)
|
|
27
|
+
- Member metadata (role, permissions, preferences)
|
|
28
|
+
- Efficient pagination
|
|
29
|
+
- Concurrent updates without contention
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
# Note: Uses normal UUID id, but pk/sk create adjacent record pattern
|
|
35
|
+
self._channel_id: str | None = None
|
|
36
|
+
self._user_id: str | None = None
|
|
37
|
+
|
|
38
|
+
# Member metadata
|
|
39
|
+
self._role: str = "member" # member, admin, owner
|
|
40
|
+
self._joined_at: float | None = None
|
|
41
|
+
self._added_by_id: str | None = None
|
|
42
|
+
|
|
43
|
+
# Notification preferences
|
|
44
|
+
self._notification_level: str = "all" # all, mentions, none
|
|
45
|
+
self._muted: bool = False
|
|
46
|
+
|
|
47
|
+
# Activity tracking
|
|
48
|
+
self._last_read_message_id: str | None = None
|
|
49
|
+
self._last_read_at: float | None = None
|
|
50
|
+
|
|
51
|
+
# Setup DynamoDB indexes (adjacent record pattern)
|
|
52
|
+
self._setup_indexes()
|
|
53
|
+
|
|
54
|
+
def _setup_indexes(self):
|
|
55
|
+
"""Setup DynamoDB indexes for member queries."""
|
|
56
|
+
|
|
57
|
+
# Primary index: Adjacent record pattern - members grouped by channel
|
|
58
|
+
# Query: pk="channel#<id>" AND sk BEGINS_WITH "member#" to get all members
|
|
59
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
60
|
+
primary.name = "primary"
|
|
61
|
+
primary.partition_key.attribute_name = "pk"
|
|
62
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("channel", self.channel_id))
|
|
63
|
+
primary.sort_key.attribute_name = "sk"
|
|
64
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("member", self.user_id))
|
|
65
|
+
self.indexes.add_primary(primary)
|
|
66
|
+
|
|
67
|
+
# GSI1: User's channels - query channels by user
|
|
68
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
69
|
+
gsi.name = "gsi1"
|
|
70
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
71
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.user_id))
|
|
72
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
73
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("channel", self.channel_id), ("ts", self.joined_at))
|
|
74
|
+
self.indexes.add_secondary(gsi)
|
|
75
|
+
|
|
76
|
+
# GSI2: Members by role - query members by role within channel
|
|
77
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
78
|
+
gsi.name = "gsi2"
|
|
79
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
80
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("channel", self.channel_id), ("role", self.role))
|
|
81
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
82
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("member", self.user_id))
|
|
83
|
+
self.indexes.add_secondary(gsi)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def model_version(self) -> str:
|
|
87
|
+
return "1.0"
|
|
88
|
+
|
|
89
|
+
def prep_for_save(self):
|
|
90
|
+
"""
|
|
91
|
+
Prepare member record for save.
|
|
92
|
+
|
|
93
|
+
Note: ChatChannelMember uses adjacent record pattern where:
|
|
94
|
+
- Normal UUID id (generated by parent)
|
|
95
|
+
- pk = "channel#<channel_id>" (groups by channel)
|
|
96
|
+
- sk = "member#<user_id>" (enables begins_with queries)
|
|
97
|
+
"""
|
|
98
|
+
# Validate required fields for adjacent record pattern
|
|
99
|
+
if not self.channel_id:
|
|
100
|
+
raise ValueError("channel_id is required for ChatChannelMember")
|
|
101
|
+
if not self.user_id:
|
|
102
|
+
raise ValueError("user_id is required for ChatChannelMember")
|
|
103
|
+
|
|
104
|
+
# Call parent to generate normal UUID id and set timestamps
|
|
105
|
+
super().prep_for_save()
|
|
106
|
+
|
|
107
|
+
# Properties
|
|
108
|
+
@property
|
|
109
|
+
def channel_id(self) -> str:
|
|
110
|
+
return self._channel_id
|
|
111
|
+
|
|
112
|
+
@channel_id.setter
|
|
113
|
+
def channel_id(self, value: str | None):
|
|
114
|
+
self._channel_id = value
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def user_id(self) -> str:
|
|
118
|
+
return self._user_id
|
|
119
|
+
|
|
120
|
+
@user_id.setter
|
|
121
|
+
def user_id(self, value: str | None):
|
|
122
|
+
self._user_id = value
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def role(self) -> str:
|
|
126
|
+
return self._role
|
|
127
|
+
|
|
128
|
+
@role.setter
|
|
129
|
+
def role(self, value: str | None):
|
|
130
|
+
valid_roles = ["member", "admin", "owner"]
|
|
131
|
+
self._role = value if value in valid_roles else "member"
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def joined_at(self) -> float:
|
|
135
|
+
return self._joined_at
|
|
136
|
+
|
|
137
|
+
@joined_at.setter
|
|
138
|
+
def joined_at(self, value: float | None):
|
|
139
|
+
self._joined_at = value
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def added_by_id(self) -> str:
|
|
143
|
+
return self._added_by_id
|
|
144
|
+
|
|
145
|
+
@added_by_id.setter
|
|
146
|
+
def added_by_id(self, value: str | None):
|
|
147
|
+
self._added_by_id = value
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def notification_level(self) -> str:
|
|
151
|
+
return self._notification_level
|
|
152
|
+
|
|
153
|
+
@notification_level.setter
|
|
154
|
+
def notification_level(self, value: str | None):
|
|
155
|
+
valid_levels = ["all", "mentions", "none"]
|
|
156
|
+
self._notification_level = value if value in valid_levels else "all"
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def muted(self) -> bool:
|
|
160
|
+
return self._muted
|
|
161
|
+
|
|
162
|
+
@muted.setter
|
|
163
|
+
def muted(self, value: bool | None):
|
|
164
|
+
self._muted = value if isinstance(value, bool) else False
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def last_read_message_id(self) -> str:
|
|
168
|
+
return self._last_read_message_id
|
|
169
|
+
|
|
170
|
+
@last_read_message_id.setter
|
|
171
|
+
def last_read_message_id(self, value: str | None):
|
|
172
|
+
self._last_read_message_id = value
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def last_read_at(self) -> float:
|
|
176
|
+
return self._last_read_at
|
|
177
|
+
|
|
178
|
+
@last_read_at.setter
|
|
179
|
+
def last_read_at(self, value: float | None):
|
|
180
|
+
self._last_read_at = value
|