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,426 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
ChatMessage model for individual messages in chat channels.
|
|
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 ChatMessage(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
ChatMessage model for individual messages in chat channels.
|
|
18
|
+
|
|
19
|
+
Stored separately from ChatChannel to avoid document size limits and enable:
|
|
20
|
+
- Efficient pagination
|
|
21
|
+
- Concurrent message posting
|
|
22
|
+
- Message-level operations (edit, delete, react)
|
|
23
|
+
- Threaded replies
|
|
24
|
+
|
|
25
|
+
Features:
|
|
26
|
+
- Message content with rich text support
|
|
27
|
+
- Sender information
|
|
28
|
+
- Threading support (parent message ID)
|
|
29
|
+
- Reactions (emoji reactions)
|
|
30
|
+
- Mentions (@user)
|
|
31
|
+
- Attachments
|
|
32
|
+
- Edit history
|
|
33
|
+
- Read receipts (via separate model)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
super().__init__()
|
|
38
|
+
self._channel_id: str | None = None
|
|
39
|
+
self._content: str | None = None
|
|
40
|
+
|
|
41
|
+
# Sender information
|
|
42
|
+
self._sender_id: str | None = None
|
|
43
|
+
self._sender_name: str | None = None
|
|
44
|
+
|
|
45
|
+
# Threading support
|
|
46
|
+
self._parent_message_id: str | None = None # For threaded replies
|
|
47
|
+
self._thread_count: int = 0 # Number of replies to this message
|
|
48
|
+
|
|
49
|
+
# Reactions {emoji: [user_ids]}
|
|
50
|
+
self._reactions: Dict[str, List[str]] = {}
|
|
51
|
+
|
|
52
|
+
# Rich content
|
|
53
|
+
self._attachments: List[Dict[str, Any]] = [] # URLs, images, files
|
|
54
|
+
self._mentions: List[str] = [] # User IDs mentioned
|
|
55
|
+
|
|
56
|
+
# Edit tracking
|
|
57
|
+
self._edited_at: float | None = None
|
|
58
|
+
self._edit_count: int = 0
|
|
59
|
+
|
|
60
|
+
# Message type
|
|
61
|
+
self._message_type: str = "message" # message, system, announcement
|
|
62
|
+
|
|
63
|
+
# Sharding configuration (runtime attribute, passed from channel)
|
|
64
|
+
# Not persisted separately - computed from channel config at write time
|
|
65
|
+
self._sharding_config: Dict[str, Any] | None = None
|
|
66
|
+
|
|
67
|
+
self._setup_indexes()
|
|
68
|
+
|
|
69
|
+
def _setup_indexes(self):
|
|
70
|
+
"""Setup DynamoDB indexes for efficient querying."""
|
|
71
|
+
|
|
72
|
+
# Primary index: messages by ID
|
|
73
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
74
|
+
primary.name = "primary"
|
|
75
|
+
primary.partition_key.attribute_name = "pk"
|
|
76
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("msg", self.id))
|
|
77
|
+
primary.sort_key.attribute_name = "sk"
|
|
78
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("msg", self.id))
|
|
79
|
+
self.indexes.add_primary(primary)
|
|
80
|
+
|
|
81
|
+
# GSI1: Query messages by channel (most common - pagination)
|
|
82
|
+
# Allows: "Show me messages in this channel, paginated by time"
|
|
83
|
+
# Supports optional sharding for high-traffic channels
|
|
84
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
85
|
+
gsi.name = "gsi1"
|
|
86
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
87
|
+
gsi.partition_key.value = self._compute_gsi1_pk # Computed with optional sharding
|
|
88
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
89
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
90
|
+
("ts", self.created_utc_ts)
|
|
91
|
+
)
|
|
92
|
+
self.indexes.add_secondary(gsi)
|
|
93
|
+
|
|
94
|
+
# GSI2: Query threaded replies by parent message
|
|
95
|
+
# Allows: "Show me all replies to this message"
|
|
96
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
97
|
+
gsi.name = "gsi2"
|
|
98
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
99
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
100
|
+
("parent", self.parent_message_id)
|
|
101
|
+
)
|
|
102
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
103
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
104
|
+
("ts", self.created_utc_ts)
|
|
105
|
+
)
|
|
106
|
+
self.indexes.add_secondary(gsi)
|
|
107
|
+
|
|
108
|
+
# GSI3: Query messages by sender (user's message history)
|
|
109
|
+
# Allows: "Show me all messages from this user"
|
|
110
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
111
|
+
gsi.name = "gsi3"
|
|
112
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
113
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
114
|
+
("sender", self.sender_id)
|
|
115
|
+
)
|
|
116
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
117
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
118
|
+
("ts", self.created_utc_ts)
|
|
119
|
+
)
|
|
120
|
+
self.indexes.add_secondary(gsi)
|
|
121
|
+
|
|
122
|
+
# GSI4: Query messages by tenant (for admin/analytics)
|
|
123
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
124
|
+
gsi.name = "gsi4"
|
|
125
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
126
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
127
|
+
("tenant", self.tenant_id)
|
|
128
|
+
)
|
|
129
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
130
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
131
|
+
("model", "message"),
|
|
132
|
+
("ts", self.created_utc_ts)
|
|
133
|
+
)
|
|
134
|
+
self.indexes.add_secondary(gsi)
|
|
135
|
+
|
|
136
|
+
# Channel ID
|
|
137
|
+
@property
|
|
138
|
+
def channel_id(self) -> str | None:
|
|
139
|
+
return self._channel_id
|
|
140
|
+
|
|
141
|
+
@channel_id.setter
|
|
142
|
+
def channel_id(self, value: str | None):
|
|
143
|
+
self._channel_id = value
|
|
144
|
+
|
|
145
|
+
# Content
|
|
146
|
+
@property
|
|
147
|
+
def content(self) -> str | None:
|
|
148
|
+
return self._content
|
|
149
|
+
|
|
150
|
+
@content.setter
|
|
151
|
+
def content(self, value: str | None):
|
|
152
|
+
self._content = value
|
|
153
|
+
|
|
154
|
+
# Sender ID
|
|
155
|
+
@property
|
|
156
|
+
def sender_id(self) -> str | None:
|
|
157
|
+
return self._sender_id
|
|
158
|
+
|
|
159
|
+
@sender_id.setter
|
|
160
|
+
def sender_id(self, value: str | None):
|
|
161
|
+
self._sender_id = value
|
|
162
|
+
|
|
163
|
+
# Sender Name
|
|
164
|
+
@property
|
|
165
|
+
def sender_name(self) -> str | None:
|
|
166
|
+
return self._sender_name
|
|
167
|
+
|
|
168
|
+
@sender_name.setter
|
|
169
|
+
def sender_name(self, value: str | None):
|
|
170
|
+
self._sender_name = value
|
|
171
|
+
|
|
172
|
+
# Parent Message ID
|
|
173
|
+
@property
|
|
174
|
+
def parent_message_id(self) -> str | None:
|
|
175
|
+
return self._parent_message_id
|
|
176
|
+
|
|
177
|
+
@parent_message_id.setter
|
|
178
|
+
def parent_message_id(self, value: str | None):
|
|
179
|
+
self._parent_message_id = value
|
|
180
|
+
|
|
181
|
+
# Thread Count
|
|
182
|
+
@property
|
|
183
|
+
def thread_count(self) -> int:
|
|
184
|
+
return self._thread_count
|
|
185
|
+
|
|
186
|
+
@thread_count.setter
|
|
187
|
+
def thread_count(self, value: int | None):
|
|
188
|
+
self._thread_count = value if value is not None else 0
|
|
189
|
+
|
|
190
|
+
# Reactions
|
|
191
|
+
@property
|
|
192
|
+
def reactions(self) -> Dict[str, List[str]]:
|
|
193
|
+
return self._reactions
|
|
194
|
+
|
|
195
|
+
@reactions.setter
|
|
196
|
+
def reactions(self, value: Dict[str, List[str]] | None):
|
|
197
|
+
if value is None:
|
|
198
|
+
self._reactions = {}
|
|
199
|
+
elif isinstance(value, dict):
|
|
200
|
+
self._reactions = value
|
|
201
|
+
else:
|
|
202
|
+
self._reactions = {}
|
|
203
|
+
|
|
204
|
+
# Attachments
|
|
205
|
+
@property
|
|
206
|
+
def attachments(self) -> List[Dict[str, Any]]:
|
|
207
|
+
return self._attachments
|
|
208
|
+
|
|
209
|
+
@attachments.setter
|
|
210
|
+
def attachments(self, value: List[Dict[str, Any]] | None):
|
|
211
|
+
if value is None:
|
|
212
|
+
self._attachments = []
|
|
213
|
+
elif isinstance(value, list):
|
|
214
|
+
self._attachments = value
|
|
215
|
+
else:
|
|
216
|
+
self._attachments = []
|
|
217
|
+
|
|
218
|
+
# Mentions
|
|
219
|
+
@property
|
|
220
|
+
def mentions(self) -> List[str]:
|
|
221
|
+
return self._mentions
|
|
222
|
+
|
|
223
|
+
@mentions.setter
|
|
224
|
+
def mentions(self, value: List[str] | None):
|
|
225
|
+
if value is None:
|
|
226
|
+
self._mentions = []
|
|
227
|
+
elif isinstance(value, list):
|
|
228
|
+
self._mentions = value
|
|
229
|
+
else:
|
|
230
|
+
self._mentions = []
|
|
231
|
+
|
|
232
|
+
# Edited At
|
|
233
|
+
@property
|
|
234
|
+
def edited_at(self) -> float | None:
|
|
235
|
+
return self._edited_at
|
|
236
|
+
|
|
237
|
+
@edited_at.setter
|
|
238
|
+
def edited_at(self, value: float | None):
|
|
239
|
+
self._edited_at = value
|
|
240
|
+
|
|
241
|
+
# Edit Count
|
|
242
|
+
@property
|
|
243
|
+
def edit_count(self) -> int:
|
|
244
|
+
return self._edit_count
|
|
245
|
+
|
|
246
|
+
@edit_count.setter
|
|
247
|
+
def edit_count(self, value: int | None):
|
|
248
|
+
self._edit_count = value if value is not None else 0
|
|
249
|
+
|
|
250
|
+
# Message Type
|
|
251
|
+
@property
|
|
252
|
+
def message_type(self) -> str:
|
|
253
|
+
return self._message_type
|
|
254
|
+
|
|
255
|
+
@message_type.setter
|
|
256
|
+
def message_type(self, value: str | None):
|
|
257
|
+
valid_types = ["message", "system", "announcement"]
|
|
258
|
+
if value in valid_types:
|
|
259
|
+
self._message_type = value
|
|
260
|
+
else:
|
|
261
|
+
self._message_type = "message" # default
|
|
262
|
+
|
|
263
|
+
# Helper Methods
|
|
264
|
+
|
|
265
|
+
def add_reaction(self, emoji: str, user_id: str):
|
|
266
|
+
"""
|
|
267
|
+
Add a reaction to this message.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
emoji: Emoji to add (e.g., "👍", "❤️")
|
|
271
|
+
user_id: User ID adding the reaction
|
|
272
|
+
"""
|
|
273
|
+
if emoji not in self._reactions:
|
|
274
|
+
self._reactions[emoji] = []
|
|
275
|
+
|
|
276
|
+
if user_id not in self._reactions[emoji]:
|
|
277
|
+
self._reactions[emoji].append(user_id)
|
|
278
|
+
|
|
279
|
+
def remove_reaction(self, emoji: str, user_id: str):
|
|
280
|
+
"""
|
|
281
|
+
Remove a reaction from this message.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
emoji: Emoji to remove
|
|
285
|
+
user_id: User ID removing the reaction
|
|
286
|
+
"""
|
|
287
|
+
if emoji in self._reactions and user_id in self._reactions[emoji]:
|
|
288
|
+
self._reactions[emoji].remove(user_id)
|
|
289
|
+
|
|
290
|
+
# Clean up empty emoji lists
|
|
291
|
+
if not self._reactions[emoji]:
|
|
292
|
+
del self._reactions[emoji]
|
|
293
|
+
|
|
294
|
+
def get_reaction_count(self, emoji: str) -> int:
|
|
295
|
+
"""
|
|
296
|
+
Get the count of reactions for a specific emoji.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
emoji: Emoji to count
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Number of users who reacted with this emoji
|
|
303
|
+
"""
|
|
304
|
+
return len(self._reactions.get(emoji, []))
|
|
305
|
+
|
|
306
|
+
def has_user_reacted(self, emoji: str, user_id: str) -> bool:
|
|
307
|
+
"""
|
|
308
|
+
Check if a user has reacted with a specific emoji.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
emoji: Emoji to check
|
|
312
|
+
user_id: User ID to check
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
True if user has reacted, False otherwise
|
|
316
|
+
"""
|
|
317
|
+
return user_id in self._reactions.get(emoji, [])
|
|
318
|
+
|
|
319
|
+
def add_mention(self, user_id: str):
|
|
320
|
+
"""
|
|
321
|
+
Add a user mention to this message.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
user_id: User ID to mention
|
|
325
|
+
"""
|
|
326
|
+
if user_id not in self._mentions:
|
|
327
|
+
self._mentions.append(user_id)
|
|
328
|
+
|
|
329
|
+
def is_thread_parent(self) -> bool:
|
|
330
|
+
"""
|
|
331
|
+
Check if this message is a parent of a thread.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
True if this message has replies, False otherwise
|
|
335
|
+
"""
|
|
336
|
+
return self._thread_count > 0
|
|
337
|
+
|
|
338
|
+
def is_thread_reply(self) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Check if this message is a reply in a thread.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
True if this is a reply, False otherwise
|
|
344
|
+
"""
|
|
345
|
+
return self._parent_message_id is not None
|
|
346
|
+
|
|
347
|
+
def mark_as_edited(self):
|
|
348
|
+
"""Mark this message as edited."""
|
|
349
|
+
self._edited_at = dt.datetime.now(dt.UTC).timestamp()
|
|
350
|
+
self._edit_count += 1
|
|
351
|
+
|
|
352
|
+
def increment_thread_count(self):
|
|
353
|
+
"""Increment the thread reply count."""
|
|
354
|
+
self._thread_count += 1
|
|
355
|
+
|
|
356
|
+
# Sharding Helper Methods
|
|
357
|
+
|
|
358
|
+
def _compute_gsi1_pk(self) -> str:
|
|
359
|
+
"""
|
|
360
|
+
Compute GSI1 partition key with optional bucketing/sharding.
|
|
361
|
+
|
|
362
|
+
Strategy:
|
|
363
|
+
- Normal channel: "channel#<id>"
|
|
364
|
+
- Sharded channel: "channel#<id>#bucket#<yyyyMMdd>#shard#<n>"
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Partition key string for GSI1
|
|
368
|
+
"""
|
|
369
|
+
base_key = ("channel", self.channel_id)
|
|
370
|
+
|
|
371
|
+
# Check if sharding is enabled (requires channel config)
|
|
372
|
+
if self._sharding_config and self._sharding_config.get("enabled"):
|
|
373
|
+
bucket = self._get_time_bucket(
|
|
374
|
+
self.created_utc_ts,
|
|
375
|
+
self._sharding_config.get("bucket_span", "day")
|
|
376
|
+
)
|
|
377
|
+
shard = self._get_shard_index(
|
|
378
|
+
self.id,
|
|
379
|
+
self._sharding_config.get("shard_count", 1)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return DynamoDBKey.build_key(
|
|
383
|
+
base_key,
|
|
384
|
+
("bucket", bucket),
|
|
385
|
+
("shard", str(shard))
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Default: no sharding
|
|
389
|
+
return DynamoDBKey.build_key(base_key)
|
|
390
|
+
|
|
391
|
+
@staticmethod
|
|
392
|
+
def _get_time_bucket(timestamp: float, span: str) -> str:
|
|
393
|
+
"""
|
|
394
|
+
Get time bucket string for partitioning messages.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
timestamp: UTC timestamp
|
|
398
|
+
span: "day" or "hour"
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Bucket string (yyyyMMdd or yyyyMMddHH)
|
|
402
|
+
"""
|
|
403
|
+
from datetime import datetime, timezone
|
|
404
|
+
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
405
|
+
return dt.strftime("%Y%m%d" if span == "day" else "%Y%m%d%H")
|
|
406
|
+
|
|
407
|
+
@staticmethod
|
|
408
|
+
def _get_shard_index(message_id: str, shard_count: int) -> int:
|
|
409
|
+
"""
|
|
410
|
+
Compute consistent shard index for message distribution.
|
|
411
|
+
|
|
412
|
+
Uses MD5 hash of message_id for consistent distribution.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
message_id: Message ID
|
|
416
|
+
shard_count: Number of shards (1-8)
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Shard index (0 to shard_count-1)
|
|
420
|
+
"""
|
|
421
|
+
if shard_count <= 1:
|
|
422
|
+
return 0
|
|
423
|
+
|
|
424
|
+
import hashlib
|
|
425
|
+
h = hashlib.md5(message_id.encode()).hexdigest()
|
|
426
|
+
return int(h[:8], 16) % shard_count
|