Updates
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
# Collaboration & War Rooms API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Collaboration & War Rooms module provides real-time incident collaboration capabilities including war rooms, conference bridges, incident command roles, and timeline reconstruction for postmortems.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time Incident Rooms**: Auto-created Slack/Teams channels per incident
|
||||
- **Conference Bridge Integration**: Zoom, Teams, Webex integration
|
||||
- **Incident Command Roles**: Assign Incident Commander, Scribe, Comms Lead
|
||||
- **Timeline Reconstruction**: Automatically ordered events + human notes for postmortems
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### War Rooms
|
||||
|
||||
#### List War Rooms
|
||||
```
|
||||
GET /api/collaboration-war-rooms/war-rooms/
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `status`: Filter by status (ACTIVE, ARCHIVED, CLOSED)
|
||||
- `privacy_level`: Filter by privacy level (PUBLIC, PRIVATE, RESTRICTED)
|
||||
- `incident__severity`: Filter by incident severity
|
||||
- `search`: Search in name, description, incident title
|
||||
- `ordering`: Order by created_at, last_activity, message_count
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"count": 10,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Incident 123 - Database Outage",
|
||||
"incident_title": "Database Outage",
|
||||
"incident_severity": "CRITICAL",
|
||||
"status": "ACTIVE",
|
||||
"privacy_level": "PRIVATE",
|
||||
"message_count": 45,
|
||||
"last_activity": "2024-01-15T10:30:00Z",
|
||||
"participant_count": 5,
|
||||
"created_at": "2024-01-15T09:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Create War Room
|
||||
```
|
||||
POST /api/collaboration-war-rooms/war-rooms/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Incident 123 - Database Outage",
|
||||
"description": "War room for database outage incident",
|
||||
"incident_id": "uuid",
|
||||
"privacy_level": "PRIVATE",
|
||||
"allowed_user_ids": ["uuid1", "uuid2"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get War Room Details
|
||||
```
|
||||
GET /api/collaboration-war-rooms/war-rooms/{id}/
|
||||
```
|
||||
|
||||
#### Update War Room
|
||||
```
|
||||
PUT /api/collaboration-war-rooms/war-rooms/{id}/
|
||||
PATCH /api/collaboration-war-rooms/war-rooms/{id}/
|
||||
```
|
||||
|
||||
#### Add Participant
|
||||
```
|
||||
POST /api/collaboration-war-rooms/war-rooms/{id}/add_participant/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"user_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
#### Remove Participant
|
||||
```
|
||||
POST /api/collaboration-war-rooms/war-rooms/{id}/remove_participant/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"user_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
#### Get War Room Messages
|
||||
```
|
||||
GET /api/collaboration-war-rooms/war-rooms/{id}/messages/
|
||||
```
|
||||
|
||||
### Conference Bridges
|
||||
|
||||
#### List Conference Bridges
|
||||
```
|
||||
GET /api/collaboration-war-rooms/conference-bridges/
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `bridge_type`: Filter by bridge type (ZOOM, TEAMS, WEBEX, etc.)
|
||||
- `status`: Filter by status (SCHEDULED, ACTIVE, ENDED, CANCELLED)
|
||||
- `incident__severity`: Filter by incident severity
|
||||
- `search`: Search in name, description, incident title
|
||||
- `ordering`: Order by scheduled_start, created_at
|
||||
|
||||
#### Create Conference Bridge
|
||||
```
|
||||
POST /api/collaboration-war-rooms/conference-bridges/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Incident 123 - Database Outage Call",
|
||||
"description": "Emergency conference call for database outage",
|
||||
"incident_id": "uuid",
|
||||
"war_room_id": "uuid",
|
||||
"bridge_type": "ZOOM",
|
||||
"scheduled_start": "2024-01-15T10:00:00Z",
|
||||
"scheduled_end": "2024-01-15T11:00:00Z",
|
||||
"invited_participant_ids": ["uuid1", "uuid2"],
|
||||
"recording_enabled": true,
|
||||
"transcription_enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Join Conference
|
||||
```
|
||||
POST /api/collaboration-war-rooms/conference-bridges/{id}/join_conference/
|
||||
```
|
||||
|
||||
#### Start Conference
|
||||
```
|
||||
POST /api/collaboration-war-rooms/conference-bridges/{id}/start_conference/
|
||||
```
|
||||
|
||||
#### End Conference
|
||||
```
|
||||
POST /api/collaboration-war-rooms/conference-bridges/{id}/end_conference/
|
||||
```
|
||||
|
||||
### Incident Command Roles
|
||||
|
||||
#### List Command Roles
|
||||
```
|
||||
GET /api/collaboration-war-rooms/command-roles/
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `role_type`: Filter by role type (INCIDENT_COMMANDER, SCRIBE, COMMS_LEAD, etc.)
|
||||
- `status`: Filter by status (ACTIVE, INACTIVE, REASSIGNED)
|
||||
- `incident__severity`: Filter by incident severity
|
||||
- `search`: Search in incident title, assigned user username
|
||||
- `ordering`: Order by assigned_at, created_at
|
||||
|
||||
#### Create Command Role
|
||||
```
|
||||
POST /api/collaboration-war-rooms/command-roles/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"incident_id": "uuid",
|
||||
"war_room_id": "uuid",
|
||||
"role_type": "INCIDENT_COMMANDER",
|
||||
"assigned_user_id": "uuid",
|
||||
"responsibilities": [
|
||||
"Overall incident coordination",
|
||||
"Decision making authority",
|
||||
"Communication with stakeholders"
|
||||
],
|
||||
"decision_authority": [
|
||||
"TECHNICAL",
|
||||
"BUSINESS",
|
||||
"ESCALATION"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Reassign Role
|
||||
```
|
||||
POST /api/collaboration-war-rooms/command-roles/{id}/reassign_role/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"new_user_id": "uuid",
|
||||
"notes": "Reassigning due to shift change"
|
||||
}
|
||||
```
|
||||
|
||||
### Timeline Events
|
||||
|
||||
#### List Timeline Events
|
||||
```
|
||||
GET /api/collaboration-war-rooms/timeline-events/
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `event_type`: Filter by event type (INCIDENT_CREATED, STATUS_CHANGED, etc.)
|
||||
- `source_type`: Filter by source type (SYSTEM, USER, INTEGRATION, AUTOMATION)
|
||||
- `is_critical_event`: Filter critical events for postmortems
|
||||
- `incident__severity`: Filter by incident severity
|
||||
- `search`: Search in title, description, incident title
|
||||
- `ordering`: Order by event_time, created_at
|
||||
|
||||
#### Get Critical Events
|
||||
```
|
||||
GET /api/collaboration-war-rooms/timeline-events/critical_events/
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"count": 5,
|
||||
"results": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"incident_title": "Database Outage",
|
||||
"event_type": "SLA_BREACHED",
|
||||
"title": "SLA Breached: Response Time",
|
||||
"description": "SLA 'Response Time' has been breached",
|
||||
"source_type": "SYSTEM",
|
||||
"event_time": "2024-01-15T10:15:00Z",
|
||||
"related_user_name": null,
|
||||
"is_critical_event": true,
|
||||
"created_at": "2024-01-15T10:15:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### War Room Messages
|
||||
|
||||
#### List Messages
|
||||
```
|
||||
GET /api/collaboration-war-rooms/war-room-messages/
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `message_type`: Filter by message type (TEXT, SYSTEM, COMMAND, ALERT, UPDATE)
|
||||
- `war_room`: Filter by war room ID
|
||||
- `sender`: Filter by sender ID
|
||||
- `search`: Search in content, sender name
|
||||
- `ordering`: Order by created_at
|
||||
|
||||
#### Create Message
|
||||
```
|
||||
POST /api/collaboration-war-rooms/war-room-messages/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"war_room_id": "uuid",
|
||||
"message_type": "TEXT",
|
||||
"content": "Database connection restored. Monitoring for stability.",
|
||||
"sender_id": "uuid",
|
||||
"sender_name": "John Doe"
|
||||
}
|
||||
```
|
||||
|
||||
### Incident Decisions
|
||||
|
||||
#### List Decisions
|
||||
```
|
||||
GET /api/collaboration-war-rooms/incident-decisions/
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `decision_type`: Filter by decision type (TECHNICAL, BUSINESS, COMMUNICATION, etc.)
|
||||
- `status`: Filter by status (PENDING, APPROVED, REJECTED, IMPLEMENTED)
|
||||
- `incident__severity`: Filter by incident severity
|
||||
- `search`: Search in title, description, incident title
|
||||
- `ordering`: Order by created_at, approved_at, implemented_at
|
||||
|
||||
#### Create Decision
|
||||
```
|
||||
POST /api/collaboration-war-rooms/incident-decisions/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"incident_id": "uuid",
|
||||
"command_role_id": "uuid",
|
||||
"decision_type": "TECHNICAL",
|
||||
"title": "Restart Database Cluster",
|
||||
"description": "Decision to restart the primary database cluster to resolve connection issues",
|
||||
"rationale": "Multiple connection timeouts indicate cluster instability. Restart should resolve the issue.",
|
||||
"requires_approval": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Approve Decision
|
||||
```
|
||||
POST /api/collaboration-war-rooms/incident-decisions/{id}/approve_decision/
|
||||
```
|
||||
|
||||
#### Implement Decision
|
||||
```
|
||||
POST /api/collaboration-war-rooms/incident-decisions/{id}/implement_decision/
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"notes": "Database cluster restarted successfully. All connections restored."
|
||||
}
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### WarRoom
|
||||
- `id`: UUID primary key
|
||||
- `name`: War room name
|
||||
- `description`: War room description
|
||||
- `incident`: Related incident (ForeignKey)
|
||||
- `status`: ACTIVE, ARCHIVED, CLOSED
|
||||
- `privacy_level`: PUBLIC, PRIVATE, RESTRICTED
|
||||
- `slack_channel_id`: Slack channel ID
|
||||
- `teams_channel_id`: Teams channel ID
|
||||
- `discord_channel_id`: Discord channel ID
|
||||
- `allowed_users`: Users with access (ManyToMany)
|
||||
- `required_clearance_level`: Required security clearance
|
||||
- `message_count`: Number of messages
|
||||
- `last_activity`: Last activity timestamp
|
||||
- `active_participants`: Number of active participants
|
||||
- `created_by`: Creator (ForeignKey to User)
|
||||
- `created_at`: Creation timestamp
|
||||
- `updated_at`: Last update timestamp
|
||||
- `archived_at`: Archive timestamp
|
||||
|
||||
### ConferenceBridge
|
||||
- `id`: UUID primary key
|
||||
- `name`: Conference name
|
||||
- `description`: Conference description
|
||||
- `incident`: Related incident (ForeignKey)
|
||||
- `war_room`: Related war room (ForeignKey)
|
||||
- `bridge_type`: ZOOM, TEAMS, WEBEX, GOTO_MEETING, CUSTOM
|
||||
- `status`: SCHEDULED, ACTIVE, ENDED, CANCELLED
|
||||
- `meeting_id`: External meeting ID
|
||||
- `meeting_url`: Meeting URL
|
||||
- `dial_in_number`: Dial-in phone number
|
||||
- `access_code`: Access code for dial-in
|
||||
- `scheduled_start`: Scheduled start time
|
||||
- `scheduled_end`: Scheduled end time
|
||||
- `actual_start`: Actual start time
|
||||
- `actual_end`: Actual end time
|
||||
- `invited_participants`: Invited users (ManyToMany)
|
||||
- `active_participants`: Active users (ManyToMany)
|
||||
- `max_participants`: Maximum participants
|
||||
- `recording_enabled`: Recording enabled flag
|
||||
- `recording_url`: Recording URL
|
||||
- `transcription_enabled`: Transcription enabled flag
|
||||
- `transcription_url`: Transcription URL
|
||||
- `integration_config`: Integration configuration (JSON)
|
||||
- `created_by`: Creator (ForeignKey to User)
|
||||
- `created_at`: Creation timestamp
|
||||
- `updated_at`: Last update timestamp
|
||||
|
||||
### IncidentCommandRole
|
||||
- `id`: UUID primary key
|
||||
- `incident`: Related incident (ForeignKey)
|
||||
- `war_room`: Related war room (ForeignKey)
|
||||
- `role_type`: INCIDENT_COMMANDER, SCRIBE, COMMS_LEAD, TECHNICAL_LEAD, BUSINESS_LEAD, EXTERNAL_LIAISON, OBSERVER
|
||||
- `assigned_user`: Assigned user (ForeignKey to User)
|
||||
- `status`: ACTIVE, INACTIVE, REASSIGNED
|
||||
- `responsibilities`: List of responsibilities (JSON)
|
||||
- `decision_authority`: Areas of decision authority (JSON)
|
||||
- `assigned_at`: Assignment timestamp
|
||||
- `reassigned_at`: Reassignment timestamp
|
||||
- `reassigned_by`: User who reassigned (ForeignKey to User)
|
||||
- `assignment_notes`: Assignment notes
|
||||
- `decisions_made`: Number of decisions made
|
||||
- `communications_sent`: Number of communications sent
|
||||
- `last_activity`: Last activity timestamp
|
||||
- `created_by`: Creator (ForeignKey to User)
|
||||
- `created_at`: Creation timestamp
|
||||
- `updated_at`: Last update timestamp
|
||||
|
||||
### TimelineEvent
|
||||
- `id`: UUID primary key
|
||||
- `incident`: Related incident (ForeignKey)
|
||||
- `event_type`: Event type (INCIDENT_CREATED, STATUS_CHANGED, etc.)
|
||||
- `title`: Event title
|
||||
- `description`: Event description
|
||||
- `source_type`: SYSTEM, USER, INTEGRATION, AUTOMATION
|
||||
- `event_time`: When the event occurred
|
||||
- `created_at`: Creation timestamp
|
||||
- `related_user`: Related user (ForeignKey to User)
|
||||
- `related_runbook_execution`: Related runbook execution (ForeignKey)
|
||||
- `related_auto_remediation`: Related auto-remediation (ForeignKey)
|
||||
- `related_sla_instance`: Related SLA instance (ForeignKey)
|
||||
- `related_escalation`: Related escalation (ForeignKey)
|
||||
- `related_war_room`: Related war room (ForeignKey)
|
||||
- `related_conference`: Related conference (ForeignKey)
|
||||
- `related_command_role`: Related command role (ForeignKey)
|
||||
- `event_data`: Additional event data (JSON)
|
||||
- `tags`: Event tags (JSON)
|
||||
- `is_critical_event`: Critical for postmortem flag
|
||||
- `postmortem_notes`: Postmortem notes
|
||||
- `created_by`: Creator (ForeignKey to User)
|
||||
|
||||
### WarRoomMessage
|
||||
- `id`: UUID primary key
|
||||
- `war_room`: Related war room (ForeignKey)
|
||||
- `message_type`: TEXT, SYSTEM, COMMAND, ALERT, UPDATE
|
||||
- `content`: Message content
|
||||
- `sender`: Sender user (ForeignKey to User)
|
||||
- `sender_name`: Display name of sender
|
||||
- `is_edited`: Edited flag
|
||||
- `edited_at`: Edit timestamp
|
||||
- `reply_to`: Reply to message (ForeignKey to self)
|
||||
- `external_message_id`: External system message ID
|
||||
- `external_data`: External system data (JSON)
|
||||
- `created_at`: Creation timestamp
|
||||
- `updated_at`: Last update timestamp
|
||||
|
||||
### IncidentDecision
|
||||
- `id`: UUID primary key
|
||||
- `incident`: Related incident (ForeignKey)
|
||||
- `command_role`: Related command role (ForeignKey)
|
||||
- `decision_type`: TECHNICAL, BUSINESS, COMMUNICATION, ESCALATION, RESOURCE, TIMELINE
|
||||
- `title`: Decision title
|
||||
- `description`: Decision description
|
||||
- `rationale`: Decision rationale
|
||||
- `status`: PENDING, APPROVED, REJECTED, IMPLEMENTED
|
||||
- `requires_approval`: Requires approval flag
|
||||
- `approved_by`: Approver (ForeignKey to User)
|
||||
- `approved_at`: Approval timestamp
|
||||
- `implementation_notes`: Implementation notes
|
||||
- `implemented_at`: Implementation timestamp
|
||||
- `implemented_by`: Implementer (ForeignKey to User)
|
||||
- `impact_assessment`: Impact assessment
|
||||
- `success_metrics`: Success metrics (JSON)
|
||||
- `created_at`: Creation timestamp
|
||||
- `updated_at`: Last update timestamp
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Automatic War Room Creation
|
||||
- War rooms are automatically created when new incidents are created
|
||||
- Incident reporter and assignee are automatically added as participants
|
||||
- Timeline events are created for war room creation
|
||||
|
||||
### Timeline Event Integration
|
||||
- Timeline events are automatically created for:
|
||||
- Incident status changes
|
||||
- Severity changes
|
||||
- Assignment changes
|
||||
- Runbook executions
|
||||
- Auto-remediation attempts
|
||||
- SLA breaches
|
||||
- Escalation triggers
|
||||
- Command role assignments
|
||||
|
||||
### Security Integration
|
||||
- War room access is controlled by incident access permissions
|
||||
- Required clearance levels can be set for war rooms
|
||||
- All actions are logged for audit purposes
|
||||
|
||||
### SLA & On-Call Integration
|
||||
- Conference bridges can be linked to SLA instances
|
||||
- Command roles can be assigned to on-call personnel
|
||||
- Timeline events track SLA breaches and escalations
|
||||
|
||||
### Automation Integration
|
||||
- Timeline events are created for runbook executions
|
||||
- Auto-remediation attempts are tracked in timeline
|
||||
- War rooms can be integrated with ChatOps platforms
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Responses
|
||||
|
||||
#### 400 Bad Request
|
||||
```json
|
||||
{
|
||||
"error": "user_id is required"
|
||||
}
|
||||
```
|
||||
|
||||
#### 403 Forbidden
|
||||
```json
|
||||
{
|
||||
"error": "You do not have permission to join this conference"
|
||||
}
|
||||
```
|
||||
|
||||
#### 404 Not Found
|
||||
```json
|
||||
{
|
||||
"error": "User not found"
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require authentication. Include the authentication token in the request headers:
|
||||
|
||||
```
|
||||
Authorization: Token your-auth-token-here
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
API requests are rate limited to prevent abuse. Standard rate limits apply:
|
||||
- 1000 requests per hour per user
|
||||
- 100 requests per minute per user
|
||||
|
||||
## Webhooks
|
||||
|
||||
The system supports webhooks for real-time notifications:
|
||||
|
||||
### War Room Events
|
||||
- `war_room.created`: War room created
|
||||
- `war_room.updated`: War room updated
|
||||
- `war_room.archived`: War room archived
|
||||
|
||||
### Conference Events
|
||||
- `conference.scheduled`: Conference scheduled
|
||||
- `conference.started`: Conference started
|
||||
- `conference.ended`: Conference ended
|
||||
|
||||
### Timeline Events
|
||||
- `timeline_event.created`: Timeline event created
|
||||
- `timeline_event.critical`: Critical timeline event created
|
||||
|
||||
### Decision Events
|
||||
- `decision.created`: Decision created
|
||||
- `decision.approved`: Decision approved
|
||||
- `decision.implemented`: Decision implemented
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Incident Response Flow
|
||||
|
||||
1. **Incident Created** → War room automatically created
|
||||
2. **Assign Command Roles** → Incident Commander, Scribe, Comms Lead
|
||||
3. **Schedule Conference** → Emergency call for critical incidents
|
||||
4. **Make Decisions** → Track all decisions with approval workflow
|
||||
5. **Timeline Reconstruction** → Automatic + manual events for postmortem
|
||||
|
||||
### Integration with External Systems
|
||||
|
||||
```python
|
||||
# Create war room with Slack integration
|
||||
war_room = WarRoom.objects.create(
|
||||
name="Incident 123 - Database Outage",
|
||||
incident=incident,
|
||||
slack_channel_id="C1234567890"
|
||||
)
|
||||
|
||||
# Create conference bridge with Zoom
|
||||
conference = ConferenceBridge.objects.create(
|
||||
name="Emergency Call - Database Outage",
|
||||
incident=incident,
|
||||
war_room=war_room,
|
||||
bridge_type="ZOOM",
|
||||
scheduled_start=timezone.now() + timedelta(minutes=5),
|
||||
scheduled_end=timezone.now() + timedelta(hours=1),
|
||||
recording_enabled=True
|
||||
)
|
||||
```
|
||||
|
||||
This module provides comprehensive collaboration capabilities for incident response, ensuring effective communication, decision tracking, and postmortem analysis.
|
||||
@@ -0,0 +1,425 @@
|
||||
# Incident-Centric Chat API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Incident-Centric Chat system provides real-time collaboration capabilities for incident response teams. Every incident automatically gets its own chat room with advanced features including pinned messages, reactions, file sharing, ChatOps commands, and AI assistant integration.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Incident-Centric Chat Rooms
|
||||
- **Auto-creation**: Chat rooms are automatically created when incidents are created
|
||||
- **Cross-linking**: Direct links between incident timeline and chat logs
|
||||
- **Access Control**: RBAC-based access control with security clearance levels
|
||||
|
||||
### 2. Collaboration Features
|
||||
- **@mentions**: Mention users with notifications
|
||||
- **Threaded Conversations**: Reply to messages for sub-discussions
|
||||
- **Reactions**: Emoji reactions (👍, 🚨, ✅) for lightweight feedback
|
||||
- **Pinned Messages**: Pin important updates for easy reference
|
||||
|
||||
### 3. Media & Files
|
||||
- **File Sharing**: Upload logs, screenshots, evidence files
|
||||
- **Compliance Integration**: Automatic file classification (PUBLIC/CONFIDENTIAL/etc.)
|
||||
- **Chain of Custody**: File hashing and access logging for evidence
|
||||
- **Encryption**: Optional encryption for sensitive files
|
||||
|
||||
### 4. ChatOps Integration
|
||||
- **Commands**: Execute automation commands via chat
|
||||
- **Status Checks**: `/status incident-123` to fetch incident status
|
||||
- **Runbook Execution**: `/run playbook ransomware-incident`
|
||||
- **Escalation**: `/escalate` to trigger escalation procedures
|
||||
|
||||
### 5. Security Features
|
||||
- **Encryption**: Chat logs encrypted at rest and in transit
|
||||
- **Audit Trail**: Immutable audit trail for compliance
|
||||
- **RBAC**: Role-based access control for sensitive incidents
|
||||
- **Data Classification**: Automatic classification of shared content
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### War Rooms
|
||||
|
||||
#### List War Rooms
|
||||
```http
|
||||
GET /api/collaboration_war_rooms/api/war-rooms/
|
||||
```
|
||||
|
||||
#### Get War Room Details
|
||||
```http
|
||||
GET /api/collaboration_war_rooms/api/war-rooms/{id}/
|
||||
```
|
||||
|
||||
#### Create Chat Room for Incident
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/war-rooms/{incident_id}/create_chat_room/
|
||||
```
|
||||
|
||||
#### Get War Room Messages
|
||||
```http
|
||||
GET /api/collaboration_war_rooms/api/war-rooms/{id}/messages/
|
||||
```
|
||||
|
||||
#### Get Pinned Messages
|
||||
```http
|
||||
GET /api/collaboration_war_rooms/api/war-rooms/{id}/pinned_messages/
|
||||
```
|
||||
|
||||
### Messages
|
||||
|
||||
#### Send Message
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"war_room_id": "uuid",
|
||||
"content": "Message content",
|
||||
"message_type": "TEXT",
|
||||
"mentioned_user_ids": ["user-uuid-1", "user-uuid-2"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Pin Message
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/pin_message/
|
||||
```
|
||||
|
||||
#### Unpin Message
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/unpin_message/
|
||||
```
|
||||
|
||||
#### Add Reaction
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/add_reaction/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"emoji": "👍"
|
||||
}
|
||||
```
|
||||
|
||||
#### Remove Reaction
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/remove_reaction/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"emoji": "👍"
|
||||
}
|
||||
```
|
||||
|
||||
#### Execute ChatOps Command
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/execute_command/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"command_text": "/status"
|
||||
}
|
||||
```
|
||||
|
||||
### File Management
|
||||
|
||||
#### Upload File
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/chat-files/
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
{
|
||||
"message": "message-uuid",
|
||||
"file": "file-data",
|
||||
"file_type": "SCREENSHOT"
|
||||
}
|
||||
```
|
||||
|
||||
#### Log File Access
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/chat-files/{id}/log_access/
|
||||
```
|
||||
|
||||
### Chat Bots
|
||||
|
||||
#### List Chat Bots
|
||||
```http
|
||||
GET /api/collaboration_war_rooms/api/chat-bots/
|
||||
```
|
||||
|
||||
#### Generate AI Response
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/chat-bots/{id}/generate_response/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message_id": "message-uuid",
|
||||
"context": {}
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket API
|
||||
|
||||
### Connection
|
||||
```javascript
|
||||
const ws = new WebSocket('ws://localhost:8000/ws/chat/{room_id}/');
|
||||
```
|
||||
|
||||
### Message Types
|
||||
|
||||
#### Send Chat Message
|
||||
```javascript
|
||||
ws.send(JSON.stringify({
|
||||
type: 'chat_message',
|
||||
content: 'Hello team!',
|
||||
message_type: 'TEXT',
|
||||
reply_to_id: 'optional-message-id'
|
||||
}));
|
||||
```
|
||||
|
||||
#### Add Reaction
|
||||
```javascript
|
||||
ws.send(JSON.stringify({
|
||||
type: 'reaction',
|
||||
message_id: 'message-uuid',
|
||||
emoji: '👍',
|
||||
action: 'add' // or 'remove'
|
||||
}));
|
||||
```
|
||||
|
||||
#### Execute Command
|
||||
```javascript
|
||||
ws.send(JSON.stringify({
|
||||
type: 'command',
|
||||
message_id: 'message-uuid',
|
||||
command_text: '/status'
|
||||
}));
|
||||
```
|
||||
|
||||
#### Typing Indicator
|
||||
```javascript
|
||||
ws.send(JSON.stringify({
|
||||
type: 'typing',
|
||||
is_typing: true
|
||||
}));
|
||||
```
|
||||
|
||||
### Receive Messages
|
||||
|
||||
#### Chat Message
|
||||
```javascript
|
||||
ws.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'chat_message') {
|
||||
// Handle new message
|
||||
console.log('New message:', data.data);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Reaction Update
|
||||
```javascript
|
||||
if (data.type === 'reaction_update') {
|
||||
// Handle reaction update
|
||||
console.log('Reaction update:', data.data);
|
||||
}
|
||||
```
|
||||
|
||||
#### Command Result
|
||||
```javascript
|
||||
if (data.type === 'command_result') {
|
||||
// Handle command execution result
|
||||
console.log('Command result:', data.data);
|
||||
}
|
||||
```
|
||||
|
||||
## ChatOps Commands
|
||||
|
||||
### Available Commands
|
||||
|
||||
#### Status Check
|
||||
```
|
||||
/status
|
||||
```
|
||||
Returns current incident status, severity, assignee, and timestamps.
|
||||
|
||||
#### Runbook Execution
|
||||
```
|
||||
/run playbook <playbook-name>
|
||||
```
|
||||
Executes a runbook for the current incident.
|
||||
|
||||
#### Escalation
|
||||
```
|
||||
/escalate [reason]
|
||||
```
|
||||
Triggers escalation procedures for the incident.
|
||||
|
||||
#### Assignment
|
||||
```
|
||||
/assign <username>
|
||||
```
|
||||
Assigns the incident to a specific user.
|
||||
|
||||
#### Status Update
|
||||
```
|
||||
/update status <new-status>
|
||||
```
|
||||
Updates the incident status.
|
||||
|
||||
### Command Response Format
|
||||
```json
|
||||
{
|
||||
"command_type": "STATUS",
|
||||
"execution_status": "SUCCESS",
|
||||
"execution_result": {
|
||||
"incident_id": "uuid",
|
||||
"title": "Incident Title",
|
||||
"status": "IN_PROGRESS",
|
||||
"severity": "HIGH",
|
||||
"assigned_to": "username",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Incident Intelligence
|
||||
- Auto-creates chat rooms when incidents are created
|
||||
- Links chat messages to incident timeline
|
||||
- Updates incident status via ChatOps commands
|
||||
|
||||
### SLA & On-Call
|
||||
- Sends notifications when SLA thresholds are hit
|
||||
- Integrates with escalation procedures
|
||||
- Notifies on-call teams of critical updates
|
||||
|
||||
### Automation Orchestration
|
||||
- Executes runbooks via chat commands
|
||||
- Triggers auto-remediation procedures
|
||||
- Provides status updates on automation execution
|
||||
|
||||
### Compliance & Governance
|
||||
- Classifies files automatically
|
||||
- Maintains audit trails for all chat activity
|
||||
- Enforces data retention policies
|
||||
|
||||
### Security
|
||||
- Encrypts sensitive messages and files
|
||||
- Enforces RBAC for incident access
|
||||
- Logs all security-relevant activities
|
||||
|
||||
### Knowledge Learning
|
||||
- AI assistant provides contextual help
|
||||
- Suggests similar past incidents
|
||||
- Learns from chat interactions
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Access Control
|
||||
- Users must have appropriate clearance level for sensitive incidents
|
||||
- War room access is controlled by incident permissions
|
||||
- File access is logged and audited
|
||||
|
||||
### Encryption
|
||||
- Messages can be encrypted for sensitive incidents
|
||||
- Files are encrypted based on classification level
|
||||
- WebSocket connections use WSS in production
|
||||
|
||||
### Audit Trail
|
||||
- All chat messages are logged with timestamps
|
||||
- File access is tracked with user and timestamp
|
||||
- Command executions are logged with results
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Message Organization
|
||||
- Use pinned messages for important updates
|
||||
- Use reactions for quick feedback
|
||||
- Use threaded replies for focused discussions
|
||||
|
||||
### File Management
|
||||
- Classify files appropriately
|
||||
- Use descriptive filenames
|
||||
- Clean up temporary files regularly
|
||||
|
||||
### Command Usage
|
||||
- Use commands for automation, not manual updates
|
||||
- Verify command results before proceeding
|
||||
- Document custom commands for team use
|
||||
|
||||
### Security
|
||||
- Be mindful of sensitive information in chat
|
||||
- Use appropriate classification levels
|
||||
- Report security incidents immediately
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Responses
|
||||
|
||||
#### Access Denied
|
||||
```json
|
||||
{
|
||||
"error": "You do not have permission to access this war room"
|
||||
}
|
||||
```
|
||||
|
||||
#### Invalid Command
|
||||
```json
|
||||
{
|
||||
"error": "Unknown command type"
|
||||
}
|
||||
```
|
||||
|
||||
#### File Upload Error
|
||||
```json
|
||||
{
|
||||
"error": "File size exceeds limit"
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket Errors
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"message": "Authentication required"
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Message sending: 60 messages per minute per user
|
||||
- File uploads: 10 files per minute per user
|
||||
- Command execution: 20 commands per minute per user
|
||||
- WebSocket connections: 5 concurrent connections per user
|
||||
|
||||
## Monitoring & Analytics
|
||||
|
||||
### Metrics Tracked
|
||||
- Message volume per incident
|
||||
- Response times for commands
|
||||
- File upload/download statistics
|
||||
- User engagement metrics
|
||||
- Error rates and types
|
||||
|
||||
### Alerts
|
||||
- High message volume incidents
|
||||
- Failed command executions
|
||||
- Security policy violations
|
||||
- System performance issues
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
- Voice messages and video calls
|
||||
- Advanced AI assistant capabilities
|
||||
- Integration with external chat platforms
|
||||
- Mobile app support
|
||||
- Advanced analytics dashboard
|
||||
|
||||
### Integration Roadmap
|
||||
- Slack/Teams integration
|
||||
- PagerDuty integration
|
||||
- Jira integration
|
||||
- Custom webhook support
|
||||
240
ETB-API/collaboration_war_rooms/README.md
Normal file
240
ETB-API/collaboration_war_rooms/README.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Incident-Centric Chat System
|
||||
|
||||
A comprehensive real-time collaboration platform for incident response teams, integrated with the ETB (Enterprise Incident Management) API.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
### Core Chat Functionality
|
||||
- **Real-time Messaging**: WebSocket-based chat with instant message delivery
|
||||
- **Incident-Centric Rooms**: Every incident automatically gets its own chat room
|
||||
- **Cross-linking**: Direct links between incident timeline and chat logs
|
||||
- **Pinned Messages**: Pin important updates for easy reference
|
||||
- **Threaded Conversations**: Reply to messages for focused discussions
|
||||
- **Reactions**: Emoji reactions (👍, 🚨, ✅) for lightweight feedback
|
||||
|
||||
### Advanced Collaboration
|
||||
- **@mentions**: Mention users with notifications
|
||||
- **File Sharing**: Upload logs, screenshots, evidence files
|
||||
- **ChatOps Commands**: Execute automation commands via chat
|
||||
- **AI Assistant**: Intelligent bot for incident guidance and knowledge queries
|
||||
|
||||
### Security & Compliance
|
||||
- **Encryption**: Chat logs encrypted at rest and in transit
|
||||
- **RBAC**: Role-based access control for sensitive incidents
|
||||
- **Audit Trail**: Immutable audit trail for compliance
|
||||
- **Data Classification**: Automatic file classification and retention
|
||||
|
||||
### Integrations
|
||||
- **Incident Intelligence**: Auto-creates chat rooms, links to timeline
|
||||
- **SLA & On-Call**: SLA threshold notifications, escalation alerts
|
||||
- **Automation Orchestration**: Execute runbooks via chat commands
|
||||
- **Compliance Governance**: File classification, audit trails
|
||||
- **Knowledge Learning**: AI assistant with knowledge base integration
|
||||
|
||||
## 📋 Quick Start
|
||||
|
||||
### 1. Setup
|
||||
```bash
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate.fish
|
||||
|
||||
# Run migrations
|
||||
python manage.py migrate
|
||||
|
||||
# Create default bots and war rooms
|
||||
python manage.py setup_chat_system --create-bots --create-war-rooms
|
||||
```
|
||||
|
||||
### 2. WebSocket Connection
|
||||
```javascript
|
||||
// Connect to chat room
|
||||
const ws = new WebSocket('ws://localhost:8000/ws/chat/{room_id}/');
|
||||
|
||||
// Send message
|
||||
ws.send(JSON.stringify({
|
||||
type: 'chat_message',
|
||||
content: 'Hello team!',
|
||||
message_type: 'TEXT'
|
||||
}));
|
||||
|
||||
// Add reaction
|
||||
ws.send(JSON.stringify({
|
||||
type: 'reaction',
|
||||
message_id: 'message-uuid',
|
||||
emoji: '👍',
|
||||
action: 'add'
|
||||
}));
|
||||
```
|
||||
|
||||
### 3. ChatOps Commands
|
||||
```
|
||||
/status # Get incident status
|
||||
/run playbook <name> # Execute runbook
|
||||
/escalate [reason] # Trigger escalation
|
||||
/assign <username> # Assign incident
|
||||
/update status <status> # Update incident status
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Models
|
||||
- **WarRoom**: Chat rooms for incidents
|
||||
- **WarRoomMessage**: Chat messages with reactions, attachments
|
||||
- **MessageReaction**: Emoji reactions to messages
|
||||
- **ChatFile**: File attachments with compliance integration
|
||||
- **ChatCommand**: ChatOps command execution
|
||||
- **ChatBot**: AI assistant bots
|
||||
|
||||
### Services
|
||||
- **SLANotificationService**: SLA threshold and escalation notifications
|
||||
- **AutomationCommandService**: ChatOps command execution
|
||||
- **ComplianceIntegrationService**: File classification and audit trails
|
||||
- **AIAssistantService**: AI-powered assistance and suggestions
|
||||
|
||||
### WebSocket Consumer
|
||||
- **ChatConsumer**: Real-time chat functionality
|
||||
- Message broadcasting
|
||||
- Reaction handling
|
||||
- Command execution
|
||||
- Typing indicators
|
||||
|
||||
## 🔧 API Endpoints
|
||||
|
||||
### War Rooms
|
||||
```http
|
||||
GET /api/collaboration_war_rooms/api/war-rooms/
|
||||
POST /api/collaboration_war_rooms/api/war-rooms/{id}/create_chat_room/
|
||||
GET /api/collaboration_war_rooms/api/war-rooms/{id}/messages/
|
||||
GET /api/collaboration_war_rooms/api/war-rooms/{id}/pinned_messages/
|
||||
```
|
||||
|
||||
### Messages
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/pin_message/
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/add_reaction/
|
||||
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/execute_command/
|
||||
```
|
||||
|
||||
### Files
|
||||
```http
|
||||
POST /api/collaboration_war_rooms/api/chat-files/
|
||||
POST /api/collaboration_war_rooms/api/chat-files/{id}/log_access/
|
||||
```
|
||||
|
||||
### AI Assistant
|
||||
```http
|
||||
GET /api/collaboration_war_rooms/api/chat-bots/
|
||||
POST /api/collaboration_war_rooms/api/chat-bots/{id}/generate_response/
|
||||
```
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
### Access Control
|
||||
- Users must have appropriate clearance level for sensitive incidents
|
||||
- War room access controlled by incident permissions
|
||||
- File access logged and audited
|
||||
|
||||
### Encryption
|
||||
- Messages can be encrypted for sensitive incidents
|
||||
- Files encrypted based on classification level
|
||||
- WebSocket connections use WSS in production
|
||||
|
||||
### Audit Trail
|
||||
- All chat messages logged with timestamps
|
||||
- File access tracked with user and timestamp
|
||||
- Command executions logged with results
|
||||
|
||||
## 📊 Monitoring & Analytics
|
||||
|
||||
### Metrics Tracked
|
||||
- Message volume per incident
|
||||
- Response times for commands
|
||||
- File upload/download statistics
|
||||
- User engagement metrics
|
||||
- Error rates and types
|
||||
|
||||
### Alerts
|
||||
- High message volume incidents
|
||||
- Failed command executions
|
||||
- Security policy violations
|
||||
- System performance issues
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Production Setup
|
||||
1. Configure WebSocket routing in your ASGI application
|
||||
2. Set up Redis for WebSocket channel layers
|
||||
3. Configure file storage for attachments
|
||||
4. Set up SSL certificates for WSS connections
|
||||
5. Configure monitoring and alerting
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# WebSocket configuration
|
||||
CHANNEL_LAYERS_REDIS_URL=redis://localhost:6379/1
|
||||
|
||||
# File storage
|
||||
DEFAULT_FILE_STORAGE=django.core.files.storage.FileSystemStorage
|
||||
MEDIA_ROOT=/var/www/media/
|
||||
|
||||
# Security
|
||||
CHAT_ENCRYPTION_KEY=your-encryption-key
|
||||
CHAT_AUDIT_LOG_LEVEL=INFO
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
python manage.py test collaboration_war_rooms
|
||||
```
|
||||
|
||||
### WebSocket Testing
|
||||
```javascript
|
||||
// Test WebSocket connection
|
||||
const ws = new WebSocket('ws://localhost:8000/ws/chat/test-room/');
|
||||
ws.onopen = () => console.log('Connected');
|
||||
ws.onmessage = (event) => console.log('Message:', event.data);
|
||||
```
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- [API Documentation](Documentations/INCIDENT_CENTRIC_CHAT_API.md)
|
||||
- [WebSocket API Reference](Documentations/INCIDENT_CENTRIC_CHAT_API.md#websocket-api)
|
||||
- [ChatOps Commands](Documentations/INCIDENT_CENTRIC_CHAT_API.md#chatops-commands)
|
||||
- [Security Guidelines](Documentations/INCIDENT_CENTRIC_CHAT_API.md#security-considerations)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests
|
||||
5. Submit a pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is part of the ETB (Enterprise Incident Management) API system.
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
For support and questions:
|
||||
- Check the documentation
|
||||
- Review the API reference
|
||||
- Contact the development team
|
||||
|
||||
## 🔮 Roadmap
|
||||
|
||||
### Planned Features
|
||||
- Voice messages and video calls
|
||||
- Advanced AI assistant capabilities
|
||||
- Integration with external chat platforms
|
||||
- Mobile app support
|
||||
- Advanced analytics dashboard
|
||||
|
||||
### Integration Roadmap
|
||||
- Slack/Teams integration
|
||||
- PagerDuty integration
|
||||
- Jira integration
|
||||
- Custom webhook support
|
||||
0
ETB-API/collaboration_war_rooms/__init__.py
Normal file
0
ETB-API/collaboration_war_rooms/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
BIN
ETB-API/collaboration_war_rooms/__pycache__/apps.cpython-312.pyc
Normal file
BIN
ETB-API/collaboration_war_rooms/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ETB-API/collaboration_war_rooms/__pycache__/urls.cpython-312.pyc
Normal file
BIN
ETB-API/collaboration_war_rooms/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
279
ETB-API/collaboration_war_rooms/admin.py
Normal file
279
ETB-API/collaboration_war_rooms/admin.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Admin configuration for Collaboration & War Rooms module
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
|
||||
from .models import (
|
||||
WarRoom, ConferenceBridge, IncidentCommandRole,
|
||||
TimelineEvent, WarRoomMessage, IncidentDecision
|
||||
)
|
||||
|
||||
|
||||
@admin.register(WarRoom)
|
||||
class WarRoomAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for WarRoom model"""
|
||||
|
||||
list_display = [
|
||||
'name', 'incident_title', 'status', 'privacy_level',
|
||||
'message_count', 'active_participants', 'created_at'
|
||||
]
|
||||
list_filter = ['status', 'privacy_level', 'created_at']
|
||||
search_fields = ['name', 'description', 'incident__title']
|
||||
readonly_fields = ['id', 'created_at', 'updated_at', 'message_count', 'last_activity']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('id', 'name', 'description', 'incident')
|
||||
}),
|
||||
('Configuration', {
|
||||
'fields': ('status', 'privacy_level', 'required_clearance_level')
|
||||
}),
|
||||
('Integrations', {
|
||||
'fields': ('slack_channel_id', 'teams_channel_id', 'discord_channel_id'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Access Control', {
|
||||
'fields': ('allowed_users',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Activity Tracking', {
|
||||
'fields': ('message_count', 'last_activity', 'active_participants'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by', 'created_at', 'updated_at', 'archived_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def incident_title(self, obj):
|
||||
"""Display incident title"""
|
||||
return obj.incident.title
|
||||
incident_title.short_description = 'Incident'
|
||||
|
||||
|
||||
@admin.register(ConferenceBridge)
|
||||
class ConferenceBridgeAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for ConferenceBridge model"""
|
||||
|
||||
list_display = [
|
||||
'name', 'incident_title', 'bridge_type', 'status',
|
||||
'scheduled_start', 'participant_count'
|
||||
]
|
||||
list_filter = ['bridge_type', 'status', 'recording_enabled', 'transcription_enabled']
|
||||
search_fields = ['name', 'description', 'incident__title']
|
||||
readonly_fields = ['id', 'created_at', 'updated_at', 'actual_start', 'actual_end']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('id', 'name', 'description', 'incident', 'war_room')
|
||||
}),
|
||||
('Bridge Configuration', {
|
||||
'fields': ('bridge_type', 'status', 'integration_config')
|
||||
}),
|
||||
('Meeting Details', {
|
||||
'fields': ('meeting_id', 'meeting_url', 'dial_in_number', 'access_code')
|
||||
}),
|
||||
('Schedule', {
|
||||
'fields': ('scheduled_start', 'scheduled_end', 'actual_start', 'actual_end')
|
||||
}),
|
||||
('Participants', {
|
||||
'fields': ('invited_participants', 'active_participants', 'max_participants'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Recording & Transcription', {
|
||||
'fields': ('recording_enabled', 'recording_url', 'transcription_enabled', 'transcription_url'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def incident_title(self, obj):
|
||||
"""Display incident title"""
|
||||
return obj.incident.title
|
||||
incident_title.short_description = 'Incident'
|
||||
|
||||
def participant_count(self, obj):
|
||||
"""Display participant count"""
|
||||
return obj.invited_participants.count()
|
||||
participant_count.short_description = 'Participants'
|
||||
|
||||
|
||||
@admin.register(IncidentCommandRole)
|
||||
class IncidentCommandRoleAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for IncidentCommandRole model"""
|
||||
|
||||
list_display = [
|
||||
'role_type', 'incident_title', 'assigned_user', 'status',
|
||||
'decisions_made', 'assigned_at'
|
||||
]
|
||||
list_filter = ['role_type', 'status', 'assigned_at']
|
||||
search_fields = ['incident__title', 'assigned_user__username']
|
||||
readonly_fields = [
|
||||
'id', 'assigned_at', 'reassigned_at', 'reassigned_by',
|
||||
'decisions_made', 'communications_sent', 'last_activity',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('id', 'incident', 'war_room', 'role_type', 'assigned_user', 'status')
|
||||
}),
|
||||
('Role Configuration', {
|
||||
'fields': ('responsibilities', 'decision_authority'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Assignment Tracking', {
|
||||
'fields': ('assigned_at', 'reassigned_at', 'reassigned_by', 'assignment_notes'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Performance Tracking', {
|
||||
'fields': ('decisions_made', 'communications_sent', 'last_activity'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def incident_title(self, obj):
|
||||
"""Display incident title"""
|
||||
return obj.incident.title
|
||||
incident_title.short_description = 'Incident'
|
||||
|
||||
|
||||
@admin.register(TimelineEvent)
|
||||
class TimelineEventAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for TimelineEvent model"""
|
||||
|
||||
list_display = [
|
||||
'event_time', 'title', 'incident_title', 'event_type',
|
||||
'source_type', 'is_critical_event'
|
||||
]
|
||||
list_filter = ['event_type', 'source_type', 'is_critical_event', 'event_time']
|
||||
search_fields = ['title', 'description', 'incident__title']
|
||||
readonly_fields = ['id', 'created_at']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('id', 'incident', 'event_type', 'title', 'description', 'source_type')
|
||||
}),
|
||||
('Timing', {
|
||||
'fields': ('event_time', 'created_at')
|
||||
}),
|
||||
('Related Objects', {
|
||||
'fields': (
|
||||
'related_user', 'related_runbook_execution', 'related_auto_remediation',
|
||||
'related_sla_instance', 'related_escalation', 'related_war_room',
|
||||
'related_conference', 'related_command_role'
|
||||
),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Event Data', {
|
||||
'fields': ('event_data', 'tags'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Postmortem', {
|
||||
'fields': ('is_critical_event', 'postmortem_notes'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('created_by',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def incident_title(self, obj):
|
||||
"""Display incident title"""
|
||||
return obj.incident.title
|
||||
incident_title.short_description = 'Incident'
|
||||
|
||||
|
||||
@admin.register(WarRoomMessage)
|
||||
class WarRoomMessageAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for WarRoomMessage model"""
|
||||
|
||||
list_display = [
|
||||
'sender_name', 'war_room_name', 'message_type', 'content_preview', 'created_at'
|
||||
]
|
||||
list_filter = ['message_type', 'is_edited', 'created_at']
|
||||
search_fields = ['content', 'sender_name', 'war_room__name']
|
||||
readonly_fields = ['id', 'created_at', 'updated_at', 'is_edited', 'edited_at']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('id', 'war_room', 'message_type', 'content')
|
||||
}),
|
||||
('Sender Information', {
|
||||
'fields': ('sender', 'sender_name')
|
||||
}),
|
||||
('Message Metadata', {
|
||||
'fields': ('is_edited', 'edited_at', 'reply_to'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Integration Data', {
|
||||
'fields': ('external_message_id', 'external_data'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def war_room_name(self, obj):
|
||||
"""Display war room name"""
|
||||
return obj.war_room.name
|
||||
war_room_name.short_description = 'War Room'
|
||||
|
||||
def content_preview(self, obj):
|
||||
"""Display content preview"""
|
||||
return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
|
||||
content_preview.short_description = 'Content Preview'
|
||||
|
||||
|
||||
@admin.register(IncidentDecision)
|
||||
class IncidentDecisionAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for IncidentDecision model"""
|
||||
|
||||
list_display = [
|
||||
'title', 'incident_title', 'decision_type', 'status',
|
||||
'approved_by', 'implemented_by', 'created_at'
|
||||
]
|
||||
list_filter = ['decision_type', 'status', 'requires_approval', 'created_at']
|
||||
search_fields = ['title', 'description', 'incident__title']
|
||||
readonly_fields = [
|
||||
'id', 'approved_by', 'approved_at', 'implemented_at',
|
||||
'implemented_by', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('id', 'incident', 'command_role', 'decision_type', 'title', 'description', 'rationale')
|
||||
}),
|
||||
('Decision Status', {
|
||||
'fields': ('status', 'requires_approval', 'approved_by', 'approved_at')
|
||||
}),
|
||||
('Implementation', {
|
||||
'fields': ('implementation_notes', 'implemented_at', 'implemented_by'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Impact Tracking', {
|
||||
'fields': ('impact_assessment', 'success_metrics'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def incident_title(self, obj):
|
||||
"""Display incident title"""
|
||||
return obj.incident.title
|
||||
incident_title.short_description = 'Incident'
|
||||
11
ETB-API/collaboration_war_rooms/apps.py
Normal file
11
ETB-API/collaboration_war_rooms/apps.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CollaborationWarRoomsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'collaboration_war_rooms'
|
||||
verbose_name = 'Collaboration & War Rooms'
|
||||
|
||||
def ready(self):
|
||||
"""Import signal handlers when the app is ready"""
|
||||
import collaboration_war_rooms.signals
|
||||
414
ETB-API/collaboration_war_rooms/consumers.py
Normal file
414
ETB-API/collaboration_war_rooms/consumers.py
Normal file
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
WebSocket consumers for real-time chat functionality
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.db import database_sync_to_async
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import WarRoom, WarRoomMessage, MessageReaction, ChatCommand
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ChatConsumer(AsyncWebsocketConsumer):
|
||||
"""WebSocket consumer for real-time chat in war rooms"""
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to WebSocket"""
|
||||
self.room_id = self.scope['url_route']['kwargs']['room_id']
|
||||
self.room_group_name = f'chat_{self.room_id}'
|
||||
|
||||
# Join room group
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.accept()
|
||||
|
||||
# Send room info
|
||||
room_info = await self.get_room_info()
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'room_info',
|
||||
'data': room_info
|
||||
}))
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
"""Disconnect from WebSocket"""
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
async def receive(self, text_data):
|
||||
"""Receive message from WebSocket"""
|
||||
try:
|
||||
data = json.loads(text_data)
|
||||
message_type = data.get('type')
|
||||
|
||||
if message_type == 'chat_message':
|
||||
await self.handle_chat_message(data)
|
||||
elif message_type == 'reaction':
|
||||
await self.handle_reaction(data)
|
||||
elif message_type == 'command':
|
||||
await self.handle_command(data)
|
||||
elif message_type == 'typing':
|
||||
await self.handle_typing(data)
|
||||
else:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Unknown message type'
|
||||
}))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Invalid JSON'
|
||||
}))
|
||||
except Exception as e:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': str(e)
|
||||
}))
|
||||
|
||||
async def handle_chat_message(self, data):
|
||||
"""Handle chat message"""
|
||||
content = data.get('content', '').strip()
|
||||
message_type = data.get('message_type', 'TEXT')
|
||||
reply_to_id = data.get('reply_to_id')
|
||||
|
||||
if not content:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Message content cannot be empty'
|
||||
}))
|
||||
return
|
||||
|
||||
# Create message
|
||||
message = await self.create_message(content, message_type, reply_to_id)
|
||||
|
||||
if message:
|
||||
# Send message to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'chat_message',
|
||||
'message': await self.serialize_message(message)
|
||||
}
|
||||
)
|
||||
|
||||
# Check for mentions and send notifications
|
||||
await self.handle_mentions(message)
|
||||
|
||||
# Check for commands
|
||||
await self.check_for_commands(message)
|
||||
|
||||
async def handle_reaction(self, data):
|
||||
"""Handle message reaction"""
|
||||
message_id = data.get('message_id')
|
||||
emoji = data.get('emoji')
|
||||
action = data.get('action', 'add') # 'add' or 'remove'
|
||||
|
||||
if not message_id or not emoji:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Message ID and emoji are required'
|
||||
}))
|
||||
return
|
||||
|
||||
# Handle reaction
|
||||
if action == 'add':
|
||||
reaction = await self.add_reaction(message_id, emoji)
|
||||
else:
|
||||
reaction = await self.remove_reaction(message_id, emoji)
|
||||
|
||||
if reaction is not False:
|
||||
# Send reaction update to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'reaction_update',
|
||||
'message_id': message_id,
|
||||
'reaction': await self.serialize_reaction(reaction) if reaction else None,
|
||||
'action': action
|
||||
}
|
||||
)
|
||||
|
||||
async def handle_command(self, data):
|
||||
"""Handle ChatOps command"""
|
||||
message_id = data.get('message_id')
|
||||
command_text = data.get('command_text')
|
||||
|
||||
if not message_id or not command_text:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Message ID and command text are required'
|
||||
}))
|
||||
return
|
||||
|
||||
# Execute command
|
||||
result = await self.execute_command(message_id, command_text)
|
||||
|
||||
# Send command result to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'command_result',
|
||||
'message_id': message_id,
|
||||
'result': result
|
||||
}
|
||||
)
|
||||
|
||||
async def handle_typing(self, data):
|
||||
"""Handle typing indicator"""
|
||||
is_typing = data.get('is_typing', False)
|
||||
|
||||
# Send typing indicator to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'typing_indicator',
|
||||
'user': await self.get_user_info(),
|
||||
'is_typing': is_typing
|
||||
}
|
||||
)
|
||||
|
||||
async def chat_message(self, event):
|
||||
"""Send chat message to WebSocket"""
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'chat_message',
|
||||
'data': event['message']
|
||||
}))
|
||||
|
||||
async def reaction_update(self, event):
|
||||
"""Send reaction update to WebSocket"""
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'reaction_update',
|
||||
'data': {
|
||||
'message_id': event['message_id'],
|
||||
'reaction': event['reaction'],
|
||||
'action': event['action']
|
||||
}
|
||||
}))
|
||||
|
||||
async def command_result(self, event):
|
||||
"""Send command result to WebSocket"""
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'command_result',
|
||||
'data': {
|
||||
'message_id': event['message_id'],
|
||||
'result': event['result']
|
||||
}
|
||||
}))
|
||||
|
||||
async def typing_indicator(self, event):
|
||||
"""Send typing indicator to WebSocket"""
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'typing_indicator',
|
||||
'data': {
|
||||
'user': event['user'],
|
||||
'is_typing': event['is_typing']
|
||||
}
|
||||
}))
|
||||
|
||||
@database_sync_to_async
|
||||
def get_room_info(self):
|
||||
"""Get room information"""
|
||||
try:
|
||||
room = WarRoom.objects.get(id=self.room_id)
|
||||
return {
|
||||
'id': str(room.id),
|
||||
'name': room.name,
|
||||
'incident_id': str(room.incident.id),
|
||||
'incident_title': room.incident.title,
|
||||
'participant_count': room.allowed_users.count(),
|
||||
'message_count': room.message_count
|
||||
}
|
||||
except WarRoom.DoesNotExist:
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user_info(self):
|
||||
"""Get current user information"""
|
||||
user = self.scope['user']
|
||||
if user.is_authenticated:
|
||||
return {
|
||||
'id': str(user.id),
|
||||
'username': user.username,
|
||||
'display_name': getattr(user, 'display_name', user.username)
|
||||
}
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def create_message(self, content, message_type, reply_to_id=None):
|
||||
"""Create a new message"""
|
||||
try:
|
||||
room = WarRoom.objects.get(id=self.room_id)
|
||||
user = self.scope['user']
|
||||
|
||||
if not user.is_authenticated:
|
||||
return None
|
||||
|
||||
# Check if user has access to room
|
||||
if not room.can_user_access(user):
|
||||
return None
|
||||
|
||||
reply_to = None
|
||||
if reply_to_id:
|
||||
try:
|
||||
reply_to = WarRoomMessage.objects.get(id=reply_to_id)
|
||||
except WarRoomMessage.DoesNotExist:
|
||||
pass
|
||||
|
||||
message = WarRoomMessage.objects.create(
|
||||
war_room=room,
|
||||
content=content,
|
||||
message_type=message_type,
|
||||
sender=user,
|
||||
sender_name=user.username,
|
||||
reply_to=reply_to
|
||||
)
|
||||
|
||||
# Update room message count
|
||||
room.message_count += 1
|
||||
room.last_activity = timezone.now()
|
||||
room.save(update_fields=['message_count', 'last_activity'])
|
||||
|
||||
return message
|
||||
|
||||
except WarRoom.DoesNotExist:
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def serialize_message(self, message):
|
||||
"""Serialize message for WebSocket"""
|
||||
return {
|
||||
'id': str(message.id),
|
||||
'content': message.content,
|
||||
'message_type': message.message_type,
|
||||
'sender': {
|
||||
'id': str(message.sender.id) if message.sender else None,
|
||||
'username': message.sender.username if message.sender else None,
|
||||
'display_name': message.sender_name
|
||||
},
|
||||
'is_pinned': message.is_pinned,
|
||||
'reply_to_id': str(message.reply_to.id) if message.reply_to else None,
|
||||
'created_at': message.created_at.isoformat(),
|
||||
'reactions': list(message.get_reactions_summary())
|
||||
}
|
||||
|
||||
@database_sync_to_async
|
||||
def add_reaction(self, message_id, emoji):
|
||||
"""Add reaction to message"""
|
||||
try:
|
||||
message = WarRoomMessage.objects.get(id=message_id)
|
||||
user = self.scope['user']
|
||||
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
reaction = message.add_reaction(user, emoji)
|
||||
return reaction
|
||||
|
||||
except WarRoomMessage.DoesNotExist:
|
||||
return False
|
||||
|
||||
@database_sync_to_async
|
||||
def remove_reaction(self, message_id, emoji):
|
||||
"""Remove reaction from message"""
|
||||
try:
|
||||
message = WarRoomMessage.objects.get(id=message_id)
|
||||
user = self.scope['user']
|
||||
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
message.remove_reaction(user, emoji)
|
||||
return True
|
||||
|
||||
except WarRoomMessage.DoesNotExist:
|
||||
return False
|
||||
|
||||
@database_sync_to_async
|
||||
def serialize_reaction(self, reaction):
|
||||
"""Serialize reaction for WebSocket"""
|
||||
return {
|
||||
'id': str(reaction.id),
|
||||
'emoji': reaction.emoji,
|
||||
'user': {
|
||||
'id': str(reaction.user.id),
|
||||
'username': reaction.user.username
|
||||
},
|
||||
'created_at': reaction.created_at.isoformat()
|
||||
}
|
||||
|
||||
@database_sync_to_async
|
||||
def execute_command(self, message_id, command_text):
|
||||
"""Execute ChatOps command"""
|
||||
try:
|
||||
message = WarRoomMessage.objects.get(id=message_id)
|
||||
user = self.scope['user']
|
||||
|
||||
if not user.is_authenticated:
|
||||
return {'error': 'Authentication required'}
|
||||
|
||||
# Parse command
|
||||
command_type = self._parse_command_type(command_text)
|
||||
parameters = self._parse_command_parameters(command_text)
|
||||
|
||||
# Create chat command
|
||||
chat_command = ChatCommand.objects.create(
|
||||
message=message,
|
||||
command_type=command_type,
|
||||
command_text=command_text,
|
||||
parameters=parameters
|
||||
)
|
||||
|
||||
# Execute command
|
||||
result = chat_command.execute_command(user)
|
||||
return result
|
||||
|
||||
except WarRoomMessage.DoesNotExist:
|
||||
return {'error': 'Message not found'}
|
||||
|
||||
def _parse_command_type(self, command_text):
|
||||
"""Parse command type from command text"""
|
||||
command_text = command_text.lower().strip()
|
||||
|
||||
if command_text.startswith('/status'):
|
||||
return 'STATUS'
|
||||
elif command_text.startswith('/runbook'):
|
||||
return 'RUNBOOK'
|
||||
elif command_text.startswith('/escalate'):
|
||||
return 'ESCALATE'
|
||||
elif command_text.startswith('/assign'):
|
||||
return 'ASSIGN'
|
||||
elif command_text.startswith('/update'):
|
||||
return 'UPDATE'
|
||||
else:
|
||||
return 'CUSTOM'
|
||||
|
||||
def _parse_command_parameters(self, command_text):
|
||||
"""Parse command parameters from command text"""
|
||||
parts = command_text.split()
|
||||
if len(parts) > 1:
|
||||
return {'args': parts[1:]}
|
||||
return {}
|
||||
|
||||
@database_sync_to_async
|
||||
def handle_mentions(self, message):
|
||||
"""Handle user mentions in message"""
|
||||
# This would integrate with notification system
|
||||
# For now, just a placeholder
|
||||
pass
|
||||
|
||||
@database_sync_to_async
|
||||
def check_for_commands(self, message):
|
||||
"""Check if message contains commands"""
|
||||
# This would check for command patterns and execute them
|
||||
# For now, just a placeholder
|
||||
pass
|
||||
Binary file not shown.
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Management command to set up the incident-centric chat system
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
|
||||
from ...models import ChatBot, WarRoom
|
||||
from incident_intelligence.models import Incident
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Set up the incident-centric chat system with default bots and configurations'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--create-bots',
|
||||
action='store_true',
|
||||
help='Create default AI assistant bots',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--create-war-rooms',
|
||||
action='store_true',
|
||||
help='Create war rooms for existing incidents',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--force',
|
||||
action='store_true',
|
||||
help='Force recreation of existing bots and war rooms',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('Setting up incident-centric chat system...')
|
||||
)
|
||||
|
||||
if options['create_bots']:
|
||||
self.create_default_bots(options['force'])
|
||||
|
||||
if options['create_war_rooms']:
|
||||
self.create_war_rooms_for_incidents(options['force'])
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('Chat system setup completed successfully!')
|
||||
)
|
||||
|
||||
def create_default_bots(self, force=False):
|
||||
"""Create default AI assistant bots"""
|
||||
self.stdout.write('Creating default AI assistant bots...')
|
||||
|
||||
bots_config = [
|
||||
{
|
||||
'name': 'Incident Assistant',
|
||||
'bot_type': 'INCIDENT_ASSISTANT',
|
||||
'description': 'AI assistant for incident management and response guidance',
|
||||
'auto_respond': True,
|
||||
'response_triggers': ['help', 'assist', 'guidance', 'incident', 'problem']
|
||||
},
|
||||
{
|
||||
'name': 'Knowledge Bot',
|
||||
'bot_type': 'KNOWLEDGE_BOT',
|
||||
'description': 'AI assistant for knowledge base queries and documentation',
|
||||
'auto_respond': False,
|
||||
'response_triggers': ['how', 'what', 'where', 'documentation', 'knowledge']
|
||||
},
|
||||
{
|
||||
'name': 'Automation Bot',
|
||||
'bot_type': 'AUTOMATION_BOT',
|
||||
'description': 'AI assistant for automation and runbook execution',
|
||||
'auto_respond': False,
|
||||
'response_triggers': ['runbook', 'automation', 'execute', 'playbook']
|
||||
},
|
||||
{
|
||||
'name': 'Compliance Bot',
|
||||
'bot_type': 'COMPLIANCE_BOT',
|
||||
'description': 'AI assistant for compliance and audit requirements',
|
||||
'auto_respond': False,
|
||||
'response_triggers': ['compliance', 'audit', 'policy', 'retention']
|
||||
}
|
||||
]
|
||||
|
||||
for bot_config in bots_config:
|
||||
bot, created = ChatBot.objects.get_or_create(
|
||||
name=bot_config['name'],
|
||||
defaults={
|
||||
'bot_type': bot_config['bot_type'],
|
||||
'description': bot_config['description'],
|
||||
'is_active': True,
|
||||
'auto_respond': bot_config['auto_respond'],
|
||||
'response_triggers': bot_config['response_triggers']
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'Created bot: {bot.name}')
|
||||
)
|
||||
elif force:
|
||||
bot.bot_type = bot_config['bot_type']
|
||||
bot.description = bot_config['description']
|
||||
bot.auto_respond = bot_config['auto_respond']
|
||||
bot.response_triggers = bot_config['response_triggers']
|
||||
bot.save()
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'Updated bot: {bot.name}')
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'Bot already exists: {bot.name}')
|
||||
)
|
||||
|
||||
def create_war_rooms_for_incidents(self, force=False):
|
||||
"""Create war rooms for existing incidents"""
|
||||
self.stdout.write('Creating war rooms for existing incidents...')
|
||||
|
||||
incidents = Incident.objects.all()
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for incident in incidents:
|
||||
war_room, created = WarRoom.objects.get_or_create(
|
||||
incident=incident,
|
||||
defaults={
|
||||
'name': f"Incident {incident.id} - {incident.title[:50]}",
|
||||
'description': f"War room for incident: {incident.title}",
|
||||
'created_by': incident.reporter,
|
||||
'privacy_level': 'PRIVATE',
|
||||
'status': 'ACTIVE'
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
# Add incident reporter and assignee to war room
|
||||
if incident.reporter:
|
||||
war_room.add_participant(incident.reporter)
|
||||
if incident.assigned_to:
|
||||
war_room.add_participant(incident.assigned_to)
|
||||
|
||||
created_count += 1
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'Created war room for incident: {incident.title}')
|
||||
)
|
||||
elif force:
|
||||
war_room.name = f"Incident {incident.id} - {incident.title[:50]}"
|
||||
war_room.description = f"War room for incident: {incident.title}"
|
||||
war_room.save()
|
||||
updated_count += 1
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'Updated war room for incident: {incident.title}')
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'Created {created_count} new war rooms, updated {updated_count} existing war rooms'
|
||||
)
|
||||
)
|
||||
|
||||
def create_sample_data(self):
|
||||
"""Create sample data for testing"""
|
||||
self.stdout.write('Creating sample data...')
|
||||
|
||||
# Create a sample incident if none exist
|
||||
if not Incident.objects.exists():
|
||||
sample_incident = Incident.objects.create(
|
||||
title='Sample Database Connection Issue',
|
||||
description='Database connection timeout affecting user authentication',
|
||||
severity='HIGH',
|
||||
status='OPEN',
|
||||
category='Database',
|
||||
subcategory='Connection'
|
||||
)
|
||||
|
||||
# Create war room for sample incident
|
||||
WarRoom.objects.create(
|
||||
incident=sample_incident,
|
||||
name=f"Incident {sample_incident.id} - {sample_incident.title}",
|
||||
description=f"War room for incident: {sample_incident.title}",
|
||||
privacy_level='PRIVATE',
|
||||
status='ACTIVE'
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('Created sample incident and war room')
|
||||
)
|
||||
261
ETB-API/collaboration_war_rooms/migrations/0001_initial.py
Normal file
261
ETB-API/collaboration_war_rooms/migrations/0001_initial.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-18 16:26
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('automation_orchestration', '0002_autoremediationexecution_sla_instance_and_more'),
|
||||
('incident_intelligence', '0004_incident_oncall_assignment_incident_sla_override_and_more'),
|
||||
('security', '0002_user_emergency_contact_user_oncall_preferences_and_more'),
|
||||
('sla_oncall', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ConferenceBridge',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('bridge_type', models.CharField(choices=[('ZOOM', 'Zoom'), ('TEAMS', 'Microsoft Teams'), ('WEBEX', 'Cisco Webex'), ('GOTO_MEETING', 'GoTo Meeting'), ('CUSTOM', 'Custom Bridge')], max_length=20)),
|
||||
('status', models.CharField(choices=[('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('ENDED', 'Ended'), ('CANCELLED', 'Cancelled')], default='SCHEDULED', max_length=20)),
|
||||
('meeting_id', models.CharField(blank=True, help_text='External meeting ID', max_length=255, null=True)),
|
||||
('meeting_url', models.URLField(blank=True, help_text='Meeting URL', null=True)),
|
||||
('dial_in_number', models.CharField(blank=True, help_text='Dial-in phone number', max_length=50, null=True)),
|
||||
('access_code', models.CharField(blank=True, help_text='Access code for dial-in', max_length=20, null=True)),
|
||||
('scheduled_start', models.DateTimeField(help_text='Scheduled start time')),
|
||||
('scheduled_end', models.DateTimeField(help_text='Scheduled end time')),
|
||||
('actual_start', models.DateTimeField(blank=True, null=True)),
|
||||
('actual_end', models.DateTimeField(blank=True, null=True)),
|
||||
('max_participants', models.PositiveIntegerField(default=50)),
|
||||
('recording_enabled', models.BooleanField(default=False)),
|
||||
('recording_url', models.URLField(blank=True, help_text='URL to recorded meeting', null=True)),
|
||||
('transcription_enabled', models.BooleanField(default=False)),
|
||||
('transcription_url', models.URLField(blank=True, help_text='URL to meeting transcription', null=True)),
|
||||
('integration_config', models.JSONField(default=dict, help_text='Configuration for external bridge integration')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('active_participants', models.ManyToManyField(blank=True, help_text='Users currently in the conference', related_name='active_conferences', to=settings.AUTH_USER_MODEL)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_conferences', to=settings.AUTH_USER_MODEL)),
|
||||
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conference_bridges', to='incident_intelligence.incident')),
|
||||
('invited_participants', models.ManyToManyField(blank=True, help_text='Users invited to the conference', related_name='invited_conferences', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-scheduled_start'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IncidentCommandRole',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('role_type', models.CharField(choices=[('INCIDENT_COMMANDER', 'Incident Commander'), ('SCRIBE', 'Scribe'), ('COMMS_LEAD', 'Communications Lead'), ('TECHNICAL_LEAD', 'Technical Lead'), ('BUSINESS_LEAD', 'Business Lead'), ('EXTERNAL_LIAISON', 'External Liaison'), ('OBSERVER', 'Observer')], max_length=30)),
|
||||
('status', models.CharField(choices=[('ACTIVE', 'Active'), ('INACTIVE', 'Inactive'), ('REASSIGNED', 'Reassigned')], default='ACTIVE', max_length=20)),
|
||||
('responsibilities', models.JSONField(default=list, help_text='List of responsibilities for this role')),
|
||||
('decision_authority', models.JSONField(default=list, help_text='Areas where this role has decision authority')),
|
||||
('assigned_at', models.DateTimeField(auto_now_add=True)),
|
||||
('reassigned_at', models.DateTimeField(blank=True, null=True)),
|
||||
('assignment_notes', models.TextField(blank=True, null=True)),
|
||||
('decisions_made', models.PositiveIntegerField(default=0)),
|
||||
('communications_sent', models.PositiveIntegerField(default=0)),
|
||||
('last_activity', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('assigned_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_command_roles', to=settings.AUTH_USER_MODEL)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_command_roles', to=settings.AUTH_USER_MODEL)),
|
||||
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='command_roles', to='incident_intelligence.incident')),
|
||||
('reassigned_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reassigned_command_roles', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-assigned_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WarRoom',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('status', models.CharField(choices=[('ACTIVE', 'Active'), ('ARCHIVED', 'Archived'), ('CLOSED', 'Closed')], default='ACTIVE', max_length=20)),
|
||||
('privacy_level', models.CharField(choices=[('PUBLIC', 'Public'), ('PRIVATE', 'Private'), ('RESTRICTED', 'Restricted')], default='PRIVATE', max_length=20)),
|
||||
('slack_channel_id', models.CharField(blank=True, help_text='Slack channel ID', max_length=100, null=True)),
|
||||
('teams_channel_id', models.CharField(blank=True, help_text='Teams channel ID', max_length=100, null=True)),
|
||||
('discord_channel_id', models.CharField(blank=True, help_text='Discord channel ID', max_length=100, null=True)),
|
||||
('message_count', models.PositiveIntegerField(default=0)),
|
||||
('last_activity', models.DateTimeField(blank=True, null=True)),
|
||||
('active_participants', models.PositiveIntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('archived_at', models.DateTimeField(blank=True, null=True)),
|
||||
('allowed_users', models.ManyToManyField(blank=True, help_text='Users with access to this war room', related_name='accessible_war_rooms', to=settings.AUTH_USER_MODEL)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_war_rooms', to=settings.AUTH_USER_MODEL)),
|
||||
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='war_rooms', to='incident_intelligence.incident')),
|
||||
('required_clearance_level', models.ForeignKey(blank=True, help_text='Required clearance level for access', null=True, on_delete=django.db.models.deletion.SET_NULL, to='security.dataclassification')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TimelineEvent',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('event_type', models.CharField(choices=[('INCIDENT_CREATED', 'Incident Created'), ('INCIDENT_UPDATED', 'Incident Updated'), ('ASSIGNMENT_CHANGED', 'Assignment Changed'), ('STATUS_CHANGED', 'Status Changed'), ('SEVERITY_CHANGED', 'Severity Changed'), ('COMMENT_ADDED', 'Comment Added'), ('RUNBOOK_EXECUTED', 'Runbook Executed'), ('AUTO_REMEDIATION_ATTEMPTED', 'Auto-remediation Attempted'), ('SLA_BREACHED', 'SLA Breached'), ('ESCALATION_TRIGGERED', 'Escalation Triggered'), ('WAR_ROOM_CREATED', 'War Room Created'), ('CONFERENCE_STARTED', 'Conference Started'), ('COMMAND_ROLE_ASSIGNED', 'Command Role Assigned'), ('DECISION_MADE', 'Decision Made'), ('COMMUNICATION_SENT', 'Communication Sent'), ('EXTERNAL_INTEGRATION', 'External Integration'), ('MANUAL_EVENT', 'Manual Event')], max_length=30)),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('description', models.TextField()),
|
||||
('source_type', models.CharField(choices=[('SYSTEM', 'System Generated'), ('USER', 'User Created'), ('INTEGRATION', 'External Integration'), ('AUTOMATION', 'Automation')], default='SYSTEM', max_length=20)),
|
||||
('event_time', models.DateTimeField(help_text='When the event occurred')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('event_data', models.JSONField(default=dict, help_text='Additional data related to the event')),
|
||||
('tags', models.JSONField(default=list, help_text='Tags for categorization and filtering')),
|
||||
('is_critical_event', models.BooleanField(default=False, help_text='Whether this event is critical for postmortem analysis')),
|
||||
('postmortem_notes', models.TextField(blank=True, help_text='Additional notes added during postmortem', null=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_timeline_events', to=settings.AUTH_USER_MODEL)),
|
||||
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='timeline_events', to='incident_intelligence.incident')),
|
||||
('related_auto_remediation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='automation_orchestration.autoremediationexecution')),
|
||||
('related_command_role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='collaboration_war_rooms.incidentcommandrole')),
|
||||
('related_conference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='collaboration_war_rooms.conferencebridge')),
|
||||
('related_escalation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='sla_oncall.escalationinstance')),
|
||||
('related_runbook_execution', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='automation_orchestration.runbookexecution')),
|
||||
('related_sla_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='sla_oncall.slainstance')),
|
||||
('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to=settings.AUTH_USER_MODEL)),
|
||||
('related_war_room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='collaboration_war_rooms.warroom')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['event_time', 'created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='incidentcommandrole',
|
||||
name='war_room',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='command_roles', to='collaboration_war_rooms.warroom'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='conferencebridge',
|
||||
name='war_room',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='conference_bridges', to='collaboration_war_rooms.warroom'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WarRoomMessage',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('message_type', models.CharField(choices=[('TEXT', 'Text Message'), ('SYSTEM', 'System Message'), ('COMMAND', 'Command Message'), ('ALERT', 'Alert Message'), ('UPDATE', 'Status Update')], default='TEXT', max_length=20)),
|
||||
('content', models.TextField()),
|
||||
('sender_name', models.CharField(help_text='Display name of sender', max_length=100)),
|
||||
('is_edited', models.BooleanField(default=False)),
|
||||
('edited_at', models.DateTimeField(blank=True, null=True)),
|
||||
('external_message_id', models.CharField(blank=True, help_text='ID in external system (Slack, Teams, etc.)', max_length=255, null=True)),
|
||||
('external_data', models.JSONField(default=dict, help_text='Additional data from external system')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('reply_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replies', to='collaboration_war_rooms.warroommessage')),
|
||||
('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='war_room_messages', to=settings.AUTH_USER_MODEL)),
|
||||
('war_room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='collaboration_war_rooms.warroom')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IncidentDecision',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('decision_type', models.CharField(choices=[('TECHNICAL', 'Technical Decision'), ('BUSINESS', 'Business Decision'), ('COMMUNICATION', 'Communication Decision'), ('ESCALATION', 'Escalation Decision'), ('RESOURCE', 'Resource Allocation'), ('TIMELINE', 'Timeline Decision')], max_length=20)),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('description', models.TextField()),
|
||||
('rationale', models.TextField(help_text='Reasoning behind the decision')),
|
||||
('status', models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('IMPLEMENTED', 'Implemented')], default='PENDING', max_length=20)),
|
||||
('requires_approval', models.BooleanField(default=False)),
|
||||
('approved_at', models.DateTimeField(blank=True, null=True)),
|
||||
('implementation_notes', models.TextField(blank=True, null=True)),
|
||||
('implemented_at', models.DateTimeField(blank=True, null=True)),
|
||||
('impact_assessment', models.TextField(blank=True, null=True)),
|
||||
('success_metrics', models.JSONField(default=list, help_text='Metrics to measure decision success')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_decisions', to=settings.AUTH_USER_MODEL)),
|
||||
('command_role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='decisions', to='collaboration_war_rooms.incidentcommandrole')),
|
||||
('implemented_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='implemented_decisions', to=settings.AUTH_USER_MODEL)),
|
||||
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='decisions', to='incident_intelligence.incident')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['incident', 'status'], name='collaborati_inciden_4f34eb_idx'), models.Index(fields=['command_role', 'decision_type'], name='collaborati_command_81be71_idx'), models.Index(fields=['status', 'created_at'], name='collaborati_status_3f5734_idx')],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='warroom',
|
||||
index=models.Index(fields=['incident', 'status'], name='collaborati_inciden_bd58db_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='warroom',
|
||||
index=models.Index(fields=['status', 'privacy_level'], name='collaborati_status_649ccc_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='warroom',
|
||||
index=models.Index(fields=['created_at'], name='collaborati_created_e3a240_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='timelineevent',
|
||||
index=models.Index(fields=['incident', 'event_time'], name='collaborati_inciden_3a611f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='timelineevent',
|
||||
index=models.Index(fields=['event_type', 'event_time'], name='collaborati_event_t_d2100a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='timelineevent',
|
||||
index=models.Index(fields=['source_type', 'event_time'], name='collaborati_source__0c3cc4_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='timelineevent',
|
||||
index=models.Index(fields=['is_critical_event', 'event_time'], name='collaborati_is_crit_28e610_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='incidentcommandrole',
|
||||
index=models.Index(fields=['incident', 'role_type'], name='collaborati_inciden_7c5ba6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='incidentcommandrole',
|
||||
index=models.Index(fields=['assigned_user', 'status'], name='collaborati_assigne_e33d48_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='incidentcommandrole',
|
||||
index=models.Index(fields=['status', 'assigned_at'], name='collaborati_status_b2ec4b_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='incidentcommandrole',
|
||||
unique_together={('incident', 'role_type', 'assigned_user')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='conferencebridge',
|
||||
index=models.Index(fields=['incident', 'status'], name='collaborati_inciden_4be2c2_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='conferencebridge',
|
||||
index=models.Index(fields=['bridge_type', 'status'], name='collaborati_bridge__44a9ea_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='conferencebridge',
|
||||
index=models.Index(fields=['scheduled_start'], name='collaborati_schedul_a93d14_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='warroommessage',
|
||||
index=models.Index(fields=['war_room', 'created_at'], name='collaborati_war_roo_6320f9_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='warroommessage',
|
||||
index=models.Index(fields=['sender', 'created_at'], name='collaborati_sender__f499b1_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='warroommessage',
|
||||
index=models.Index(fields=['message_type', 'created_at'], name='collaborati_message_a29f3d_idx'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,215 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-18 18:10
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('automation_orchestration', '0002_autoremediationexecution_sla_instance_and_more'),
|
||||
('collaboration_war_rooms', '0001_initial'),
|
||||
('knowledge_learning', '0001_initial'),
|
||||
('security', '0003_adaptiveauthentication_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChatBot',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('bot_type', models.CharField(choices=[('INCIDENT_ASSISTANT', 'Incident Assistant'), ('KNOWLEDGE_BOT', 'Knowledge Bot'), ('AUTOMATION_BOT', 'Automation Bot'), ('COMPLIANCE_BOT', 'Compliance Bot')], max_length=30)),
|
||||
('description', models.TextField()),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('auto_respond', models.BooleanField(default=False)),
|
||||
('response_triggers', models.JSONField(default=list, help_text='Keywords that trigger bot responses')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ChatCommand',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('command_type', models.CharField(choices=[('STATUS', 'Status Check'), ('RUNBOOK', 'Execute Runbook'), ('ESCALATE', 'Escalate Incident'), ('ASSIGN', 'Assign Incident'), ('UPDATE', 'Update Status'), ('CUSTOM', 'Custom Command')], max_length=20)),
|
||||
('command_text', models.CharField(help_text='Full command text', max_length=500)),
|
||||
('parameters', models.JSONField(default=dict, help_text='Parsed command parameters')),
|
||||
('executed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('execution_status', models.CharField(choices=[('PENDING', 'Pending'), ('EXECUTING', 'Executing'), ('SUCCESS', 'Success'), ('FAILED', 'Failed'), ('CANCELLED', 'Cancelled')], default='PENDING', max_length=20)),
|
||||
('execution_result', models.JSONField(default=dict, help_text='Result of command execution')),
|
||||
('error_message', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-executed_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ChatFile',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('filename', models.CharField(max_length=255)),
|
||||
('original_filename', models.CharField(max_length=255)),
|
||||
('file_type', models.CharField(choices=[('IMAGE', 'Image'), ('DOCUMENT', 'Document'), ('LOG', 'Log File'), ('SCREENSHOT', 'Screenshot'), ('EVIDENCE', 'Evidence'), ('OTHER', 'Other')], max_length=20)),
|
||||
('file_size', models.PositiveIntegerField(help_text='File size in bytes')),
|
||||
('mime_type', models.CharField(max_length=100)),
|
||||
('file_path', models.CharField(help_text='Path to stored file', max_length=500)),
|
||||
('file_url', models.URLField(blank=True, help_text='Public URL for file access', null=True)),
|
||||
('is_encrypted', models.BooleanField(default=False)),
|
||||
('encryption_key_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('file_hash', models.CharField(help_text='SHA-256 hash of file', max_length=64)),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
||||
('access_log', models.JSONField(default=list, help_text='Log of who accessed this file and when')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-uploaded_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MessageReaction',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('emoji', models.CharField(help_text='Emoji reaction', max_length=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warroommessage',
|
||||
name='attachments',
|
||||
field=models.JSONField(default=list, help_text='List of file attachments with metadata'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warroommessage',
|
||||
name='encryption_key_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warroommessage',
|
||||
name='is_encrypted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warroommessage',
|
||||
name='is_pinned',
|
||||
field=models.BooleanField(default=False, help_text='Whether this message is pinned'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warroommessage',
|
||||
name='mentioned_users',
|
||||
field=models.ManyToManyField(blank=True, help_text='Users mentioned in this message', related_name='mentioned_in_messages', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warroommessage',
|
||||
name='notification_sent',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warroommessage',
|
||||
name='pinned_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warroommessage',
|
||||
name='pinned_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pinned_messages', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='warroommessage',
|
||||
name='message_type',
|
||||
field=models.CharField(choices=[('TEXT', 'Text Message'), ('SYSTEM', 'System Message'), ('COMMAND', 'Command Message'), ('ALERT', 'Alert Message'), ('UPDATE', 'Status Update'), ('FILE', 'File Attachment'), ('BOT', 'Bot Message')], default='TEXT', max_length=20),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='warroommessage',
|
||||
index=models.Index(fields=['is_pinned', 'created_at'], name='collaborati_is_pinn_7a25dc_idx'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chatbot',
|
||||
name='knowledge_base',
|
||||
field=models.ForeignKey(blank=True, help_text='Knowledge base article for bot responses', null=True, on_delete=django.db.models.deletion.SET_NULL, to='knowledge_learning.knowledgebasearticle'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chatcommand',
|
||||
name='automation_execution',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chat_commands', to='automation_orchestration.runbookexecution'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chatcommand',
|
||||
name='executed_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chatcommand',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_commands', to='collaboration_war_rooms.warroommessage'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chatfile',
|
||||
name='data_classification',
|
||||
field=models.ForeignKey(blank=True, help_text='Data classification level for this file', null=True, on_delete=django.db.models.deletion.SET_NULL, to='security.dataclassification'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chatfile',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_files', to='collaboration_war_rooms.warroommessage'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chatfile',
|
||||
name='uploaded_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagereaction',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='collaboration_war_rooms.warroommessage'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagereaction',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='chatbot',
|
||||
index=models.Index(fields=['bot_type', 'is_active'], name='collaborati_bot_typ_6fc3ba_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='chatcommand',
|
||||
index=models.Index(fields=['command_type', 'execution_status'], name='collaborati_command_915116_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='chatcommand',
|
||||
index=models.Index(fields=['executed_by', 'executed_at'], name='collaborati_execute_d1badb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='chatfile',
|
||||
index=models.Index(fields=['message', 'file_type'], name='collaborati_message_358b62_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='chatfile',
|
||||
index=models.Index(fields=['data_classification'], name='collaborati_data_cl_e26657_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='chatfile',
|
||||
index=models.Index(fields=['uploaded_at'], name='collaborati_uploade_a6b9bd_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='messagereaction',
|
||||
index=models.Index(fields=['message', 'emoji'], name='collaborati_message_817163_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='messagereaction',
|
||||
index=models.Index(fields=['user', 'created_at'], name='collaborati_user_id_2d3a22_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='messagereaction',
|
||||
unique_together={('message', 'user', 'emoji')},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
1083
ETB-API/collaboration_war_rooms/models.py
Normal file
1083
ETB-API/collaboration_war_rooms/models.py
Normal file
File diff suppressed because it is too large
Load Diff
9
ETB-API/collaboration_war_rooms/routing.py
Normal file
9
ETB-API/collaboration_war_rooms/routing.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
WebSocket routing configuration for collaboration_war_rooms
|
||||
"""
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<room_id>[0-9a-f-]+)/$', consumers.ChatConsumer.as_asgi()),
|
||||
]
|
||||
1
ETB-API/collaboration_war_rooms/serializers/__init__.py
Normal file
1
ETB-API/collaboration_war_rooms/serializers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Serializers for Collaboration & War Rooms module
|
||||
Binary file not shown.
Binary file not shown.
354
ETB-API/collaboration_war_rooms/serializers/collaboration.py
Normal file
354
ETB-API/collaboration_war_rooms/serializers/collaboration.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
Serializers for Collaboration & War Rooms models
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from ..models import (
|
||||
WarRoom, ConferenceBridge, IncidentCommandRole,
|
||||
TimelineEvent, WarRoomMessage, IncidentDecision,
|
||||
MessageReaction, ChatFile, ChatCommand, ChatBot
|
||||
)
|
||||
from incident_intelligence.serializers.incident import IncidentSerializer
|
||||
from security.serializers.security import UserSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class WarRoomSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for WarRoom model"""
|
||||
|
||||
incident = IncidentSerializer(read_only=True)
|
||||
incident_id = serializers.UUIDField(write_only=True)
|
||||
allowed_users = UserSerializer(many=True, read_only=True)
|
||||
allowed_user_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
write_only=True,
|
||||
required=False
|
||||
)
|
||||
created_by = UserSerializer(read_only=True)
|
||||
created_by_id = serializers.UUIDField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = WarRoom
|
||||
fields = [
|
||||
'id', 'name', 'description', 'incident', 'incident_id',
|
||||
'status', 'privacy_level', 'slack_channel_id', 'teams_channel_id',
|
||||
'discord_channel_id', 'allowed_users', 'allowed_user_ids',
|
||||
'required_clearance_level', 'message_count', 'last_activity',
|
||||
'active_participants', 'created_by', 'created_by_id',
|
||||
'created_at', 'updated_at', 'archived_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'message_count', 'last_activity', 'active_participants']
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create war room with allowed users"""
|
||||
allowed_user_ids = validated_data.pop('allowed_user_ids', [])
|
||||
war_room = WarRoom.objects.create(**validated_data)
|
||||
|
||||
# Add allowed users
|
||||
if allowed_user_ids:
|
||||
users = User.objects.filter(id__in=allowed_user_ids)
|
||||
war_room.allowed_users.set(users)
|
||||
|
||||
return war_room
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update war room with allowed users"""
|
||||
allowed_user_ids = validated_data.pop('allowed_user_ids', None)
|
||||
|
||||
# Update war room fields
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
# Update allowed users if provided
|
||||
if allowed_user_ids is not None:
|
||||
users = User.objects.filter(id__in=allowed_user_ids)
|
||||
instance.allowed_users.set(users)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class ConferenceBridgeSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ConferenceBridge model"""
|
||||
|
||||
incident = IncidentSerializer(read_only=True)
|
||||
incident_id = serializers.UUIDField(write_only=True)
|
||||
war_room = WarRoomSerializer(read_only=True)
|
||||
war_room_id = serializers.UUIDField(write_only=True, required=False)
|
||||
invited_participants = UserSerializer(many=True, read_only=True)
|
||||
invited_participant_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
write_only=True,
|
||||
required=False
|
||||
)
|
||||
active_participants = UserSerializer(many=True, read_only=True)
|
||||
created_by = UserSerializer(read_only=True)
|
||||
created_by_id = serializers.UUIDField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConferenceBridge
|
||||
fields = [
|
||||
'id', 'name', 'description', 'incident', 'incident_id',
|
||||
'war_room', 'war_room_id', 'bridge_type', 'status',
|
||||
'meeting_id', 'meeting_url', 'dial_in_number', 'access_code',
|
||||
'scheduled_start', 'scheduled_end', 'actual_start', 'actual_end',
|
||||
'invited_participants', 'invited_participant_ids',
|
||||
'active_participants', 'max_participants',
|
||||
'recording_enabled', 'recording_url', 'transcription_enabled',
|
||||
'transcription_url', 'integration_config',
|
||||
'created_by', 'created_by_id', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'actual_start', 'actual_end', 'active_participants']
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create conference bridge with invited participants"""
|
||||
invited_participant_ids = validated_data.pop('invited_participant_ids', [])
|
||||
conference = ConferenceBridge.objects.create(**validated_data)
|
||||
|
||||
# Add invited participants
|
||||
if invited_participant_ids:
|
||||
users = User.objects.filter(id__in=invited_participant_ids)
|
||||
conference.invited_participants.set(users)
|
||||
|
||||
return conference
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update conference bridge with invited participants"""
|
||||
invited_participant_ids = validated_data.pop('invited_participant_ids', None)
|
||||
|
||||
# Update conference fields
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
# Update invited participants if provided
|
||||
if invited_participant_ids is not None:
|
||||
users = User.objects.filter(id__in=invited_participant_ids)
|
||||
instance.invited_participants.set(users)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class IncidentCommandRoleSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for IncidentCommandRole model"""
|
||||
|
||||
incident = IncidentSerializer(read_only=True)
|
||||
incident_id = serializers.UUIDField(write_only=True)
|
||||
war_room = WarRoomSerializer(read_only=True)
|
||||
war_room_id = serializers.UUIDField(write_only=True, required=False)
|
||||
assigned_user = UserSerializer(read_only=True)
|
||||
assigned_user_id = serializers.UUIDField(write_only=True, required=False)
|
||||
reassigned_by = UserSerializer(read_only=True)
|
||||
created_by = UserSerializer(read_only=True)
|
||||
created_by_id = serializers.UUIDField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = IncidentCommandRole
|
||||
fields = [
|
||||
'id', 'incident', 'incident_id', 'war_room', 'war_room_id',
|
||||
'role_type', 'assigned_user', 'assigned_user_id', 'status',
|
||||
'responsibilities', 'decision_authority', 'assigned_at',
|
||||
'reassigned_at', 'reassigned_by', 'assignment_notes',
|
||||
'decisions_made', 'communications_sent', 'last_activity',
|
||||
'created_by', 'created_by_id', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'assigned_at', 'reassigned_at', 'reassigned_by',
|
||||
'decisions_made', 'communications_sent', 'last_activity',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
|
||||
class TimelineEventSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for TimelineEvent model"""
|
||||
|
||||
incident = IncidentSerializer(read_only=True)
|
||||
incident_id = serializers.UUIDField(write_only=True)
|
||||
related_user = UserSerializer(read_only=True)
|
||||
related_user_id = serializers.UUIDField(write_only=True, required=False)
|
||||
created_by = UserSerializer(read_only=True)
|
||||
created_by_id = serializers.UUIDField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = TimelineEvent
|
||||
fields = [
|
||||
'id', 'incident', 'incident_id', 'event_type', 'title',
|
||||
'description', 'source_type', 'event_time', 'created_at',
|
||||
'related_user', 'related_user_id', 'event_data', 'tags',
|
||||
'is_critical_event', 'postmortem_notes',
|
||||
'created_by', 'created_by_id'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
|
||||
class MessageReactionSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for MessageReaction model"""
|
||||
|
||||
user = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = MessageReaction
|
||||
fields = ['id', 'user', 'emoji', 'created_at']
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
|
||||
class ChatFileSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ChatFile model"""
|
||||
|
||||
uploaded_by = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ChatFile
|
||||
fields = [
|
||||
'id', 'message', 'filename', 'original_filename', 'file_type',
|
||||
'file_size', 'mime_type', 'file_path', 'file_url',
|
||||
'data_classification', 'is_encrypted', 'file_hash',
|
||||
'uploaded_by', 'uploaded_at', 'access_log'
|
||||
]
|
||||
read_only_fields = ['id', 'uploaded_at', 'access_log']
|
||||
|
||||
|
||||
class ChatCommandSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ChatCommand model"""
|
||||
|
||||
executed_by = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ChatCommand
|
||||
fields = [
|
||||
'id', 'message', 'command_type', 'command_text', 'parameters',
|
||||
'executed_by', 'executed_at', 'execution_status',
|
||||
'execution_result', 'error_message', 'automation_execution'
|
||||
]
|
||||
read_only_fields = ['id', 'executed_at']
|
||||
|
||||
|
||||
class ChatBotSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ChatBot model"""
|
||||
|
||||
class Meta:
|
||||
model = ChatBot
|
||||
fields = [
|
||||
'id', 'name', 'bot_type', 'description', 'is_active',
|
||||
'auto_respond', 'response_triggers', 'knowledge_base',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class WarRoomMessageSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for WarRoomMessage model"""
|
||||
|
||||
war_room = WarRoomSerializer(read_only=True)
|
||||
war_room_id = serializers.UUIDField(write_only=True)
|
||||
sender = UserSerializer(read_only=True)
|
||||
sender_id = serializers.UUIDField(write_only=True, required=False)
|
||||
reply_to = serializers.PrimaryKeyRelatedField(
|
||||
queryset=WarRoomMessage.objects.all(),
|
||||
required=False
|
||||
)
|
||||
pinned_by = UserSerializer(read_only=True)
|
||||
mentioned_users = UserSerializer(many=True, read_only=True)
|
||||
mentioned_user_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
write_only=True,
|
||||
required=False
|
||||
)
|
||||
reactions = MessageReactionSerializer(many=True, read_only=True)
|
||||
chat_files = ChatFileSerializer(many=True, read_only=True)
|
||||
chat_commands = ChatCommandSerializer(many=True, read_only=True)
|
||||
reactions_summary = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = WarRoomMessage
|
||||
fields = [
|
||||
'id', 'war_room', 'war_room_id', 'message_type', 'content',
|
||||
'sender', 'sender_id', 'sender_name', 'is_edited', 'edited_at',
|
||||
'is_pinned', 'pinned_at', 'pinned_by', 'reply_to',
|
||||
'attachments', 'mentioned_users', 'mentioned_user_ids',
|
||||
'notification_sent', 'external_message_id', 'external_data',
|
||||
'is_encrypted', 'encryption_key_id', 'reactions', 'reactions_summary',
|
||||
'chat_files', 'chat_commands', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'is_edited', 'edited_at', 'pinned_at', 'pinned_by',
|
||||
'notification_sent', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
def get_reactions_summary(self, obj):
|
||||
"""Get reactions summary for the message"""
|
||||
return obj.get_reactions_summary()
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create message with mentioned users"""
|
||||
mentioned_user_ids = validated_data.pop('mentioned_user_ids', [])
|
||||
message = WarRoomMessage.objects.create(**validated_data)
|
||||
|
||||
# Add mentioned users
|
||||
if mentioned_user_ids:
|
||||
users = User.objects.filter(id__in=mentioned_user_ids)
|
||||
message.mentioned_users.set(users)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
class IncidentDecisionSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for IncidentDecision model"""
|
||||
|
||||
incident = IncidentSerializer(read_only=True)
|
||||
incident_id = serializers.UUIDField(write_only=True)
|
||||
command_role = IncidentCommandRoleSerializer(read_only=True)
|
||||
command_role_id = serializers.UUIDField(write_only=True)
|
||||
approved_by = UserSerializer(read_only=True)
|
||||
implemented_by = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IncidentDecision
|
||||
fields = [
|
||||
'id', 'incident', 'incident_id', 'command_role', 'command_role_id',
|
||||
'decision_type', 'title', 'description', 'rationale', 'status',
|
||||
'requires_approval', 'approved_by', 'approved_at',
|
||||
'implementation_notes', 'implemented_at', 'implemented_by',
|
||||
'impact_assessment', 'success_metrics', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'approved_by', 'approved_at', 'implemented_at',
|
||||
'implemented_by', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
|
||||
class WarRoomSummarySerializer(serializers.ModelSerializer):
|
||||
"""Simplified serializer for WarRoom list views"""
|
||||
|
||||
incident_title = serializers.CharField(source='incident.title', read_only=True)
|
||||
incident_severity = serializers.CharField(source='incident.severity', read_only=True)
|
||||
participant_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = WarRoom
|
||||
fields = [
|
||||
'id', 'name', 'incident_title', 'incident_severity',
|
||||
'status', 'privacy_level', 'message_count', 'last_activity',
|
||||
'participant_count', 'created_at'
|
||||
]
|
||||
|
||||
def get_participant_count(self, obj):
|
||||
"""Get count of allowed users"""
|
||||
return obj.allowed_users.count()
|
||||
|
||||
|
||||
class TimelineEventSummarySerializer(serializers.ModelSerializer):
|
||||
"""Simplified serializer for TimelineEvent list views"""
|
||||
|
||||
incident_title = serializers.CharField(source='incident.title', read_only=True)
|
||||
related_user_name = serializers.CharField(source='related_user.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TimelineEvent
|
||||
fields = [
|
||||
'id', 'incident_title', 'event_type', 'title', 'description',
|
||||
'source_type', 'event_time', 'related_user_name',
|
||||
'is_critical_event', 'created_at'
|
||||
]
|
||||
433
ETB-API/collaboration_war_rooms/services/ai_assistant.py
Normal file
433
ETB-API/collaboration_war_rooms/services/ai_assistant.py
Normal file
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
AI Assistant Service for Chat Integration
|
||||
Handles AI-powered assistance, incident suggestions, and knowledge base integration
|
||||
"""
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
from typing import Dict, Any, Optional, List
|
||||
import re
|
||||
|
||||
from ..models import ChatBot, WarRoomMessage
|
||||
from knowledge_learning.models import KnowledgeBaseArticle, Postmortem, IncidentPattern
|
||||
from incident_intelligence.models import Incident
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class AIAssistantService:
|
||||
"""Service for AI-powered chat assistance"""
|
||||
|
||||
@staticmethod
|
||||
def generate_response(bot: ChatBot, message: WarRoomMessage, context: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""Generate AI response to a chat message"""
|
||||
try:
|
||||
incident = message.war_room.incident
|
||||
user = message.sender
|
||||
|
||||
# Analyze message content
|
||||
message_analysis = AIAssistantService._analyze_message(message.content)
|
||||
|
||||
# Determine response type based on analysis
|
||||
if message_analysis['contains_question']:
|
||||
response = AIAssistantService._generate_question_response(
|
||||
bot, message, message_analysis, context
|
||||
)
|
||||
elif message_analysis['contains_incident_keywords']:
|
||||
response = AIAssistantService._generate_incident_response(
|
||||
bot, message, message_analysis, context
|
||||
)
|
||||
elif message_analysis['contains_help_request']:
|
||||
response = AIAssistantService._generate_help_response(
|
||||
bot, message, message_analysis, context
|
||||
)
|
||||
else:
|
||||
response = AIAssistantService._generate_general_response(
|
||||
bot, message, message_analysis, context
|
||||
)
|
||||
|
||||
# Add confidence score and sources
|
||||
response['confidence'] = AIAssistantService._calculate_confidence(response)
|
||||
response['sources'] = AIAssistantService._get_response_sources(response)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'response': f"I encountered an error while processing your message: {str(e)}",
|
||||
'confidence': 0.0,
|
||||
'sources': []
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def suggest_similar_incidents(incident: Incident, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
"""Suggest similar past incidents based on current incident"""
|
||||
try:
|
||||
# Find similar incidents based on category and keywords
|
||||
similar_incidents = []
|
||||
|
||||
# Search by category
|
||||
category_incidents = Incident.objects.filter(
|
||||
category=incident.category,
|
||||
status__in=['RESOLVED', 'CLOSED']
|
||||
).exclude(id=incident.id)[:limit]
|
||||
|
||||
for similar_incident in category_incidents:
|
||||
similarity_score = AIAssistantService._calculate_incident_similarity(
|
||||
incident, similar_incident
|
||||
)
|
||||
|
||||
if similarity_score > 0.3: # Minimum similarity threshold
|
||||
similar_incidents.append({
|
||||
'id': str(similar_incident.id),
|
||||
'title': similar_incident.title,
|
||||
'severity': similar_incident.severity,
|
||||
'status': similar_incident.status,
|
||||
'created_at': similar_incident.created_at.isoformat(),
|
||||
'resolved_at': similar_incident.resolved_at.isoformat() if similar_incident.resolved_at else None,
|
||||
'resolution_time': str(similar_incident.resolution_time) if similar_incident.resolution_time else None,
|
||||
'similarity_score': similarity_score,
|
||||
'has_postmortem': similar_incident.postmortems.exists()
|
||||
})
|
||||
|
||||
# Sort by similarity score
|
||||
similar_incidents.sort(key=lambda x: x['similarity_score'], reverse=True)
|
||||
|
||||
return similar_incidents[:limit]
|
||||
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def suggest_knowledge_articles(incident: Incident, message_content: str = None) -> List[Dict[str, Any]]:
|
||||
"""Suggest relevant knowledge base articles"""
|
||||
try:
|
||||
# Extract keywords from incident and message
|
||||
keywords = AIAssistantService._extract_keywords(incident, message_content)
|
||||
|
||||
# Search knowledge base articles
|
||||
articles = KnowledgeBaseArticle.objects.filter(
|
||||
is_active=True,
|
||||
tags__overlap=keywords
|
||||
).distinct()[:10]
|
||||
|
||||
suggested_articles = []
|
||||
for article in articles:
|
||||
relevance_score = AIAssistantService._calculate_article_relevance(
|
||||
article, incident, keywords
|
||||
)
|
||||
|
||||
if relevance_score > 0.2: # Minimum relevance threshold
|
||||
suggested_articles.append({
|
||||
'id': str(article.id),
|
||||
'title': article.title,
|
||||
'summary': article.summary,
|
||||
'category': article.category,
|
||||
'tags': article.tags,
|
||||
'relevance_score': relevance_score,
|
||||
'url': f"/knowledge/articles/{article.id}/"
|
||||
})
|
||||
|
||||
# Sort by relevance score
|
||||
suggested_articles.sort(key=lambda x: x['relevance_score'], reverse=True)
|
||||
|
||||
return suggested_articles[:5]
|
||||
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def suggest_runbooks(incident: Incident) -> List[Dict[str, Any]]:
|
||||
"""Suggest relevant runbooks for the incident"""
|
||||
try:
|
||||
from automation_orchestration.models import Runbook
|
||||
|
||||
# Find runbooks that match incident characteristics
|
||||
runbooks = Runbook.objects.filter(
|
||||
is_active=True,
|
||||
categories__contains=[incident.category]
|
||||
)
|
||||
|
||||
suggested_runbooks = []
|
||||
for runbook in runbooks:
|
||||
if incident.severity in runbook.severity_levels:
|
||||
suggested_runbooks.append({
|
||||
'id': str(runbook.id),
|
||||
'name': runbook.name,
|
||||
'description': runbook.description,
|
||||
'category': runbook.category,
|
||||
'estimated_duration': runbook.estimated_duration,
|
||||
'success_rate': runbook.success_rate,
|
||||
'last_used': runbook.last_used.isoformat() if runbook.last_used else None
|
||||
})
|
||||
|
||||
return suggested_runbooks[:5]
|
||||
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def detect_incident_patterns(incident: Incident) -> List[Dict[str, Any]]:
|
||||
"""Detect patterns in the incident"""
|
||||
try:
|
||||
# Find matching patterns
|
||||
patterns = IncidentPattern.objects.filter(
|
||||
is_active=True,
|
||||
incidents=incident
|
||||
)
|
||||
|
||||
detected_patterns = []
|
||||
for pattern in patterns:
|
||||
detected_patterns.append({
|
||||
'id': str(pattern.id),
|
||||
'name': pattern.name,
|
||||
'pattern_type': pattern.pattern_type,
|
||||
'description': pattern.description,
|
||||
'confidence_score': pattern.confidence_score,
|
||||
'frequency': pattern.frequency,
|
||||
'last_occurrence': pattern.last_occurrence.isoformat() if pattern.last_occurrence else None,
|
||||
'next_predicted_occurrence': pattern.next_predicted_occurrence.isoformat() if pattern.next_predicted_occurrence else None
|
||||
})
|
||||
|
||||
return detected_patterns
|
||||
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _analyze_message(content: str) -> Dict[str, Any]:
|
||||
"""Analyze message content to determine intent"""
|
||||
content_lower = content.lower()
|
||||
|
||||
# Check for questions
|
||||
question_patterns = [
|
||||
r'\?', r'how\s+', r'what\s+', r'when\s+', r'where\s+', r'why\s+', r'who\s+',
|
||||
r'can\s+you\s+', r'could\s+you\s+', r'would\s+you\s+'
|
||||
]
|
||||
contains_question = any(re.search(pattern, content_lower) for pattern in question_patterns)
|
||||
|
||||
# Check for incident-related keywords
|
||||
incident_keywords = [
|
||||
'incident', 'issue', 'problem', 'outage', 'error', 'failure', 'down',
|
||||
'severity', 'priority', 'escalate', 'resolve', 'fix'
|
||||
]
|
||||
contains_incident_keywords = any(keyword in content_lower for keyword in incident_keywords)
|
||||
|
||||
# Check for help requests
|
||||
help_patterns = [
|
||||
r'help\s+', r'assist\s+', r'support\s+', r'guidance\s+', r'advice\s+'
|
||||
]
|
||||
contains_help_request = any(re.search(pattern, content_lower) for pattern in help_patterns)
|
||||
|
||||
return {
|
||||
'contains_question': contains_question,
|
||||
'contains_incident_keywords': contains_incident_keywords,
|
||||
'contains_help_request': contains_help_request,
|
||||
'word_count': len(content.split()),
|
||||
'sentiment': AIAssistantService._analyze_sentiment(content)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _generate_question_response(bot: ChatBot, message: WarRoomMessage, analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate response to a question"""
|
||||
incident = message.war_room.incident
|
||||
|
||||
# Get relevant knowledge articles
|
||||
articles = AIAssistantService.suggest_knowledge_articles(incident, message.content)
|
||||
|
||||
if articles:
|
||||
response_text = f"Based on your question, here are some relevant resources:\n\n"
|
||||
for article in articles[:3]:
|
||||
response_text += f"• **{article['title']}** - {article['summary'][:100]}...\n"
|
||||
|
||||
response_text += f"\nYou can find more information in our knowledge base."
|
||||
else:
|
||||
response_text = "I'd be happy to help with your question. Let me search our knowledge base for relevant information."
|
||||
|
||||
return {
|
||||
'response': response_text,
|
||||
'response_type': 'question_answer',
|
||||
'suggested_articles': articles[:3]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _generate_incident_response(bot: ChatBot, message: WarRoomMessage, analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate response related to incident management"""
|
||||
incident = message.war_room.incident
|
||||
|
||||
# Suggest similar incidents
|
||||
similar_incidents = AIAssistantService.suggest_similar_incidents(incident)
|
||||
|
||||
# Suggest runbooks
|
||||
runbooks = AIAssistantService.suggest_runbooks(incident)
|
||||
|
||||
response_text = f"I can help with incident management. Here's what I found:\n\n"
|
||||
|
||||
if similar_incidents:
|
||||
response_text += f"**Similar Past Incidents:**\n"
|
||||
for incident_data in similar_incidents[:2]:
|
||||
response_text += f"• {incident_data['title']} (Similarity: {incident_data['similarity_score']:.1%})\n"
|
||||
response_text += "\n"
|
||||
|
||||
if runbooks:
|
||||
response_text += f"**Suggested Runbooks:**\n"
|
||||
for runbook in runbooks[:2]:
|
||||
response_text += f"• {runbook['name']} - {runbook['description'][:100]}...\n"
|
||||
|
||||
return {
|
||||
'response': response_text,
|
||||
'response_type': 'incident_assistance',
|
||||
'similar_incidents': similar_incidents[:3],
|
||||
'suggested_runbooks': runbooks[:3]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _generate_help_response(bot: ChatBot, message: WarRoomMessage, analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate help response"""
|
||||
response_text = (
|
||||
"I'm here to help! I can assist you with:\n\n"
|
||||
"• **Incident Management** - Find similar incidents, suggest runbooks\n"
|
||||
"• **Knowledge Base** - Search for relevant articles and documentation\n"
|
||||
"• **ChatOps Commands** - Execute automation commands like `/status`, `/run playbook <name>`\n"
|
||||
"• **Pattern Detection** - Identify recurring issues and patterns\n\n"
|
||||
"Just ask me a question or mention what you need help with!"
|
||||
)
|
||||
|
||||
return {
|
||||
'response': response_text,
|
||||
'response_type': 'help'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _generate_general_response(bot: ChatBot, message: WarRoomMessage, analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate general response"""
|
||||
incident = message.war_room.incident
|
||||
|
||||
# Check for patterns
|
||||
patterns = AIAssistantService.detect_incident_patterns(incident)
|
||||
|
||||
if patterns:
|
||||
response_text = f"I noticed this incident matches some known patterns:\n\n"
|
||||
for pattern in patterns[:2]:
|
||||
response_text += f"• **{pattern['name']}** - {pattern['description'][:100]}...\n"
|
||||
else:
|
||||
response_text = "I'm monitoring this incident. Let me know if you need any assistance with incident response or have questions about similar past incidents."
|
||||
|
||||
return {
|
||||
'response': response_text,
|
||||
'response_type': 'general',
|
||||
'detected_patterns': patterns
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_confidence(response: Dict[str, Any]) -> float:
|
||||
"""Calculate confidence score for the response"""
|
||||
base_confidence = 0.7
|
||||
|
||||
# Increase confidence based on available data
|
||||
if response.get('suggested_articles'):
|
||||
base_confidence += 0.1
|
||||
if response.get('similar_incidents'):
|
||||
base_confidence += 0.1
|
||||
if response.get('suggested_runbooks'):
|
||||
base_confidence += 0.1
|
||||
|
||||
return min(base_confidence, 1.0)
|
||||
|
||||
@staticmethod
|
||||
def _get_response_sources(response: Dict[str, Any]) -> List[str]:
|
||||
"""Get sources for the response"""
|
||||
sources = []
|
||||
|
||||
if response.get('suggested_articles'):
|
||||
sources.append('Knowledge Base')
|
||||
if response.get('similar_incidents'):
|
||||
sources.append('Historical Incidents')
|
||||
if response.get('suggested_runbooks'):
|
||||
sources.append('Runbook Library')
|
||||
if response.get('detected_patterns'):
|
||||
sources.append('Pattern Analysis')
|
||||
|
||||
return sources
|
||||
|
||||
@staticmethod
|
||||
def _calculate_incident_similarity(incident1: Incident, incident2: Incident) -> float:
|
||||
"""Calculate similarity score between two incidents"""
|
||||
similarity = 0.0
|
||||
|
||||
# Category similarity
|
||||
if incident1.category == incident2.category:
|
||||
similarity += 0.3
|
||||
|
||||
# Severity similarity
|
||||
if incident1.severity == incident2.severity:
|
||||
similarity += 0.2
|
||||
|
||||
# Text similarity (simplified)
|
||||
text1_words = set(incident1.title.lower().split())
|
||||
text2_words = set(incident2.title.lower().split())
|
||||
|
||||
if text1_words and text2_words:
|
||||
text_similarity = len(text1_words.intersection(text2_words)) / len(text1_words.union(text2_words))
|
||||
similarity += text_similarity * 0.5
|
||||
|
||||
return similarity
|
||||
|
||||
@staticmethod
|
||||
def _extract_keywords(incident: Incident, message_content: str = None) -> List[str]:
|
||||
"""Extract keywords from incident and message"""
|
||||
keywords = []
|
||||
|
||||
# Add incident keywords
|
||||
if incident.category:
|
||||
keywords.append(incident.category.lower())
|
||||
if incident.title:
|
||||
keywords.extend(incident.title.lower().split())
|
||||
|
||||
# Add message keywords
|
||||
if message_content:
|
||||
keywords.extend(message_content.lower().split())
|
||||
|
||||
# Remove common words
|
||||
stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'}
|
||||
keywords = [word for word in keywords if word not in stop_words and len(word) > 2]
|
||||
|
||||
return list(set(keywords)) # Remove duplicates
|
||||
|
||||
@staticmethod
|
||||
def _calculate_article_relevance(article: KnowledgeBaseArticle, incident: Incident, keywords: List[str]) -> float:
|
||||
"""Calculate relevance score for a knowledge article"""
|
||||
relevance = 0.0
|
||||
|
||||
# Tag overlap
|
||||
if article.tags:
|
||||
tag_overlap = len(set(article.tags).intersection(set(keywords)))
|
||||
relevance += tag_overlap * 0.1
|
||||
|
||||
# Category match
|
||||
if article.category == incident.category:
|
||||
relevance += 0.3
|
||||
|
||||
# Title keyword match
|
||||
title_words = set(article.title.lower().split())
|
||||
keyword_overlap = len(title_words.intersection(set(keywords)))
|
||||
if title_words:
|
||||
relevance += (keyword_overlap / len(title_words)) * 0.4
|
||||
|
||||
return min(relevance, 1.0)
|
||||
|
||||
@staticmethod
|
||||
def _analyze_sentiment(content: str) -> str:
|
||||
"""Simple sentiment analysis"""
|
||||
positive_words = ['good', 'great', 'excellent', 'fixed', 'resolved', 'working', 'success']
|
||||
negative_words = ['bad', 'terrible', 'broken', 'failed', 'error', 'issue', 'problem']
|
||||
|
||||
content_lower = content.lower()
|
||||
positive_count = sum(1 for word in positive_words if word in content_lower)
|
||||
negative_count = sum(1 for word in negative_words if word in content_lower)
|
||||
|
||||
if positive_count > negative_count:
|
||||
return 'positive'
|
||||
elif negative_count > positive_count:
|
||||
return 'negative'
|
||||
else:
|
||||
return 'neutral'
|
||||
366
ETB-API/collaboration_war_rooms/services/automation_commands.py
Normal file
366
ETB-API/collaboration_war_rooms/services/automation_commands.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
Automation Commands Service for ChatOps Integration
|
||||
Handles execution of automation commands via chat interface
|
||||
"""
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from ..models import ChatCommand, WarRoomMessage
|
||||
from automation_orchestration.models import Runbook, RunbookExecution, AutoRemediation, AutoRemediationExecution
|
||||
from incident_intelligence.models import Incident
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class AutomationCommandService:
|
||||
"""Service for handling automation commands in chat"""
|
||||
|
||||
@staticmethod
|
||||
def execute_runbook_command(chat_command: ChatCommand, runbook_name: str, user: User) -> Dict[str, Any]:
|
||||
"""Execute a runbook via chat command"""
|
||||
try:
|
||||
incident = chat_command.message.war_room.incident
|
||||
|
||||
# Find the runbook
|
||||
runbook = Runbook.objects.filter(
|
||||
name__icontains=runbook_name,
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
if not runbook:
|
||||
return {
|
||||
'error': f'Runbook "{runbook_name}" not found or inactive',
|
||||
'suggestions': AutomationCommandService._get_runbook_suggestions(runbook_name)
|
||||
}
|
||||
|
||||
# Check if runbook applies to this incident
|
||||
if not AutomationCommandService._runbook_applies_to_incident(runbook, incident):
|
||||
return {
|
||||
'error': f'Runbook "{runbook.name}" does not apply to this incident type',
|
||||
'incident_category': incident.category,
|
||||
'incident_severity': incident.severity
|
||||
}
|
||||
|
||||
# Execute the runbook
|
||||
execution = RunbookExecution.objects.create(
|
||||
runbook=runbook,
|
||||
incident=incident,
|
||||
triggered_by=user,
|
||||
trigger_type='CHAT_COMMAND',
|
||||
trigger_data={
|
||||
'chat_command_id': str(chat_command.id),
|
||||
'command_text': chat_command.command_text
|
||||
}
|
||||
)
|
||||
|
||||
# Update chat command with execution reference
|
||||
chat_command.automation_execution = execution
|
||||
chat_command.save()
|
||||
|
||||
# Create status message in chat
|
||||
AutomationCommandService._create_execution_status_message(
|
||||
chat_command.message.war_room,
|
||||
f"🚀 **Runbook Execution Started**\n\n"
|
||||
f"**Runbook:** {runbook.name}\n"
|
||||
f"**Execution ID:** {execution.id}\n"
|
||||
f"**Triggered by:** {user.username}\n"
|
||||
f"**Status:** {execution.status}\n\n"
|
||||
f"Monitor progress in the automation dashboard."
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'execution_id': str(execution.id),
|
||||
'runbook_name': runbook.name,
|
||||
'status': execution.status,
|
||||
'message': f'Runbook "{runbook.name}" execution started successfully'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'error': f'Failed to execute runbook: {str(e)}'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def execute_auto_remediation_command(chat_command: ChatCommand, remediation_name: str, user: User) -> Dict[str, Any]:
|
||||
"""Execute auto-remediation via chat command"""
|
||||
try:
|
||||
incident = chat_command.message.war_room.incident
|
||||
|
||||
# Find the auto-remediation
|
||||
remediation = AutoRemediation.objects.filter(
|
||||
name__icontains=remediation_name,
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
if not remediation:
|
||||
return {
|
||||
'error': f'Auto-remediation "{remediation_name}" not found or inactive',
|
||||
'suggestions': AutomationCommandService._get_remediation_suggestions(remediation_name)
|
||||
}
|
||||
|
||||
# Check if remediation applies to this incident
|
||||
if not AutomationCommandService._remediation_applies_to_incident(remediation, incident):
|
||||
return {
|
||||
'error': f'Auto-remediation "{remediation.name}" does not apply to this incident type',
|
||||
'incident_category': incident.category,
|
||||
'incident_severity': incident.severity
|
||||
}
|
||||
|
||||
# Execute the auto-remediation
|
||||
execution = AutoRemediationExecution.objects.create(
|
||||
auto_remediation=remediation,
|
||||
incident=incident,
|
||||
triggered_by=user,
|
||||
trigger_type='CHAT_COMMAND',
|
||||
trigger_data={
|
||||
'chat_command_id': str(chat_command.id),
|
||||
'command_text': chat_command.command_text
|
||||
}
|
||||
)
|
||||
|
||||
# Create status message in chat
|
||||
AutomationCommandService._create_execution_status_message(
|
||||
chat_command.message.war_room,
|
||||
f"🔧 **Auto-Remediation Started**\n\n"
|
||||
f"**Remediation:** {remediation.name}\n"
|
||||
f"**Execution ID:** {execution.id}\n"
|
||||
f"**Triggered by:** {user.username}\n"
|
||||
f"**Status:** {execution.status}\n\n"
|
||||
f"Monitor progress in the automation dashboard."
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'execution_id': str(execution.id),
|
||||
'remediation_name': remediation.name,
|
||||
'status': execution.status,
|
||||
'message': f'Auto-remediation "{remediation.name}" execution started successfully'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'error': f'Failed to execute auto-remediation: {str(e)}'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_incident_status(chat_command: ChatCommand) -> Dict[str, Any]:
|
||||
"""Get comprehensive incident status"""
|
||||
try:
|
||||
incident = chat_command.message.war_room.incident
|
||||
|
||||
# Get SLA status
|
||||
from .sla_notifications import SLANotificationService
|
||||
sla_status = SLANotificationService.get_sla_status_for_incident(incident)
|
||||
|
||||
# Get recent runbook executions
|
||||
recent_executions = RunbookExecution.objects.filter(
|
||||
incident=incident
|
||||
).order_by('-created_at')[:5]
|
||||
|
||||
executions_data = []
|
||||
for execution in recent_executions:
|
||||
executions_data.append({
|
||||
'id': str(execution.id),
|
||||
'runbook_name': execution.runbook.name,
|
||||
'status': execution.status,
|
||||
'started_at': execution.started_at.isoformat() if execution.started_at else None,
|
||||
'completed_at': execution.completed_at.isoformat() if execution.completed_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
'incident_id': str(incident.id),
|
||||
'title': incident.title,
|
||||
'status': incident.status,
|
||||
'severity': incident.severity,
|
||||
'priority': incident.priority,
|
||||
'category': incident.category,
|
||||
'assigned_to': incident.assigned_to.username if incident.assigned_to else None,
|
||||
'reporter': incident.reporter.username if incident.reporter else None,
|
||||
'created_at': incident.created_at.isoformat(),
|
||||
'updated_at': incident.updated_at.isoformat(),
|
||||
'resolution_time': str(incident.resolution_time) if incident.resolution_time else None,
|
||||
'sla_status': sla_status,
|
||||
'recent_executions': executions_data,
|
||||
'automation_enabled': incident.automation_enabled,
|
||||
'runbook_suggested': incident.runbook_suggested,
|
||||
'auto_remediation_attempted': incident.auto_remediation_attempted
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'error': f'Failed to get incident status: {str(e)}'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def list_available_runbooks(incident: Incident) -> List[Dict[str, Any]]:
|
||||
"""List runbooks available for the incident"""
|
||||
try:
|
||||
runbooks = Runbook.objects.filter(is_active=True)
|
||||
available_runbooks = []
|
||||
|
||||
for runbook in runbooks:
|
||||
if AutomationCommandService._runbook_applies_to_incident(runbook, incident):
|
||||
available_runbooks.append({
|
||||
'id': str(runbook.id),
|
||||
'name': runbook.name,
|
||||
'description': runbook.description,
|
||||
'category': runbook.category,
|
||||
'severity_levels': runbook.severity_levels,
|
||||
'estimated_duration': runbook.estimated_duration
|
||||
})
|
||||
|
||||
return available_runbooks
|
||||
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def list_available_remediations(incident: Incident) -> List[Dict[str, Any]]:
|
||||
"""List auto-remediations available for the incident"""
|
||||
try:
|
||||
remediations = AutoRemediation.objects.filter(is_active=True)
|
||||
available_remediations = []
|
||||
|
||||
for remediation in remediations:
|
||||
if AutomationCommandService._remediation_applies_to_incident(remediation, incident):
|
||||
available_remediations.append({
|
||||
'id': str(remediation.id),
|
||||
'name': remediation.name,
|
||||
'description': remediation.description,
|
||||
'category': remediation.category,
|
||||
'severity_levels': remediation.severity_levels,
|
||||
'estimated_duration': remediation.estimated_duration
|
||||
})
|
||||
|
||||
return available_remediations
|
||||
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _runbook_applies_to_incident(runbook: Runbook, incident: Incident) -> bool:
|
||||
"""Check if runbook applies to the incident"""
|
||||
# Check categories
|
||||
if runbook.categories and incident.category not in runbook.categories:
|
||||
return False
|
||||
|
||||
# Check severity levels
|
||||
if runbook.severity_levels and incident.severity not in runbook.severity_levels:
|
||||
return False
|
||||
|
||||
# Check if runbook is active
|
||||
if not runbook.is_active:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _remediation_applies_to_incident(remediation: AutoRemediation, incident: Incident) -> bool:
|
||||
"""Check if auto-remediation applies to the incident"""
|
||||
# Check categories
|
||||
if remediation.categories and incident.category not in remediation.categories:
|
||||
return False
|
||||
|
||||
# Check severity levels
|
||||
if remediation.severity_levels and incident.severity not in remediation.severity_levels:
|
||||
return False
|
||||
|
||||
# Check if remediation is active
|
||||
if not remediation.is_active:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _get_runbook_suggestions(partial_name: str) -> List[str]:
|
||||
"""Get runbook name suggestions based on partial input"""
|
||||
try:
|
||||
runbooks = Runbook.objects.filter(
|
||||
name__icontains=partial_name,
|
||||
is_active=True
|
||||
).values_list('name', flat=True)[:5]
|
||||
|
||||
return list(runbooks)
|
||||
except:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _get_remediation_suggestions(partial_name: str) -> List[str]:
|
||||
"""Get auto-remediation name suggestions based on partial input"""
|
||||
try:
|
||||
remediations = AutoRemediation.objects.filter(
|
||||
name__icontains=partial_name,
|
||||
is_active=True
|
||||
).values_list('name', flat=True)[:5]
|
||||
|
||||
return list(remediations)
|
||||
except:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _create_execution_status_message(war_room: 'WarRoom', content: str):
|
||||
"""Create a status message in the war room"""
|
||||
try:
|
||||
WarRoomMessage.objects.create(
|
||||
war_room=war_room,
|
||||
content=content,
|
||||
message_type='SYSTEM',
|
||||
sender=None,
|
||||
sender_name='Automation System',
|
||||
external_data={
|
||||
'message_type': 'automation_status'
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error creating status message: {e}")
|
||||
|
||||
@staticmethod
|
||||
def update_execution_status(execution_id: str, status: str, result: Dict[str, Any] = None):
|
||||
"""Update execution status and notify chat room"""
|
||||
try:
|
||||
# Find the chat command that triggered this execution
|
||||
chat_command = ChatCommand.objects.filter(
|
||||
automation_execution_id=execution_id
|
||||
).first()
|
||||
|
||||
if not chat_command:
|
||||
return False
|
||||
|
||||
# Update chat command status
|
||||
chat_command.execution_status = status
|
||||
if result:
|
||||
chat_command.execution_result = result
|
||||
chat_command.save()
|
||||
|
||||
# Create status update message
|
||||
status_emoji = {
|
||||
'SUCCESS': '✅',
|
||||
'FAILED': '❌',
|
||||
'RUNNING': '🔄',
|
||||
'CANCELLED': '⏹️'
|
||||
}.get(status, '📊')
|
||||
|
||||
message_content = (
|
||||
f"{status_emoji} **Execution Status Update**\n\n"
|
||||
f"**Status:** {status}\n"
|
||||
f"**Execution ID:** {execution_id}\n"
|
||||
)
|
||||
|
||||
if result:
|
||||
if 'error' in result:
|
||||
message_content += f"**Error:** {result['error']}\n"
|
||||
if 'output' in result:
|
||||
message_content += f"**Output:** {result['output'][:200]}...\n"
|
||||
|
||||
AutomationCommandService._create_execution_status_message(
|
||||
chat_command.message.war_room,
|
||||
message_content
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating execution status: {e}")
|
||||
return False
|
||||
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
Compliance Integration Service for Chat
|
||||
Handles file classification, audit trails, and compliance requirements
|
||||
"""
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
from typing import Dict, Any, Optional, List
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
from ..models import ChatFile, WarRoomMessage
|
||||
from compliance_governance.models import DataClassification, AuditLog, CompliancePolicy
|
||||
from security.models import DataClassification as SecurityDataClassification
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ComplianceIntegrationService:
|
||||
"""Service for handling compliance requirements in chat"""
|
||||
|
||||
@staticmethod
|
||||
def classify_file(file_path: str, filename: str, file_size: int, user: User) -> Dict[str, Any]:
|
||||
"""Classify a file based on content and context"""
|
||||
try:
|
||||
# Get file MIME type
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
|
||||
# Calculate file hash
|
||||
file_hash = ComplianceIntegrationService._calculate_file_hash(file_path)
|
||||
|
||||
# Determine classification based on file type and content
|
||||
classification_level = ComplianceIntegrationService._determine_classification(
|
||||
filename, mime_type, file_size
|
||||
)
|
||||
|
||||
# Get or create data classification
|
||||
data_classification = ComplianceIntegrationService._get_or_create_classification(
|
||||
classification_level
|
||||
)
|
||||
|
||||
return {
|
||||
'classification_level': classification_level,
|
||||
'data_classification_id': str(data_classification.id) if data_classification else None,
|
||||
'file_hash': file_hash,
|
||||
'mime_type': mime_type,
|
||||
'is_encrypted': classification_level in ['CONFIDENTIAL', 'RESTRICTED', 'TOP_SECRET'],
|
||||
'retention_period': ComplianceIntegrationService._get_retention_period(classification_level)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'error': f'Failed to classify file: {str(e)}',
|
||||
'classification_level': 'PUBLIC', # Default to public on error
|
||||
'file_hash': None,
|
||||
'mime_type': None,
|
||||
'is_encrypted': False
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_audit_log_entry(
|
||||
action: str,
|
||||
user: User,
|
||||
resource_type: str,
|
||||
resource_id: str,
|
||||
details: Dict[str, Any] = None
|
||||
) -> bool:
|
||||
"""Create an audit log entry for compliance"""
|
||||
try:
|
||||
AuditLog.objects.create(
|
||||
action=action,
|
||||
user=user,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
timestamp=timezone.now(),
|
||||
details=details or {},
|
||||
ip_address=ComplianceIntegrationService._get_user_ip(user),
|
||||
user_agent=ComplianceIntegrationService._get_user_agent(user)
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating audit log entry: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def log_chat_message_access(message: WarRoomMessage, user: User, action: str = 'access'):
|
||||
"""Log access to chat messages for audit trail"""
|
||||
try:
|
||||
ComplianceIntegrationService.create_audit_log_entry(
|
||||
action=f'chat_message_{action}',
|
||||
user=user,
|
||||
resource_type='chat_message',
|
||||
resource_id=str(message.id),
|
||||
details={
|
||||
'war_room_id': str(message.war_room.id),
|
||||
'incident_id': str(message.war_room.incident.id),
|
||||
'message_type': message.message_type,
|
||||
'sender': message.sender.username if message.sender else None,
|
||||
'content_length': len(message.content),
|
||||
'is_encrypted': message.is_encrypted
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error logging chat message access: {e}")
|
||||
|
||||
@staticmethod
|
||||
def log_file_access(file_obj: ChatFile, user: User, action: str = 'access'):
|
||||
"""Log file access for audit trail"""
|
||||
try:
|
||||
ComplianceIntegrationService.create_audit_log_entry(
|
||||
action=f'file_{action}',
|
||||
user=user,
|
||||
resource_type='chat_file',
|
||||
resource_id=str(file_obj.id),
|
||||
details={
|
||||
'filename': file_obj.original_filename,
|
||||
'file_type': file_obj.file_type,
|
||||
'file_size': file_obj.file_size,
|
||||
'data_classification': file_obj.data_classification.level if file_obj.data_classification else None,
|
||||
'is_encrypted': file_obj.is_encrypted,
|
||||
'message_id': str(file_obj.message.id),
|
||||
'incident_id': str(file_obj.message.war_room.incident.id)
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error logging file access: {e}")
|
||||
|
||||
@staticmethod
|
||||
def check_compliance_policies(incident_id: str, user: User) -> Dict[str, Any]:
|
||||
"""Check compliance policies for incident chat access"""
|
||||
try:
|
||||
# Get applicable compliance policies
|
||||
policies = CompliancePolicy.objects.filter(
|
||||
is_active=True,
|
||||
applies_to_incidents=True
|
||||
)
|
||||
|
||||
compliance_status = {
|
||||
'policies_checked': len(policies),
|
||||
'violations': [],
|
||||
'warnings': [],
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
for policy in policies:
|
||||
# Check if policy applies to this incident
|
||||
if ComplianceIntegrationService._policy_applies_to_incident(policy, incident_id):
|
||||
violations = ComplianceIntegrationService._check_policy_violations(
|
||||
policy, incident_id, user
|
||||
)
|
||||
|
||||
if violations:
|
||||
compliance_status['violations'].extend(violations)
|
||||
|
||||
return compliance_status
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'error': f'Failed to check compliance policies: {str(e)}'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def export_chat_logs_for_compliance(
|
||||
incident_id: str,
|
||||
start_date: timezone.datetime,
|
||||
end_date: timezone.datetime,
|
||||
user: User
|
||||
) -> Dict[str, Any]:
|
||||
"""Export chat logs for compliance reporting"""
|
||||
try:
|
||||
from ..models import WarRoom
|
||||
|
||||
# Get war room for incident
|
||||
war_room = WarRoom.objects.filter(incident_id=incident_id).first()
|
||||
if not war_room:
|
||||
return {'error': 'War room not found for incident'}
|
||||
|
||||
# Get messages in date range
|
||||
messages = WarRoomMessage.objects.filter(
|
||||
war_room=war_room,
|
||||
created_at__gte=start_date,
|
||||
created_at__lte=end_date
|
||||
).order_by('created_at')
|
||||
|
||||
# Prepare export data
|
||||
export_data = {
|
||||
'incident_id': incident_id,
|
||||
'war_room_id': str(war_room.id),
|
||||
'export_date': timezone.now().isoformat(),
|
||||
'exported_by': user.username,
|
||||
'date_range': {
|
||||
'start': start_date.isoformat(),
|
||||
'end': end_date.isoformat()
|
||||
},
|
||||
'message_count': messages.count(),
|
||||
'messages': []
|
||||
}
|
||||
|
||||
for message in messages:
|
||||
message_data = {
|
||||
'id': str(message.id),
|
||||
'timestamp': message.created_at.isoformat(),
|
||||
'sender': message.sender.username if message.sender else message.sender_name,
|
||||
'message_type': message.message_type,
|
||||
'content': message.content,
|
||||
'is_encrypted': message.is_encrypted,
|
||||
'is_pinned': message.is_pinned,
|
||||
'attachments': [
|
||||
{
|
||||
'id': str(attachment.id),
|
||||
'filename': attachment.original_filename,
|
||||
'file_type': attachment.file_type,
|
||||
'file_size': attachment.file_size,
|
||||
'data_classification': attachment.data_classification.level if attachment.data_classification else None,
|
||||
'is_encrypted': attachment.is_encrypted,
|
||||
'file_hash': attachment.file_hash
|
||||
}
|
||||
for attachment in message.chat_files.all()
|
||||
]
|
||||
}
|
||||
export_data['messages'].append(message_data)
|
||||
|
||||
# Log the export action
|
||||
ComplianceIntegrationService.create_audit_log_entry(
|
||||
action='export_chat_logs',
|
||||
user=user,
|
||||
resource_type='incident',
|
||||
resource_id=incident_id,
|
||||
details={
|
||||
'message_count': export_data['message_count'],
|
||||
'date_range': export_data['date_range']
|
||||
}
|
||||
)
|
||||
|
||||
return export_data
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'error': f'Failed to export chat logs: {str(e)}'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_file_hash(file_path: str) -> str:
|
||||
"""Calculate SHA-256 hash of file"""
|
||||
try:
|
||||
hash_sha256 = hashlib.sha256()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_sha256.update(chunk)
|
||||
return hash_sha256.hexdigest()
|
||||
except:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _determine_classification(filename: str, mime_type: str, file_size: int) -> str:
|
||||
"""Determine data classification level based on file characteristics"""
|
||||
# Check for sensitive file extensions
|
||||
sensitive_extensions = ['.key', '.pem', '.p12', '.pfx', '.crt', '.cer']
|
||||
if any(filename.lower().endswith(ext) for ext in sensitive_extensions):
|
||||
return 'CONFIDENTIAL'
|
||||
|
||||
# Check for log files
|
||||
if filename.lower().endswith('.log') or 'log' in filename.lower():
|
||||
return 'INTERNAL'
|
||||
|
||||
# Check for configuration files
|
||||
config_extensions = ['.conf', '.config', '.ini', '.yaml', '.yml', '.json']
|
||||
if any(filename.lower().endswith(ext) for ext in config_extensions):
|
||||
return 'INTERNAL'
|
||||
|
||||
# Check for database files
|
||||
db_extensions = ['.db', '.sqlite', '.sql', '.dump']
|
||||
if any(filename.lower().endswith(ext) for ext in db_extensions):
|
||||
return 'CONFIDENTIAL'
|
||||
|
||||
# Check file size (large files might be sensitive)
|
||||
if file_size > 100 * 1024 * 1024: # 100MB
|
||||
return 'INTERNAL'
|
||||
|
||||
# Default classification
|
||||
return 'PUBLIC'
|
||||
|
||||
@staticmethod
|
||||
def _get_or_create_classification(level: str):
|
||||
"""Get or create data classification object"""
|
||||
try:
|
||||
# Try to get from security module first
|
||||
classification = SecurityDataClassification.objects.filter(level=level).first()
|
||||
if classification:
|
||||
return classification
|
||||
|
||||
# Create new classification if not found
|
||||
classification = SecurityDataClassification.objects.create(
|
||||
level=level,
|
||||
description=f'Data classification level: {level}',
|
||||
retention_period_days=ComplianceIntegrationService._get_retention_period(level)
|
||||
)
|
||||
return classification
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting/creating classification: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_retention_period(classification_level: str) -> int:
|
||||
"""Get retention period in days based on classification level"""
|
||||
retention_periods = {
|
||||
'PUBLIC': 365, # 1 year
|
||||
'INTERNAL': 1095, # 3 years
|
||||
'CONFIDENTIAL': 2555, # 7 years
|
||||
'RESTRICTED': 3650, # 10 years
|
||||
'TOP_SECRET': 3650 # 10 years
|
||||
}
|
||||
return retention_periods.get(classification_level, 365)
|
||||
|
||||
@staticmethod
|
||||
def _get_user_ip(user: User) -> str:
|
||||
"""Get user IP address (placeholder implementation)"""
|
||||
# This would be implemented based on your request handling
|
||||
return "127.0.0.1"
|
||||
|
||||
@staticmethod
|
||||
def _get_user_agent(user: User) -> str:
|
||||
"""Get user agent (placeholder implementation)"""
|
||||
# This would be implemented based on your request handling
|
||||
return "Chat System"
|
||||
|
||||
@staticmethod
|
||||
def _policy_applies_to_incident(policy: CompliancePolicy, incident_id: str) -> bool:
|
||||
"""Check if compliance policy applies to incident"""
|
||||
# This would check policy conditions against incident
|
||||
# For now, return True for all active policies
|
||||
return policy.is_active
|
||||
|
||||
@staticmethod
|
||||
def _check_policy_violations(policy: CompliancePolicy, incident_id: str, user: User) -> List[str]:
|
||||
"""Check for policy violations"""
|
||||
violations = []
|
||||
|
||||
# Example policy checks
|
||||
if policy.name == "Data Retention Policy":
|
||||
# Check if chat logs are being retained properly
|
||||
pass
|
||||
|
||||
if policy.name == "Access Control Policy":
|
||||
# Check if user has appropriate access
|
||||
pass
|
||||
|
||||
return violations
|
||||
287
ETB-API/collaboration_war_rooms/services/sla_notifications.py
Normal file
287
ETB-API/collaboration_war_rooms/services/sla_notifications.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
SLA Notifications Service for Chat Integration
|
||||
Handles SLA threshold notifications and escalation alerts in chat rooms
|
||||
"""
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from ..models import WarRoom, WarRoomMessage
|
||||
from sla_oncall.models import SLAInstance, EscalationInstance, EscalationPolicy
|
||||
from incident_intelligence.models import Incident
|
||||
|
||||
|
||||
class SLANotificationService:
|
||||
"""Service for handling SLA-related notifications in chat rooms"""
|
||||
|
||||
@staticmethod
|
||||
def send_sla_warning_notification(sla_instance: SLAInstance, threshold_percent: float = 80.0):
|
||||
"""Send SLA warning notification to incident chat room"""
|
||||
try:
|
||||
war_room = WarRoom.objects.filter(incident=sla_instance.incident).first()
|
||||
if not war_room:
|
||||
return False
|
||||
|
||||
# Calculate time remaining
|
||||
time_remaining = sla_instance.time_remaining
|
||||
time_remaining_minutes = int(time_remaining.total_seconds() / 60)
|
||||
|
||||
# Create warning message
|
||||
message_content = (
|
||||
f"🚨 **SLA Warning** 🚨\n\n"
|
||||
f"**SLA:** {sla_instance.sla_definition.name}\n"
|
||||
f"**Type:** {sla_instance.sla_definition.get_sla_type_display()}\n"
|
||||
f"**Time Remaining:** {time_remaining_minutes} minutes\n"
|
||||
f"**Threshold:** {threshold_percent}% reached\n\n"
|
||||
f"Please take immediate action to meet the SLA target."
|
||||
)
|
||||
|
||||
# Create system message
|
||||
WarRoomMessage.objects.create(
|
||||
war_room=war_room,
|
||||
content=message_content,
|
||||
message_type='ALERT',
|
||||
sender=None,
|
||||
sender_name='SLA Monitor',
|
||||
external_data={
|
||||
'sla_instance_id': str(sla_instance.id),
|
||||
'notification_type': 'sla_warning',
|
||||
'threshold_percent': threshold_percent
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending SLA warning notification: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_sla_breach_notification(sla_instance: SLAInstance):
|
||||
"""Send SLA breach notification to incident chat room"""
|
||||
try:
|
||||
war_room = WarRoom.objects.filter(incident=sla_instance.incident).first()
|
||||
if not war_room:
|
||||
return False
|
||||
|
||||
# Calculate breach time
|
||||
breach_time = sla_instance.breach_time
|
||||
breach_minutes = int(breach_time.total_seconds() / 60)
|
||||
|
||||
# Create breach message
|
||||
message_content = (
|
||||
f"🚨 **SLA BREACHED** 🚨\n\n"
|
||||
f"**SLA:** {sla_instance.sla_definition.name}\n"
|
||||
f"**Type:** {sla_instance.sla_definition.get_sla_type_display()}\n"
|
||||
f"**Breach Time:** {breach_minutes} minutes ago\n"
|
||||
f"**Target Time:** {sla_instance.target_time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
f"**IMMEDIATE ACTION REQUIRED**\n"
|
||||
f"Escalation procedures have been triggered."
|
||||
)
|
||||
|
||||
# Create system message
|
||||
WarRoomMessage.objects.create(
|
||||
war_room=war_room,
|
||||
content=message_content,
|
||||
message_type='ALERT',
|
||||
sender=None,
|
||||
sender_name='SLA Monitor',
|
||||
external_data={
|
||||
'sla_instance_id': str(sla_instance.id),
|
||||
'notification_type': 'sla_breach',
|
||||
'breach_minutes': breach_minutes
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending SLA breach notification: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_escalation_notification(escalation_instance: EscalationInstance):
|
||||
"""Send escalation notification to incident chat room"""
|
||||
try:
|
||||
war_room = WarRoom.objects.filter(incident=escalation_instance.incident).first()
|
||||
if not war_room:
|
||||
return False
|
||||
|
||||
# Create escalation message
|
||||
message_content = (
|
||||
f"📢 **ESCALATION TRIGGERED** 📢\n\n"
|
||||
f"**Policy:** {escalation_instance.escalation_policy.name}\n"
|
||||
f"**Level:** {escalation_instance.escalation_level}\n"
|
||||
f"**Trigger:** {escalation_instance.escalation_policy.get_trigger_condition_display()}\n"
|
||||
f"**Time:** {escalation_instance.triggered_at.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
f"**Actions Taken:**\n"
|
||||
)
|
||||
|
||||
# Add actions taken
|
||||
for action in escalation_instance.actions_taken:
|
||||
message_content += f"• {action}\n"
|
||||
|
||||
# Create system message
|
||||
WarRoomMessage.objects.create(
|
||||
war_room=war_room,
|
||||
content=message_content,
|
||||
message_type='ALERT',
|
||||
sender=None,
|
||||
sender_name='Escalation System',
|
||||
external_data={
|
||||
'escalation_instance_id': str(escalation_instance.id),
|
||||
'notification_type': 'escalation',
|
||||
'escalation_level': escalation_instance.escalation_level
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending escalation notification: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_oncall_handoff_notification(incident: Incident, old_oncall_user, new_oncall_user):
|
||||
"""Send on-call handoff notification to incident chat room"""
|
||||
try:
|
||||
war_room = WarRoom.objects.filter(incident=incident).first()
|
||||
if not war_room:
|
||||
return False
|
||||
|
||||
# Create handoff message
|
||||
message_content = (
|
||||
f"🔄 **ON-CALL HANDOFF** 🔄\n\n"
|
||||
f"**From:** {old_oncall_user.username if old_oncall_user else 'System'}\n"
|
||||
f"**To:** {new_oncall_user.username}\n"
|
||||
f"**Time:** {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
f"Please review the incident status and continue response activities."
|
||||
)
|
||||
|
||||
# Create system message
|
||||
WarRoomMessage.objects.create(
|
||||
war_room=war_room,
|
||||
content=message_content,
|
||||
message_type='UPDATE',
|
||||
sender=None,
|
||||
sender_name='On-Call System',
|
||||
external_data={
|
||||
'notification_type': 'oncall_handoff',
|
||||
'old_oncall_user_id': str(old_oncall_user.id) if old_oncall_user else None,
|
||||
'new_oncall_user_id': str(new_oncall_user.id)
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending on-call handoff notification: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_sla_met_notification(sla_instance: SLAInstance):
|
||||
"""Send SLA met notification to incident chat room"""
|
||||
try:
|
||||
war_room = WarRoom.objects.filter(incident=sla_instance.incident).first()
|
||||
if not war_room:
|
||||
return False
|
||||
|
||||
# Calculate response time
|
||||
response_time = sla_instance.response_time
|
||||
response_minutes = int(response_time.total_seconds() / 60) if response_time else 0
|
||||
|
||||
# Create success message
|
||||
message_content = (
|
||||
f"✅ **SLA MET** ✅\n\n"
|
||||
f"**SLA:** {sla_instance.sla_definition.name}\n"
|
||||
f"**Type:** {sla_instance.sla_definition.get_sla_type_display()}\n"
|
||||
f"**Response Time:** {response_minutes} minutes\n"
|
||||
f"**Target Time:** {sla_instance.target_time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
f"Great job meeting the SLA target!"
|
||||
)
|
||||
|
||||
# Create system message
|
||||
WarRoomMessage.objects.create(
|
||||
war_room=war_room,
|
||||
content=message_content,
|
||||
message_type='UPDATE',
|
||||
sender=None,
|
||||
sender_name='SLA Monitor',
|
||||
external_data={
|
||||
'sla_instance_id': str(sla_instance.id),
|
||||
'notification_type': 'sla_met',
|
||||
'response_minutes': response_minutes
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending SLA met notification: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_sla_status_for_incident(incident: Incident) -> Dict[str, Any]:
|
||||
"""Get SLA status summary for an incident"""
|
||||
try:
|
||||
sla_instances = SLAInstance.objects.filter(incident=incident)
|
||||
|
||||
status_summary = {
|
||||
'total_slas': sla_instances.count(),
|
||||
'active_slas': sla_instances.filter(status='ACTIVE').count(),
|
||||
'met_slas': sla_instances.filter(status='MET').count(),
|
||||
'breached_slas': sla_instances.filter(status='BREACHED').count(),
|
||||
'sla_details': []
|
||||
}
|
||||
|
||||
for sla in sla_instances:
|
||||
sla_detail = {
|
||||
'id': str(sla.id),
|
||||
'name': sla.sla_definition.name,
|
||||
'type': sla.sla_definition.get_sla_type_display(),
|
||||
'status': sla.status,
|
||||
'target_time': sla.target_time.isoformat(),
|
||||
'time_remaining': int(sla.time_remaining.total_seconds() / 60) if sla.status == 'ACTIVE' else 0,
|
||||
'breach_time': int(sla.breach_time.total_seconds() / 60) if sla.is_breached else 0
|
||||
}
|
||||
status_summary['sla_details'].append(sla_detail)
|
||||
|
||||
return status_summary
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting SLA status: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
@staticmethod
|
||||
def check_and_send_threshold_notifications():
|
||||
"""Check all active SLAs and send threshold notifications"""
|
||||
try:
|
||||
active_slas = SLAInstance.objects.filter(status='ACTIVE')
|
||||
notifications_sent = 0
|
||||
|
||||
for sla in active_slas:
|
||||
# Check if SLA is approaching threshold
|
||||
if sla.sla_definition.escalation_enabled:
|
||||
threshold_percent = sla.sla_definition.escalation_threshold_percent
|
||||
time_elapsed = timezone.now() - sla.started_at
|
||||
total_duration = sla.target_time - sla.started_at
|
||||
elapsed_percent = (time_elapsed.total_seconds() / total_duration.total_seconds()) * 100
|
||||
|
||||
if elapsed_percent >= threshold_percent:
|
||||
# Check if we haven't already sent a warning
|
||||
existing_warning = WarRoomMessage.objects.filter(
|
||||
war_room__incident=sla.incident,
|
||||
message_type='ALERT',
|
||||
external_data__notification_type='sla_warning',
|
||||
external_data__sla_instance_id=str(sla.id)
|
||||
).exists()
|
||||
|
||||
if not existing_warning:
|
||||
if SLANotificationService.send_sla_warning_notification(sla, threshold_percent):
|
||||
notifications_sent += 1
|
||||
|
||||
return notifications_sent
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking SLA thresholds: {e}")
|
||||
return 0
|
||||
261
ETB-API/collaboration_war_rooms/signals.py
Normal file
261
ETB-API/collaboration_war_rooms/signals.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
Signals for Collaboration & War Rooms module
|
||||
Handles automatic war room creation, timeline events, and integration with other modules
|
||||
"""
|
||||
from django.db.models.signals import post_save, pre_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import WarRoom, TimelineEvent, IncidentCommandRole, WarRoomMessage, MessageReaction, ChatCommand
|
||||
from incident_intelligence.models import Incident
|
||||
from automation_orchestration.models import RunbookExecution, AutoRemediationExecution
|
||||
from sla_oncall.models import SLAInstance, EscalationInstance
|
||||
|
||||
|
||||
@receiver(post_save, sender=Incident)
|
||||
def create_war_room_for_incident(sender, instance, created, **kwargs):
|
||||
"""Automatically create a war room when a new incident is created"""
|
||||
if created:
|
||||
# Create war room for the incident
|
||||
war_room = WarRoom.objects.create(
|
||||
name=f"Incident {instance.id} - {instance.title[:50]}",
|
||||
description=f"War room for incident: {instance.title}",
|
||||
incident=instance,
|
||||
created_by=instance.reporter,
|
||||
privacy_level='PRIVATE'
|
||||
)
|
||||
|
||||
# Add incident reporter and assignee to war room
|
||||
if instance.reporter:
|
||||
war_room.add_participant(instance.reporter)
|
||||
if instance.assigned_to:
|
||||
war_room.add_participant(instance.assigned_to)
|
||||
|
||||
# Create timeline event for war room creation
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance,
|
||||
event_type='WAR_ROOM_CREATED',
|
||||
title='War Room Created',
|
||||
description=f'War room "{war_room.name}" was automatically created for this incident',
|
||||
related_war_room=war_room,
|
||||
event_data={'war_room_id': str(war_room.id)}
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Incident)
|
||||
def create_timeline_events_for_incident_changes(sender, instance, created, **kwargs):
|
||||
"""Create timeline events for incident changes"""
|
||||
if not created:
|
||||
# Check for status changes
|
||||
if hasattr(instance, '_old_status') and instance._old_status != instance.status:
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance,
|
||||
event_type='STATUS_CHANGED',
|
||||
title=f'Status Changed to {instance.get_status_display()}',
|
||||
description=f'Incident status changed from {instance._old_status} to {instance.status}',
|
||||
event_data={
|
||||
'old_status': instance._old_status,
|
||||
'new_status': instance.status
|
||||
}
|
||||
)
|
||||
|
||||
# Check for severity changes
|
||||
if hasattr(instance, '_old_severity') and instance._old_severity != instance.severity:
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance,
|
||||
event_type='SEVERITY_CHANGED',
|
||||
title=f'Severity Changed to {instance.get_severity_display()}',
|
||||
description=f'Incident severity changed from {instance._old_severity} to {instance.severity}',
|
||||
event_data={
|
||||
'old_severity': instance._old_severity,
|
||||
'new_severity': instance.severity
|
||||
}
|
||||
)
|
||||
|
||||
# Check for assignment changes
|
||||
if hasattr(instance, '_old_assigned_to') and instance._old_assigned_to != instance.assigned_to:
|
||||
old_user = instance._old_assigned_to.username if instance._old_assigned_to else 'Unassigned'
|
||||
new_user = instance.assigned_to.username if instance.assigned_to else 'Unassigned'
|
||||
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance,
|
||||
event_type='ASSIGNMENT_CHANGED',
|
||||
title=f'Assignment Changed to {new_user}',
|
||||
description=f'Incident assignment changed from {old_user} to {new_user}',
|
||||
event_data={
|
||||
'old_assigned_to': str(instance._old_assigned_to.id) if instance._old_assigned_to else None,
|
||||
'new_assigned_to': str(instance.assigned_to.id) if instance.assigned_to else None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Incident)
|
||||
def store_old_values_for_incident(sender, instance, **kwargs):
|
||||
"""Store old values before saving to detect changes"""
|
||||
if instance.pk:
|
||||
try:
|
||||
old_instance = Incident.objects.get(pk=instance.pk)
|
||||
instance._old_status = old_instance.status
|
||||
instance._old_severity = old_instance.severity
|
||||
instance._old_assigned_to = old_instance.assigned_to
|
||||
except Incident.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(post_save, sender=RunbookExecution)
|
||||
def create_timeline_event_for_runbook_execution(sender, instance, created, **kwargs):
|
||||
"""Create timeline event when runbook is executed"""
|
||||
if created and instance.incident:
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance.incident,
|
||||
event_type='RUNBOOK_EXECUTED',
|
||||
title=f'Runbook Executed: {instance.runbook.name}',
|
||||
description=f'Runbook "{instance.runbook.name}" was executed for this incident',
|
||||
related_runbook_execution=instance,
|
||||
event_data={
|
||||
'runbook_id': str(instance.runbook.id),
|
||||
'runbook_name': instance.runbook.name,
|
||||
'triggered_by': str(instance.triggered_by.id) if instance.triggered_by else None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=AutoRemediationExecution)
|
||||
def create_timeline_event_for_auto_remediation(sender, instance, created, **kwargs):
|
||||
"""Create timeline event when auto-remediation is executed"""
|
||||
if created:
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance.incident,
|
||||
event_type='AUTO_REMEDIATION_ATTEMPTED',
|
||||
title=f'Auto-remediation Attempted: {instance.auto_remediation.name}',
|
||||
description=f'Auto-remediation "{instance.auto_remediation.name}" was attempted for this incident',
|
||||
related_auto_remediation=instance,
|
||||
event_data={
|
||||
'auto_remediation_id': str(instance.auto_remediation.id),
|
||||
'auto_remediation_name': instance.auto_remediation.name,
|
||||
'status': instance.status
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=SLAInstance)
|
||||
def create_timeline_event_for_sla_breach(sender, instance, created, **kwargs):
|
||||
"""Create timeline event when SLA is breached"""
|
||||
if not created and instance.status == 'BREACHED':
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance.incident,
|
||||
event_type='SLA_BREACHED',
|
||||
title=f'SLA Breached: {instance.sla_definition.name}',
|
||||
description=f'SLA "{instance.sla_definition.name}" has been breached',
|
||||
related_sla_instance=instance,
|
||||
event_data={
|
||||
'sla_definition_id': str(instance.sla_definition.id),
|
||||
'sla_definition_name': instance.sla_definition.name,
|
||||
'breach_time': instance.breached_at.isoformat() if instance.breached_at else None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=EscalationInstance)
|
||||
def create_timeline_event_for_escalation(sender, instance, created, **kwargs):
|
||||
"""Create timeline event when escalation is triggered"""
|
||||
if created:
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance.incident,
|
||||
event_type='ESCALATION_TRIGGERED',
|
||||
title=f'Escalation Triggered: {instance.escalation_policy.name}',
|
||||
description=f'Escalation policy "{instance.escalation_policy.name}" was triggered',
|
||||
related_escalation=instance,
|
||||
event_data={
|
||||
'escalation_policy_id': str(instance.escalation_policy.id),
|
||||
'escalation_policy_name': instance.escalation_policy.name,
|
||||
'escalation_level': instance.escalation_level
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=IncidentCommandRole)
|
||||
def create_timeline_event_for_command_role_assignment(sender, instance, created, **kwargs):
|
||||
"""Create timeline event when command role is assigned"""
|
||||
if created and instance.assigned_user:
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance.incident,
|
||||
event_type='COMMAND_ROLE_ASSIGNED',
|
||||
title=f'Command Role Assigned: {instance.get_role_type_display()}',
|
||||
description=f'{instance.get_role_type_display()} role assigned to {instance.assigned_user.username}',
|
||||
related_command_role=instance,
|
||||
event_data={
|
||||
'role_type': instance.role_type,
|
||||
'assigned_user_id': str(instance.assigned_user.id),
|
||||
'assigned_user_name': instance.assigned_user.username
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=WarRoomMessage)
|
||||
def create_timeline_event_for_important_messages(sender, instance, created, **kwargs):
|
||||
"""Create timeline event for important messages (pinned, commands, etc.)"""
|
||||
if created:
|
||||
# Create timeline event for command messages
|
||||
if instance.message_type == 'COMMAND':
|
||||
TimelineEvent.create_user_event(
|
||||
incident=instance.war_room.incident,
|
||||
user=instance.sender,
|
||||
event_type='MANUAL_EVENT',
|
||||
title='Chat Command Executed',
|
||||
description=f'Command "{instance.content[:50]}..." was executed in war room',
|
||||
event_data={
|
||||
'message_id': str(instance.id),
|
||||
'command_type': 'chat_command'
|
||||
}
|
||||
)
|
||||
|
||||
# Create timeline event for system messages
|
||||
elif instance.message_type == 'SYSTEM':
|
||||
TimelineEvent.create_system_event(
|
||||
incident=instance.war_room.incident,
|
||||
event_type='MANUAL_EVENT',
|
||||
title='System Message Posted',
|
||||
description=f'System message posted in war room: {instance.content[:50]}...',
|
||||
event_data={
|
||||
'message_id': str(instance.id),
|
||||
'message_type': 'system'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=MessageReaction)
|
||||
def create_timeline_event_for_reactions(sender, instance, created, **kwargs):
|
||||
"""Create timeline event for reactions on important messages"""
|
||||
if created and instance.message.is_pinned:
|
||||
TimelineEvent.create_user_event(
|
||||
incident=instance.message.war_room.incident,
|
||||
user=instance.user,
|
||||
event_type='MANUAL_EVENT',
|
||||
title='Reaction Added to Pinned Message',
|
||||
description=f'User {instance.user.username} reacted {instance.emoji} to pinned message',
|
||||
event_data={
|
||||
'message_id': str(instance.message.id),
|
||||
'reaction_emoji': instance.emoji,
|
||||
'user_id': str(instance.user.id)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=ChatCommand)
|
||||
def create_timeline_event_for_chat_commands(sender, instance, created, **kwargs):
|
||||
"""Create timeline event when chat commands are executed"""
|
||||
if created:
|
||||
TimelineEvent.create_user_event(
|
||||
incident=instance.message.war_room.incident,
|
||||
user=instance.executed_by,
|
||||
event_type='MANUAL_EVENT',
|
||||
title=f'ChatOps Command Executed: {instance.command_type}',
|
||||
description=f'ChatOps command "{instance.command_text}" was executed',
|
||||
event_data={
|
||||
'command_id': str(instance.id),
|
||||
'command_type': instance.command_type,
|
||||
'command_text': instance.command_text,
|
||||
'execution_status': instance.execution_status
|
||||
}
|
||||
)
|
||||
3
ETB-API/collaboration_war_rooms/tests.py
Normal file
3
ETB-API/collaboration_war_rooms/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
30
ETB-API/collaboration_war_rooms/urls.py
Normal file
30
ETB-API/collaboration_war_rooms/urls.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
URL configuration for Collaboration & War Rooms module
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views.collaboration import (
|
||||
WarRoomViewSet, ConferenceBridgeViewSet, IncidentCommandRoleViewSet,
|
||||
TimelineEventViewSet, WarRoomMessageViewSet, IncidentDecisionViewSet,
|
||||
MessageReactionViewSet, ChatFileViewSet, ChatCommandViewSet, ChatBotViewSet
|
||||
)
|
||||
|
||||
# Create router and register viewsets
|
||||
router = DefaultRouter()
|
||||
router.register(r'war-rooms', WarRoomViewSet, basename='warroom')
|
||||
router.register(r'conference-bridges', ConferenceBridgeViewSet, basename='conferencebridge')
|
||||
router.register(r'command-roles', IncidentCommandRoleViewSet, basename='incidentcommandrole')
|
||||
router.register(r'timeline-events', TimelineEventViewSet, basename='timelineevent')
|
||||
router.register(r'war-room-messages', WarRoomMessageViewSet, basename='warroommessage')
|
||||
router.register(r'incident-decisions', IncidentDecisionViewSet, basename='incidentdecision')
|
||||
router.register(r'message-reactions', MessageReactionViewSet, basename='messagereaction')
|
||||
router.register(r'chat-files', ChatFileViewSet, basename='chatfile')
|
||||
router.register(r'chat-commands', ChatCommandViewSet, basename='chatcommand')
|
||||
router.register(r'chat-bots', ChatBotViewSet, basename='chatbot')
|
||||
|
||||
app_name = 'collaboration_war_rooms'
|
||||
|
||||
urlpatterns = [
|
||||
path('api/', include(router.urls)),
|
||||
]
|
||||
3
ETB-API/collaboration_war_rooms/views.py
Normal file
3
ETB-API/collaboration_war_rooms/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
1
ETB-API/collaboration_war_rooms/views/__init__.py
Normal file
1
ETB-API/collaboration_war_rooms/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Views for Collaboration & War Rooms module
|
||||
Binary file not shown.
Binary file not shown.
646
ETB-API/collaboration_war_rooms/views/collaboration.py
Normal file
646
ETB-API/collaboration_war_rooms/views/collaboration.py
Normal file
@@ -0,0 +1,646 @@
|
||||
"""
|
||||
Views for Collaboration & War Rooms module
|
||||
"""
|
||||
from rest_framework import viewsets, status, permissions
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from ..models import (
|
||||
WarRoom, ConferenceBridge, IncidentCommandRole,
|
||||
TimelineEvent, WarRoomMessage, IncidentDecision,
|
||||
MessageReaction, ChatFile, ChatCommand, ChatBot
|
||||
)
|
||||
from ..serializers.collaboration import (
|
||||
WarRoomSerializer, ConferenceBridgeSerializer, IncidentCommandRoleSerializer,
|
||||
TimelineEventSerializer, WarRoomMessageSerializer, IncidentDecisionSerializer,
|
||||
WarRoomSummarySerializer, TimelineEventSummarySerializer,
|
||||
MessageReactionSerializer, ChatFileSerializer, ChatCommandSerializer, ChatBotSerializer
|
||||
)
|
||||
from incident_intelligence.models import Incident
|
||||
|
||||
|
||||
class WarRoomViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for WarRoom model"""
|
||||
|
||||
queryset = WarRoom.objects.all()
|
||||
serializer_class = WarRoomSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['status', 'privacy_level', 'incident__severity']
|
||||
search_fields = ['name', 'description', 'incident__title']
|
||||
ordering_fields = ['created_at', 'last_activity', 'message_count']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter war rooms based on user access"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# Filter by user access
|
||||
accessible_war_rooms = []
|
||||
for war_room in queryset:
|
||||
if war_room.can_user_access(user):
|
||||
accessible_war_rooms.append(war_room.id)
|
||||
|
||||
return queryset.filter(id__in=accessible_war_rooms)
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return appropriate serializer based on action"""
|
||||
if self.action == 'list':
|
||||
return WarRoomSummarySerializer
|
||||
return WarRoomSerializer
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def add_participant(self, request, pk=None):
|
||||
"""Add a participant to the war room"""
|
||||
war_room = self.get_object()
|
||||
user_id = request.data.get('user_id')
|
||||
|
||||
if not user_id:
|
||||
return Response(
|
||||
{'error': 'user_id is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
if war_room.can_user_access(user):
|
||||
war_room.add_participant(user)
|
||||
return Response({'message': 'Participant added successfully'})
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'User does not have access to this war room'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
except User.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'User not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def remove_participant(self, request, pk=None):
|
||||
"""Remove a participant from the war room"""
|
||||
war_room = self.get_object()
|
||||
user_id = request.data.get('user_id')
|
||||
|
||||
if not user_id:
|
||||
return Response(
|
||||
{'error': 'user_id is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
war_room.remove_participant(user)
|
||||
return Response({'message': 'Participant removed successfully'})
|
||||
except User.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'User not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def messages(self, request, pk=None):
|
||||
"""Get messages for a war room"""
|
||||
war_room = self.get_object()
|
||||
messages = war_room.messages.all().order_by('created_at')
|
||||
|
||||
# Apply pagination
|
||||
page = self.paginate_queryset(messages)
|
||||
if page is not None:
|
||||
serializer = WarRoomMessageSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = WarRoomMessageSerializer(messages, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def pinned_messages(self, request, pk=None):
|
||||
"""Get pinned messages for a war room"""
|
||||
war_room = self.get_object()
|
||||
pinned_messages = war_room.messages.filter(is_pinned=True).order_by('-pinned_at')
|
||||
|
||||
serializer = WarRoomMessageSerializer(pinned_messages, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def create_chat_room(self, request, pk=None):
|
||||
"""Auto-create chat room for incident"""
|
||||
incident = self.get_object()
|
||||
|
||||
# Check if war room already exists for this incident
|
||||
existing_war_room = WarRoom.objects.filter(incident=incident).first()
|
||||
if existing_war_room:
|
||||
return Response(
|
||||
{'message': 'War room already exists for this incident', 'war_room_id': existing_war_room.id},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
# Create new war room
|
||||
war_room = WarRoom.objects.create(
|
||||
name=f"Incident Chat - {incident.title}",
|
||||
description=f"Chat room for incident: {incident.title}",
|
||||
incident=incident,
|
||||
created_by=request.user,
|
||||
privacy_level='PRIVATE'
|
||||
)
|
||||
|
||||
# Add incident reporter and assignee to war room
|
||||
if incident.reporter:
|
||||
war_room.add_participant(incident.reporter)
|
||||
if incident.assigned_to:
|
||||
war_room.add_participant(incident.assigned_to)
|
||||
|
||||
# Create timeline event
|
||||
TimelineEvent.create_system_event(
|
||||
incident=incident,
|
||||
event_type='WAR_ROOM_CREATED',
|
||||
title='War Room Created',
|
||||
description=f'War room "{war_room.name}" was automatically created for incident collaboration',
|
||||
event_data={'war_room_id': str(war_room.id)}
|
||||
)
|
||||
|
||||
serializer = WarRoomSerializer(war_room)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class ConferenceBridgeViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for ConferenceBridge model"""
|
||||
|
||||
queryset = ConferenceBridge.objects.all()
|
||||
serializer_class = ConferenceBridgeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['bridge_type', 'status', 'incident__severity']
|
||||
search_fields = ['name', 'description', 'incident__title']
|
||||
ordering_fields = ['scheduled_start', 'created_at']
|
||||
ordering = ['-scheduled_start']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter conference bridges based on user access"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# Filter by user access
|
||||
accessible_conferences = []
|
||||
for conference in queryset:
|
||||
if conference.can_user_join(user):
|
||||
accessible_conferences.append(conference.id)
|
||||
|
||||
return queryset.filter(id__in=accessible_conferences)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def join_conference(self, request, pk=None):
|
||||
"""Join a conference"""
|
||||
conference = self.get_object()
|
||||
user = request.user
|
||||
|
||||
if conference.can_user_join(user):
|
||||
conference.add_participant(user)
|
||||
return Response({'message': 'Successfully joined conference'})
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'You do not have permission to join this conference'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def start_conference(self, request, pk=None):
|
||||
"""Start a conference"""
|
||||
conference = self.get_object()
|
||||
|
||||
if conference.status == 'SCHEDULED':
|
||||
conference.status = 'ACTIVE'
|
||||
conference.actual_start = timezone.now()
|
||||
conference.save()
|
||||
|
||||
return Response({'message': 'Conference started successfully'})
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Conference cannot be started in current status'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def end_conference(self, request, pk=None):
|
||||
"""End a conference"""
|
||||
conference = self.get_object()
|
||||
|
||||
if conference.status == 'ACTIVE':
|
||||
conference.status = 'ENDED'
|
||||
conference.actual_end = timezone.now()
|
||||
conference.save()
|
||||
|
||||
return Response({'message': 'Conference ended successfully'})
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Conference cannot be ended in current status'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class IncidentCommandRoleViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for IncidentCommandRole model"""
|
||||
|
||||
queryset = IncidentCommandRole.objects.all()
|
||||
serializer_class = IncidentCommandRoleSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['role_type', 'status', 'incident__severity']
|
||||
search_fields = ['incident__title', 'assigned_user__username']
|
||||
ordering_fields = ['assigned_at', 'created_at']
|
||||
ordering = ['-assigned_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter command roles based on user access"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# Filter by incident access
|
||||
accessible_incidents = []
|
||||
for role in queryset:
|
||||
if role.incident.is_accessible_by_user(user):
|
||||
accessible_incidents.append(role.incident.id)
|
||||
|
||||
return queryset.filter(incident_id__in=accessible_incidents)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def reassign_role(self, request, pk=None):
|
||||
"""Reassign a command role to a new user"""
|
||||
command_role = self.get_object()
|
||||
new_user_id = request.data.get('new_user_id')
|
||||
notes = request.data.get('notes', '')
|
||||
|
||||
if not new_user_id:
|
||||
return Response(
|
||||
{'error': 'new_user_id is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
new_user = User.objects.get(id=new_user_id)
|
||||
|
||||
command_role.reassign_role(new_user, request.user, notes)
|
||||
return Response({'message': 'Role reassigned successfully'})
|
||||
except User.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'User not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
class TimelineEventViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for TimelineEvent model (read-only)"""
|
||||
|
||||
queryset = TimelineEvent.objects.all()
|
||||
serializer_class = TimelineEventSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['event_type', 'source_type', 'is_critical_event', 'incident__severity']
|
||||
search_fields = ['title', 'description', 'incident__title']
|
||||
ordering_fields = ['event_time', 'created_at']
|
||||
ordering = ['event_time']
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return appropriate serializer based on action"""
|
||||
if self.action == 'list':
|
||||
return TimelineEventSummarySerializer
|
||||
return TimelineEventSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter timeline events based on user access"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# Filter by incident access
|
||||
accessible_incidents = []
|
||||
for event in queryset:
|
||||
if event.incident.is_accessible_by_user(user):
|
||||
accessible_incidents.append(event.incident.id)
|
||||
|
||||
return queryset.filter(incident_id__in=accessible_incidents)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def critical_events(self, request):
|
||||
"""Get critical events for postmortem analysis"""
|
||||
queryset = self.get_queryset().filter(is_critical_event=True)
|
||||
|
||||
# Apply pagination
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class WarRoomMessageViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for WarRoomMessage model"""
|
||||
|
||||
queryset = WarRoomMessage.objects.all()
|
||||
serializer_class = WarRoomMessageSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['message_type', 'war_room', 'sender', 'is_pinned']
|
||||
search_fields = ['content', 'sender_name']
|
||||
ordering_fields = ['created_at', 'pinned_at']
|
||||
ordering = ['created_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter messages based on war room access"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# Filter by war room access
|
||||
accessible_war_rooms = []
|
||||
for message in queryset:
|
||||
if message.war_room.can_user_access(user):
|
||||
accessible_war_rooms.append(message.war_room.id)
|
||||
|
||||
return queryset.filter(war_room_id__in=accessible_war_rooms)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def pin_message(self, request, pk=None):
|
||||
"""Pin a message"""
|
||||
message = self.get_object()
|
||||
message.pin_message(request.user)
|
||||
|
||||
# Create timeline event
|
||||
TimelineEvent.create_user_event(
|
||||
incident=message.war_room.incident,
|
||||
user=request.user,
|
||||
event_type='MANUAL_EVENT',
|
||||
title='Message Pinned',
|
||||
description=f'Message "{message.content[:50]}..." was pinned by {request.user.username}',
|
||||
event_data={'message_id': str(message.id), 'action': 'pinned'}
|
||||
)
|
||||
|
||||
return Response({'message': 'Message pinned successfully'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def unpin_message(self, request, pk=None):
|
||||
"""Unpin a message"""
|
||||
message = self.get_object()
|
||||
message.unpin_message()
|
||||
|
||||
return Response({'message': 'Message unpinned successfully'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def add_reaction(self, request, pk=None):
|
||||
"""Add a reaction to a message"""
|
||||
message = self.get_object()
|
||||
emoji = request.data.get('emoji')
|
||||
|
||||
if not emoji:
|
||||
return Response(
|
||||
{'error': 'emoji is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
reaction = message.add_reaction(request.user, emoji)
|
||||
serializer = MessageReactionSerializer(reaction)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def remove_reaction(self, request, pk=None):
|
||||
"""Remove a reaction from a message"""
|
||||
message = self.get_object()
|
||||
emoji = request.data.get('emoji')
|
||||
|
||||
if not emoji:
|
||||
return Response(
|
||||
{'error': 'emoji is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
message.remove_reaction(request.user, emoji)
|
||||
return Response({'message': 'Reaction removed successfully'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def execute_command(self, request, pk=None):
|
||||
"""Execute a ChatOps command"""
|
||||
message = self.get_object()
|
||||
command_text = request.data.get('command_text')
|
||||
|
||||
if not command_text:
|
||||
return Response(
|
||||
{'error': 'command_text is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Parse command
|
||||
command_type = self._parse_command_type(command_text)
|
||||
parameters = self._parse_command_parameters(command_text)
|
||||
|
||||
# Create chat command
|
||||
chat_command = ChatCommand.objects.create(
|
||||
message=message,
|
||||
command_type=command_type,
|
||||
command_text=command_text,
|
||||
parameters=parameters
|
||||
)
|
||||
|
||||
# Execute command
|
||||
result = chat_command.execute_command(request.user)
|
||||
|
||||
serializer = ChatCommandSerializer(chat_command)
|
||||
return Response(serializer.data)
|
||||
|
||||
def _parse_command_type(self, command_text):
|
||||
"""Parse command type from command text"""
|
||||
command_text = command_text.lower().strip()
|
||||
|
||||
if command_text.startswith('/status'):
|
||||
return 'STATUS'
|
||||
elif command_text.startswith('/runbook'):
|
||||
return 'RUNBOOK'
|
||||
elif command_text.startswith('/escalate'):
|
||||
return 'ESCALATE'
|
||||
elif command_text.startswith('/assign'):
|
||||
return 'ASSIGN'
|
||||
elif command_text.startswith('/update'):
|
||||
return 'UPDATE'
|
||||
else:
|
||||
return 'CUSTOM'
|
||||
|
||||
def _parse_command_parameters(self, command_text):
|
||||
"""Parse command parameters from command text"""
|
||||
parts = command_text.split()
|
||||
if len(parts) > 1:
|
||||
return {'args': parts[1:]}
|
||||
return {}
|
||||
|
||||
|
||||
class IncidentDecisionViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for IncidentDecision model"""
|
||||
|
||||
queryset = IncidentDecision.objects.all()
|
||||
serializer_class = IncidentDecisionSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['decision_type', 'status', 'incident__severity']
|
||||
search_fields = ['title', 'description', 'incident__title']
|
||||
ordering_fields = ['created_at', 'approved_at', 'implemented_at']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter decisions based on incident access"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# Filter by incident access
|
||||
accessible_incidents = []
|
||||
for decision in queryset:
|
||||
if decision.incident.is_accessible_by_user(user):
|
||||
accessible_incidents.append(decision.incident.id)
|
||||
|
||||
return queryset.filter(incident_id__in=accessible_incidents)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def approve_decision(self, request, pk=None):
|
||||
"""Approve a decision"""
|
||||
decision = self.get_object()
|
||||
|
||||
if decision.status == 'PENDING':
|
||||
decision.approve(request.user)
|
||||
return Response({'message': 'Decision approved successfully'})
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Decision cannot be approved in current status'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def implement_decision(self, request, pk=None):
|
||||
"""Mark a decision as implemented"""
|
||||
decision = self.get_object()
|
||||
notes = request.data.get('notes', '')
|
||||
|
||||
if decision.status == 'APPROVED':
|
||||
decision.implement(request.user, notes)
|
||||
return Response({'message': 'Decision implemented successfully'})
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Decision must be approved before implementation'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class MessageReactionViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for MessageReaction model"""
|
||||
|
||||
queryset = MessageReaction.objects.all()
|
||||
serializer_class = MessageReactionSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['message', 'user', 'emoji']
|
||||
ordering_fields = ['created_at']
|
||||
ordering = ['created_at']
|
||||
|
||||
|
||||
class ChatFileViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for ChatFile model"""
|
||||
|
||||
queryset = ChatFile.objects.all()
|
||||
serializer_class = ChatFileSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['message', 'file_type', 'data_classification']
|
||||
search_fields = ['filename', 'original_filename']
|
||||
ordering_fields = ['uploaded_at', 'file_size']
|
||||
ordering = ['-uploaded_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter files based on message access"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# Filter by message access
|
||||
accessible_messages = []
|
||||
for file_obj in queryset:
|
||||
if file_obj.message.war_room.can_user_access(user):
|
||||
accessible_messages.append(file_obj.message.id)
|
||||
|
||||
return queryset.filter(message_id__in=accessible_messages)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def log_access(self, request, pk=None):
|
||||
"""Log file access for audit trail"""
|
||||
file_obj = self.get_object()
|
||||
file_obj.log_access(request.user)
|
||||
return Response({'message': 'Access logged successfully'})
|
||||
|
||||
|
||||
class ChatCommandViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for ChatCommand model (read-only)"""
|
||||
|
||||
queryset = ChatCommand.objects.all()
|
||||
serializer_class = ChatCommandSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['command_type', 'execution_status', 'executed_by']
|
||||
search_fields = ['command_text']
|
||||
ordering_fields = ['executed_at', 'created_at']
|
||||
ordering = ['-executed_at']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter commands based on message access"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# Filter by message access
|
||||
accessible_messages = []
|
||||
for command in queryset:
|
||||
if command.message.war_room.can_user_access(user):
|
||||
accessible_messages.append(command.message.id)
|
||||
|
||||
return queryset.filter(message_id__in=accessible_messages)
|
||||
|
||||
|
||||
class ChatBotViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for ChatBot model"""
|
||||
|
||||
queryset = ChatBot.objects.all()
|
||||
serializer_class = ChatBotSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['bot_type', 'is_active']
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['name', 'created_at']
|
||||
ordering = ['name']
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def generate_response(self, request, pk=None):
|
||||
"""Generate AI response to a message"""
|
||||
bot = self.get_object()
|
||||
message_id = request.data.get('message_id')
|
||||
|
||||
if not message_id:
|
||||
return Response(
|
||||
{'error': 'message_id is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
message = WarRoomMessage.objects.get(id=message_id)
|
||||
response = bot.generate_response(message, request.data.get('context', {}))
|
||||
return Response(response)
|
||||
except WarRoomMessage.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'Message not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
Reference in New Issue
Block a user