diff --git a/.gitignore b/.gitignore
index b75533c..42d14ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,5 +44,4 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc
/eveai_repo.txt
*repo.txt
/docker/eveai_logs/
-/common/utils/model_utils_orig.py
/integrations/Wordpress/eveai_sync.zip
diff --git a/.repopackignore_eveai_api b/.repopackignore_eveai_api
index d26376e..351eca4 100644
--- a/.repopackignore_eveai_api
+++ b/.repopackignore_eveai_api
@@ -5,6 +5,6 @@ eveai_chat/
eveai_chat_workers/
eveai_entitlements/
instance/
-integrations/Wordpress/eveai-chat-widget
+integrations/Wordpress/eveai-chat
nginx/
scripts/
\ No newline at end of file
diff --git a/.repopackignore_eveai_chat b/.repopackignore_eveai_chat
index ed3aac1..3acf128 100644
--- a/.repopackignore_eveai_chat
+++ b/.repopackignore_eveai_chat
@@ -1,5 +1,4 @@
docker/
-eveai_api/
eveai_app/
eveai_beat/
eveai_chat_workers/
diff --git a/common/utils/cache/eveai_cache_manager.py b/common/utils/cache/eveai_cache_manager.py
index fe68367..b1b7617 100644
--- a/common/utils/cache/eveai_cache_manager.py
+++ b/common/utils/cache/eveai_cache_manager.py
@@ -9,21 +9,28 @@ class EveAICacheManager:
"""Cache manager with registration capabilities"""
def __init__(self):
- self.model_region = None
- self.eveai_chat_workers_region = None
- self.eveai_workers_region = None
+ self._regions = {}
self._handlers = {}
def init_app(self, app: Flask):
"""Initialize cache regions"""
from common.utils.cache.regions import create_cache_regions
- self.model_region, self.eveai_chat_workers_region, self.eveai_workers_region = create_cache_regions(app)
+ self._regions = create_cache_regions(app)
+
+ # Store regions in instance
+ for region_name, region in self._regions.items():
+ setattr(self, f"{region_name}_region", region)
# Initialize all registered handlers with their regions
for handler_class, region_name in self._handlers.items():
- region = getattr(self, f"{region_name}_region")
+ region = self._regions[region_name]
handler_instance = handler_class(region)
- setattr(self, handler_class.handler_name, handler_instance)
+ handler_name = getattr(handler_class, 'handler_name', None)
+ if handler_name:
+ app.logger.debug(f"{handler_name} is registered")
+ setattr(self, handler_name, handler_instance)
+
+ app.logger.info('Cache regions initialized: ' + ', '.join(self._regions.keys()))
def register_handler(self, handler_class: Type[CacheHandler], region: str):
"""Register a cache handler class with its region"""
diff --git a/common/utils/cache/regions.py b/common/utils/cache/regions.py
index d4c20f9..1e1e43c 100644
--- a/common/utils/cache/regions.py
+++ b/common/utils/cache/regions.py
@@ -1,7 +1,6 @@
# common/utils/cache/regions.py
from dogpile.cache import make_region
-from flask import current_app
from urllib.parse import urlparse
import os
@@ -36,27 +35,31 @@ def get_redis_config(app):
def create_cache_regions(app):
"""Initialize all cache regions with app config"""
redis_config = get_redis_config(app)
+ regions = {}
# Region for model-related caching (ModelVariables etc)
- model_region = make_region(name='model').configure(
+ model_region = make_region(name='eveai_model').configure(
'dogpile.cache.redis',
arguments=redis_config,
replace_existing_backend=True
)
+ regions['eveai_model'] = model_region
# Region for eveai_chat_workers components (Specialists, Retrievers, ...)
- eveai_chat_workers_region = make_region(name='chat_workers').configure(
+ eveai_chat_workers_region = make_region(name='eveai_chat_workers').configure(
'dogpile.cache.redis',
arguments=redis_config, # arguments={**redis_config, 'db': 4}, # Different DB
replace_existing_backend=True
)
+ regions['eveai_chat_workers'] = eveai_chat_workers_region
# Region for eveai_workers components (Processors, ...)
- eveai_workers_region = make_region(name='workers').configure(
+ eveai_workers_region = make_region(name='eveai_workers').configure(
'dogpile.cache.redis',
arguments=redis_config, # Same config for now
replace_existing_backend=True
)
+ regions['eveai_workers'] = eveai_workers_region
- return model_region, eveai_chat_workers_region, eveai_workers_region
+ return regions
diff --git a/common/utils/cors_utils.py b/common/utils/cors_utils.py
index fc88785..0961c64 100644
--- a/common/utils/cors_utils.py
+++ b/common/utils/cors_utils.py
@@ -1,4 +1,6 @@
from flask import request, current_app, session
+from flask_jwt_extended import decode_token, verify_jwt_in_request, get_jwt_identity
+
from common.models.user import Tenant, TenantDomain
@@ -23,31 +25,45 @@ def cors_after_request(response, prefix):
response.headers.add('Access-Control-Allow-Methods', '*')
return response
+ # Handle OPTIONS preflight requests
+ if request.method == 'OPTIONS':
+ response.headers.add('Access-Control-Allow-Origin', '*')
+ response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Tenant-ID')
+ response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
+ response.headers.add('Access-Control-Allow-Credentials', 'true')
+ return response
+
tenant_id = None
allowed_origins = []
- # Try to get tenant_id from JSON payload
- json_data = request.get_json(silent=True)
-
- if json_data and 'tenant_id' in json_data:
- tenant_id = json_data['tenant_id']
+ # Check Socket.IO connection
+ if 'socket.io' in request.path:
+ token = request.args.get('token')
+ if token:
+ try:
+ decoded = decode_token(token)
+ tenant_id = decoded['sub']
+ except Exception as e:
+ current_app.logger.error(f'Error decoding token: {e}')
+ return response
else:
- # Fallback to get tenant_id from query parameters or headers if JSON is not available
- tenant_id = request.args.get('tenant_id') or request.args.get('tenantId') or request.headers.get('X-Tenant-ID')
+ # Regular API requests
+ try:
+ if verify_jwt_in_request(optional=True):
+ tenant_id = get_jwt_identity()
+ except Exception as e:
+ current_app.logger.error(f'Error verifying JWT: {e}')
+ return response
if tenant_id:
+ origin = request.headers.get('Origin')
allowed_origins = get_allowed_origins(tenant_id)
- else:
- current_app.logger.warning('tenant_id not found in request')
- origin = request.headers.get('Origin')
- if origin in allowed_origins:
- response.headers.add('Access-Control-Allow-Origin', origin)
- response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
- response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
- response.headers.add('Access-Control-Allow-Credentials', 'true')
- else:
- current_app.logger.warning(f'Origin {origin} not allowed')
+ if origin in allowed_origins:
+ response.headers.add('Access-Control-Allow-Origin', origin)
+ response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
+ response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
+ response.headers.add('Access-Control-Allow-Credentials', 'true')
return response
diff --git a/common/utils/eveai_exceptions.py b/common/utils/eveai_exceptions.py
index 286ed6d..782d565 100644
--- a/common/utils/eveai_exceptions.py
+++ b/common/utils/eveai_exceptions.py
@@ -118,3 +118,10 @@ class EveAIInvalidDocumentVersion(EveAIException):
# Construct the message dynamically
message = f"Tenant with ID '{tenant_id}' has no document version with ID {document_version_id}."
super().__init__(message, status_code, payload)
+
+
+class EveAISocketInputException(EveAIException):
+ """Raised when a socket call receives an invalid payload"""
+
+ def __init__(self, message, status_code=400, payload=None):
+ super.__init__(message, status_code, payload)
\ No newline at end of file
diff --git a/common/utils/model_utils.py b/common/utils/model_utils.py
index 0c8f5b9..8b93d72 100644
--- a/common/utils/model_utils.py
+++ b/common/utils/model_utils.py
@@ -252,7 +252,7 @@ class ModelVariablesCacheHandler(CacheHandler[ModelVariables]):
# Register the handler with the cache manager
-cache_manager.register_handler(ModelVariablesCacheHandler, 'model')
+cache_manager.register_handler(ModelVariablesCacheHandler, 'eveai_model')
# Helper function to get cached model variables
diff --git a/common/utils/token_validation.py b/common/utils/token_validation.py
new file mode 100644
index 0000000..98e1a61
--- /dev/null
+++ b/common/utils/token_validation.py
@@ -0,0 +1,60 @@
+from dataclasses import dataclass
+from typing import Optional
+from datetime import datetime
+from flask_jwt_extended import decode_token, verify_jwt_in_request
+from flask import current_app
+
+
+@dataclass
+class TokenValidationResult:
+ """Clean, simple validation result"""
+ is_valid: bool
+ tenant_id: Optional[int] = None
+ error_message: Optional[str] = None
+
+
+class TokenValidator:
+ """Simplified token validator focused on JWT validation"""
+
+ def validate_token(self, token: str) -> TokenValidationResult:
+ """
+ Validate JWT token
+
+ Args:
+ token: The JWT token to validate
+
+ Returns:
+ TokenValidationResult with validation status and tenant_id if valid
+ """
+ try:
+ # Decode and validate token
+ decoded_token = decode_token(token)
+
+ # Extract tenant_id from token subject
+ tenant_id = decoded_token.get('sub')
+ if not tenant_id:
+ return TokenValidationResult(
+ is_valid=False,
+ error_message="Missing tenant ID in token"
+ )
+
+ # Verify token timestamps
+ now = datetime.utcnow().timestamp()
+ if not (decoded_token.get('exp', 0) > now >= decoded_token.get('nbf', 0)):
+ return TokenValidationResult(
+ is_valid=False,
+ error_message="Token expired or not yet valid"
+ )
+
+ # Token is valid
+ return TokenValidationResult(
+ is_valid=True,
+ tenant_id=tenant_id
+ )
+
+ except Exception as e:
+ current_app.logger.error(f"Token validation error: {str(e)}")
+ return TokenValidationResult(
+ is_valid=False,
+ error_message=str(e)
+ )
diff --git a/docker/compose_dev.yaml b/docker/compose_dev.yaml
index d82d89b..aa43d9f 100644
--- a/docker/compose_dev.yaml
+++ b/docker/compose_dev.yaml
@@ -54,9 +54,10 @@ services:
- ../nginx/sites-enabled:/etc/nginx/sites-enabled
- ../nginx/static:/etc/nginx/static
- ../nginx/public:/etc/nginx/public
- - ../integrations/Wordpress/eveai-chat-widget/public/css/eveai-chat-style.css:/etc/nginx/static/css/eveai-chat-style.css
- - ../integrations/Wordpress/eveai-chat-widget/public/js/eveai-chat-widget.js:/etc/nginx/static/js/eveai-chat-widget.js
- - ../integrations/Wordpress/eveai-chat-widget/public/js/eveai-sdk.js:/etc/nginx/static/js/eveai-sdk.js
+ - ../integrations/Wordpress/eveai-chat/assets/css/eveai-chat-style.css:/etc/nginx/static/css/eveai-chat-style.css
+ - ../integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js:/etc/nginx/static/js/eveai-chat-widget.js
+ - ../integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js:/etc/nginx/static/js/eveai-token-manager.js
+ - ../integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js:/etc/nginx/static/js/eveai-sdk.js
- ./logs/nginx:/var/log/nginx
depends_on:
- eveai_app
diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile
index 77801f5..4b84791 100644
--- a/docker/nginx/Dockerfile
+++ b/docker/nginx/Dockerfile
@@ -10,9 +10,10 @@ COPY ../../nginx/mime.types /etc/nginx/mime.types
# Copy static & public files
RUN mkdir -p /etc/nginx/static /etc/nginx/public
COPY ../../nginx/static /etc/nginx/static
-COPY ../../integrations/Wordpress/eveai-chat-widget/public/css/eveai-chat-style.css /etc/nginx/static/css/
-COPY ../../integrations/Wordpress/eveai-chat-widget/public/js/eveai-chat-widget.js /etc/nginx/static/js/
-COPY ../../integrations/Wordpress/eveai-chat-widget/public/js/eveai-sdk.js /etc/nginx/static/js
+COPY ../../integrations/Wordpress/eveai-chat/assets/css/eveai-chat-style.css /etc/nginx/static/css/
+COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js /etc/nginx/static/js/
+COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-token-manager.js /etc/nginx/static/js/
+COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js /etc/nginx/static/js
COPY ../../nginx/public /etc/nginx/public
# Copy site-specific configurations
diff --git a/eveai_api/__init__.py b/eveai_api/__init__.py
index b968a87..7480b1c 100644
--- a/eveai_api/__init__.py
+++ b/eveai_api/__init__.py
@@ -5,10 +5,12 @@ from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request
from sqlalchemy.exc import SQLAlchemyError
from werkzeug.exceptions import HTTPException
-from common.extensions import db, api_rest, jwt, minio_client, simple_encryption
+from common.extensions import db, api_rest, jwt, minio_client, simple_encryption, cors
import os
import logging.config
+from common.models.user import TenantDomain
+from common.utils.cors_utils import get_allowed_origins
from common.utils.database import Database
from config.logging_config import LOGGING
from .api.document_api import document_ns
@@ -54,7 +56,32 @@ def create_app(config_file=None):
register_error_handlers(app)
@app.before_request
- def before_request():
+ def check_cors():
+ if request.method == 'OPTIONS':
+ app.logger.debug("Handling OPTIONS request")
+ return '', 200 # Allow OPTIONS to pass through
+
+ origin = request.headers.get('Origin')
+ if not origin:
+ return # Not a CORS request
+
+ # Get tenant ID from request
+ if verify_jwt_in_request():
+ tenant_id = get_jwt_identity()
+ if not tenant_id:
+ return
+ else:
+ return
+
+ # Check if origin is allowed for this tenant
+ allowed_origins = get_allowed_origins(tenant_id)
+
+ if origin not in allowed_origins:
+ app.logger.warning(f'Origin {origin} not allowed for tenant {tenant_id}')
+ return {'error': 'Origin not allowed'}, 403
+
+ @app.before_request
+ def set_tenant_schema():
# Check if this a health check request
if request.path.startswith('/_healthz') or request.path.startswith('/healthz'):
pass
@@ -83,6 +110,17 @@ def register_extensions(app):
jwt.init_app(app)
minio_client.init_app(app)
simple_encryption.init_app(app)
+ cors.init_app(app, resources={
+ r"/api/v1/*": {
+ "origins": "*",
+ "methods": ["GET", "POST", "PUT", "OPTIONS"],
+ "allow_headers": ["Content-Type", "Authorization", "X-Requested-With"],
+ "expose_headers": ["Content-Length", "Content-Range"],
+ "supports_credentials": True,
+ "max_age": 1728000, # 20 days
+ "allow_credentials": True
+ }
+ })
def register_namespaces(app):
diff --git a/eveai_api/api/auth.py b/eveai_api/api/auth.py
index 84ca56c..fe087c0 100644
--- a/eveai_api/api/auth.py
+++ b/eveai_api/api/auth.py
@@ -1,7 +1,7 @@
-from datetime import timedelta
+from datetime import timedelta, datetime as dt, timezone as tz
from flask_restx import Namespace, Resource, fields
-from flask_jwt_extended import create_access_token
+from flask_jwt_extended import create_access_token, verify_jwt_in_request, get_jwt
from common.models.user import Tenant, TenantProject
from common.extensions import simple_encryption
from flask import current_app, request
@@ -18,6 +18,12 @@ token_response = auth_ns.model('TokenResponse', {
'expires_in': fields.Integer(description='Token expiration time in seconds')
})
+token_verification = auth_ns.model('TokenVerification', {
+ 'is_valid': fields.Boolean(description='Token validity status'),
+ 'expires_in': fields.Integer(description='Seconds until token expiration'),
+ 'tenant_id': fields.Integer(description='Tenant ID from token')
+})
+
@auth_ns.route('/token')
class Token(Resource):
@@ -82,3 +88,61 @@ class Token(Resource):
except Exception as e:
current_app.logger.error(f"Error creating access token: {e}")
return {'message': "Internal server error"}, 500
+
+
+@auth_ns.route('/verify')
+class TokenVerification(Resource):
+ @auth_ns.doc('verify_token')
+ @auth_ns.response(200, 'Token verification result', token_verification)
+ @auth_ns.response(401, 'Invalid token')
+ def get(self):
+ """Verify a token's validity and get expiration information"""
+ try:
+ verify_jwt_in_request()
+ jwt_data = get_jwt()
+
+ # Get expiration timestamp from token
+ exp_timestamp = jwt_data['exp']
+ current_timestamp = dt.now().timestamp()
+
+ return {
+ 'is_valid': True,
+ 'expires_in': int(exp_timestamp - current_timestamp),
+ 'tenant_id': jwt_data['sub'] # tenant_id is stored in 'sub' claim
+ }, 200
+ except Exception as e:
+ current_app.logger.error(f"Token verification failed: {str(e)}")
+ return {
+ 'is_valid': False,
+ 'message': 'Invalid token'
+ }, 401
+
+
+@auth_ns.route('/refresh')
+class TokenRefresh(Resource):
+ @auth_ns.doc('refresh_token')
+ @auth_ns.response(200, 'New token', token_response)
+ @auth_ns.response(401, 'Invalid token')
+ def post(self):
+ """Get a new token before the current one expires"""
+ try:
+ verify_jwt_in_request()
+ jwt_data = get_jwt()
+ tenant_id = jwt_data['sub']
+
+ # Optional: Add additional verification here if needed
+
+ # Create new token
+ expires_delta = current_app.config.get('JWT_ACCESS_TOKEN_EXPIRES', timedelta(minutes=15))
+ new_token = create_access_token(
+ identity=tenant_id,
+ expires_delta=expires_delta
+ )
+
+ return {
+ 'access_token': new_token,
+ 'expires_in': int(expires_delta.total_seconds())
+ }, 200
+ except Exception as e:
+ current_app.logger.error(f"Token refresh failed: {str(e)}")
+ return {'message': 'Token refresh failed'}, 401
\ No newline at end of file
diff --git a/eveai_chat/__init__.py b/eveai_chat/__init__.py
index 0ce50e5..21f24ed 100644
--- a/eveai_chat/__init__.py
+++ b/eveai_chat/__init__.py
@@ -45,7 +45,7 @@ def register_extensions(app):
async_mode=app.config.get('SOCKETIO_ASYNC_MODE'),
logger=app.config.get('SOCKETIO_LOGGER'),
engineio_logger=app.config.get('SOCKETIO_ENGINEIO_LOGGER'),
- path='/socket.io',
+ path='/socket.io/',
ping_timeout=app.config.get('SOCKETIO_PING_TIMEOUT'),
ping_interval=app.config.get('SOCKETIO_PING_INTERVAL'),
)
diff --git a/eveai_chat/socket_handlers/chat_handler.py b/eveai_chat/socket_handlers/chat_handler.py
index 6a7ace3..025c072 100644
--- a/eveai_chat/socket_handlers/chat_handler.py
+++ b/eveai_chat/socket_handlers/chat_handler.py
@@ -8,18 +8,77 @@ from sqlalchemy.exc import SQLAlchemyError
from datetime import datetime, timedelta
from prometheus_client import Counter, Histogram
from time import time
+import re
from common.extensions import socketio, db, simple_encryption
from common.models.user import Tenant
from common.models.interaction import Interaction
from common.utils.celery_utils import current_celery
from common.utils.database import Database
+from common.utils.token_validation import TokenValidator
+from common.utils.eveai_exceptions import EveAISocketInputException
# Define custom metrics
socketio_message_counter = Counter('socketio_message_count', 'Count of SocketIO messages', ['event_type'])
socketio_message_latency = Histogram('socketio_message_latency_seconds', 'Latency of SocketIO message processing', ['event_type'])
+class RoomManager:
+ def __init__(self):
+ self.active_rooms = {} # Store active room metadata
+
+ def validate_room_format(self, room_id: str) -> bool:
+ """Validate room ID format: tenant_id_sessionid_timestamp"""
+ pattern = r'^\d+_[a-zA-Z0-9]+_\d+$'
+ return bool(re.match(pattern, room_id))
+
+ def is_room_active(self, room_id: str) -> bool:
+ return room_id in self.active_rooms
+
+ def validate_room_ownership(self, room_id: str, tenant_id: int, token: str) -> bool:
+ if not self.is_room_active(room_id):
+ return False
+
+ room_data = self.active_rooms[room_id]
+ return (room_data['tenant_id'] == tenant_id and
+ room_data['token'] == token)
+
+ def create_room(self, tenant_id: int, token: str) -> str:
+ """Create new room with metadata"""
+ timestamp = int(datetime.now().timestamp())
+ room_id = f"{tenant_id}_{request.sid}_{timestamp}"
+
+ self.active_rooms[room_id] = {
+ 'tenant_id': tenant_id,
+ 'token': token,
+ 'created_at': datetime.now(),
+ 'last_activity': datetime.now()
+ }
+
+ return room_id
+
+ def update_room_activity(self, room_id: str):
+ """Update room's last activity timestamp"""
+ if room_id in self.active_rooms:
+ self.active_rooms[room_id]['last_activity'] = datetime.now()
+
+ def cleanup_inactive_rooms(self, max_age_hours: int = 1):
+ """Remove inactive rooms"""
+ now = datetime.now()
+ cutoff = now - timedelta(hours=max_age_hours)
+
+ inactive_rooms = [
+ room_id for room_id, data in self.active_rooms.items()
+ if data['last_activity'] < cutoff
+ ]
+
+ for room_id in inactive_rooms:
+ del self.active_rooms[room_id]
+
+
+room_manager = RoomManager()
+
+
# Decorator to measure SocketIO events
def track_socketio_event(func):
@wraps(func)
@@ -37,42 +96,89 @@ def track_socketio_event(func):
@socketio.on('connect')
@track_socketio_event
def handle_connect():
+ """Handle incoming socket connections with enhanced security"""
try:
- tenant_id = request.args.get('tenantId')
- if not tenant_id:
- raise Exception("Missing Tenant ID")
- api_key = request.args.get('apiKey')
- if not api_key:
- raise Exception("Missing API Key")
- current_app.logger.info(f'SocketIO: Connection handling found Tenant {tenant_id} with API Key {api_key}')
+ current_app.logger.debug('Handle Connection')
+ token = request.args.get('token')
+ if not token:
+ raise ValueError("Missing token")
- if not validate_api_key(tenant_id, api_key):
- raise Exception("Invalid tenant_id - api_key combination")
+ current_app.logger.debug(f"Token received: {token}")
- # Create JWT token
- token = create_access_token(identity={"tenant_id": tenant_id, "api_key": api_key})
+ if not token:
+ raise ValueError("Missing token")
- # Create a unique room for this client
- room = f"{tenant_id}_{request.sid}"
+ current_app.logger.info(f"Trying to connect with: {token}")
+
+ validator = TokenValidator()
+ validation_result = validator.validate_token(token)
+
+ if not validation_result.is_valid:
+ current_app.logger.error(f"Socket connection failed: {validation_result.error_message}")
+ emit('connect_error', {'error': validation_result.error_message})
+ disconnect()
+ return
+
+ # Create room and setup session
+ room = room_manager.create_room(validation_result.tenant_id, token)
join_room(room)
- # Create a unique session ID
- if 'session_id' not in session:
- session['session_id'] = str(uuid.uuid4())
-
+ session['session_id'] = str(uuid.uuid4())
session['last_activity'] = datetime.now()
session['room'] = room
- # Communicate connection to client
- emit('connect', {'status': 'Connected', 'tenant_id': tenant_id, 'room': room})
- emit('authenticated', {'token': token, 'room': room}) # Emit custom event with the token
+ # Emit success events
+ emit('connect', {
+ 'status': 'Connected',
+ 'tenant_id': validation_result.tenant_id,
+ 'room': room
+ })
+ emit('authenticated', {'token': token, 'room': room})
+ current_app.logger.info(f"Socket connection succeeded: {token} / {room}")
+
except Exception as e:
- current_app.logger.error(f'SocketIO: Connection failed: {e}')
- # communicate connection problem to client
+ current_app.logger.error(f"Socket connection failed: {str(e)}")
emit('connect_error', {'status': 'Connection Failed'})
disconnect()
+@socketio.on('rejoin_room')
+def handle_room_rejoin(data):
+ try:
+ token = data.get('token')
+ tenant_id = data.get('tenant_id')
+ previous_room = data.get('previousRoom')
+
+ validator = TokenValidator()
+ validation_result = validator.validate_token(token, require_session=True)
+ if not validation_result.is_valid:
+ emit('room_rejoin_result', {'success': False, 'error': validation_result.error_message})
+ return
+
+ if not all([token, tenant_id, previous_room]):
+ raise ValueError("Missing required rejoin data")
+
+ # Validate room ownership
+ if not room_manager.validate_room_ownership(previous_room, tenant_id, token):
+ raise ValueError("Invalid room ownership")
+
+ # Rejoin room
+ join_room(previous_room)
+ session['room'] = previous_room
+ room_manager.update_room_activity(previous_room)
+
+ emit('room_rejoin_result', {
+ 'success': True,
+ 'room': previous_room
+ })
+ except Exception as e:
+ current_app.logger.error(f'Room rejoin failed: {e}')
+ emit('room_rejoin_result', {
+ 'success': False,
+ 'error': str(e)
+ })
+
+
@socketio.on('disconnect')
@track_socketio_event
def handle_disconnect():
@@ -90,37 +196,71 @@ def handle_heartbeat():
@socketio.on('user_message')
def handle_message(data):
+ current_app.logger.debug(f"SocketIO: Received message: {data}")
try:
+ validator = TokenValidator()
+ validation_result = validator.validate_token(data.get('token'))
+
+ if not validation_result.is_valid:
+ emit('error', {'message': validation_result.error_message})
+ return
+
+ current_app.logger.debug(f"SocketIO: token validated: {validation_result}")
+
+ room = session.get('room')
+ current_app.logger.debug(f"SocketIO: Room in session: {room}, Room in arguments: {data.get('room')}")
+
+ current_app.logger.debug(f"SocketIO: Room: {room}")
+ if not room or not room_manager.is_room_active(room):
+ raise Exception("Invalid or inactive room")
+ current_app.logger.debug(f"SocketIO: Room active: {room}")
+
+ if not room_manager.validate_room_ownership(room, data['tenant_id'], data['token']):
+ raise Exception("Room ownership validation failed")
+ current_app.logger.debug(f"SocketIO: Room ownership validated: {room}")
+
+ room_manager.update_room_activity(room)
+ current_app.logger.debug(f"SocketIO: Room activity updated: {room}")
session['last_activity'] = datetime.now()
current_tenant_id = validate_incoming_data(data)
- room = session.get('room')
+
+ current_app.logger.debug(f"SocketIO: Incoming data validated: {current_tenant_id}")
# Offload actual processing of question
task = current_celery.send_task('execute_specialist',
queue='llm_interactions',
args=[
current_tenant_id,
- data['specialistId'],
+ data['specialist_id'],
data['arguments'],
session['session_id'],
data['timezone'],
room
])
response = {
- 'tenantId': data['tenantId'],
+ 'tenantId': current_tenant_id,
'message': f'Processing question ... Session ID = {session["session_id"]}',
'taskId': task.id,
+ 'room': room,
}
- current_app.logger.debug(f"Sent message with {data}, response {response}")
+ current_app.logger.debug(f"SocketIO: Sent response {response}")
emit('bot_response', response, room=room)
except Exception as e:
current_app.logger.error(f'SocketIO: Message handling failed: {str(e)}')
- disconnect()
+ emit('error', {'message': 'Failed to process message'}, room=room)
@socketio.on('check_task_status')
def check_task_status(data):
current_app.logger.debug(f'SocketIO: Checking Task Status ... {data}')
+
+ validator = TokenValidator()
+ validation_result = validator.validate_token(data.get('token'))
+
+ if not validation_result.is_valid:
+ emit('feedback_received', {'status': 'error', 'error': validation_result.error_message})
+ return
+
task_id = data.get('task_id')
room = session.get('room')
if not task_id:
@@ -145,6 +285,7 @@ def check_task_status(data):
'insufficient_info': specialist_result.get('insufficient_info', False)
},
'interaction_id': result['interaction_id'],
+ 'room': room
}
emit('task_status', response, room=room)
else:
@@ -153,7 +294,15 @@ def check_task_status(data):
@socketio.on('feedback')
def handle_feedback(data):
+ current_app.logger.debug(f'SocketIO: Received feedback: {data}')
try:
+ validator = TokenValidator()
+ validation_result = validator.validate_token(data.get('token'))
+
+ if not validation_result.is_valid:
+ emit('feedback_received', {'status': 'error', 'error': validation_result.error_message})
+ return
+
current_tenant_id = validate_incoming_data(data)
interaction_id = data.get('interactionId')
@@ -163,9 +312,15 @@ def handle_feedback(data):
interaction = Interaction.query.get_or_404(interaction_id)
interaction.appreciation = 0 if feedback == 'down' else 100
+
+ room = session.get('room')
+ if not room:
+ emit('feedback_received', {'status': 'error', 'message': 'No active room'})
+ return
+
try:
db.session.commit()
- emit('feedback_received', {'status': 'success', 'interaction_id': interaction_id})
+ emit('feedback_received', {'status': 'success', 'interaction_id': interaction_id, 'room': room}, room=room)
except SQLAlchemyError as e:
current_app.logger.error(f'SocketIO: Feedback handling failed: {e}')
db.session.rollback()
@@ -184,25 +339,20 @@ def validate_api_key(tenant_id, api_key):
def validate_incoming_data(data):
+ current_app.logger.debug(f'SocketIO: Validating incoming data: {data}')
token = data.get('token')
if not token:
- raise Exception("Missing token")
+ raise EveAISocketInputException("SocketIO: Missing token in input")
decoded_token = decode_token(token)
if not decoded_token:
- raise Exception("Invalid token")
+ raise EveAISocketInputException("SocketIO: Invalid token in input")
- token_sub = decoded_token.get('sub')
+ current_app.logger.debug(f'SocketIO: Decoded token: {decoded_token}')
- if not token_sub:
- raise Exception("Missing token subject")
+ current_tenant_id = decoded_token.get('sub')
- current_tenant_id = token_sub.get('tenant_id')
if not current_tenant_id:
- raise Exception("Missing tenant_id")
-
- current_api_key = token_sub.get('api_key')
- if not current_api_key:
- raise Exception("Missing api_key")
+ raise EveAISocketInputException("SocketIO: Missing tenant_id (sub) in input")
return current_tenant_id
diff --git a/integrations/Wordpress/eveai-chat-widget/admin/class-eveai-admin.php b/integrations/Wordpress/eveai-chat-widget/admin/class-eveai-admin.php
deleted file mode 100644
index dd376f9..0000000
--- a/integrations/Wordpress/eveai-chat-widget/admin/class-eveai-admin.php
+++ /dev/null
@@ -1,74 +0,0 @@
-version = $version;
- }
-
- public function add_plugin_admin_menu() {
- add_options_page(
- 'EveAI Chat Settings', // Page title
- 'EveAI Chat', // Menu title
- 'manage_options', // Capability required
- 'eveai-chat-settings', // Menu slug
- array($this, 'display_plugin_settings_page') // Callback function
- );
- }
-
- public function register_settings() {
- register_setting(
- 'eveai_chat_settings', // Option group
- 'eveai_chat_settings', // Option name
- array($this, 'validate_settings') // Sanitization callback
- );
-
- add_settings_section(
- 'eveai_chat_general', // ID
- 'General Settings', // Title
- array($this, 'section_info'), // Callback
- 'eveai-chat-settings' // Page
- );
-
- add_settings_field(
- 'api_key', // ID
- 'API Key', // Title
- array($this, 'api_key_callback'), // Callback
- 'eveai-chat-settings', // Page
- 'eveai_chat_general' // Section
- );
-
- // Add more settings fields as needed
- }
-
- public function section_info() {
- echo 'Enter your EveAI Chat configuration settings below:';
- }
-
- public function api_key_callback() {
- $options = get_option('eveai_chat_settings');
- $api_key = isset($options['api_key']) ? $options['api_key'] : '';
- ?>
-
-
Enter your EveAI API key. You can find this in your EveAI dashboard.
-
-
-
-
-
-
-
How to Use EveAI Chat
-
To add the chat widget to your pages or posts, use the following shortcode:
-
[eveai_chat tenant_id="YOUR_TENANT_ID" language="en" supported_languages="en,fr,de,es"]
-
-
Available Shortcode Parameters:
-
- - tenant_id (required): Your EveAI tenant ID
- - language (optional): Default language for the chat widget (default: en)
- - supported_languages (optional): Comma-separated list of supported languages (default: en,fr,de,es)
- - server_url (optional): EveAI server URL (default: https://evie.askeveai.com)
- - specialist_id (optional): ID of the specialist to use (default: 1)
-
-
-
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat-widget/eveai-chat-widget.php b/integrations/Wordpress/eveai-chat-widget/eveai-chat-widget.php
deleted file mode 100644
index d251b0e..0000000
--- a/integrations/Wordpress/eveai-chat-widget/eveai-chat-widget.php
+++ /dev/null
@@ -1,26 +0,0 @@
-run();
-}
-
-run_eveai_chat();
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php b/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php
deleted file mode 100644
index a1e5e11..0000000
--- a/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php
+++ /dev/null
@@ -1,70 +0,0 @@
- '',
- 'api_key' => '',
- 'domain' => '',
- 'language' => 'en',
- 'supported_languages' => 'en,fr,de,es',
- 'server_url' => 'https://evie.askeveai.com',
- 'specialist_id' => '1' // Added specialist_id parameter
- );
-
- // Merge provided attributes with defaults
- $atts = shortcode_atts($defaults, $atts, 'eveai_chat');
-
- // Sanitize inputs
- $tenant_id = sanitize_text_field($atts['tenant_id']);
- $api_key = sanitize_text_field($atts['api_key']);
- $domain = esc_url_raw($atts['domain']);
- $language = sanitize_text_field($atts['language']);
- $supported_languages = sanitize_text_field($atts['supported_languages']);
- $server_url = esc_url_raw($atts['server_url']);
- $specialist_id = sanitize_text_field($atts['specialist_id']); // Sanitize specialist_id
-
- // Generate a unique ID for this instance of the chat widget
- $chat_id = 'chat-container-' . uniqid();
-
- $output = "";
- $output .= "";
-
- return $output;
-}
-add_shortcode('eveai_chat', 'eveai_chat_shortcode');
-
diff --git a/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-api.php b/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-api.php
deleted file mode 100644
index 8ed4215..0000000
--- a/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-api.php
+++ /dev/null
@@ -1,164 +0,0 @@
-security = new EveAI_Chat_Security();
- $this->eveai_api_url = 'https://api.eveai.com'; // Should come from settings
- }
-
- public function register_routes() {
- register_rest_route('eveai/v1', '/session-token', array(
- 'methods' => 'POST',
- 'callback' => array($this, 'get_session_token'),
- 'permission_callback' => array($this, 'verify_request'),
- 'args' => array(
- 'tenant_id' => array(
- 'required' => true,
- 'validate_callback' => function($param) {
- return is_numeric($param);
- }
- ),
- 'domain' => array(
- 'required' => true,
- 'validate_callback' => function($param) {
- return is_string($param) && !empty($param);
- }
- )
- )
- ));
- }
-
- public function verify_request($request) {
- // Origin verification
- $origin = $request->get_header('origin');
- if (!$this->security->verify_origin($origin)) {
- return new WP_Error(
- 'invalid_origin',
- 'Invalid request origin',
- array('status' => 403)
- );
- }
-
- // Nonce verification
- $nonce = $request->get_header('X-WP-Nonce');
- if (!wp_verify_nonce($nonce, 'wp_rest')) {
- return new WP_Error(
- 'invalid_nonce',
- 'Invalid nonce',
- array('status' => 403)
- );
- }
-
- return true;
- }
-
- public function get_session_token($request) {
- try {
- // Get the API key from WordPress options and decrypt it
- $settings = get_option('eveai_chat_settings');
- $encrypted_api_key = $settings['api_key'] ?? '';
-
- if (empty($encrypted_api_key)) {
- return new WP_Error(
- 'no_api_key',
- 'API key not configured',
- array('status' => 500)
- );
- }
-
- $api_key = $this->security->decrypt_sensitive_data($encrypted_api_key);
-
- // Get parameters from request
- $tenant_id = $request->get_param('tenant_id');
- $domain = $request->get_param('domain');
-
- // Request a session token from EveAI server
- $response = wp_remote_post(
- $this->eveai_api_url . '/session',
- array(
- 'headers' => array(
- 'Authorization' => 'Bearer ' . $api_key,
- 'Content-Type' => 'application/json'
- ),
- 'body' => json_encode(array(
- 'tenant_id' => $tenant_id,
- 'domain' => $domain,
- 'origin' => get_site_url()
- )),
- 'timeout' => 15,
- 'data_format' => 'body'
- )
- );
-
- if (is_wp_error($response)) {
- throw new Exception($response->get_error_message());
- }
-
- $response_code = wp_remote_retrieve_response_code($response);
- if ($response_code !== 200) {
- throw new Exception('Invalid response from EveAI server: ' . $response_code);
- }
-
- $body = json_decode(wp_remote_retrieve_body($response), true);
-
- if (empty($body['token'])) {
- throw new Exception('No token received from EveAI server');
- }
-
- // Log the token generation (optional, for debugging)
- error_log(sprintf(
- 'Generated session token for tenant %d from domain %s',
- $tenant_id,
- $domain
- ));
-
- return array(
- 'success' => true,
- 'session_token' => $body['token']
- );
-
- } catch (Exception $e) {
- error_log('EveAI session token generation failed: ' . $e->getMessage());
-
- return new WP_Error(
- 'token_generation_failed',
- 'Failed to generate session token: ' . $e->getMessage(),
- array('status' => 500)
- );
- }
- }
-
- /**
- * Validates the session token with EveAI server
- * Can be used for additional security checks
- */
- public function validate_session_token($token) {
- try {
- $response = wp_remote_post(
- $this->eveai_api_url . '/validate-token',
- array(
- 'headers' => array(
- 'Content-Type' => 'application/json'
- ),
- 'body' => json_encode(array(
- 'token' => $token
- )),
- 'timeout' => 15
- )
- );
-
- if (is_wp_error($response)) {
- return false;
- }
-
- $body = json_decode(wp_remote_retrieve_body($response), true);
- return isset($body['valid']) && $body['valid'] === true;
-
- } catch (Exception $e) {
- error_log('Token validation failed: ' . $e->getMessage());
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-loader.php b/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-loader.php
deleted file mode 100644
index d06dc63..0000000
--- a/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-loader.php
+++ /dev/null
@@ -1,129 +0,0 @@
-version = EVEAI_CHAT_VERSION;
- $this->load_dependencies();
- }
-
- private function load_dependencies() {
- // Load required files
- require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-eveai-api.php';
- require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-eveai-security.php';
-
- // Load admin if in admin area
- if (is_admin()) {
- require_once EVEAI_CHAT_PLUGIN_DIR . 'admin/class-eveai-admin.php';
- }
- }
-
- public function run() {
- // Initialize components
- $this->define_admin_hooks();
- $this->define_public_hooks();
- $this->define_shortcodes();
- }
-
- private function define_admin_hooks() {
- if (is_admin()) {
- $admin = new EveAI_Chat_Admin($this->version);
- add_action('admin_menu', array($admin, 'add_plugin_admin_menu'));
- add_action('admin_init', array($admin, 'register_settings'));
- }
- }
-
- private function define_public_hooks() {
- // Enqueue scripts and styles
- add_action('wp_enqueue_scripts', array($this, 'enqueue_assets'));
-
- // Register REST API endpoints
- add_action('rest_api_init', array($this, 'register_rest_routes'));
- }
-
- private function define_shortcodes() {
- add_shortcode('eveai_chat', array($this, 'render_chat_widget'));
- }
-
- public function enqueue_assets() {
- // Enqueue required scripts
- wp_enqueue_script('socket-io', 'https://cdn.socket.io/4.0.1/socket.io.min.js', array(), '4.0.1', true);
- wp_enqueue_script('marked', 'https://cdn.jsdelivr.net/npm/marked/marked.min.js', array(), '1.0.0', true);
-
- // Enqueue our scripts
- wp_enqueue_script(
- 'eveai-sdk',
- EVEAI_CHAT_PLUGIN_URL . 'public/js/eveai-sdk.js',
- array('socket-io', 'marked'),
- $this->version,
- true
- );
-
- wp_enqueue_script(
- 'eveai-chat-widget',
- EVEAI_CHAT_PLUGIN_URL . 'public/js/eveai-chat-widget.js',
- array('eveai-sdk'),
- $this->version,
- true
- );
-
- // Enqueue styles
- wp_enqueue_style('material-icons', 'https://fonts.googleapis.com/icon?family=Material+Icons');
- wp_enqueue_style(
- 'eveai-chat-style',
- EVEAI_CHAT_PLUGIN_URL . 'public/css/eveai-chat-style.css',
- array(),
- $this->version
- );
-
- // Add WordPress-specific configuration
- wp_localize_script('eveai-sdk', 'eveaiWP', array(
- 'nonce' => wp_create_nonce('wp_rest'),
- 'ajaxUrl' => admin_url('admin-ajax.php'),
- 'restUrl' => rest_url('eveai/v1/')
- ));
- }
-
- public function register_rest_routes() {
- $api = new EveAI_Chat_API();
- $api->register_routes();
- }
-
- public function render_chat_widget($atts) {
- $defaults = array(
- 'tenant_id' => '',
- 'language' => 'en',
- 'supported_languages' => 'en,fr,de,es',
- 'server_url' => 'https://evie.askeveai.com',
- 'specialist_id' => '1'
- );
-
- $atts = shortcode_atts($defaults, $atts, 'eveai_chat');
- $chat_id = 'chat-container-' . uniqid();
-
- return sprintf(
- '
- ',
- $chat_id,
- esc_js($atts['tenant_id']),
- esc_js($atts['language']),
- esc_js($atts['supported_languages']),
- esc_js($atts['server_url']),
- esc_js($atts['specialist_id']),
- esc_js(rest_url('eveai/v1/session-token')),
- esc_js($chat_id)
- );
- }
-}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-security.php b/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-security.php
deleted file mode 100644
index 427dfb2..0000000
--- a/integrations/Wordpress/eveai-chat-widget/includes/class-eveai-security.php
+++ /dev/null
@@ -1,133 +0,0 @@
-get_header('X-WP-Nonce');
- if (!wp_verify_nonce($nonce, 'wp_rest')) {
- return false;
- }
-
- // Verify origin
- $origin = $request->get_header('origin');
- if (!$this->verify_origin($origin)) {
- return false;
- }
-
- return true;
- }
-
- private function verify_origin($origin) {
- // Get the site URL
- $site_url = parse_url(get_site_url(), PHP_URL_HOST);
- $origin_host = parse_url($origin, PHP_URL_HOST);
-
- // Check if origin matches site URL or is a subdomain
- return $origin_host === $site_url ||
- strpos($origin_host, '.' . $site_url) !== false;
- }
-
- public function encrypt_sensitive_data($data) {
- if (empty($data)) {
- return '';
- }
-
- $encryption_key = $this->get_encryption_key();
- $iv = openssl_random_pseudo_bytes(16);
- $encrypted = openssl_encrypt(
- $data,
- 'AES-256-CBC',
- $encryption_key,
- 0,
- $iv
- );
-
- return base64_encode($iv . $encrypted);
- }
-
- public function decrypt_sensitive_data($encrypted_data) {
- if (empty($encrypted_data)) {
- return '';
- }
-
- $encryption_key = $this->get_encryption_key();
- $data = base64_decode($encrypted_data);
- $iv = substr($data, 0, 16);
- $encrypted = substr($data, 16);
-
- return openssl_decrypt(
- $encrypted,
- 'AES-256-CBC',
- $encryption_key,
- 0,
- $iv
- );
- }
-
- private function get_encryption_key() {
- $key = get_option('eveai_chat_encryption_key');
- if (!$key) {
- $key = bin2hex(random_bytes(32));
- update_option('eveai_chat_encryption_key', $key);
- }
- return $key;
- }
-
- /**
- * Generates a local temporary token for additional security
- */
- public function generate_local_token($tenant_id, $domain) {
- $data = array(
- 'tenant_id' => $tenant_id,
- 'domain' => $domain,
- 'timestamp' => time(),
- 'site_url' => get_site_url()
- );
-
- return $this->encrypt_sensitive_data(json_encode($data));
- }
-
- /**
- * Verifies if the domain is allowed for the given tenant
- */
- public function verify_tenant_domain($tenant_id, $domain) {
- // This could be enhanced with a database check of allowed domains per tenant
- $allowed_domains = array(
- parse_url(get_site_url(), PHP_URL_HOST),
- 'localhost',
- // Add other allowed domains as needed
- );
-
- $domain_host = parse_url($domain, PHP_URL_HOST);
- return in_array($domain_host, $allowed_domains);
- }
-
- /**
- * Enhanced origin verification
- */
- public function verify_origin($origin) {
- if (empty($origin)) {
- return false;
- }
-
- // Get the allowed origins
- $site_url = parse_url(get_site_url(), PHP_URL_HOST);
- $allowed_origins = array(
- $site_url,
- 'www.' . $site_url,
- 'localhost',
- // Add any additional allowed origins
- );
-
- $origin_host = parse_url($origin, PHP_URL_HOST);
-
- // Check if origin matches allowed origins or is a subdomain
- foreach ($allowed_origins as $allowed_origin) {
- if ($origin_host === $allowed_origin ||
- strpos($origin_host, '.' . $allowed_origin) !== false) {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/integrations/Wordpress/eveai-chat-widget/index.php b/integrations/Wordpress/eveai-chat-widget/index.php
deleted file mode 100644
index 7e91415..0000000
--- a/integrations/Wordpress/eveai-chat-widget/index.php
+++ /dev/null
@@ -1,2 +0,0 @@
- {
- chatWidget.setAttribute(attr, value);
- });
-
- container.appendChild(chatWidget);
- this.initialized = true;
-
- return chatWidget;
- } catch (error) {
- console.error('Failed to initialize chat:', error);
- // Re-throw to allow custom error handling
- throw error;
- }
- }
-}
-
-// Make available globally
-window.EveAI = EveAI;
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat-widget/readme.txt b/integrations/Wordpress/eveai-chat-widget/readme.txt
deleted file mode 100644
index 884ec2b..0000000
--- a/integrations/Wordpress/eveai-chat-widget/readme.txt
+++ /dev/null
@@ -1,79 +0,0 @@
-=== EveAI Chat Widget ===
-Contributors: Josako
-Tags: chat, ai
-Requires at least: 5.0
-Tested up to: 5.9
-Stable tag: 1.5.0
-License: GPLv2 or later
-License URI: http://www.gnu.org/licenses/gpl-2.0.html
-
-Integrates the EveAI chat interface into your WordPress site.
-
-== Description ==
-
-This plugin allows you to easily add the EveAI chat widget to your WordPress site. It provides a configurable interface to set up your EveAI chat parameters.
-
-== Installation ==
-
-1. Upload the `eveai-chat-widget` folder to the `/wp-content/plugins/` directory
-2. Activate the plugin through the 'Plugins' menu in WordPress
-3. Add EveAI Chat Widget to your page or post using the instructions below.
-
-== Usage ==
-
-To add an EveAI Chat Widget to your page or post, use the following shortcode:
-
-[eveai_chat tenant_id="YOUR_TENANT_ID" api_key="YOUR_API_KEY" domain="YOUR_DOMAIN" language="LANGUAGE_CODE" supported_languages="COMMA_SEPARATED_LANGUAGE_CODES" server_url="Server URL for Evie"]
-
-Example:
-[eveai_chat tenant_id="123456" api_key="your_api_key_here" domain="https://your-domain.com" language="en" supported_languages="en,fr,de,es" server_url="https://evie.askeveai.com"]
-
-You can add multiple chat widgets with different configurations by using the shortcode multiple times with different parameters.
-
-== Frequently Asked Questions ==
-
-= Where do I get my EveAI credentials? =
-
-Contact your EveAI service provider to obtain your Tenant ID, API Key, and Domain.
-
-== Changelog ==
-
-= 1.5.0 =
-* Allow for multiple servers to serve Evie
-
-= 1.4.1 - 1.4...=
-* Bug fixes
-
-= 1.4.0 =
-* Allow for multiple instances of Evie on the same website
-* Parametrization of the shortcode
-
-= 1.3.3 - =
-* ensure all attributes (also height and supportedLanguages) are set before initializing the socket
-* Bugfixing
-
-= 1.3.2 =
-* Correct supportedLanguages to be an Array
-
-= 1.3.1 =
-* Correct evie domain
-
-= 1.3.0 =
-* Enable user to select language
-* Make Question area multi-line
-* Enable height to be set in shortcode
-
-= 1.2.0 =
-* Create shortcodes
-
-= 1.1.0 =
-* Added configurable settings
-* Improved security with server-side API key handling
-
-= 1.0.0 =
-* Initial release
-
-== Upgrade Notice ==
-
-= 1.1.0 =
-This version adds configurable settings and improves security. Please update your EveAI credentials after upgrading.
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat/admin/class-admin.php b/integrations/Wordpress/eveai-chat/admin/class-admin.php
new file mode 100644
index 0000000..7ea0011
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/admin/class-admin.php
@@ -0,0 +1,130 @@
+add_settings_fields();
+ }
+
+ private function add_settings_fields() {
+ $fields = [
+ 'tenant_id' => [
+ 'label' => __('Tenant ID', 'eveai-chat'),
+ 'type' => 'number'
+ ],
+ 'api_key' => [
+ 'label' => __('API Key', 'eveai-chat'),
+ 'type' => 'password'
+ ],
+ 'socket_url' => [
+ 'label' => __('Socket URL', 'eveai-chat'),
+ 'type' => 'url'
+ ],
+ 'auth_url' => [
+ 'label' => __('Auth URL', 'eveai-chat'),
+ 'type' => 'url'
+ ]
+ ];
+
+ foreach ($fields as $key => $field) {
+ add_settings_field(
+ "eveai_chat_{$key}",
+ $field['label'],
+ [$this, 'render_field'],
+ 'eveai-chat-settings',
+ 'eveai_chat_general',
+ [
+ 'key' => $key,
+ 'type' => $field['type'],
+ 'label_for' => "eveai_chat_{$key}"
+ ]
+ );
+ }
+ }
+
+ public function render_section_info() {
+ echo '' . esc_html__('Configure your EveAI Chat settings below.', 'eveai-chat') . '
';
+ }
+
+ public function render_field($args) {
+ $options = get_option('eveai_chat_settings');
+ $key = $args['key'];
+ $type = $args['type'];
+ $value = isset($options[$key]) ? $options[$key] : '';
+
+ // If it's an API key and not empty, show placeholder
+ if ($key === 'api_key' && !empty($value)) {
+ $value = str_repeat('•', 20);
+ }
+
+ printf(
+ '',
+ esc_attr($type),
+ esc_attr($key),
+ esc_attr($key),
+ esc_attr($value)
+ );
+ }
+
+ public function sanitize_settings($input) {
+ $sanitized = [];
+
+ // Sanitize tenant_id
+ $sanitized['tenant_id'] = isset($input['tenant_id']) ?
+ absint($input['tenant_id']) : '';
+
+ // Handle API key (only update if changed)
+ $old_settings = get_option('eveai_chat_settings');
+ if (isset($input['api_key']) && !empty($input['api_key']) &&
+ $input['api_key'] !== str_repeat('•', 20)) {
+ $sanitized['api_key'] = Security::encrypt_api_key($input['api_key']);
+ } else {
+ $sanitized['api_key'] = $old_settings['api_key'] ?? '';
+ }
+
+ // Sanitize URLs
+ $sanitized['socket_url'] = isset($input['socket_url']) ?
+ esc_url_raw($input['socket_url']) : 'https://chat.askeveai.com';
+ $sanitized['auth_url'] = isset($input['auth_url']) ?
+ esc_url_raw($input['auth_url']) : 'https://api.askeveai.com';
+
+ return $sanitized;
+ }
+
+ public function render_settings_page() {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+
+ require_once EVEAI_CHAT_PLUGIN_DIR . 'admin/views/settings-page.php';
+ }
+}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat/admin/views/settings-page.php b/integrations/Wordpress/eveai-chat/admin/views/settings-page.php
new file mode 100644
index 0000000..f31d791
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/admin/views/settings-page.php
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
[eveai_chat language="en" languages="en,fr,de" specialist_id="1"]
+
+
+
+ - language:
+ - languages:
+ - specialist_id:
+
+
+
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat-widget/public/css/eveai-chat-style.css b/integrations/Wordpress/eveai-chat/assets/css/eveai-chat-style.css
similarity index 100%
rename from integrations/Wordpress/eveai-chat-widget/public/css/eveai-chat-style.css
rename to integrations/Wordpress/eveai-chat/assets/css/eveai-chat-style.css
diff --git a/integrations/Wordpress/eveai-chat-widget/public/js/eveai-chat-widget.js b/integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js
similarity index 60%
rename from integrations/Wordpress/eveai-chat-widget/public/js/eveai-chat-widget.js
rename to integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js
index c46d1fd..5f9a6de 100644
--- a/integrations/Wordpress/eveai-chat-widget/public/js/eveai-chat-widget.js
+++ b/integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js
@@ -1,34 +1,53 @@
class EveAIChatWidget extends HTMLElement {
static get observedAttributes() {
- return ['tenant-id', 'session-token', 'domain', 'language', 'languages', 'server-url', 'specialist-id'];
+ return [
+ 'tenant-id',
+ 'session-token',
+ 'language',
+ 'languages',
+ 'specialist-id',
+ 'server-url'
+ ];
}
constructor() {
super();
+
+ // Networking attributes
this.socket = null; // Initialize socket to null
- this.attributesSet = false; // Flag to check if all attributes are set
this.room = null;
+ this.lastRoom = null; // Store last known room
this.userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Detect user's timezone
this.heartbeatInterval = null;
this.idleTime = 0; // in milliseconds
this.maxConnectionIdleTime = 1 * 60 * 60 * 1000; // 1 hour in milliseconds
+ this.reconnectAttempts = 0;
+ this.maxReconnectAttempts = 5;
+
+ // EveAI specific attributes
this.languages = []
+ this.currentLanguage = null;
this.specialistId = null;
console.log('EveAIChatWidget constructor called');
+
// Bind methods to ensure correct 'this' context
this.handleSendMessage = this.handleSendMessage.bind(this);
+ this.handleTokenUpdate = this.handleTokenUpdate.bind(this);
this.updateAttributes = this.updateAttributes.bind(this);
}
connectedCallback() {
- console.log('connectedCallback called');
+ console.log('Chat Widget Connected');
this.innerHTML = this.getTemplate();
this.setupElements()
+ this.populateLanguageDropdown()
this.addEventListeners()
- if (this.areAllAttributesSet() && !this.socket) {
- console.log('Attributes already set in connectedCallback, initializing socket');
- this.initializeSocket();
+ if (this.areAllAttributesSet()) {
+ console.log('All attributes are set, initializing socket');
+ this.initializeSocket();
+ } else {
+ console.warn('Not all required attributes are set yet');
}
}
@@ -79,20 +98,39 @@ class EveAIChatWidget extends HTMLElement {
}
attributeChangedCallback(name, oldValue, newValue) {
- console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
- this.updateAttributes();
+ console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
- if (this.areAllAttributesSet() && !this.socket) {
- this.attributesSet = true;
- this.populateLanguageDropdown();
- this.initializeSocket();
- }
+ // Handle token updates specially
+ if (name === 'session-token' && oldValue !== newValue) {
+ this.updateAttributes();
+ if (newValue) {
+ console.log('Received new session token');
+ this.sessionToken = newValue;
+
+ // If socket exists, reconnect with new token
+ if (this.socket) {
+ this.socket.disconnect();
+ this.initializeSocket();
+ } else if (this.areAllAttributesSet()) {
+ // Initialize socket if all other attributes are ready
+ this.initializeSocket();
+ }
+ }
+ return;
+ }
+
+ if (name === 'languages' || name === 'language') {
+ this.updateAttributes();
+ this.populateLanguageDropdown();
+ return;
+ }
+
+ this.updateAttributes();
}
updateAttributes() {
this.tenantId = parseInt(this.getAttribute('tenant-id'));
- this.sessionToken = this.getAttribute('session_token');
- this.domain = this.getAttribute('domain');
+ this.sessionToken = this.getAttribute('session-token');
this.language = this.getAttribute('language');
const languageAttr = this.getAttribute('languages');
this.languages = languageAttr ? languageAttr.split(',') : [];
@@ -102,7 +140,6 @@ class EveAIChatWidget extends HTMLElement {
console.log('Updated attributes:', {
tenantId: this.tenantId,
sessionToken: this.sessionToken,
- domain: this.domain,
language: this.language,
currentLanguage: this.currentLanguage,
languages: this.languages,
@@ -112,69 +149,67 @@ class EveAIChatWidget extends HTMLElement {
}
areAllAttributesSet() {
- const tenantId = this.getAttribute('tenant-id');
- const sessionToken = this.getAttribute('session-token');
- const domain = this.getAttribute('domain');
- const language = this.getAttribute('language');
- const languages = this.getAttribute('languages');
- const serverUrl = this.getAttribute('server-url');
- const specialistId = this.getAttribute('specialist-id')
console.log('Checking if all attributes are set:', {
- tenantId,
- sessionToken,
- domain,
- language,
- languages,
- serverUrl,
- specialistId
+ tenantId: this.tenantId,
+ sessionToken: this.sessionToken,
+ language: this.language,
+ languages: this.languages,
+ serverUrl: this.serverUrl,
+ specialistId: this.specialistId
});
- return tenantId && sessionToken && domain && language && languages && serverUrl && specialistId;
- }
- createLanguageDropdown() {
- const select = document.createElement('select');
- select.id = 'languageSelect';
- this.languages.forEach(lang => {
- const option = document.createElement('option');
- option.value = lang;
- option.textContent = lang.toUpperCase();
- if (lang === this.currentLanguage) {
- option.selected = true;
- }
- select.appendChild(option);
- });
- select.addEventListener('change', (e) => {
- this.currentLanguage = e.target.value;
- // You might want to emit an event or update the backend about the language change
- });
- return select;
+ const requiredAttributes = [
+ 'tenant-id',
+ 'session-token',
+ 'language',
+ 'languages',
+ 'specialist-id',
+ 'server-url'
+ ];
+
+ return requiredAttributes.every(attr => this.getAttribute(attr));
+}
+
+ handleTokenUpdate(newToken) {
+ if (this.socket && this.socket.connected) {
+ console.log('Updating socket connection with new token');
+ // Emit token update event to server
+ this.socket.emit('update_token', { token: newToken });
+ } else if (newToken && !this.socket) {
+ // If we have a new token but no socket, try to initialize
+ this.initializeSocket();
+ }
}
initializeSocket() {
+ console.log(`Initializing socket connection to Evie at ${this.serverUrl}`);
if (this.socket) {
console.log('Socket already initialized');
return;
}
- console.log(`Initializing socket connection to Evie`);
+ if (!this.sessionToken) {
+ console.error('Cannot initialize socket without session token');
+ return;
+ }
- // Ensure apiKey is passed in the query parameters
this.socket = io(this.serverUrl, {
- path: '/chat/socket.io/',
- transports: ['websocket', 'polling'],
- query: {
- tenantId: this.tenantId,
- sessionToken: this.sessionToken
+ path: '/socket.io/',
+ transports: ['websocket'],
+ query: { // Change from auth to query
+ token: this.sessionToken
},
- auth: {
- token: this.sessionToken // Ensure token is included here
- // token: 'Bearer ' + this.sessionToken // Old setup - remove if everything works fine without Bearer
- },
- reconnectionAttempts: Infinity, // Infinite reconnection attempts
+ reconnectionAttempts: 5, // Infinite reconnection attempts
reconnectionDelay: 5000, // Delay between reconnections
timeout: 20000, // Connection timeout
});
+ if (!this.socket) {
+ console.error('Error initializing socket')
+ } else {
+ console.log('Socket initialized')
+ }
+
this.setupSocketEventHandlers();
}
@@ -185,8 +220,10 @@ class EveAIChatWidget extends HTMLElement {
this.setStatusMessage('Connected to EveAI.');
this.updateConnectionStatus(true);
this.startHeartbeat();
+
if (data?.room) {
this.room = data.room;
+ this.lastRoom = this.room;
console.log(`Joined room: ${this.room}`);
} else {
console.log('Room information not received on connect');
@@ -199,12 +236,23 @@ class EveAIChatWidget extends HTMLElement {
this.setStatusMessage('Authenticated.');
if (data?.room) {
this.room = data.room;
+ this.lastRoom = this.room;
console.log(`Confirmed room: ${this.room}`);
} else {
console.log('Room information not received on authentication');
}
});
+ // Room join handler ------------------------------------------------------
+ this.socket.on('room_join', (data) => {
+ console.log('Room join event received:', data);
+ if (data?.room) {
+ this.room = data.room;
+ this.lastRoom = this.room;
+ console.log(`Joined room: ${this.room}`);
+ }
+ });
+
// connect-error handler --------------------------------------------------
this.socket.on('connect_error', (err) => {
console.error('Socket connection error:', err);
@@ -229,12 +277,21 @@ class EveAIChatWidget extends HTMLElement {
this.setStatusMessage('Disconnected from EveAI. Please refresh the page for further interaction.');
this.updateConnectionStatus(false);
this.stopHeartbeat();
+ this.room = null;
+ });
+
+ // Token related handlers -------------------------------------------------
+ this.socket.on('token_expired', () => {
+ console.log('Token expired');
+ this.setStatusMessage('Session expired. Please refresh the page.');
+ this.updateConnectionStatus(false);
});
// reconnect_attempt handler ----------------------------------------------
- this.socket.on('reconnect_attempt', () => {
- console.log('Attempting to reconnect to the server...');
- this.setStatusMessage('Attempting to reconnect...');
+ this.socket.on('reconnect_attempt', (attemptNumber) => {
+ console.log(`Reconnection attempt ${attemptNumber}`);
+ this.setStatusMessage(`Reconnecting... (Attempt ${attemptNumber})`);
+ this.reconnectAttempts = attemptNumber;
});
// reconnect handler ------------------------------------------------------
@@ -245,20 +302,103 @@ class EveAIChatWidget extends HTMLElement {
this.startHeartbeat();
});
+ // reconnect failed -------------------------------------------------------
+ this.socket.on('reconnect_failed', () => {
+ console.log('Reconnection failed');
+ this.setStatusMessage('Unable to reconnect. Please refresh the page.');
+ this.handleReconnectFailure();
+ });
+
+ // room rejoin result -----------------------------------------------------
+ this.socket.on('room_rejoin_result', (response) => {
+ if (response.success) {
+ console.log('Successfully rejoined room');
+ this.room = response.room;
+ this.setStatusMessage('Reconnected successfully.');
+ } else {
+ console.error('Failed to rejoin room');
+ this.handleRoomRejoinFailure();
+ }
+ });
+
// bot_response handler ---------------------------------------------------
this.socket.on('bot_response', (data) => {
console.log('Bot response received: ', data);
- if (data.tenantId === this.tenantId) {
+ if (data.tenantId === this.tenantId && data?.room === this.room) {
setTimeout(() => this.startTaskCheck(data.taskId), 1000);
this.setStatusMessage('Processing...');
+ } else {
+ console.log('Received message for different room or tenant, ignoring');
}
});
// task_status handler ----------------------------------------------------
this.socket.on('task_status', (data) => {
console.log('Task status received:', data);
+ if (!this.room) {
+ console.log('No room assigned, cannot process task status');
+ return;
+ }
this.handleTaskStatus(data);
});
+
+ // Feedback handler -------------------------------------------------------
+ this.socket.on('feedback_received', (data) => {
+ if (data?.room === this.room) {
+ this.setStatusMessage(data.status === 'success' ? 'Feedback recorded.' : 'Failed to record feedback.');
+ }
+ });
+ }
+
+ attemptRoomRejoin() {
+ console.log(`Attempting to rejoin room: ${this.lastRoom}`);
+ this.socket.emit('rejoin_room', {
+ token: this.sessionToken,
+ tenantId: this.tenantId,
+ previousRoom: this.lastRoom,
+ timestamp: Date.now()
+ });
+ }
+
+ handleReconnectFailure() {
+ this.room = null;
+ this.lastRoom = null;
+ this.reconnectAttempts = 0;
+ this.updateConnectionStatus(false);
+
+ // Optionally reload the widget
+ if (confirm('Connection lost. Would you like to refresh the chat?')) {
+ window.location.reload();
+ }
+ }
+
+ handleRoomRejoinFailure() {
+ // Clear room state
+ this.room = null;
+ this.lastRoom = null;
+
+ // Request new room
+ this.socket.emit('request_new_room', {
+ token: this.sessionToken,
+ tenantId: this.tenantId
+ });
+ }
+
+ clearRoomState() {
+ // Use when intentionally leaving/clearing a room
+ this.room = null;
+ this.lastRoom = null;
+ this.reconnectAttempts = 0;
+ }
+
+ handleAuthError(error) {
+ console.error('Authentication error:', error);
+ this.setStatusMessage('Authentication failed. Please refresh the page.');
+ this.updateConnectionStatus(false);
+
+ if (this.socket) {
+ this.socket.disconnect();
+ }
}
setStatusMessage(message) {
@@ -353,13 +493,14 @@ class EveAIChatWidget extends HTMLElement {
console.error('Socket is not initialized');
return;
}
- if (!this.jwtToken) {
- console.error('JWT token is not available');
+
+ if (!this.validateRoom()) {
+ console.log("No valid room to handle feedback")
return;
}
- console.log('Sending message to backend');
- console.log(`Feedback for ${interactionId}: ${feedback}`);
- this.socket.emit('feedback', { tenantId: this.tenantId, token: this.jwtToken, feedback, interactionId });
+
+ console.log(`Sending feedback for ${interactionId}: ${feedback}`);
+ this.socket.emit('feedback', { tenant_id: this.tenantId, token: this.sessionToken, feedback, interactionId, room: this.room });
this.setStatusMessage('Feedback sent.');
}
@@ -408,24 +549,24 @@ class EveAIChatWidget extends HTMLElement {
this.messagesArea.scrollTop = this.messagesArea.scrollHeight;
}
-toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
- console.log('feedback called');
- this.idleTime = 0; // Reset idle time
- if (feedback === 'up') {
- thumbsUp.textContent = 'thumb_up'; // Change to filled icon
- thumbsUp.classList.remove('outlined');
- thumbsUp.classList.add('filled');
- thumbsDown.textContent = 'thumb_down_off_alt'; // Keep the other icon outlined
- thumbsDown.classList.add('outlined');
- thumbsDown.classList.remove('filled');
- } else {
- thumbsDown.textContent = 'thumb_down'; // Change to filled icon
- thumbsDown.classList.remove('outlined');
- thumbsDown.classList.add('filled');
- thumbsUp.textContent = 'thumb_up_off_alt'; // Keep the other icon outlined
- thumbsUp.classList.add('outlined');
- thumbsUp.classList.remove('filled');
- }
+ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
+ console.log('feedback called');
+ this.idleTime = 0; // Reset idle time
+ if (feedback === 'up') {
+ thumbsUp.textContent = 'thumb_up'; // Change to filled icon
+ thumbsUp.classList.remove('outlined');
+ thumbsUp.classList.add('filled');
+ thumbsDown.textContent = 'thumb_down_off_alt'; // Keep the other icon outlined
+ thumbsDown.classList.add('outlined');
+ thumbsDown.classList.remove('filled');
+ } else {
+ thumbsDown.textContent = 'thumb_down'; // Change to filled icon
+ thumbsDown.classList.remove('outlined');
+ thumbsDown.classList.add('filled');
+ thumbsUp.textContent = 'thumb_up_off_alt'; // Keep the other icon outlined
+ thumbsUp.classList.add('outlined');
+ thumbsUp.classList.remove('filled');
+ }
// Send feedback to the backend
this.handleFeedback(feedback, interactionId);
@@ -434,6 +575,21 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
handleSendMessage() {
console.log('handleSendMessage called');
this.idleTime = 0; // Reset idle time
+ if (!this.socket?.connected) {
+ console.error('Cannot send message: socket not connected');
+ this.setStatusMessage('Not connected to server. Please try again.');
+ return;
+ }
+
+ if (!this.room) {
+ console.error('Cannot send message: no room assigned');
+ this.setStatusMessage('Connection not ready. Please wait...');
+ // Try to rejoin room if we have a last known room
+ if (this.lastRoom) {
+ this.attemptRoomRejoin();
+ }
+ return;
+ }
const message = this.questionInput.value.trim();
if (message) {
this.addUserMessage(message);
@@ -444,12 +600,17 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
}
startTaskCheck(taskId) {
- console.log('Emitting check_task_status for:', taskId);
- this.socket.emit('check_task_status', {
- task_id: taskId,
- token: this.jwtToken,
- tenantId: this.tenantId
- });
+ if (!this.validateRoom()) {
+ console.error('Cannot check task status: no room assigned');
+ return;
+ }
+ console.log('Emitting check_task_status for:', taskId);
+ this.socket.emit('check_task_status', {
+ task_id: taskId,
+ token: this.sessionToken,
+ tenant_id: this.tenantId,
+ room: this.room
+ });
}
handleTaskStatus(data) {
@@ -474,23 +635,26 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
}
}
sendMessageToBackend(message) {
- console.log('sendMessageToBackend called');
- if (!this.socket) {
- console.error('Socket is not initialized');
+ if (!this.socket || !this.room) {
+ console.error('Cannot send message: socket or room not available');
+ return;
+ }
+
+ if (!this.validateRoom()) {
return;
}
const selectedLanguage = this.languageSelect.value;
-
const messageData = {
- tenantId: parseInt(this.tenantId),
- token: this.jwtToken,
- specialistId: parseInt(this.specialistId),
- arguments: {
- language: selectedLanguage,
- query: message
- },
- timezone: this.userTimezone
+ tenant_id: parseInt(this.tenantId),
+ token: this.sessionToken,
+ specialist_id: parseInt(this.specialistId),
+ arguments: {
+ language: selectedLanguage,
+ query: message
+ },
+ timezone: this.userTimezone,
+ room: this.room
};
console.log('Sending message to backend:', messageData);
@@ -513,6 +677,19 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
this.sendButton.style.pointerEvents = 'auto'; // Re-enable click events
}
}
+
+ validateRoom() {
+ if (!this.room) {
+ console.error('No room assigned');
+ this.setStatusMessage('Connection not ready. Please wait...');
+ // Try to rejoin room if we have a last known room
+ if (this.lastRoom) {
+ this.attemptRoomRejoin();
+ }
+ return false;
+ }
+ return true;
+ }
}
customElements.define('eveai-chat-widget', EveAIChatWidget);
diff --git a/integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js b/integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js
new file mode 100644
index 0000000..75d5380
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js
@@ -0,0 +1,167 @@
+class EveAI {
+ constructor(config) {
+ // Required parameters
+ this.tenantId = config.tenantId;
+
+ // Chat configuration
+ this.language = config.language || 'en';
+ this.languages = config.languages?.split(',') || ['en'];
+ this.specialistId = config.specialistId;
+
+ // Server Configuration
+ this.socketUrl = config.socketUrl || 'https://chat.askeveai.com';
+ this.authUrl = config.authUrl || 'https://api.askeveai.com';
+ this.proxyUrl = config.proxyUrl; // URL for auth proxy (WP or standalone)
+ this.wpRestNamespace = 'eveai/v1'; // This should match the PHP constant
+ this.wpRestUrl = `${config.wpBaseUrl || '/wp-json'}/${this.wpRestNamespace}`;
+
+ // Initialize token management
+ this.tokenManager = new EveAITokenManager({
+ proxyUrl: this.proxyUrl,
+ onTokenChange: this.handleTokenChange.bind(this),
+ onError: this.handleAuthError.bind(this)
+ });
+
+ this.chatWidget = null;
+ }
+
+ async initialize(containerId) {
+ try {
+ if (!containerId) {
+ throw new Error('Container ID is required');
+ }
+
+ console.log('Starting initialization with settings:', {
+ tenantId: this.tenantId,
+ wpRestUrl: this.wpRestUrl
+ });
+
+ // Get the WordPress nonce
+ const wpNonce = window.eveaiWP?.nonce;
+ if (!wpNonce) {
+ throw new Error('WordPress nonce not found');
+ }
+
+ // Use WordPress REST API endpoint instead of direct API call
+ const response = await fetch(`${this.wpRestUrl}/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': wpNonce,
+ },
+ credentials: 'same-origin', // Important for WP cookie handling
+ body: JSON.stringify({
+ tenant_id: this.tenantId
+ })
+ });
+
+ console.log('Token request response status:', response.status);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('Auth error response:', errorText);
+ throw new Error('Authentication failed');
+ }
+
+ const { access_token, expires_in } = await response.json();
+ console.log('Token received:', access_token);
+ console.log('Token Expiry:', expires_in);
+
+ // Store token and expiry
+ this.sessionToken = access_token;
+ this.tokenExpiry = Date.now() + (expires_in * 1000);
+
+ // Initialize token refresh timer
+ this.setupTokenRefresh(expires_in);
+
+ return this.initializeChat(containerId, access_token);
+ } catch (error) {
+ console.error('Full initialization error:', error);
+ throw error;
+ }
+ }
+
+ setupTokenRefresh(expiresIn) {
+ // Set up refresh 5 minutes before expiry
+ const refreshTime = (expiresIn - 300) * 1000; // Convert to milliseconds
+ setTimeout(() => this.refreshToken(), refreshTime);
+ }
+
+ async refreshToken() {
+ try {
+ const response = await fetch(`${this.wpRestUrl}/refresh`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${this.sessionToken}`
+ }
+ });
+
+ if (response.ok) {
+ const { access_token, expires_in } = await response.json();
+ this.sessionToken = access_token;
+ this.tokenExpiry = Date.now() + (expires_in * 1000);
+
+ // Update token in chat widget
+ if (this.chatWidget) {
+ this.chatWidget.setAttribute('session-token', access_token);
+ }
+
+ // Setup next refresh
+ this.setupTokenRefresh(expires_in);
+ } else {
+ console.error('Token refresh failed');
+ }
+ } catch (error) {
+ console.error('Token refresh error:', error);
+ }
+ }
+
+ async initializeChat(containerId) {
+ const container = document.getElementById(containerId);
+ if (!container) {
+ throw new Error('Container not found');
+ }
+
+ // Create chat widget with all necessary attributes
+ const chatWidget = document.createElement('eveai-chat-widget');
+
+ // Set all required attributes
+ const attributes = {
+ 'tenant-id': this.tenantId,
+ 'session-token': this.sessionToken,
+ 'language': this.language,
+ 'languages': this.languages.join(','),
+ 'specialist-id': this.specialistId,
+ 'server-url': this.socketUrl
+ };
+
+ console.log('Setting widget attributes:', attributes);
+
+ Object.entries(attributes).forEach(([attr, value]) => {
+ if (value === null || value === undefined) {
+ console.warn(`Warning: ${attr} is ${value}`);
+ }
+ chatWidget.setAttribute(attr, value);
+ });
+
+ container.appendChild(chatWidget);
+ this.chatWidget = chatWidget;
+ return chatWidget;
+ }
+
+ handleTokenChange(newToken) {
+ if (this.chatWidget) {
+ this.chatWidget.setAttribute('session-token', newToken);
+ }
+ }
+
+ handleAuthError(error) {
+ if (this.chatWidget) {
+ this.chatWidget.handleAuthError(error);
+ }
+ }
+}
+
+// Make available globally
+// window.EveAI = EveAI;
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat/assets/js/eveai-token-manager.js b/integrations/Wordpress/eveai-chat/assets/js/eveai-token-manager.js
new file mode 100644
index 0000000..b9c1d59
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/assets/js/eveai-token-manager.js
@@ -0,0 +1,69 @@
+// eveai-token-manager.js
+class EveAITokenManager extends EventTarget {
+ constructor() {
+ super();
+ this.token = null;
+ this.checkInterval = null;
+ this.isRefreshing = false;
+ this.refreshThreshold = 60; // Refresh token if less than 60s remaining
+ }
+
+ // Initialize with a token
+ async initialize(token) {
+ this.token = token;
+ this.startTokenCheck();
+ this.dispatchEvent(new CustomEvent('tokenChanged', { detail: { token } }));
+ }
+
+ // Start periodic token verification
+ startTokenCheck() {
+ if (this.checkInterval) {
+ clearInterval(this.checkInterval);
+ }
+
+ this.checkInterval = setInterval(async () => {
+ await this.verifyAndRefreshToken();
+ }, 5000); // Check every 5 seconds
+ }
+
+ // Verify token and refresh if needed
+ async verifyAndRefreshToken() {
+ if (!this.token || this.isRefreshing) return;
+
+ try {
+ const response = await fetch(`${this.proxyUrl}/verify`, {
+ headers: {
+ 'Authorization': `Bearer ${this.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Token verification failed');
+ }
+
+ const data = await response.json();
+ if (data.expires_in < this.refreshThreshold) {
+ await this.refreshToken();
+ }
+ } catch (error) {
+ this.handleTokenError(error);
+ }
+ }
+
+ // Handle any token errors
+ handleTokenError(error) {
+ this.dispatchEvent(new CustomEvent('tokenError', { detail: { error } }));
+ this.token = null;
+ if (this.checkInterval) {
+ clearInterval(this.checkInterval);
+ }
+ }
+
+ // Clean up
+ destroy() {
+ if (this.checkInterval) {
+ clearInterval(this.checkInterval);
+ }
+ this.token = null;
+ }
+}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat/eveai-chat.php b/integrations/Wordpress/eveai-chat/eveai-chat.php
new file mode 100644
index 0000000..6ebca8f
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/eveai-chat.php
@@ -0,0 +1,48 @@
+post_content, 'eveai_chat')) {
+ $this->load_assets();
+ }
+ }
+
+ private function load_assets() {
+ // Enqueue all required scripts
+ wp_enqueue_script('socket-io');
+ wp_enqueue_script('marked');
+ wp_enqueue_script('eveai-token-manager');
+ wp_enqueue_script('eveai-sdk');
+ wp_enqueue_script('eveai-chat-widget');
+
+ // Enqueue styles
+ wp_enqueue_style('material-icons');
+ wp_enqueue_style('eveai-chat');
+
+ // Localize script with WordPress-specific data
+ wp_localize_script('eveai-sdk', 'eveaiWP', [
+ 'nonce' => wp_create_nonce('wp_rest'),
+ 'settings' => $this->get_public_settings()
+ ]);
+ }
+
+ private function get_public_settings() {
+ $settings = get_option('eveai_chat_settings', []);
+
+ return [
+ 'socket_url' => $settings['socket_url'] ?? 'http://localhost:5002',
+ 'auth_url' => $settings['auth_url'] ?? 'http://localhost:5001',
+ 'tenant_id' => $settings['tenant_id'] ?? '',
+ 'wpBaseUrl' => rest_url(),
+ ];
+ }
+}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat/includes/class-cache-manager.php b/integrations/Wordpress/eveai-chat/includes/class-cache-manager.php
new file mode 100644
index 0000000..aa268de
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/includes/class-cache-manager.php
@@ -0,0 +1,30 @@
+load_dependencies();
+ $this->init_components();
+ $this->register_hooks();
+ }
+
+ /**
+ * Load dependencies
+ */
+ private function load_dependencies() {
+ // Core files
+ require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/interface-loadable.php';
+ require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-assets.php';
+ require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-shortcode.php';
+ require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-rest-controller.php';
+ require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-security.php';
+
+ // Admin
+ if (is_admin()) {
+ require_once EVEAI_CHAT_PLUGIN_DIR . 'admin/class-admin.php';
+ }
+ }
+
+ /**
+ * Initialize components
+ */
+ private function init_components() {
+ // Initialize REST controller
+ $this->components['rest'] = new RESTController();
+
+ // Initialize assets manager
+ $this->components['assets'] = new Assets();
+
+ // Initialize shortcode handler
+ $this->components['shortcode'] = new Shortcode();
+
+ // Initialize admin if in admin area
+ if (is_admin()) {
+ $this->components['admin'] = new Admin();
+ }
+
+ // Initialize all components
+ foreach ($this->components as $component) {
+ if ($component instanceof Loadable) {
+ $component->init();
+ }
+ }
+ }
+
+ /**
+ * Register WordPress hooks
+ */
+ private function register_hooks() {
+ // Plugin activation/deactivation
+ register_activation_hook(EVEAI_CHAT_PLUGIN_DIR . 'eveai-chat.php', [$this, 'activate']);
+ register_deactivation_hook(EVEAI_CHAT_PLUGIN_DIR . 'eveai-chat.php', [$this, 'deactivate']);
+
+ // Load text domain
+ add_action('plugins_loaded', [$this, 'load_plugin_textdomain']);
+ }
+
+ /**
+ * Plugin activation
+ */
+ public function activate() {
+ // Set default options if not exists
+ if (!get_option('eveai_chat_settings')) {
+ add_option('eveai_chat_settings', [
+ 'auth_url' => 'https://api.askeveai.com',
+ 'socket_url' => 'https://chat.askeveai.com',
+ 'tenant_id' => '',
+ 'api_key' => ''
+ ]);
+ }
+
+ // Clear permalinks
+ flush_rewrite_rules();
+ }
+
+ /**
+ * Plugin deactivation
+ */
+ public function deactivate() {
+ // Clear any scheduled hooks, clean up temporary data, etc.
+ flush_rewrite_rules();
+ }
+
+ /**
+ * Load plugin textdomain
+ */
+ public function load_plugin_textdomain() {
+ load_plugin_textdomain(
+ 'eveai-chat',
+ false,
+ dirname(plugin_basename(EVEAI_CHAT_PLUGIN_DIR)) . '/languages/'
+ );
+ }
+}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat/includes/class-rest-controller.php b/integrations/Wordpress/eveai-chat/includes/class-rest-controller.php
new file mode 100644
index 0000000..97e593d
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/includes/class-rest-controller.php
@@ -0,0 +1,188 @@
+ 'POST',
+ 'callback' => [$this, 'get_token'],
+ 'permission_callback' => [$this, 'verify_request'],
+ ]
+ );
+
+ register_rest_route(
+ self::API_NAMESPACE,
+ '/verify',
+ [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'verify_token'],
+ 'permission_callback' => [$this, 'verify_request'],
+ ]
+ );
+
+ register_rest_route(
+ self::API_NAMESPACE,
+ '/refresh',
+ [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'refresh_token'],
+ 'permission_callback' => [$this, 'verify_request'],
+ ]
+ );
+ }
+
+ public function verify_request(\WP_REST_Request $request): bool {
+// error_log('Verifying EveAI request: ' . print_r([
+// 'route' => $request->get_route(),
+// 'headers' => $request->get_headers(),
+// 'params' => $request->get_params()
+// ], true));
+
+ // Verify nonce
+ $nonce = $request->get_header('X-WP-Nonce');
+ if (!wp_verify_nonce($nonce, 'wp_rest')) {
+ error_log('EveAI nonce verification failed');
+ return false;
+ }
+
+ // Verify origin
+ $origin = $request->get_header('origin');
+ if (!$this->verify_origin($origin)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function get_token(\WP_REST_Request $request) {
+ try {
+ $settings = get_option('eveai_chat_settings');
+ if (empty($settings['tenant_id']) || empty($settings['api_key'])) {
+ return new \WP_Error(
+ 'configuration_error',
+ 'EveAI Chat is not properly configured.',
+ ['status' => 500]
+ );
+ }
+
+ $auth_url = rtrim($settings['auth_url'], '/');
+ $token_endpoint = '/api/v1/auth/token';
+ $full_url = $auth_url . $token_endpoint;
+
+ error_log('Attempting to get token from: ' . $full_url);
+
+ // Get decrypted API key
+ $api_key = Security::decrypt_api_key($settings['api_key']);
+
+ $response = wp_remote_post($full_url, [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json'
+ ],
+ 'body' => json_encode([
+ 'tenant_id' => $settings['tenant_id'],
+ 'api_key' => $api_key
+ ])
+ ]);
+
+ error_log('EveAI API Response: ' . print_r($response, true));
+
+ if (is_wp_error($response)) {
+ throw new \Exception($response->get_error_message());
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($response), true);
+
+ error_log('Token response body: ' . print_r($body, true)); // Add this for debugging
+
+ if (!isset($body['access_token'])) {
+ throw new \Exception('No token in response');
+ }
+
+ return new \WP_REST_Response($body, 200);
+
+ } catch (\Exception $e) {
+ error_log('EveAI token error: ' . $e->getMessage());
+ return new \WP_Error(
+ 'token_error',
+ $e->getMessage(),
+ ['status' => 500]
+ );
+ }
+ }
+
+ public function verify_token(\WP_REST_Request $request) {
+ try {
+ $token = $request->get_header('Authorization');
+ if (!$token) {
+ throw new \Exception('No token provided');
+ }
+
+ $settings = get_option('eveai_chat_settings');
+ $response = wp_remote_post($settings['auth_url'] . '/auth/verify', [
+ 'headers' => ['Authorization' => $token]
+ ]);
+
+ if (is_wp_error($response)) {
+ throw new \Exception($response->get_error_message());
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($response), true);
+ return new \WP_REST_Response($body, 200);
+ } catch (\Exception $e) {
+ return new \WP_Error(
+ 'verify_error',
+ $e->getMessage(),
+ ['status' => 401]
+ );
+ }
+ }
+
+ public function refresh_token(\WP_REST_Request $request) {
+ try {
+ $token = $request->get_header('Authorization');
+ if (!$token) {
+ throw new \Exception('No token provided');
+ }
+
+ $settings = get_option('eveai_chat_settings');
+ $response = wp_remote_post($settings['auth_url'] . '/auth/refresh', [
+ 'headers' => ['Authorization' => $token]
+ ]);
+
+ if (is_wp_error($response)) {
+ throw new \Exception($response->get_error_message());
+ }
+
+ $body = json_decode(wp_remote_retrieve_body($response), true);
+ return new \WP_REST_Response($body, 200);
+ } catch (\Exception $e) {
+ return new \WP_Error(
+ 'refresh_error',
+ $e->getMessage(),
+ ['status' => 401]
+ );
+ }
+ }
+
+ private function verify_origin($origin): bool {
+ if (empty($origin)) {
+ return false;
+ }
+
+ $site_url = parse_url(get_site_url(), PHP_URL_HOST);
+ $origin_host = parse_url($origin, PHP_URL_HOST);
+
+ return $origin_host === $site_url;
+ }
+}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat/includes/class-security.php b/integrations/Wordpress/eveai-chat/includes/class-security.php
new file mode 100644
index 0000000..1f53ae6
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/includes/class-security.php
@@ -0,0 +1,50 @@
+' .
+ esc_html__('EveAI Chat is not properly configured. Please check the admin settings.', 'eveai-chat') .
+ '';
+ }
+
+ // Parse shortcode attributes
+ $atts = shortcode_atts([
+ 'language' => 'en',
+ 'languages' => 'en',
+ 'specialist_id' => '1'
+ ], $atts, 'eveai_chat');
+
+ // Generate unique container ID
+ $container_id = 'eveai-chat-' . uniqid();
+
+ ob_start();
+ ?>
+
+
+ = self::MAX_REQUESTS) {
+ return false;
+ }
+
+ set_transient($key, $requests + 1, self::WINDOW_SECONDS);
+ return true;
+ }
+
+ public static function get_remaining_requests($identifier): int {
+ $key = self::RATE_LIMIT_KEY . $identifier;
+ $requests = get_transient($key);
+
+ if ($requests === false) {
+ return self::MAX_REQUESTS;
+ }
+
+ return max(0, self::MAX_REQUESTS - $requests);
+ }
+}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat/includes/interface-loadable.php b/integrations/Wordpress/eveai-chat/includes/interface-loadable.php
new file mode 100644
index 0000000..b9338e4
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat/includes/interface-loadable.php
@@ -0,0 +1,9 @@
+get_col("SELECT blog_id FROM $wpdb->blogs");
foreach ($blog_ids as $blog_id) {
switch_to_blog($blog_id);
-
- // Delete options for each site
delete_option('eveai_chat_settings');
- delete_option('eveai_chat_encryption_key');
-
restore_current_blog();
}
}
\ No newline at end of file
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 3b493ec..b5450aa 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -142,7 +142,26 @@ http {
}
location /api/ {
- proxy_pass http://eveai_api:5003;
+ # Handle preflight requests
+ if ($request_method = 'OPTIONS') {
+ add_header 'Access-Control-Allow-Origin' $http_origin always;
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
+ add_header 'Access-Control-Max-Age' 1728000;
+ add_header 'Content-Type' 'text/plain charset=UTF-8';
+ add_header 'Content-Length' 0;
+ return 204;
+ }
+ # Mirror the Origin header if it's allowed by the application
+ # The application will handle the actual origin validation
+ add_header 'Access-Control-Allow-Origin' $http_origin always;
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
+
+ proxy_pass http://eveai_api:5003/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;