- Created a new eveai_chat plugin to support the new dynamic possibilities of the Specialists. Currently only supports standard Rag retrievers (i.e. no extra arguments).
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,5 +44,4 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc
|
|||||||
/eveai_repo.txt
|
/eveai_repo.txt
|
||||||
*repo.txt
|
*repo.txt
|
||||||
/docker/eveai_logs/
|
/docker/eveai_logs/
|
||||||
/common/utils/model_utils_orig.py
|
|
||||||
/integrations/Wordpress/eveai_sync.zip
|
/integrations/Wordpress/eveai_sync.zip
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ eveai_chat/
|
|||||||
eveai_chat_workers/
|
eveai_chat_workers/
|
||||||
eveai_entitlements/
|
eveai_entitlements/
|
||||||
instance/
|
instance/
|
||||||
integrations/Wordpress/eveai-chat-widget
|
integrations/Wordpress/eveai-chat
|
||||||
nginx/
|
nginx/
|
||||||
scripts/
|
scripts/
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
docker/
|
docker/
|
||||||
eveai_api/
|
|
||||||
eveai_app/
|
eveai_app/
|
||||||
eveai_beat/
|
eveai_beat/
|
||||||
eveai_chat_workers/
|
eveai_chat_workers/
|
||||||
|
|||||||
19
common/utils/cache/eveai_cache_manager.py
vendored
19
common/utils/cache/eveai_cache_manager.py
vendored
@@ -9,21 +9,28 @@ class EveAICacheManager:
|
|||||||
"""Cache manager with registration capabilities"""
|
"""Cache manager with registration capabilities"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.model_region = None
|
self._regions = {}
|
||||||
self.eveai_chat_workers_region = None
|
|
||||||
self.eveai_workers_region = None
|
|
||||||
self._handlers = {}
|
self._handlers = {}
|
||||||
|
|
||||||
def init_app(self, app: Flask):
|
def init_app(self, app: Flask):
|
||||||
"""Initialize cache regions"""
|
"""Initialize cache regions"""
|
||||||
from common.utils.cache.regions import create_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
|
# Initialize all registered handlers with their regions
|
||||||
for handler_class, region_name in self._handlers.items():
|
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)
|
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):
|
def register_handler(self, handler_class: Type[CacheHandler], region: str):
|
||||||
"""Register a cache handler class with its region"""
|
"""Register a cache handler class with its region"""
|
||||||
|
|||||||
13
common/utils/cache/regions.py
vendored
13
common/utils/cache/regions.py
vendored
@@ -1,7 +1,6 @@
|
|||||||
# common/utils/cache/regions.py
|
# common/utils/cache/regions.py
|
||||||
|
|
||||||
from dogpile.cache import make_region
|
from dogpile.cache import make_region
|
||||||
from flask import current_app
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -36,27 +35,31 @@ def get_redis_config(app):
|
|||||||
def create_cache_regions(app):
|
def create_cache_regions(app):
|
||||||
"""Initialize all cache regions with app config"""
|
"""Initialize all cache regions with app config"""
|
||||||
redis_config = get_redis_config(app)
|
redis_config = get_redis_config(app)
|
||||||
|
regions = {}
|
||||||
|
|
||||||
# Region for model-related caching (ModelVariables etc)
|
# 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',
|
'dogpile.cache.redis',
|
||||||
arguments=redis_config,
|
arguments=redis_config,
|
||||||
replace_existing_backend=True
|
replace_existing_backend=True
|
||||||
)
|
)
|
||||||
|
regions['eveai_model'] = model_region
|
||||||
|
|
||||||
# Region for eveai_chat_workers components (Specialists, Retrievers, ...)
|
# 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',
|
'dogpile.cache.redis',
|
||||||
arguments=redis_config, # arguments={**redis_config, 'db': 4}, # Different DB
|
arguments=redis_config, # arguments={**redis_config, 'db': 4}, # Different DB
|
||||||
replace_existing_backend=True
|
replace_existing_backend=True
|
||||||
)
|
)
|
||||||
|
regions['eveai_chat_workers'] = eveai_chat_workers_region
|
||||||
|
|
||||||
# Region for eveai_workers components (Processors, ...)
|
# 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',
|
'dogpile.cache.redis',
|
||||||
arguments=redis_config, # Same config for now
|
arguments=redis_config, # Same config for now
|
||||||
replace_existing_backend=True
|
replace_existing_backend=True
|
||||||
)
|
)
|
||||||
|
regions['eveai_workers'] = eveai_workers_region
|
||||||
|
|
||||||
return model_region, eveai_chat_workers_region, eveai_workers_region
|
return regions
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from flask import request, current_app, session
|
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
|
from common.models.user import Tenant, TenantDomain
|
||||||
|
|
||||||
|
|
||||||
@@ -23,31 +25,45 @@ def cors_after_request(response, prefix):
|
|||||||
response.headers.add('Access-Control-Allow-Methods', '*')
|
response.headers.add('Access-Control-Allow-Methods', '*')
|
||||||
return response
|
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
|
tenant_id = None
|
||||||
allowed_origins = []
|
allowed_origins = []
|
||||||
|
|
||||||
# Try to get tenant_id from JSON payload
|
# Check Socket.IO connection
|
||||||
json_data = request.get_json(silent=True)
|
if 'socket.io' in request.path:
|
||||||
|
token = request.args.get('token')
|
||||||
if json_data and 'tenant_id' in json_data:
|
if token:
|
||||||
tenant_id = json_data['tenant_id']
|
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:
|
else:
|
||||||
# Fallback to get tenant_id from query parameters or headers if JSON is not available
|
# Regular API requests
|
||||||
tenant_id = request.args.get('tenant_id') or request.args.get('tenantId') or request.headers.get('X-Tenant-ID')
|
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:
|
if tenant_id:
|
||||||
|
origin = request.headers.get('Origin')
|
||||||
allowed_origins = get_allowed_origins(tenant_id)
|
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:
|
||||||
if origin in allowed_origins:
|
response.headers.add('Access-Control-Allow-Origin', origin)
|
||||||
response.headers.add('Access-Control-Allow-Origin', origin)
|
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
|
||||||
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-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
|
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
||||||
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
|
||||||
else:
|
|
||||||
current_app.logger.warning(f'Origin {origin} not allowed')
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -118,3 +118,10 @@ class EveAIInvalidDocumentVersion(EveAIException):
|
|||||||
# Construct the message dynamically
|
# Construct the message dynamically
|
||||||
message = f"Tenant with ID '{tenant_id}' has no document version with ID {document_version_id}."
|
message = f"Tenant with ID '{tenant_id}' has no document version with ID {document_version_id}."
|
||||||
super().__init__(message, status_code, payload)
|
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)
|
||||||
@@ -252,7 +252,7 @@ class ModelVariablesCacheHandler(CacheHandler[ModelVariables]):
|
|||||||
|
|
||||||
|
|
||||||
# Register the handler with the cache manager
|
# 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
|
# Helper function to get cached model variables
|
||||||
|
|||||||
60
common/utils/token_validation.py
Normal file
60
common/utils/token_validation.py
Normal file
@@ -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)
|
||||||
|
)
|
||||||
@@ -54,9 +54,10 @@ services:
|
|||||||
- ../nginx/sites-enabled:/etc/nginx/sites-enabled
|
- ../nginx/sites-enabled:/etc/nginx/sites-enabled
|
||||||
- ../nginx/static:/etc/nginx/static
|
- ../nginx/static:/etc/nginx/static
|
||||||
- ../nginx/public:/etc/nginx/public
|
- ../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/assets/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/assets/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/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
|
- ./logs/nginx:/var/log/nginx
|
||||||
depends_on:
|
depends_on:
|
||||||
- eveai_app
|
- eveai_app
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ COPY ../../nginx/mime.types /etc/nginx/mime.types
|
|||||||
# Copy static & public files
|
# Copy static & public files
|
||||||
RUN mkdir -p /etc/nginx/static /etc/nginx/public
|
RUN mkdir -p /etc/nginx/static /etc/nginx/public
|
||||||
COPY ../../nginx/static /etc/nginx/static
|
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/assets/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/assets/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/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 ../../nginx/public /etc/nginx/public
|
||||||
|
|
||||||
# Copy site-specific configurations
|
# Copy site-specific configurations
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request
|
|||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from werkzeug.exceptions import HTTPException
|
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 os
|
||||||
import logging.config
|
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 common.utils.database import Database
|
||||||
from config.logging_config import LOGGING
|
from config.logging_config import LOGGING
|
||||||
from .api.document_api import document_ns
|
from .api.document_api import document_ns
|
||||||
@@ -54,7 +56,32 @@ def create_app(config_file=None):
|
|||||||
register_error_handlers(app)
|
register_error_handlers(app)
|
||||||
|
|
||||||
@app.before_request
|
@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
|
# Check if this a health check request
|
||||||
if request.path.startswith('/_healthz') or request.path.startswith('/healthz'):
|
if request.path.startswith('/_healthz') or request.path.startswith('/healthz'):
|
||||||
pass
|
pass
|
||||||
@@ -83,6 +110,17 @@ def register_extensions(app):
|
|||||||
jwt.init_app(app)
|
jwt.init_app(app)
|
||||||
minio_client.init_app(app)
|
minio_client.init_app(app)
|
||||||
simple_encryption.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):
|
def register_namespaces(app):
|
||||||
|
|||||||
@@ -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_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.models.user import Tenant, TenantProject
|
||||||
from common.extensions import simple_encryption
|
from common.extensions import simple_encryption
|
||||||
from flask import current_app, request
|
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')
|
'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')
|
@auth_ns.route('/token')
|
||||||
class Token(Resource):
|
class Token(Resource):
|
||||||
@@ -82,3 +88,61 @@ class Token(Resource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error creating access token: {e}")
|
current_app.logger.error(f"Error creating access token: {e}")
|
||||||
return {'message': "Internal server error"}, 500
|
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
|
||||||
@@ -45,7 +45,7 @@ def register_extensions(app):
|
|||||||
async_mode=app.config.get('SOCKETIO_ASYNC_MODE'),
|
async_mode=app.config.get('SOCKETIO_ASYNC_MODE'),
|
||||||
logger=app.config.get('SOCKETIO_LOGGER'),
|
logger=app.config.get('SOCKETIO_LOGGER'),
|
||||||
engineio_logger=app.config.get('SOCKETIO_ENGINEIO_LOGGER'),
|
engineio_logger=app.config.get('SOCKETIO_ENGINEIO_LOGGER'),
|
||||||
path='/socket.io',
|
path='/socket.io/',
|
||||||
ping_timeout=app.config.get('SOCKETIO_PING_TIMEOUT'),
|
ping_timeout=app.config.get('SOCKETIO_PING_TIMEOUT'),
|
||||||
ping_interval=app.config.get('SOCKETIO_PING_INTERVAL'),
|
ping_interval=app.config.get('SOCKETIO_PING_INTERVAL'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,18 +8,77 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from prometheus_client import Counter, Histogram
|
from prometheus_client import Counter, Histogram
|
||||||
from time import time
|
from time import time
|
||||||
|
import re
|
||||||
|
|
||||||
from common.extensions import socketio, db, simple_encryption
|
from common.extensions import socketio, db, simple_encryption
|
||||||
from common.models.user import Tenant
|
from common.models.user import Tenant
|
||||||
from common.models.interaction import Interaction
|
from common.models.interaction import Interaction
|
||||||
from common.utils.celery_utils import current_celery
|
from common.utils.celery_utils import current_celery
|
||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
|
from common.utils.token_validation import TokenValidator
|
||||||
|
from common.utils.eveai_exceptions import EveAISocketInputException
|
||||||
|
|
||||||
# Define custom metrics
|
# Define custom metrics
|
||||||
socketio_message_counter = Counter('socketio_message_count', 'Count of SocketIO messages', ['event_type'])
|
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'])
|
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
|
# Decorator to measure SocketIO events
|
||||||
def track_socketio_event(func):
|
def track_socketio_event(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@@ -37,42 +96,89 @@ def track_socketio_event(func):
|
|||||||
@socketio.on('connect')
|
@socketio.on('connect')
|
||||||
@track_socketio_event
|
@track_socketio_event
|
||||||
def handle_connect():
|
def handle_connect():
|
||||||
|
"""Handle incoming socket connections with enhanced security"""
|
||||||
try:
|
try:
|
||||||
tenant_id = request.args.get('tenantId')
|
current_app.logger.debug('Handle Connection')
|
||||||
if not tenant_id:
|
token = request.args.get('token')
|
||||||
raise Exception("Missing Tenant ID")
|
if not token:
|
||||||
api_key = request.args.get('apiKey')
|
raise ValueError("Missing token")
|
||||||
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}')
|
|
||||||
|
|
||||||
if not validate_api_key(tenant_id, api_key):
|
current_app.logger.debug(f"Token received: {token}")
|
||||||
raise Exception("Invalid tenant_id - api_key combination")
|
|
||||||
|
|
||||||
# Create JWT token
|
if not token:
|
||||||
token = create_access_token(identity={"tenant_id": tenant_id, "api_key": api_key})
|
raise ValueError("Missing token")
|
||||||
|
|
||||||
# Create a unique room for this client
|
current_app.logger.info(f"Trying to connect with: {token}")
|
||||||
room = f"{tenant_id}_{request.sid}"
|
|
||||||
|
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)
|
join_room(room)
|
||||||
|
|
||||||
# Create a unique session ID
|
session['session_id'] = str(uuid.uuid4())
|
||||||
if 'session_id' not in session:
|
|
||||||
session['session_id'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
session['last_activity'] = datetime.now()
|
session['last_activity'] = datetime.now()
|
||||||
session['room'] = room
|
session['room'] = room
|
||||||
|
|
||||||
# Communicate connection to client
|
# Emit success events
|
||||||
emit('connect', {'status': 'Connected', 'tenant_id': tenant_id, 'room': room})
|
emit('connect', {
|
||||||
emit('authenticated', {'token': token, 'room': room}) # Emit custom event with the token
|
'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:
|
except Exception as e:
|
||||||
current_app.logger.error(f'SocketIO: Connection failed: {e}')
|
current_app.logger.error(f"Socket connection failed: {str(e)}")
|
||||||
# communicate connection problem to client
|
|
||||||
emit('connect_error', {'status': 'Connection Failed'})
|
emit('connect_error', {'status': 'Connection Failed'})
|
||||||
disconnect()
|
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')
|
@socketio.on('disconnect')
|
||||||
@track_socketio_event
|
@track_socketio_event
|
||||||
def handle_disconnect():
|
def handle_disconnect():
|
||||||
@@ -90,37 +196,71 @@ def handle_heartbeat():
|
|||||||
|
|
||||||
@socketio.on('user_message')
|
@socketio.on('user_message')
|
||||||
def handle_message(data):
|
def handle_message(data):
|
||||||
|
current_app.logger.debug(f"SocketIO: Received message: {data}")
|
||||||
try:
|
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()
|
session['last_activity'] = datetime.now()
|
||||||
current_tenant_id = validate_incoming_data(data)
|
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
|
# Offload actual processing of question
|
||||||
task = current_celery.send_task('execute_specialist',
|
task = current_celery.send_task('execute_specialist',
|
||||||
queue='llm_interactions',
|
queue='llm_interactions',
|
||||||
args=[
|
args=[
|
||||||
current_tenant_id,
|
current_tenant_id,
|
||||||
data['specialistId'],
|
data['specialist_id'],
|
||||||
data['arguments'],
|
data['arguments'],
|
||||||
session['session_id'],
|
session['session_id'],
|
||||||
data['timezone'],
|
data['timezone'],
|
||||||
room
|
room
|
||||||
])
|
])
|
||||||
response = {
|
response = {
|
||||||
'tenantId': data['tenantId'],
|
'tenantId': current_tenant_id,
|
||||||
'message': f'Processing question ... Session ID = {session["session_id"]}',
|
'message': f'Processing question ... Session ID = {session["session_id"]}',
|
||||||
'taskId': task.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)
|
emit('bot_response', response, room=room)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f'SocketIO: Message handling failed: {str(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')
|
@socketio.on('check_task_status')
|
||||||
def check_task_status(data):
|
def check_task_status(data):
|
||||||
current_app.logger.debug(f'SocketIO: Checking 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')
|
task_id = data.get('task_id')
|
||||||
room = session.get('room')
|
room = session.get('room')
|
||||||
if not task_id:
|
if not task_id:
|
||||||
@@ -145,6 +285,7 @@ def check_task_status(data):
|
|||||||
'insufficient_info': specialist_result.get('insufficient_info', False)
|
'insufficient_info': specialist_result.get('insufficient_info', False)
|
||||||
},
|
},
|
||||||
'interaction_id': result['interaction_id'],
|
'interaction_id': result['interaction_id'],
|
||||||
|
'room': room
|
||||||
}
|
}
|
||||||
emit('task_status', response, room=room)
|
emit('task_status', response, room=room)
|
||||||
else:
|
else:
|
||||||
@@ -153,7 +294,15 @@ def check_task_status(data):
|
|||||||
|
|
||||||
@socketio.on('feedback')
|
@socketio.on('feedback')
|
||||||
def handle_feedback(data):
|
def handle_feedback(data):
|
||||||
|
current_app.logger.debug(f'SocketIO: Received feedback: {data}')
|
||||||
try:
|
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)
|
current_tenant_id = validate_incoming_data(data)
|
||||||
|
|
||||||
interaction_id = data.get('interactionId')
|
interaction_id = data.get('interactionId')
|
||||||
@@ -163,9 +312,15 @@ def handle_feedback(data):
|
|||||||
|
|
||||||
interaction = Interaction.query.get_or_404(interaction_id)
|
interaction = Interaction.query.get_or_404(interaction_id)
|
||||||
interaction.appreciation = 0 if feedback == 'down' else 100
|
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:
|
try:
|
||||||
db.session.commit()
|
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:
|
except SQLAlchemyError as e:
|
||||||
current_app.logger.error(f'SocketIO: Feedback handling failed: {e}')
|
current_app.logger.error(f'SocketIO: Feedback handling failed: {e}')
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
@@ -184,25 +339,20 @@ def validate_api_key(tenant_id, api_key):
|
|||||||
|
|
||||||
|
|
||||||
def validate_incoming_data(data):
|
def validate_incoming_data(data):
|
||||||
|
current_app.logger.debug(f'SocketIO: Validating incoming data: {data}')
|
||||||
token = data.get('token')
|
token = data.get('token')
|
||||||
if not token:
|
if not token:
|
||||||
raise Exception("Missing token")
|
raise EveAISocketInputException("SocketIO: Missing token in input")
|
||||||
|
|
||||||
decoded_token = decode_token(token)
|
decoded_token = decode_token(token)
|
||||||
if not decoded_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:
|
current_tenant_id = decoded_token.get('sub')
|
||||||
raise Exception("Missing token subject")
|
|
||||||
|
|
||||||
current_tenant_id = token_sub.get('tenant_id')
|
|
||||||
if not current_tenant_id:
|
if not current_tenant_id:
|
||||||
raise Exception("Missing tenant_id")
|
raise EveAISocketInputException("SocketIO: Missing tenant_id (sub) in input")
|
||||||
|
|
||||||
current_api_key = token_sub.get('api_key')
|
|
||||||
if not current_api_key:
|
|
||||||
raise Exception("Missing api_key")
|
|
||||||
|
|
||||||
return current_tenant_id
|
return current_tenant_id
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
<?php
|
|
||||||
class EveAI_Chat_Admin {
|
|
||||||
private $version;
|
|
||||||
|
|
||||||
public function __construct($version) {
|
|
||||||
$this->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'] : '';
|
|
||||||
?>
|
|
||||||
<input type="password"
|
|
||||||
id="api_key"
|
|
||||||
name="eveai_chat_settings[api_key]"
|
|
||||||
value="<?php echo esc_attr($api_key); ?>"
|
|
||||||
class="regular-text">
|
|
||||||
<p class="description">Enter your EveAI API key. You can find this in your EveAI dashboard.</p>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
public function validate_settings($input) {
|
|
||||||
$new_input = array();
|
|
||||||
|
|
||||||
if(isset($input['api_key']))
|
|
||||||
$new_input['api_key'] = sanitize_text_field($input['api_key']);
|
|
||||||
|
|
||||||
return $new_input;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function display_plugin_settings_page() {
|
|
||||||
// Load the settings page template
|
|
||||||
require_once plugin_dir_path(__FILE__) . 'views/settings-page.php';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<div class="wrap">
|
|
||||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
|
||||||
|
|
||||||
<form action="options.php" method="post">
|
|
||||||
<?php
|
|
||||||
// Output security fields
|
|
||||||
settings_fields('eveai_chat_settings');
|
|
||||||
|
|
||||||
// Output setting sections and their fields
|
|
||||||
do_settings_sections('eveai-chat-settings');
|
|
||||||
|
|
||||||
// Output save settings button
|
|
||||||
submit_button('Save Settings');
|
|
||||||
?>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="eveai-chat-help">
|
|
||||||
<h2>How to Use EveAI Chat</h2>
|
|
||||||
<p>To add the chat widget to your pages or posts, use the following shortcode:</p>
|
|
||||||
<code>[eveai_chat tenant_id="YOUR_TENANT_ID" language="en" supported_languages="en,fr,de,es"]</code>
|
|
||||||
|
|
||||||
<h3>Available Shortcode Parameters:</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>tenant_id</strong> (required): Your EveAI tenant ID</li>
|
|
||||||
<li><strong>language</strong> (optional): Default language for the chat widget (default: en)</li>
|
|
||||||
<li><strong>supported_languages</strong> (optional): Comma-separated list of supported languages (default: en,fr,de,es)</li>
|
|
||||||
<li><strong>server_url</strong> (optional): EveAI server URL (default: https://evie.askeveai.com)</li>
|
|
||||||
<li><strong>specialist_id</strong> (optional): ID of the specialist to use (default: 1)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Plugin Name: EveAI Chat Widget
|
|
||||||
* Description: Integrates the EveAI chat interface into your WordPress site.
|
|
||||||
* Version: 2.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('WPINC')) {
|
|
||||||
die;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define plugin constants
|
|
||||||
define('EVEAI_CHAT_VERSION', '2.0.0');
|
|
||||||
define('EVEAI_CHAT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
|
||||||
define('EVEAI_CHAT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
|
||||||
|
|
||||||
// Require the loader class
|
|
||||||
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-eveai-loader.php';
|
|
||||||
|
|
||||||
// Initialize the plugin
|
|
||||||
function run_eveai_chat() {
|
|
||||||
$plugin = new EveAI_Chat_Loader();
|
|
||||||
$plugin->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
run_eveai_chat();
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
Plugin Name: EveAI Chat Widget
|
|
||||||
Plugin URI: https://askeveai.com/
|
|
||||||
Description: Integrates the EveAI chat interface into your WordPress site.
|
|
||||||
Version: 1.5.0
|
|
||||||
Author: Josako, Pieter Laroy
|
|
||||||
Author URI: https://askeveai.com/about/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Enqueue necessary scripts and styles
|
|
||||||
function eveai_chat_enqueue_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);
|
|
||||||
wp_enqueue_script('eveai-sdk', plugin_dir_url(__FILE__) . 'js/eveai-sdk.js', array(), '1.0.0', true);
|
|
||||||
wp_enqueue_script('eveai-chat-widget', plugin_dir_url(__FILE__) . 'js/eveai-chat-widget.js', array('eveai-sdk'), '1.0.0', true);
|
|
||||||
wp_enqueue_style('material-icons', 'https://fonts.googleapis.com/icon?family=Material+Icons');
|
|
||||||
wp_enqueue_style('eveai-chat-style', plugin_dir_url(__FILE__) . 'css/eveai-chat-style.css');
|
|
||||||
}
|
|
||||||
add_action('wp_enqueue_scripts', 'eveai_chat_enqueue_scripts');
|
|
||||||
add_action('admin_enqueue_scripts', 'eveai_chat_enqueue_scripts');
|
|
||||||
|
|
||||||
// Shortcode function
|
|
||||||
function eveai_chat_shortcode($atts) {
|
|
||||||
// Default values
|
|
||||||
$defaults = array(
|
|
||||||
'tenant_id' => '',
|
|
||||||
'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 = "<div id='$chat_id'></div>";
|
|
||||||
$output .= "<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const eveAI = new EveAI(
|
|
||||||
'$tenant_id',
|
|
||||||
'$api_key',
|
|
||||||
'$domain',
|
|
||||||
'$language',
|
|
||||||
'$supported_languages',
|
|
||||||
'$server_url',
|
|
||||||
'$specialist_id'
|
|
||||||
);
|
|
||||||
eveAI.initializeChat('$chat_id');
|
|
||||||
});
|
|
||||||
</script>";
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
add_shortcode('eveai_chat', 'eveai_chat_shortcode');
|
|
||||||
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
<?php
|
|
||||||
class EveAI_Chat_API {
|
|
||||||
private $security;
|
|
||||||
private $eveai_api_url;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
$this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
class EveAI_Chat_Loader {
|
|
||||||
private $version;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
$this->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(
|
|
||||||
'<div id="%s"></div>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const eveAI = new EveAI({
|
|
||||||
tenantId: "%s",
|
|
||||||
language: "%s",
|
|
||||||
languages: "%s",
|
|
||||||
serverUrl: "%s",
|
|
||||||
specialistId: "%s",
|
|
||||||
proxyUrl: "%s"
|
|
||||||
});
|
|
||||||
eveAI.initializeChat("%s");
|
|
||||||
});
|
|
||||||
</script>',
|
|
||||||
$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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
<?php
|
|
||||||
class EveAI_Chat_Security {
|
|
||||||
public function verify_request($request) {
|
|
||||||
// Verify nonce
|
|
||||||
$nonce = $request->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Silence is golden.
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
class EveAI {
|
|
||||||
constructor(config) {
|
|
||||||
// Required parameters
|
|
||||||
this.tenantId = config.tenantId;
|
|
||||||
this.serverUrl = config.serverUrl;
|
|
||||||
this.specialistId = config.specialistId;
|
|
||||||
this.proxyUrl = config.proxyUrl;
|
|
||||||
|
|
||||||
// Optional parameters with defaults
|
|
||||||
this.language = config.language || 'en';
|
|
||||||
this.languages = config.languages?.split(',') || ['en'];
|
|
||||||
this.domain = config.domain || window.location.origin;
|
|
||||||
|
|
||||||
// Internal state
|
|
||||||
this.sessionToken = null;
|
|
||||||
this.initialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSessionToken() {
|
|
||||||
try {
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add WordPress-specific headers if they exist
|
|
||||||
if (window.eveaiWP?.nonce) {
|
|
||||||
headers['X-WP-Nonce'] = window.eveaiWP.nonce;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(this.proxyUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: headers,
|
|
||||||
body: JSON.stringify({
|
|
||||||
tenant_id: this.tenantId,
|
|
||||||
domain: this.domain
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to get session token');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return data.session_token;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting session token:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeChat(containerId) {
|
|
||||||
try {
|
|
||||||
// Get session token before initializing chat
|
|
||||||
this.sessionToken = await this.getSessionToken();
|
|
||||||
|
|
||||||
const container = document.getElementById(containerId);
|
|
||||||
if (!container) {
|
|
||||||
throw new Error('Container not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatWidget = document.createElement('eveai-chat-widget');
|
|
||||||
Object.entries({
|
|
||||||
'tenant-id': this.tenantId,
|
|
||||||
'session-token': this.sessionToken,
|
|
||||||
'domain': this.domain,
|
|
||||||
'language': this.language,
|
|
||||||
'languages': this.languages.join(','),
|
|
||||||
'server-url': this.serverUrl,
|
|
||||||
'specialist-id': this.specialistId
|
|
||||||
}).forEach(([attr, value]) => {
|
|
||||||
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;
|
|
||||||
@@ -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.
|
|
||||||
130
integrations/Wordpress/eveai-chat/admin/class-admin.php
Normal file
130
integrations/Wordpress/eveai-chat/admin/class-admin.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
class Admin implements Loadable {
|
||||||
|
public function init() {
|
||||||
|
add_action('admin_menu', [$this, 'add_plugin_page']);
|
||||||
|
add_action('admin_init', [$this, 'register_settings']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_plugin_page() {
|
||||||
|
add_options_page(
|
||||||
|
__('EveAI Chat Settings', 'eveai-chat'),
|
||||||
|
__('EveAI Chat', 'eveai-chat'),
|
||||||
|
'manage_options',
|
||||||
|
'eveai-chat-settings',
|
||||||
|
[$this, 'render_settings_page']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register_settings() {
|
||||||
|
register_setting(
|
||||||
|
'eveai_chat_settings',
|
||||||
|
'eveai_chat_settings',
|
||||||
|
[$this, 'sanitize_settings']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_section(
|
||||||
|
'eveai_chat_general',
|
||||||
|
__('General Settings', 'eveai-chat'),
|
||||||
|
[$this, 'render_section_info'],
|
||||||
|
'eveai-chat-settings'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add settings fields
|
||||||
|
$this->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 '<p>' . esc_html__('Configure your EveAI Chat settings below.', 'eveai-chat') . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
'<input type="%s" id="eveai_chat_%s" name="eveai_chat_settings[%s]" value="%s" class="regular-text" />',
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<div class="wrap">
|
||||||
|
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||||
|
|
||||||
|
<form action="options.php" method="post">
|
||||||
|
<?php
|
||||||
|
settings_fields('eveai_chat_settings');
|
||||||
|
do_settings_sections('eveai-chat-settings');
|
||||||
|
submit_button('Save Settings');
|
||||||
|
?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="eveai-chat-help">
|
||||||
|
<h2><?php esc_html_e('How to Use EveAI Chat', 'eveai-chat'); ?></h2>
|
||||||
|
<p><?php esc_html_e('To add the chat widget to your pages or posts, use the following shortcode:', 'eveai-chat'); ?></p>
|
||||||
|
<code>[eveai_chat language="en" languages="en,fr,de" specialist_id="1"]</code>
|
||||||
|
|
||||||
|
<h3><?php esc_html_e('Available Shortcode Parameters:', 'eveai-chat'); ?></h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>language</strong>: <?php esc_html_e('Default language for the chat widget (default: en)', 'eveai-chat'); ?></li>
|
||||||
|
<li><strong>languages</strong>: <?php esc_html_e('Comma-separated list of supported languages (default: en)', 'eveai-chat'); ?></li>
|
||||||
|
<li><strong>specialist_id</strong>: <?php esc_html_e('ID of the specialist to use (default: 1)', 'eveai-chat'); ?></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,34 +1,53 @@
|
|||||||
class EveAIChatWidget extends HTMLElement {
|
class EveAIChatWidget extends HTMLElement {
|
||||||
static get observedAttributes() {
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
// Networking attributes
|
||||||
this.socket = null; // Initialize socket to null
|
this.socket = null; // Initialize socket to null
|
||||||
this.attributesSet = false; // Flag to check if all attributes are set
|
|
||||||
this.room = null;
|
this.room = null;
|
||||||
|
this.lastRoom = null; // Store last known room
|
||||||
this.userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Detect user's timezone
|
this.userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Detect user's timezone
|
||||||
this.heartbeatInterval = null;
|
this.heartbeatInterval = null;
|
||||||
this.idleTime = 0; // in milliseconds
|
this.idleTime = 0; // in milliseconds
|
||||||
this.maxConnectionIdleTime = 1 * 60 * 60 * 1000; // 1 hour in milliseconds
|
this.maxConnectionIdleTime = 1 * 60 * 60 * 1000; // 1 hour in milliseconds
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.maxReconnectAttempts = 5;
|
||||||
|
|
||||||
|
// EveAI specific attributes
|
||||||
this.languages = []
|
this.languages = []
|
||||||
|
this.currentLanguage = null;
|
||||||
this.specialistId = null;
|
this.specialistId = null;
|
||||||
console.log('EveAIChatWidget constructor called');
|
console.log('EveAIChatWidget constructor called');
|
||||||
|
|
||||||
// Bind methods to ensure correct 'this' context
|
// Bind methods to ensure correct 'this' context
|
||||||
this.handleSendMessage = this.handleSendMessage.bind(this);
|
this.handleSendMessage = this.handleSendMessage.bind(this);
|
||||||
|
this.handleTokenUpdate = this.handleTokenUpdate.bind(this);
|
||||||
this.updateAttributes = this.updateAttributes.bind(this);
|
this.updateAttributes = this.updateAttributes.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
console.log('connectedCallback called');
|
console.log('Chat Widget Connected');
|
||||||
this.innerHTML = this.getTemplate();
|
this.innerHTML = this.getTemplate();
|
||||||
this.setupElements()
|
this.setupElements()
|
||||||
|
this.populateLanguageDropdown()
|
||||||
this.addEventListeners()
|
this.addEventListeners()
|
||||||
|
|
||||||
if (this.areAllAttributesSet() && !this.socket) {
|
if (this.areAllAttributesSet()) {
|
||||||
console.log('Attributes already set in connectedCallback, initializing socket');
|
console.log('All attributes are set, initializing socket');
|
||||||
this.initializeSocket();
|
this.initializeSocket();
|
||||||
|
} else {
|
||||||
|
console.warn('Not all required attributes are set yet');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,20 +98,39 @@ class EveAIChatWidget extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
|
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
|
||||||
this.updateAttributes();
|
|
||||||
|
|
||||||
if (this.areAllAttributesSet() && !this.socket) {
|
// Handle token updates specially
|
||||||
this.attributesSet = true;
|
if (name === 'session-token' && oldValue !== newValue) {
|
||||||
this.populateLanguageDropdown();
|
this.updateAttributes();
|
||||||
this.initializeSocket();
|
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() {
|
updateAttributes() {
|
||||||
this.tenantId = parseInt(this.getAttribute('tenant-id'));
|
this.tenantId = parseInt(this.getAttribute('tenant-id'));
|
||||||
this.sessionToken = this.getAttribute('session_token');
|
this.sessionToken = this.getAttribute('session-token');
|
||||||
this.domain = this.getAttribute('domain');
|
|
||||||
this.language = this.getAttribute('language');
|
this.language = this.getAttribute('language');
|
||||||
const languageAttr = this.getAttribute('languages');
|
const languageAttr = this.getAttribute('languages');
|
||||||
this.languages = languageAttr ? languageAttr.split(',') : [];
|
this.languages = languageAttr ? languageAttr.split(',') : [];
|
||||||
@@ -102,7 +140,6 @@ class EveAIChatWidget extends HTMLElement {
|
|||||||
console.log('Updated attributes:', {
|
console.log('Updated attributes:', {
|
||||||
tenantId: this.tenantId,
|
tenantId: this.tenantId,
|
||||||
sessionToken: this.sessionToken,
|
sessionToken: this.sessionToken,
|
||||||
domain: this.domain,
|
|
||||||
language: this.language,
|
language: this.language,
|
||||||
currentLanguage: this.currentLanguage,
|
currentLanguage: this.currentLanguage,
|
||||||
languages: this.languages,
|
languages: this.languages,
|
||||||
@@ -112,69 +149,67 @@ class EveAIChatWidget extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
areAllAttributesSet() {
|
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:', {
|
console.log('Checking if all attributes are set:', {
|
||||||
tenantId,
|
tenantId: this.tenantId,
|
||||||
sessionToken,
|
sessionToken: this.sessionToken,
|
||||||
domain,
|
language: this.language,
|
||||||
language,
|
languages: this.languages,
|
||||||
languages,
|
serverUrl: this.serverUrl,
|
||||||
serverUrl,
|
specialistId: this.specialistId
|
||||||
specialistId
|
|
||||||
});
|
});
|
||||||
return tenantId && sessionToken && domain && language && languages && serverUrl && specialistId;
|
|
||||||
}
|
|
||||||
|
|
||||||
createLanguageDropdown() {
|
const requiredAttributes = [
|
||||||
const select = document.createElement('select');
|
'tenant-id',
|
||||||
select.id = 'languageSelect';
|
'session-token',
|
||||||
this.languages.forEach(lang => {
|
'language',
|
||||||
const option = document.createElement('option');
|
'languages',
|
||||||
option.value = lang;
|
'specialist-id',
|
||||||
option.textContent = lang.toUpperCase();
|
'server-url'
|
||||||
if (lang === this.currentLanguage) {
|
];
|
||||||
option.selected = true;
|
|
||||||
}
|
return requiredAttributes.every(attr => this.getAttribute(attr));
|
||||||
select.appendChild(option);
|
}
|
||||||
});
|
|
||||||
select.addEventListener('change', (e) => {
|
handleTokenUpdate(newToken) {
|
||||||
this.currentLanguage = e.target.value;
|
if (this.socket && this.socket.connected) {
|
||||||
// You might want to emit an event or update the backend about the language change
|
console.log('Updating socket connection with new token');
|
||||||
});
|
// Emit token update event to server
|
||||||
return select;
|
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() {
|
initializeSocket() {
|
||||||
|
console.log(`Initializing socket connection to Evie at ${this.serverUrl}`);
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
console.log('Socket already initialized');
|
console.log('Socket already initialized');
|
||||||
return;
|
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, {
|
this.socket = io(this.serverUrl, {
|
||||||
path: '/chat/socket.io/',
|
path: '/socket.io/',
|
||||||
transports: ['websocket', 'polling'],
|
transports: ['websocket'],
|
||||||
query: {
|
query: { // Change from auth to query
|
||||||
tenantId: this.tenantId,
|
token: this.sessionToken
|
||||||
sessionToken: this.sessionToken
|
|
||||||
},
|
},
|
||||||
auth: {
|
reconnectionAttempts: 5, // Infinite reconnection attempts
|
||||||
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
|
|
||||||
reconnectionDelay: 5000, // Delay between reconnections
|
reconnectionDelay: 5000, // Delay between reconnections
|
||||||
timeout: 20000, // Connection timeout
|
timeout: 20000, // Connection timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!this.socket) {
|
||||||
|
console.error('Error initializing socket')
|
||||||
|
} else {
|
||||||
|
console.log('Socket initialized')
|
||||||
|
}
|
||||||
|
|
||||||
this.setupSocketEventHandlers();
|
this.setupSocketEventHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +220,10 @@ class EveAIChatWidget extends HTMLElement {
|
|||||||
this.setStatusMessage('Connected to EveAI.');
|
this.setStatusMessage('Connected to EveAI.');
|
||||||
this.updateConnectionStatus(true);
|
this.updateConnectionStatus(true);
|
||||||
this.startHeartbeat();
|
this.startHeartbeat();
|
||||||
|
|
||||||
if (data?.room) {
|
if (data?.room) {
|
||||||
this.room = data.room;
|
this.room = data.room;
|
||||||
|
this.lastRoom = this.room;
|
||||||
console.log(`Joined room: ${this.room}`);
|
console.log(`Joined room: ${this.room}`);
|
||||||
} else {
|
} else {
|
||||||
console.log('Room information not received on connect');
|
console.log('Room information not received on connect');
|
||||||
@@ -199,12 +236,23 @@ class EveAIChatWidget extends HTMLElement {
|
|||||||
this.setStatusMessage('Authenticated.');
|
this.setStatusMessage('Authenticated.');
|
||||||
if (data?.room) {
|
if (data?.room) {
|
||||||
this.room = data.room;
|
this.room = data.room;
|
||||||
|
this.lastRoom = this.room;
|
||||||
console.log(`Confirmed room: ${this.room}`);
|
console.log(`Confirmed room: ${this.room}`);
|
||||||
} else {
|
} else {
|
||||||
console.log('Room information not received on authentication');
|
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 --------------------------------------------------
|
// connect-error handler --------------------------------------------------
|
||||||
this.socket.on('connect_error', (err) => {
|
this.socket.on('connect_error', (err) => {
|
||||||
console.error('Socket connection 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.setStatusMessage('Disconnected from EveAI. Please refresh the page for further interaction.');
|
||||||
this.updateConnectionStatus(false);
|
this.updateConnectionStatus(false);
|
||||||
this.stopHeartbeat();
|
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 ----------------------------------------------
|
// reconnect_attempt handler ----------------------------------------------
|
||||||
this.socket.on('reconnect_attempt', () => {
|
this.socket.on('reconnect_attempt', (attemptNumber) => {
|
||||||
console.log('Attempting to reconnect to the server...');
|
console.log(`Reconnection attempt ${attemptNumber}`);
|
||||||
this.setStatusMessage('Attempting to reconnect...');
|
this.setStatusMessage(`Reconnecting... (Attempt ${attemptNumber})`);
|
||||||
|
this.reconnectAttempts = attemptNumber;
|
||||||
});
|
});
|
||||||
|
|
||||||
// reconnect handler ------------------------------------------------------
|
// reconnect handler ------------------------------------------------------
|
||||||
@@ -245,20 +302,103 @@ class EveAIChatWidget extends HTMLElement {
|
|||||||
this.startHeartbeat();
|
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 ---------------------------------------------------
|
// bot_response handler ---------------------------------------------------
|
||||||
this.socket.on('bot_response', (data) => {
|
this.socket.on('bot_response', (data) => {
|
||||||
console.log('Bot response received: ', 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);
|
setTimeout(() => this.startTaskCheck(data.taskId), 1000);
|
||||||
this.setStatusMessage('Processing...');
|
this.setStatusMessage('Processing...');
|
||||||
|
} else {
|
||||||
|
console.log('Received message for different room or tenant, ignoring');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// task_status handler ----------------------------------------------------
|
// task_status handler ----------------------------------------------------
|
||||||
this.socket.on('task_status', (data) => {
|
this.socket.on('task_status', (data) => {
|
||||||
console.log('Task status received:', data);
|
console.log('Task status received:', data);
|
||||||
|
if (!this.room) {
|
||||||
|
console.log('No room assigned, cannot process task status');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.handleTaskStatus(data);
|
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) {
|
setStatusMessage(message) {
|
||||||
@@ -353,13 +493,14 @@ class EveAIChatWidget extends HTMLElement {
|
|||||||
console.error('Socket is not initialized');
|
console.error('Socket is not initialized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.jwtToken) {
|
|
||||||
console.error('JWT token is not available');
|
if (!this.validateRoom()) {
|
||||||
|
console.log("No valid room to handle feedback")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Sending message to backend');
|
|
||||||
console.log(`Feedback for ${interactionId}: ${feedback}`);
|
console.log(`Sending feedback for ${interactionId}: ${feedback}`);
|
||||||
this.socket.emit('feedback', { tenantId: this.tenantId, token: this.jwtToken, feedback, interactionId });
|
this.socket.emit('feedback', { tenant_id: this.tenantId, token: this.sessionToken, feedback, interactionId, room: this.room });
|
||||||
this.setStatusMessage('Feedback sent.');
|
this.setStatusMessage('Feedback sent.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,24 +549,24 @@ class EveAIChatWidget extends HTMLElement {
|
|||||||
this.messagesArea.scrollTop = this.messagesArea.scrollHeight;
|
this.messagesArea.scrollTop = this.messagesArea.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
|
toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
|
||||||
console.log('feedback called');
|
console.log('feedback called');
|
||||||
this.idleTime = 0; // Reset idle time
|
this.idleTime = 0; // Reset idle time
|
||||||
if (feedback === 'up') {
|
if (feedback === 'up') {
|
||||||
thumbsUp.textContent = 'thumb_up'; // Change to filled icon
|
thumbsUp.textContent = 'thumb_up'; // Change to filled icon
|
||||||
thumbsUp.classList.remove('outlined');
|
thumbsUp.classList.remove('outlined');
|
||||||
thumbsUp.classList.add('filled');
|
thumbsUp.classList.add('filled');
|
||||||
thumbsDown.textContent = 'thumb_down_off_alt'; // Keep the other icon outlined
|
thumbsDown.textContent = 'thumb_down_off_alt'; // Keep the other icon outlined
|
||||||
thumbsDown.classList.add('outlined');
|
thumbsDown.classList.add('outlined');
|
||||||
thumbsDown.classList.remove('filled');
|
thumbsDown.classList.remove('filled');
|
||||||
} else {
|
} else {
|
||||||
thumbsDown.textContent = 'thumb_down'; // Change to filled icon
|
thumbsDown.textContent = 'thumb_down'; // Change to filled icon
|
||||||
thumbsDown.classList.remove('outlined');
|
thumbsDown.classList.remove('outlined');
|
||||||
thumbsDown.classList.add('filled');
|
thumbsDown.classList.add('filled');
|
||||||
thumbsUp.textContent = 'thumb_up_off_alt'; // Keep the other icon outlined
|
thumbsUp.textContent = 'thumb_up_off_alt'; // Keep the other icon outlined
|
||||||
thumbsUp.classList.add('outlined');
|
thumbsUp.classList.add('outlined');
|
||||||
thumbsUp.classList.remove('filled');
|
thumbsUp.classList.remove('filled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send feedback to the backend
|
// Send feedback to the backend
|
||||||
this.handleFeedback(feedback, interactionId);
|
this.handleFeedback(feedback, interactionId);
|
||||||
@@ -434,6 +575,21 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
|
|||||||
handleSendMessage() {
|
handleSendMessage() {
|
||||||
console.log('handleSendMessage called');
|
console.log('handleSendMessage called');
|
||||||
this.idleTime = 0; // Reset idle time
|
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();
|
const message = this.questionInput.value.trim();
|
||||||
if (message) {
|
if (message) {
|
||||||
this.addUserMessage(message);
|
this.addUserMessage(message);
|
||||||
@@ -444,12 +600,17 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startTaskCheck(taskId) {
|
startTaskCheck(taskId) {
|
||||||
console.log('Emitting check_task_status for:', taskId);
|
if (!this.validateRoom()) {
|
||||||
this.socket.emit('check_task_status', {
|
console.error('Cannot check task status: no room assigned');
|
||||||
task_id: taskId,
|
return;
|
||||||
token: this.jwtToken,
|
}
|
||||||
tenantId: this.tenantId
|
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) {
|
handleTaskStatus(data) {
|
||||||
@@ -474,23 +635,26 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendMessageToBackend(message) {
|
sendMessageToBackend(message) {
|
||||||
console.log('sendMessageToBackend called');
|
if (!this.socket || !this.room) {
|
||||||
if (!this.socket) {
|
console.error('Cannot send message: socket or room not available');
|
||||||
console.error('Socket is not initialized');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateRoom()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedLanguage = this.languageSelect.value;
|
const selectedLanguage = this.languageSelect.value;
|
||||||
|
|
||||||
const messageData = {
|
const messageData = {
|
||||||
tenantId: parseInt(this.tenantId),
|
tenant_id: parseInt(this.tenantId),
|
||||||
token: this.jwtToken,
|
token: this.sessionToken,
|
||||||
specialistId: parseInt(this.specialistId),
|
specialist_id: parseInt(this.specialistId),
|
||||||
arguments: {
|
arguments: {
|
||||||
language: selectedLanguage,
|
language: selectedLanguage,
|
||||||
query: message
|
query: message
|
||||||
},
|
},
|
||||||
timezone: this.userTimezone
|
timezone: this.userTimezone,
|
||||||
|
room: this.room
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Sending message to backend:', messageData);
|
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
|
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);
|
customElements.define('eveai-chat-widget', EveAIChatWidget);
|
||||||
167
integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js
Normal file
167
integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js
Normal file
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
integrations/Wordpress/eveai-chat/eveai-chat.php
Normal file
48
integrations/Wordpress/eveai-chat/eveai-chat.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: EveAI Chat Widget
|
||||||
|
* Description: Integrates the EveAI chat interface into your WordPress site.
|
||||||
|
* Version: 2.0.16
|
||||||
|
* Author: Your Company
|
||||||
|
* Text Domain: eveai-chat
|
||||||
|
* Domain Path: /languages
|
||||||
|
* Requires at least: 5.8
|
||||||
|
* Requires PHP: 7.4
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define plugin constants
|
||||||
|
define('EVEAI_CHAT_VERSION', '2.0.16');
|
||||||
|
define('EVEAI_CHAT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
|
define('EVEAI_CHAT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
|
||||||
|
// Autoloader
|
||||||
|
spl_autoload_register(function ($class) {
|
||||||
|
$prefix = 'EveAI\\Chat\\';
|
||||||
|
$base_dir = EVEAI_CHAT_PLUGIN_DIR . 'includes/';
|
||||||
|
|
||||||
|
$len = strlen($prefix);
|
||||||
|
if (strncmp($prefix, $class, $len) !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relative_class = substr($class, $len);
|
||||||
|
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||||
|
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require $file;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the main plugin class
|
||||||
|
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-plugin.php';
|
||||||
|
|
||||||
|
// Initialize the plugin
|
||||||
|
function run_eveai_chat() {
|
||||||
|
$plugin = \EveAI\Chat\Plugin::get_instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
run_eveai_chat();
|
||||||
108
integrations/Wordpress/eveai-chat/includes/class-assets.php
Normal file
108
integrations/Wordpress/eveai-chat/includes/class-assets.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
class Assets implements Loadable {
|
||||||
|
public function init() {
|
||||||
|
add_action('wp_enqueue_scripts', [$this, 'register_assets']);
|
||||||
|
add_action('wp_enqueue_scripts', [$this, 'maybe_load_assets']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register_assets() {
|
||||||
|
// Register Socket.IO (from CDN)
|
||||||
|
wp_register_script(
|
||||||
|
'socket-io',
|
||||||
|
'https://cdn.socket.io/4.0.1/socket.io.min.js',
|
||||||
|
[],
|
||||||
|
'4.0.1',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register Marked library for markdown (from CDN)
|
||||||
|
wp_register_script(
|
||||||
|
'marked',
|
||||||
|
'https://cdn.jsdelivr.net/npm/marked/marked.min.js',
|
||||||
|
[],
|
||||||
|
'1.0.0',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register EveAI core scripts
|
||||||
|
wp_register_script(
|
||||||
|
'eveai-token-manager',
|
||||||
|
EVEAI_CHAT_PLUGIN_URL . 'assets/js/eveai-token-manager.js',
|
||||||
|
['socket-io'],
|
||||||
|
EVEAI_CHAT_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_register_script(
|
||||||
|
'eveai-sdk',
|
||||||
|
EVEAI_CHAT_PLUGIN_URL . 'assets/js/eveai-sdk.js',
|
||||||
|
['eveai-token-manager'],
|
||||||
|
EVEAI_CHAT_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_register_script(
|
||||||
|
'eveai-chat-widget',
|
||||||
|
EVEAI_CHAT_PLUGIN_URL . 'assets/js/eveai-chat-widget.js',
|
||||||
|
['eveai-sdk', 'marked'],
|
||||||
|
EVEAI_CHAT_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register styles
|
||||||
|
wp_register_style(
|
||||||
|
'eveai-chat',
|
||||||
|
EVEAI_CHAT_PLUGIN_URL . 'assets/css/eveai-chat-style.css',
|
||||||
|
[],
|
||||||
|
EVEAI_CHAT_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_register_style(
|
||||||
|
'material-icons',
|
||||||
|
'https://fonts.googleapis.com/icon?family=Material+Icons',
|
||||||
|
[],
|
||||||
|
EVEAI_CHAT_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function maybe_load_assets() {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
// Only load if shortcode is present
|
||||||
|
if (is_a($post, 'WP_Post') && has_shortcode($post->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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
class CacheManager {
|
||||||
|
private const SETTINGS_CACHE_KEY = 'eveai_settings_cache';
|
||||||
|
private const CACHE_EXPIRATION = 3600; // 1 hour
|
||||||
|
|
||||||
|
public static function get_settings(): array {
|
||||||
|
$cached = wp_cache_get(self::SETTINGS_CACHE_KEY);
|
||||||
|
|
||||||
|
if ($cached !== false) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = get_option('eveai_chat_settings', []);
|
||||||
|
wp_cache_set(self::SETTINGS_CACHE_KEY, $settings, '', self::CACHE_EXPIRATION);
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clear_settings_cache(): void {
|
||||||
|
wp_cache_delete(self::SETTINGS_CACHE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function maybe_clear_caches(): void {
|
||||||
|
if (defined('WP_CACHE') && WP_CACHE) {
|
||||||
|
wp_cache_flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
integrations/Wordpress/eveai-chat/includes/class-plugin.php
Normal file
125
integrations/Wordpress/eveai-chat/includes/class-plugin.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
class Plugin {
|
||||||
|
/**
|
||||||
|
* Plugin instance
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin components
|
||||||
|
*/
|
||||||
|
private $components = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin instance
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize plugin
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->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/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
class RESTController implements Loadable {
|
||||||
|
const API_NAMESPACE = 'eveai/v1';
|
||||||
|
|
||||||
|
public function init() {
|
||||||
|
add_action('rest_api_init', [$this, 'register_routes']);
|
||||||
|
error_log('REST routes registered for EveAI Chat');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register_routes() {
|
||||||
|
register_rest_route(
|
||||||
|
self::API_NAMESPACE,
|
||||||
|
'/token',
|
||||||
|
[
|
||||||
|
'methods' => '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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
class Security {
|
||||||
|
private static $encryption_method = 'aes-256-cbc';
|
||||||
|
|
||||||
|
public static function encrypt_api_key(string $key): string {
|
||||||
|
if (empty($key)) return '';
|
||||||
|
|
||||||
|
$salt = wp_salt('auth');
|
||||||
|
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::$encryption_method));
|
||||||
|
|
||||||
|
$encrypted = openssl_encrypt(
|
||||||
|
$key,
|
||||||
|
self::$encryption_method,
|
||||||
|
$salt,
|
||||||
|
0,
|
||||||
|
$iv
|
||||||
|
);
|
||||||
|
|
||||||
|
return base64_encode($iv . $encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function decrypt_api_key(string $encrypted): string {
|
||||||
|
if (empty($encrypted)) return '';
|
||||||
|
|
||||||
|
$salt = wp_salt('auth');
|
||||||
|
$data = base64_decode($encrypted);
|
||||||
|
|
||||||
|
$iv_length = openssl_cipher_iv_length(self::$encryption_method);
|
||||||
|
$iv = substr($data, 0, $iv_length);
|
||||||
|
$encrypted_data = substr($data, $iv_length);
|
||||||
|
|
||||||
|
return openssl_decrypt(
|
||||||
|
$encrypted_data,
|
||||||
|
self::$encryption_method,
|
||||||
|
$salt,
|
||||||
|
0,
|
||||||
|
$iv
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generate_nonce(): string {
|
||||||
|
return wp_create_nonce('eveai_chat_nonce');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function verify_nonce(string $nonce): bool {
|
||||||
|
return wp_verify_nonce($nonce, 'eveai_chat_nonce');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
class Shortcode implements Loadable {
|
||||||
|
public function init() {
|
||||||
|
add_shortcode('eveai_chat', [$this, 'render_chat']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_chat($atts) {
|
||||||
|
$settings = get_option('eveai_chat_settings');
|
||||||
|
error_log('Rendering chat with settings: ' . print_r($settings, true));
|
||||||
|
|
||||||
|
if (empty($settings['tenant_id']) || empty($settings['api_key'])) {
|
||||||
|
return '<div class="eveai-error">' .
|
||||||
|
esc_html__('EveAI Chat is not properly configured. Please check the admin settings.', 'eveai-chat') .
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
?>
|
||||||
|
<div id="<?php echo esc_attr($container_id); ?>" class="eveai-chat-container"></div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
console.log('Initializing EveAI Chat');
|
||||||
|
const eveAI = new EveAI({
|
||||||
|
tenantId: <?php echo esc_js($settings['tenant_id']); ?>,
|
||||||
|
language: '<?php echo esc_js($atts['language']); ?>',
|
||||||
|
languages: '<?php echo esc_js($atts['languages']); ?>',
|
||||||
|
specialistId: '<?php echo esc_js($atts['specialist_id']); ?>',
|
||||||
|
socketUrl: '<?php echo esc_js($settings['socket_url']); ?>',
|
||||||
|
authUrl: '<?php echo esc_js($settings['auth_url']); ?>'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await eveAI.initialize('<?php echo esc_js($container_id); ?>');
|
||||||
|
console.log('Chat initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize chat:', error);
|
||||||
|
document.getElementById('<?php echo esc_js($container_id); ?>').innerHTML =
|
||||||
|
'<div class="eveai-error">Failed to initialize chat. Please check console for details.</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
class RateLimiter {
|
||||||
|
private const RATE_LIMIT_KEY = 'eveai_rate_limit_';
|
||||||
|
private const MAX_REQUESTS = 60; // Maximum requests per window
|
||||||
|
private const WINDOW_SECONDS = 60; // Time window in seconds
|
||||||
|
|
||||||
|
public static function check_rate_limit($identifier): bool {
|
||||||
|
$key = self::RATE_LIMIT_KEY . $identifier;
|
||||||
|
$requests = get_transient($key);
|
||||||
|
|
||||||
|
if ($requests === false) {
|
||||||
|
set_transient($key, 1, self::WINDOW_SECONDS);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requests >= 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
namespace EveAI\Chat;
|
||||||
|
|
||||||
|
interface Loadable {
|
||||||
|
/**
|
||||||
|
* Initialize the component
|
||||||
|
*/
|
||||||
|
public function init();
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ if (!defined('WP_UNINSTALL_PLUGIN')) {
|
|||||||
|
|
||||||
// Delete plugin options
|
// Delete plugin options
|
||||||
delete_option('eveai_chat_settings');
|
delete_option('eveai_chat_settings');
|
||||||
delete_option('eveai_chat_encryption_key');
|
|
||||||
|
|
||||||
// Clean up any additional options or data that your plugin may have created
|
// Clean up any additional options or data that your plugin may have created
|
||||||
// For example, if you've created any custom tables, you might want to drop them here
|
// For example, if you've created any custom tables, you might want to drop them here
|
||||||
@@ -17,11 +16,7 @@ if (is_multisite()) {
|
|||||||
$blog_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs");
|
$blog_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs");
|
||||||
foreach ($blog_ids as $blog_id) {
|
foreach ($blog_ids as $blog_id) {
|
||||||
switch_to_blog($blog_id);
|
switch_to_blog($blog_id);
|
||||||
|
|
||||||
// Delete options for each site
|
|
||||||
delete_option('eveai_chat_settings');
|
delete_option('eveai_chat_settings');
|
||||||
delete_option('eveai_chat_encryption_key');
|
|
||||||
|
|
||||||
restore_current_blog();
|
restore_current_blog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,26 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /api/ {
|
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 Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|||||||
Reference in New Issue
Block a user