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,700 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
ChatChannelService for managing chat channels (Slack-like functionality).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
10
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBKey
|
|
11
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
12
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
13
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
|
|
14
|
+
from geek_cafe_saas_sdk.domains.messaging.models import ChatChannel, ChatChannelMember
|
|
15
|
+
import datetime as dt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChatChannelService(DatabaseService[ChatChannel]):
|
|
19
|
+
"""Service for ChatChannel database operations."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
22
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
23
|
+
|
|
24
|
+
def create(self, tenant_id: str, user_id: str, payload: Dict[str, Any]) -> ServiceResult[ChatChannel]:
|
|
25
|
+
"""
|
|
26
|
+
Create a new chat channel from a payload.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
tenant_id: Tenant ID
|
|
30
|
+
user_id: Owner user ID (who the channel belongs to)
|
|
31
|
+
payload: Channel data including:
|
|
32
|
+
- name: Channel name (required)
|
|
33
|
+
- created_by_id: Admin who created it (optional, for audit trail)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
ServiceResult with ChatChannel
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# Validate required fields
|
|
40
|
+
required_fields = ['name']
|
|
41
|
+
self._validate_required_fields(payload, required_fields)
|
|
42
|
+
|
|
43
|
+
# Create and map channel instance from the payload
|
|
44
|
+
channel = ChatChannel().map(payload)
|
|
45
|
+
channel.tenant_id = tenant_id
|
|
46
|
+
channel.user_id = user_id # Owner
|
|
47
|
+
|
|
48
|
+
# Set created_by from payload if provided (admin scenario), else use owner
|
|
49
|
+
if not channel.created_by_id:
|
|
50
|
+
channel.created_by_id = user_id
|
|
51
|
+
if not channel.created_by:
|
|
52
|
+
channel.created_by = user_id
|
|
53
|
+
|
|
54
|
+
# Set defaults
|
|
55
|
+
if not channel.channel_type:
|
|
56
|
+
channel.channel_type = "public"
|
|
57
|
+
|
|
58
|
+
# Prepare for save (sets ID and timestamps)
|
|
59
|
+
channel.prep_for_save()
|
|
60
|
+
|
|
61
|
+
# Save channel metadata
|
|
62
|
+
save_result = self._save_model(channel)
|
|
63
|
+
if not save_result.success:
|
|
64
|
+
return save_result
|
|
65
|
+
|
|
66
|
+
# Add creator as the first member
|
|
67
|
+
member_result = self._add_member_record(channel.id, user_id, user_id, role="owner")
|
|
68
|
+
if not member_result.success:
|
|
69
|
+
# Rollback channel creation if member add fails
|
|
70
|
+
# In production, consider using transactions
|
|
71
|
+
return member_result
|
|
72
|
+
|
|
73
|
+
channel.increment_member_count()
|
|
74
|
+
self._save_model(channel)
|
|
75
|
+
|
|
76
|
+
return ServiceResult.success_result(channel)
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
return self._handle_service_exception(e, 'create_chat_channel', tenant_id=tenant_id, user_id=user_id)
|
|
80
|
+
|
|
81
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[ChatChannel]:
|
|
82
|
+
"""
|
|
83
|
+
Get chat channel by ID with access control.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
resource_id: Channel ID
|
|
87
|
+
tenant_id: Tenant ID
|
|
88
|
+
user_id: User ID requesting access
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
ServiceResult with ChatChannel
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
channel = self._get_model_by_id(resource_id, ChatChannel)
|
|
95
|
+
|
|
96
|
+
if not channel:
|
|
97
|
+
raise NotFoundError(f"Chat channel with ID {resource_id} not found")
|
|
98
|
+
|
|
99
|
+
# Check if deleted
|
|
100
|
+
if channel.is_deleted():
|
|
101
|
+
raise NotFoundError(f"Chat channel with ID {resource_id} not found")
|
|
102
|
+
|
|
103
|
+
# Validate tenant access
|
|
104
|
+
if hasattr(channel, 'tenant_id'):
|
|
105
|
+
self._validate_tenant_access(channel.tenant_id, tenant_id)
|
|
106
|
+
|
|
107
|
+
# Check if user can access this channel
|
|
108
|
+
# Public channels: everyone can access
|
|
109
|
+
# Private channels: only members can access
|
|
110
|
+
if channel.channel_type == "private":
|
|
111
|
+
if not self.is_member(resource_id, user_id):
|
|
112
|
+
raise AccessDeniedError("Access denied to this private channel")
|
|
113
|
+
|
|
114
|
+
return ServiceResult.success_result(channel)
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
return self._handle_service_exception(e, 'get_chat_channel', resource_id=resource_id, tenant_id=tenant_id)
|
|
118
|
+
|
|
119
|
+
def list_by_type(self, tenant_id: str, channel_type: str, user_id: str,
|
|
120
|
+
limit: int = 50) -> ServiceResult[List[ChatChannel]]:
|
|
121
|
+
"""
|
|
122
|
+
List chat channels by type using GSI1.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
tenant_id: Tenant ID
|
|
126
|
+
channel_type: Channel type filter (public, private, direct)
|
|
127
|
+
user_id: User ID for access control
|
|
128
|
+
limit: Maximum number of results
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
ServiceResult with list of ChatChannels
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
temp_channel = ChatChannel()
|
|
135
|
+
temp_channel.tenant_id = tenant_id
|
|
136
|
+
temp_channel.channel_type = channel_type
|
|
137
|
+
|
|
138
|
+
result = self._query_by_index(
|
|
139
|
+
temp_channel,
|
|
140
|
+
"gsi1",
|
|
141
|
+
ascending=False, # Most recent activity first
|
|
142
|
+
limit=limit
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not result.success:
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
# Filter: public channels visible to all, private only if member
|
|
149
|
+
accessible_channels = []
|
|
150
|
+
for channel in result.data:
|
|
151
|
+
if not channel.is_deleted():
|
|
152
|
+
if channel.channel_type == "public" or self.is_member(channel.id, user_id):
|
|
153
|
+
accessible_channels.append(channel)
|
|
154
|
+
|
|
155
|
+
return ServiceResult.success_result(accessible_channels)
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
return self._handle_service_exception(e, 'list_channels_by_type',
|
|
159
|
+
tenant_id=tenant_id, channel_type=channel_type)
|
|
160
|
+
|
|
161
|
+
def list_all(self, tenant_id: str, user_id: str, limit: int = 100) -> ServiceResult[List[ChatChannel]]:
|
|
162
|
+
"""
|
|
163
|
+
List all chat channels for a tenant using GSI2.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
tenant_id: Tenant ID
|
|
167
|
+
user_id: User ID for access control
|
|
168
|
+
limit: Maximum number of results
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
ServiceResult with list of ChatChannels
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
temp_channel = ChatChannel()
|
|
175
|
+
temp_channel.tenant_id = tenant_id
|
|
176
|
+
|
|
177
|
+
result = self._query_by_index(
|
|
178
|
+
temp_channel,
|
|
179
|
+
"gsi2",
|
|
180
|
+
ascending=True, # Alphabetical by name
|
|
181
|
+
limit=limit
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if not result.success:
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
# Filter accessible channels
|
|
188
|
+
accessible_channels = []
|
|
189
|
+
for channel in result.data:
|
|
190
|
+
if not channel.is_deleted():
|
|
191
|
+
if channel.channel_type == "public" or self.is_member(channel.id, user_id):
|
|
192
|
+
accessible_channels.append(channel)
|
|
193
|
+
|
|
194
|
+
return ServiceResult.success_result(accessible_channels)
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return self._handle_service_exception(e, 'list_all_channels', tenant_id=tenant_id)
|
|
198
|
+
|
|
199
|
+
def list_default_channels(self, tenant_id: str, limit: int = 50) -> ServiceResult[List[ChatChannel]]:
|
|
200
|
+
"""
|
|
201
|
+
List default channels (auto-join for new users) using GSI3.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
tenant_id: Tenant ID
|
|
205
|
+
limit: Maximum number of results
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
ServiceResult with list of default ChatChannels
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
temp_channel = ChatChannel()
|
|
212
|
+
temp_channel.tenant_id = tenant_id
|
|
213
|
+
temp_channel.is_default = True
|
|
214
|
+
|
|
215
|
+
result = self._query_by_index(
|
|
216
|
+
temp_channel,
|
|
217
|
+
"gsi3",
|
|
218
|
+
ascending=False,
|
|
219
|
+
limit=limit
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if not result.success:
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
# Filter out deleted channels
|
|
226
|
+
active_channels = [c for c in result.data if not c.is_deleted()]
|
|
227
|
+
return ServiceResult.success_result(active_channels)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
return self._handle_service_exception(e, 'list_default_channels', tenant_id=tenant_id)
|
|
231
|
+
|
|
232
|
+
def list_user_channels(self, tenant_id: str, user_id: str,
|
|
233
|
+
include_archived: bool = False, limit: int = 100) -> ServiceResult[List[ChatChannel]]:
|
|
234
|
+
"""
|
|
235
|
+
List all channels a user is a member of.
|
|
236
|
+
|
|
237
|
+
Uses GSI1 for fast lookup via membership records.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
tenant_id: Tenant ID
|
|
241
|
+
user_id: User ID
|
|
242
|
+
include_archived: Whether to include archived channels
|
|
243
|
+
limit: Maximum number of results
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
ServiceResult with list of ChatChannels
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
# Get user's channel memberships (fast via GSI1)
|
|
250
|
+
memberships_result = self.list_user_channels_fast(user_id, limit)
|
|
251
|
+
if not memberships_result.success:
|
|
252
|
+
return memberships_result
|
|
253
|
+
|
|
254
|
+
# Fetch full channel details for each membership
|
|
255
|
+
user_channels = []
|
|
256
|
+
for membership in memberships_result.data:
|
|
257
|
+
channel = self._get_model_by_id(membership["channel_id"], ChatChannel)
|
|
258
|
+
if channel and not channel.is_deleted():
|
|
259
|
+
# Filter by tenant and archived status
|
|
260
|
+
if channel.tenant_id == tenant_id:
|
|
261
|
+
if include_archived or not channel.is_archived:
|
|
262
|
+
user_channels.append(channel)
|
|
263
|
+
|
|
264
|
+
# Sort by last activity
|
|
265
|
+
user_channels.sort(key=lambda c: c.last_message_at or c.created_utc_ts, reverse=True)
|
|
266
|
+
|
|
267
|
+
return ServiceResult.success_result(user_channels)
|
|
268
|
+
|
|
269
|
+
except Exception as e:
|
|
270
|
+
return self._handle_service_exception(e, 'list_user_channels',
|
|
271
|
+
tenant_id=tenant_id, user_id=user_id)
|
|
272
|
+
|
|
273
|
+
def add_member(self, channel_id: str, tenant_id: str, user_id: str,
|
|
274
|
+
member_to_add: str) -> ServiceResult[ChatChannel]:
|
|
275
|
+
"""
|
|
276
|
+
Add a member to a chat channel.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
channel_id: Channel ID
|
|
280
|
+
tenant_id: Tenant ID
|
|
281
|
+
user_id: User ID performing the action
|
|
282
|
+
member_to_add: User ID to add as member
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
ServiceResult with updated ChatChannel
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
channel_result = self.get_by_id(channel_id, tenant_id, user_id)
|
|
289
|
+
if not channel_result.success:
|
|
290
|
+
return channel_result
|
|
291
|
+
|
|
292
|
+
channel = channel_result.data
|
|
293
|
+
|
|
294
|
+
# For private channels, only members can add new members
|
|
295
|
+
if channel.channel_type == "private" and not self.is_member(channel_id, user_id):
|
|
296
|
+
raise AccessDeniedError("Only members can add new members to private channels")
|
|
297
|
+
|
|
298
|
+
# Check if already a member
|
|
299
|
+
if self.is_member(channel_id, member_to_add):
|
|
300
|
+
return ServiceResult.success_result(channel) # Already a member, no-op
|
|
301
|
+
|
|
302
|
+
# Add member record
|
|
303
|
+
member_result = self._add_member_record(channel_id, member_to_add, user_id)
|
|
304
|
+
if not member_result.success:
|
|
305
|
+
return member_result
|
|
306
|
+
|
|
307
|
+
# Update member count
|
|
308
|
+
channel.increment_member_count()
|
|
309
|
+
channel.updated_by_id = user_id
|
|
310
|
+
channel.prep_for_save()
|
|
311
|
+
self._save_model(channel)
|
|
312
|
+
|
|
313
|
+
return ServiceResult.success_result(channel)
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
return self._handle_service_exception(e, 'add_channel_member',
|
|
317
|
+
channel_id=channel_id, member_to_add=member_to_add)
|
|
318
|
+
|
|
319
|
+
def remove_member(self, channel_id: str, tenant_id: str, user_id: str,
|
|
320
|
+
member_to_remove: str) -> ServiceResult[ChatChannel]:
|
|
321
|
+
"""
|
|
322
|
+
Remove a member from a chat channel.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
channel_id: Channel ID
|
|
326
|
+
tenant_id: Tenant ID
|
|
327
|
+
user_id: User ID performing the action
|
|
328
|
+
member_to_remove: User ID to remove
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
ServiceResult with updated ChatChannel
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
channel_result = self.get_by_id(channel_id, tenant_id, user_id)
|
|
335
|
+
if not channel_result.success:
|
|
336
|
+
return channel_result
|
|
337
|
+
|
|
338
|
+
channel = channel_result.data
|
|
339
|
+
|
|
340
|
+
# Users can remove themselves, or creator can remove others
|
|
341
|
+
if user_id != member_to_remove and channel.created_by != user_id:
|
|
342
|
+
raise AccessDeniedError("Only channel creator can remove other members")
|
|
343
|
+
|
|
344
|
+
# Check if member exists
|
|
345
|
+
if not self.is_member(channel_id, member_to_remove):
|
|
346
|
+
return ServiceResult.success_result(channel) # Not a member, no-op
|
|
347
|
+
|
|
348
|
+
# Delete member record using adjacent record pattern
|
|
349
|
+
pk = DynamoDBKey.build_key(("channel", channel_id))
|
|
350
|
+
sk = DynamoDBKey.build_key(("member", member_to_remove))
|
|
351
|
+
|
|
352
|
+
delete_result = self._delete_by_composite_key(pk=pk, sk=sk)
|
|
353
|
+
if not delete_result.success:
|
|
354
|
+
return delete_result
|
|
355
|
+
|
|
356
|
+
# Update member count
|
|
357
|
+
channel.decrement_member_count()
|
|
358
|
+
channel.updated_by_id = user_id
|
|
359
|
+
channel.prep_for_save()
|
|
360
|
+
self._save_model(channel)
|
|
361
|
+
|
|
362
|
+
return ServiceResult.success_result(channel)
|
|
363
|
+
|
|
364
|
+
except Exception as e:
|
|
365
|
+
return self._handle_service_exception(e, 'remove_channel_member',
|
|
366
|
+
channel_id=channel_id, member_to_remove=member_to_remove)
|
|
367
|
+
|
|
368
|
+
def update_last_message(self, channel_id: str, tenant_id: str,
|
|
369
|
+
message_id: str, timestamp: float) -> ServiceResult[ChatChannel]:
|
|
370
|
+
"""
|
|
371
|
+
Update channel's last message tracking (called by ChatMessageService).
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
channel_id: Channel ID
|
|
375
|
+
tenant_id: Tenant ID
|
|
376
|
+
message_id: ID of the last message
|
|
377
|
+
timestamp: Timestamp of the message
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
ServiceResult with updated ChatChannel
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
channel = self._get_model_by_id(channel_id, ChatChannel)
|
|
384
|
+
if not channel:
|
|
385
|
+
raise NotFoundError(f"Chat channel with ID {channel_id} not found")
|
|
386
|
+
|
|
387
|
+
# Validate tenant
|
|
388
|
+
self._validate_tenant_access(channel.tenant_id, tenant_id)
|
|
389
|
+
|
|
390
|
+
channel.update_last_message(message_id, timestamp)
|
|
391
|
+
channel.prep_for_save()
|
|
392
|
+
|
|
393
|
+
return self._save_model(channel)
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
return self._handle_service_exception(e, 'update_channel_last_message',
|
|
397
|
+
channel_id=channel_id)
|
|
398
|
+
|
|
399
|
+
def archive(self, channel_id: str, tenant_id: str, user_id: str) -> ServiceResult[ChatChannel]:
|
|
400
|
+
"""
|
|
401
|
+
Archive a chat channel.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
channel_id: Channel ID
|
|
405
|
+
tenant_id: Tenant ID
|
|
406
|
+
user_id: User ID performing the action
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
ServiceResult with updated ChatChannel
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
channel_result = self.get_by_id(channel_id, tenant_id, user_id)
|
|
413
|
+
if not channel_result.success:
|
|
414
|
+
return channel_result
|
|
415
|
+
|
|
416
|
+
channel = channel_result.data
|
|
417
|
+
|
|
418
|
+
# Only creator can archive
|
|
419
|
+
if channel.created_by != user_id:
|
|
420
|
+
raise AccessDeniedError("Only channel creator can archive the channel")
|
|
421
|
+
|
|
422
|
+
channel.is_archived = True
|
|
423
|
+
channel.updated_by_id = user_id
|
|
424
|
+
channel.prep_for_save()
|
|
425
|
+
|
|
426
|
+
return self._save_model(channel)
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
return self._handle_service_exception(e, 'archive_channel', channel_id=channel_id)
|
|
430
|
+
|
|
431
|
+
def unarchive(self, channel_id: str, tenant_id: str, user_id: str) -> ServiceResult[ChatChannel]:
|
|
432
|
+
"""
|
|
433
|
+
Unarchive a chat channel.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
channel_id: Channel ID
|
|
437
|
+
tenant_id: Tenant ID
|
|
438
|
+
user_id: User ID performing the action
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
ServiceResult with updated ChatChannel
|
|
442
|
+
"""
|
|
443
|
+
try:
|
|
444
|
+
# For unarchive, we need to get the channel even if archived
|
|
445
|
+
channel = self._get_model_by_id(channel_id, ChatChannel)
|
|
446
|
+
if not channel:
|
|
447
|
+
raise NotFoundError(f"Chat channel with ID {channel_id} not found")
|
|
448
|
+
|
|
449
|
+
self._validate_tenant_access(channel.tenant_id, tenant_id)
|
|
450
|
+
|
|
451
|
+
# Only creator can unarchive
|
|
452
|
+
if channel.created_by != user_id:
|
|
453
|
+
raise AccessDeniedError("Only channel creator can unarchive the channel")
|
|
454
|
+
|
|
455
|
+
channel.is_archived = False
|
|
456
|
+
channel.updated_by_id = user_id
|
|
457
|
+
channel.prep_for_save()
|
|
458
|
+
|
|
459
|
+
return self._save_model(channel)
|
|
460
|
+
|
|
461
|
+
except Exception as e:
|
|
462
|
+
return self._handle_service_exception(e, 'unarchive_channel', channel_id=channel_id)
|
|
463
|
+
|
|
464
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
465
|
+
updates: Dict[str, Any]) -> ServiceResult[ChatChannel]:
|
|
466
|
+
"""
|
|
467
|
+
Update chat channel with access control.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
resource_id: Channel ID
|
|
471
|
+
tenant_id: Tenant ID
|
|
472
|
+
user_id: User ID performing the update
|
|
473
|
+
updates: Dictionary of fields to update
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
ServiceResult with updated ChatChannel
|
|
477
|
+
"""
|
|
478
|
+
try:
|
|
479
|
+
channel = self._get_model_by_id(resource_id, ChatChannel)
|
|
480
|
+
|
|
481
|
+
if not channel:
|
|
482
|
+
raise NotFoundError(f"Chat channel with ID {resource_id} not found")
|
|
483
|
+
|
|
484
|
+
# Validate tenant access
|
|
485
|
+
if hasattr(channel, 'tenant_id'):
|
|
486
|
+
self._validate_tenant_access(channel.tenant_id, tenant_id)
|
|
487
|
+
|
|
488
|
+
# Check permissions - only creator or members can update
|
|
489
|
+
if not self.is_member(resource_id, user_id):
|
|
490
|
+
raise AccessDeniedError("Only channel members can update the channel")
|
|
491
|
+
|
|
492
|
+
# Apply updates (limited fields)
|
|
493
|
+
allowed_fields = ['name', 'description', 'topic', 'icon', 'is_announcement', 'is_default']
|
|
494
|
+
for field, value in updates.items():
|
|
495
|
+
if field in allowed_fields and hasattr(channel, field):
|
|
496
|
+
# Only creator can change certain settings
|
|
497
|
+
if field in ['is_announcement', 'is_default'] and channel.created_by != user_id:
|
|
498
|
+
continue
|
|
499
|
+
setattr(channel, field, value)
|
|
500
|
+
|
|
501
|
+
# Update metadata
|
|
502
|
+
channel.updated_by_id = user_id
|
|
503
|
+
channel.prep_for_save()
|
|
504
|
+
|
|
505
|
+
# Save updated channel
|
|
506
|
+
return self._save_model(channel)
|
|
507
|
+
|
|
508
|
+
except Exception as e:
|
|
509
|
+
return self._handle_service_exception(e, 'update_chat_channel', resource_id=resource_id, tenant_id=tenant_id)
|
|
510
|
+
|
|
511
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
512
|
+
"""
|
|
513
|
+
Soft delete chat channel with access control.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
resource_id: Channel ID
|
|
517
|
+
tenant_id: Tenant ID
|
|
518
|
+
user_id: User ID performing the deletion
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
ServiceResult with boolean success
|
|
522
|
+
"""
|
|
523
|
+
try:
|
|
524
|
+
channel = self._get_model_by_id(resource_id, ChatChannel)
|
|
525
|
+
|
|
526
|
+
if not channel:
|
|
527
|
+
raise NotFoundError(f"Chat channel with ID {resource_id} not found")
|
|
528
|
+
|
|
529
|
+
# Check if already deleted
|
|
530
|
+
if channel.is_deleted():
|
|
531
|
+
return ServiceResult.success_result(True)
|
|
532
|
+
|
|
533
|
+
# Validate tenant access
|
|
534
|
+
if hasattr(channel, 'tenant_id'):
|
|
535
|
+
self._validate_tenant_access(channel.tenant_id, tenant_id)
|
|
536
|
+
|
|
537
|
+
# Only creator can delete
|
|
538
|
+
if channel.created_by != user_id:
|
|
539
|
+
raise AccessDeniedError("Only channel creator can delete the channel")
|
|
540
|
+
|
|
541
|
+
# Soft delete: set deleted timestamp and metadata
|
|
542
|
+
channel.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
543
|
+
channel.deleted_by_id = user_id
|
|
544
|
+
channel.prep_for_save()
|
|
545
|
+
|
|
546
|
+
# Save the updated channel
|
|
547
|
+
save_result = self._save_model(channel)
|
|
548
|
+
if save_result.success:
|
|
549
|
+
return ServiceResult.success_result(True)
|
|
550
|
+
else:
|
|
551
|
+
return save_result
|
|
552
|
+
|
|
553
|
+
except Exception as e:
|
|
554
|
+
return self._handle_service_exception(e, 'delete_chat_channel', resource_id=resource_id, tenant_id=tenant_id)
|
|
555
|
+
|
|
556
|
+
# Membership Management Methods (using adjacent records for scalability)
|
|
557
|
+
|
|
558
|
+
def _add_member_record(self, channel_id: str, user_id: str, added_by_id: str,
|
|
559
|
+
role: str = "member") -> ServiceResult[ChatChannelMember]:
|
|
560
|
+
"""
|
|
561
|
+
Add a member record to the channel (internal helper).
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
channel_id: Channel ID
|
|
565
|
+
user_id: User ID to add as member
|
|
566
|
+
added_by_id: User ID performing the add
|
|
567
|
+
role: Member role (owner, admin, member)
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
ServiceResult with ChatChannelMember
|
|
571
|
+
"""
|
|
572
|
+
try:
|
|
573
|
+
member = ChatChannelMember()
|
|
574
|
+
member.channel_id = channel_id
|
|
575
|
+
member.user_id = user_id
|
|
576
|
+
member.role = role
|
|
577
|
+
member.joined_at = dt.datetime.now(dt.UTC).timestamp()
|
|
578
|
+
member.added_by_id = added_by_id
|
|
579
|
+
member.tenant_id = None # Inherited from channel
|
|
580
|
+
member.prep_for_save()
|
|
581
|
+
|
|
582
|
+
return self._save_model(member)
|
|
583
|
+
except Exception as e:
|
|
584
|
+
return self._handle_service_exception(e, 'add_member_record',
|
|
585
|
+
channel_id=channel_id, user_id=user_id)
|
|
586
|
+
|
|
587
|
+
def is_member(self, channel_id: str, user_id: str) -> bool:
|
|
588
|
+
"""
|
|
589
|
+
Check if a user is a member of a channel (fast lookup).
|
|
590
|
+
Uses adjacent record pattern: pk="channel#<id>", sk="member#<user_id>"
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
channel_id: Channel ID
|
|
594
|
+
user_id: User ID to check
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
True if user is a member, False otherwise
|
|
598
|
+
"""
|
|
599
|
+
try:
|
|
600
|
+
# Build composite key for adjacent record lookup
|
|
601
|
+
pk = DynamoDBKey.build_key(("channel", channel_id))
|
|
602
|
+
sk = DynamoDBKey.build_key(("member", user_id))
|
|
603
|
+
key = {"pk": pk, "sk": sk}
|
|
604
|
+
|
|
605
|
+
result = self.dynamodb.get(table_name=self.table_name, key=key)
|
|
606
|
+
return result and "Item" in result
|
|
607
|
+
except Exception:
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
def get_member(self, channel_id: str, user_id: str) -> Optional[ChatChannelMember]:
|
|
611
|
+
"""
|
|
612
|
+
Get member record for a user in a channel.
|
|
613
|
+
Uses adjacent record pattern: pk="channel#<id>", sk="member#<user_id>"
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
channel_id: Channel ID
|
|
617
|
+
user_id: User ID
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
ChatChannelMember if found, None otherwise
|
|
621
|
+
"""
|
|
622
|
+
try:
|
|
623
|
+
# Build composite key for adjacent record lookup
|
|
624
|
+
pk = DynamoDBKey.build_key(("channel", channel_id))
|
|
625
|
+
sk = DynamoDBKey.build_key(("member", user_id))
|
|
626
|
+
key = {"pk": pk, "sk": sk}
|
|
627
|
+
|
|
628
|
+
result = self.dynamodb.get(table_name=self.table_name, key=key)
|
|
629
|
+
if result and "Item" in result:
|
|
630
|
+
member_obj = ChatChannelMember()
|
|
631
|
+
member_obj.map(result["Item"])
|
|
632
|
+
return member_obj
|
|
633
|
+
return None
|
|
634
|
+
except Exception:
|
|
635
|
+
return None
|
|
636
|
+
|
|
637
|
+
def list_channel_members(self, channel_id: str, limit: int = 100) -> ServiceResult[List[ChatChannelMember]]:
|
|
638
|
+
"""
|
|
639
|
+
List all members of a channel (paginated).
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
channel_id: Channel ID
|
|
643
|
+
limit: Maximum number of members to return
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
ServiceResult with list of ChatChannelMembers
|
|
647
|
+
"""
|
|
648
|
+
try:
|
|
649
|
+
# Query for all members in this channel using adjacent record pattern
|
|
650
|
+
pk = DynamoDBKey.build_key(("channel", channel_id))
|
|
651
|
+
|
|
652
|
+
return self._query_by_pk_with_sk_prefix(
|
|
653
|
+
model_class=ChatChannelMember,
|
|
654
|
+
pk=pk,
|
|
655
|
+
sk_prefix="member#",
|
|
656
|
+
limit=limit
|
|
657
|
+
)
|
|
658
|
+
except Exception as e:
|
|
659
|
+
return self._handle_service_exception(e, 'list_channel_members', channel_id=channel_id)
|
|
660
|
+
|
|
661
|
+
def list_user_channels_fast(self, user_id: str, limit: int = 100) -> ServiceResult[List[Dict[str, Any]]]:
|
|
662
|
+
"""
|
|
663
|
+
List all channels a user is a member of (using GSI1 for fast lookup).
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
user_id: User ID
|
|
667
|
+
limit: Maximum number of channels to return
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
ServiceResult with list of channel membership records
|
|
671
|
+
"""
|
|
672
|
+
try:
|
|
673
|
+
# Query GSI1 for all channels this user is in
|
|
674
|
+
temp_member = ChatChannelMember()
|
|
675
|
+
temp_member.user_id = user_id
|
|
676
|
+
|
|
677
|
+
result = self._query_by_index(
|
|
678
|
+
model=temp_member,
|
|
679
|
+
index_name="gsi1",
|
|
680
|
+
limit=limit
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
if not result.success:
|
|
684
|
+
return result
|
|
685
|
+
|
|
686
|
+
# Extract channel info from memberships
|
|
687
|
+
memberships = []
|
|
688
|
+
for member in result.data:
|
|
689
|
+
memberships.append({
|
|
690
|
+
"channel_id": member.channel_id,
|
|
691
|
+
"role": member.role,
|
|
692
|
+
"joined_at": member.joined_at,
|
|
693
|
+
"last_read_at": member.last_read_at
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
return ServiceResult.success_result(memberships)
|
|
697
|
+
except Exception as e:
|
|
698
|
+
return self._handle_service_exception(e, 'list_user_channels_fast', user_id=user_id)
|
|
699
|
+
|
|
700
|
+
# Removed custom _handle_service_exception - using base class implementation from DatabaseService
|