feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
117
f_jobs/BaseJob.php
Normal file
117
f_jobs/BaseJob.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| Base Job Class
|
||||
| All queue jobs should extend this class
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
abstract class BaseJob
|
||||
{
|
||||
protected $logger;
|
||||
protected $startTime;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logger = VLogger::getInstance();
|
||||
$this->startTime = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the job - must be implemented by child classes
|
||||
* @param array $data Job data
|
||||
* @return mixed Job result
|
||||
*/
|
||||
abstract public function handle($data);
|
||||
|
||||
/**
|
||||
* Log job progress
|
||||
* @param string $message Progress message
|
||||
* @param array $context Additional context
|
||||
*/
|
||||
protected function logProgress($message, $context = [])
|
||||
{
|
||||
$context['job_class'] = get_class($this);
|
||||
$context['elapsed_time'] = microtime(true) - $this->startTime;
|
||||
|
||||
$this->logger->info($message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log job error
|
||||
* @param string $message Error message
|
||||
* @param array $context Additional context
|
||||
*/
|
||||
protected function logError($message, $context = [])
|
||||
{
|
||||
$context['job_class'] = get_class($this);
|
||||
$context['elapsed_time'] = microtime(true) - $this->startTime;
|
||||
|
||||
$this->logger->error($message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required data fields
|
||||
* @param array $data Job data
|
||||
* @param array $required Required field names
|
||||
* @throws Exception If required fields are missing
|
||||
*/
|
||||
protected function validateData($data, $required = [])
|
||||
{
|
||||
foreach ($required as $field) {
|
||||
if (!isset($data[$field])) {
|
||||
throw new Exception("Required field '{$field}' is missing from job data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database connection
|
||||
* @return object Database connection
|
||||
*/
|
||||
protected function getDatabase()
|
||||
{
|
||||
global $class_database;
|
||||
return $class_database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Redis connection
|
||||
* @return VRedis Redis instance
|
||||
*/
|
||||
protected function getRedis()
|
||||
{
|
||||
return VRedis::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification (can be overridden)
|
||||
* @param string $message Notification message
|
||||
* @param array $data Notification data
|
||||
*/
|
||||
protected function sendNotification($message, $data = [])
|
||||
{
|
||||
// Default implementation - log the notification
|
||||
$this->logger->info('Job notification', [
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
'job_class' => get_class($this)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update job progress (for long-running jobs)
|
||||
* @param int $current Current progress
|
||||
* @param int $total Total items
|
||||
* @param string $message Progress message
|
||||
*/
|
||||
protected function updateProgress($current, $total, $message = '')
|
||||
{
|
||||
$percentage = $total > 0 ? round(($current / $total) * 100, 2) : 0;
|
||||
|
||||
$this->logProgress("Job progress: {$percentage}%", [
|
||||
'current' => $current,
|
||||
'total' => $total,
|
||||
'percentage' => $percentage,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
450
f_jobs/CleanupJob.php
Normal file
450
f_jobs/CleanupJob.php
Normal file
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| System Cleanup Job
|
||||
| Handles automated system maintenance and cleanup tasks
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
class CleanupJob extends BaseJob
|
||||
{
|
||||
private $queueManager;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->queueManager = new VQueueManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cleanup job
|
||||
* @param array $data Cleanup configuration data
|
||||
* @return array Cleanup result
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$cleanupType = $data['type'] ?? 'full';
|
||||
|
||||
$this->logProgress('Starting system cleanup', [
|
||||
'type' => $cleanupType,
|
||||
'data' => $data
|
||||
]);
|
||||
|
||||
$results = [];
|
||||
|
||||
try {
|
||||
switch ($cleanupType) {
|
||||
case 'full':
|
||||
$results = $this->performFullCleanup($data);
|
||||
break;
|
||||
|
||||
case 'logs':
|
||||
$results['logs'] = $this->cleanupLogs($data);
|
||||
break;
|
||||
|
||||
case 'temp_files':
|
||||
$results['temp_files'] = $this->cleanupTempFiles($data);
|
||||
break;
|
||||
|
||||
case 'old_sessions':
|
||||
$results['sessions'] = $this->cleanupOldSessions($data);
|
||||
break;
|
||||
|
||||
case 'failed_uploads':
|
||||
$results['failed_uploads'] = $this->cleanupFailedUploads($data);
|
||||
break;
|
||||
|
||||
case 'analytics':
|
||||
$results['analytics'] = $this->cleanupOldAnalytics($data);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown cleanup type: {$cleanupType}");
|
||||
}
|
||||
|
||||
$this->logProgress('System cleanup completed', [
|
||||
'type' => $cleanupType,
|
||||
'results' => $results
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'type' => $cleanupType,
|
||||
'results' => $results,
|
||||
'message' => 'Cleanup completed successfully'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('System cleanup failed', [
|
||||
'type' => $cleanupType,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform full system cleanup
|
||||
* @param array $data Configuration data
|
||||
* @return array Cleanup results
|
||||
*/
|
||||
private function performFullCleanup($data)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$this->updateProgress(1, 8, 'Cleaning up logs');
|
||||
$results['logs'] = $this->cleanupLogs($data);
|
||||
|
||||
$this->updateProgress(2, 8, 'Cleaning up temporary files');
|
||||
$results['temp_files'] = $this->cleanupTempFiles($data);
|
||||
|
||||
$this->updateProgress(3, 8, 'Cleaning up old sessions');
|
||||
$results['sessions'] = $this->cleanupOldSessions($data);
|
||||
|
||||
$this->updateProgress(4, 8, 'Cleaning up failed uploads');
|
||||
$results['failed_uploads'] = $this->cleanupFailedUploads($data);
|
||||
|
||||
$this->updateProgress(5, 8, 'Cleaning up old analytics');
|
||||
$results['analytics'] = $this->cleanupOldAnalytics($data);
|
||||
|
||||
$this->updateProgress(6, 8, 'Cleaning up queue jobs');
|
||||
$results['queue_jobs'] = $this->queueManager->cleanupOldJobs(
|
||||
$data['queue_retention_hours'] ?? 24
|
||||
);
|
||||
|
||||
$this->updateProgress(7, 8, 'Cleaning up orphaned files');
|
||||
$results['orphaned_files'] = $this->cleanupOrphanedFiles($data);
|
||||
|
||||
$this->updateProgress(8, 8, 'Optimizing database');
|
||||
$results['database'] = $this->optimizeDatabase($data);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old log files
|
||||
* @param array $data Configuration data
|
||||
* @return array Cleanup result
|
||||
*/
|
||||
private function cleanupLogs($data)
|
||||
{
|
||||
$logDir = _FPATH . 'f_data/logs/';
|
||||
$retentionDays = $data['log_retention_days'] ?? 30;
|
||||
$cutoffTime = time() - ($retentionDays * 24 * 3600);
|
||||
|
||||
$deletedFiles = 0;
|
||||
$deletedSize = 0;
|
||||
|
||||
if (is_dir($logDir)) {
|
||||
$files = glob($logDir . '*.log');
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (filemtime($file) < $cutoffTime) {
|
||||
$size = filesize($file);
|
||||
if (unlink($file)) {
|
||||
$deletedFiles++;
|
||||
$deletedSize += $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'deleted_files' => $deletedFiles,
|
||||
'deleted_size' => $deletedSize,
|
||||
'retention_days' => $retentionDays
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup temporary files
|
||||
* @param array $data Configuration data
|
||||
* @return array Cleanup result
|
||||
*/
|
||||
private function cleanupTempFiles($data)
|
||||
{
|
||||
$tempDir = _FPATH . 'f_data/temp/';
|
||||
$maxAge = $data['temp_file_max_age_hours'] ?? 24;
|
||||
$cutoffTime = time() - ($maxAge * 3600);
|
||||
|
||||
$deletedFiles = 0;
|
||||
$deletedSize = 0;
|
||||
|
||||
if (is_dir($tempDir)) {
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($tempDir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getMTime() < $cutoffTime) {
|
||||
$size = $file->getSize();
|
||||
if (unlink($file->getPathname())) {
|
||||
$deletedFiles++;
|
||||
$deletedSize += $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'deleted_files' => $deletedFiles,
|
||||
'deleted_size' => $deletedSize,
|
||||
'max_age_hours' => $maxAge
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old sessions
|
||||
* @param array $data Configuration data
|
||||
* @return array Cleanup result
|
||||
*/
|
||||
private function cleanupOldSessions($data)
|
||||
{
|
||||
$maxAge = $data['session_max_age_hours'] ?? 168; // 7 days
|
||||
$cutoffTime = date('Y-m-d H:i:s', time() - ($maxAge * 3600));
|
||||
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Cleanup database sessions if using database session storage
|
||||
$query = "DELETE FROM db_sessions WHERE last_activity < ?";
|
||||
$db->doQuery($query, [$cutoffTime]);
|
||||
$deletedSessions = $db->getAffectedRows();
|
||||
|
||||
return [
|
||||
'deleted_db_sessions' => $deletedSessions,
|
||||
'max_age_hours' => $maxAge
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Session cleanup failed', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return [
|
||||
'error' => $e->getMessage(),
|
||||
'deleted_db_sessions' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup failed uploads
|
||||
* @param array $data Configuration data
|
||||
* @return array Cleanup result
|
||||
*/
|
||||
private function cleanupFailedUploads($data)
|
||||
{
|
||||
$maxAge = $data['failed_upload_max_age_hours'] ?? 48;
|
||||
$cutoffTime = date('Y-m-d H:i:s', time() - ($maxAge * 3600));
|
||||
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Find failed uploads
|
||||
$query = "SELECT file_key, usr_id FROM db_videofiles
|
||||
WHERE processing_status = 'failed'
|
||||
AND processed_at < ?";
|
||||
|
||||
$result = $db->doQuery($query, [$cutoffTime]);
|
||||
|
||||
$deletedRecords = 0;
|
||||
$deletedFiles = 0;
|
||||
$deletedSize = 0;
|
||||
|
||||
while ($row = $db->doFetch($result)) {
|
||||
// Delete associated files
|
||||
$userDir = _FPATH . 'f_data/media/' . $row['usr_id'] . '/';
|
||||
$videoFiles = glob($userDir . $row['file_key'] . '*');
|
||||
|
||||
foreach ($videoFiles as $file) {
|
||||
if (is_file($file)) {
|
||||
$deletedSize += filesize($file);
|
||||
if (unlink($file)) {
|
||||
$deletedFiles++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete database record
|
||||
$deleteQuery = "DELETE FROM db_videofiles WHERE file_key = ?";
|
||||
$db->doQuery($deleteQuery, [$row['file_key']]);
|
||||
$deletedRecords++;
|
||||
}
|
||||
|
||||
return [
|
||||
'deleted_records' => $deletedRecords,
|
||||
'deleted_files' => $deletedFiles,
|
||||
'deleted_size' => $deletedSize,
|
||||
'max_age_hours' => $maxAge
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed upload cleanup failed', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return [
|
||||
'error' => $e->getMessage(),
|
||||
'deleted_records' => 0,
|
||||
'deleted_files' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old analytics data
|
||||
* @param array $data Configuration data
|
||||
* @return array Cleanup result
|
||||
*/
|
||||
private function cleanupOldAnalytics($data)
|
||||
{
|
||||
$retentionDays = $data['analytics_retention_days'] ?? 90;
|
||||
$cutoffTime = date('Y-m-d H:i:s', time() - ($retentionDays * 24 * 3600));
|
||||
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
$deletedRecords = 0;
|
||||
|
||||
// Cleanup video analytics
|
||||
$query = "DELETE FROM db_video_analytics WHERE created_at < ?";
|
||||
$db->doQuery($query, [$cutoffTime]);
|
||||
$deletedRecords += $db->getAffectedRows();
|
||||
|
||||
// Cleanup video views
|
||||
$query = "DELETE FROM db_video_views WHERE created_at < ?";
|
||||
$db->doQuery($query, [$cutoffTime]);
|
||||
$deletedRecords += $db->getAffectedRows();
|
||||
|
||||
return [
|
||||
'deleted_records' => $deletedRecords,
|
||||
'retention_days' => $retentionDays
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Analytics cleanup failed', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return [
|
||||
'error' => $e->getMessage(),
|
||||
'deleted_records' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup orphaned files
|
||||
* @param array $data Configuration data
|
||||
* @return array Cleanup result
|
||||
*/
|
||||
private function cleanupOrphanedFiles($data)
|
||||
{
|
||||
$mediaDir = _FPATH . 'f_data/media/';
|
||||
$deletedFiles = 0;
|
||||
$deletedSize = 0;
|
||||
|
||||
try {
|
||||
if (!is_dir($mediaDir)) {
|
||||
return [
|
||||
'deleted_files' => 0,
|
||||
'deleted_size' => 0,
|
||||
'message' => 'Media directory not found'
|
||||
];
|
||||
}
|
||||
|
||||
// Get all file keys from database
|
||||
$db = $this->getDatabase();
|
||||
$query = "SELECT file_key FROM db_videofiles
|
||||
UNION SELECT file_key FROM db_imagefiles
|
||||
UNION SELECT file_key FROM db_audiofiles
|
||||
UNION SELECT file_key FROM db_documentfiles";
|
||||
|
||||
$result = $db->doQuery($query);
|
||||
$validKeys = [];
|
||||
|
||||
while ($row = $db->doFetch($result)) {
|
||||
$validKeys[] = $row['file_key'];
|
||||
}
|
||||
|
||||
// Scan media directory for orphaned files
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($mediaDir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$filename = $file->getFilename();
|
||||
|
||||
// Extract file key from filename (assuming format: filekey.extension)
|
||||
$fileKey = pathinfo($filename, PATHINFO_FILENAME);
|
||||
|
||||
if (!in_array($fileKey, $validKeys)) {
|
||||
$size = $file->getSize();
|
||||
if (unlink($file->getPathname())) {
|
||||
$deletedFiles++;
|
||||
$deletedSize += $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'deleted_files' => $deletedFiles,
|
||||
'deleted_size' => $deletedSize,
|
||||
'scanned_keys' => count($validKeys)
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Orphaned files cleanup failed', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return [
|
||||
'error' => $e->getMessage(),
|
||||
'deleted_files' => 0,
|
||||
'deleted_size' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize database
|
||||
* @param array $data Configuration data
|
||||
* @return array Optimization result
|
||||
*/
|
||||
private function optimizeDatabase($data)
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
$optimizedTables = 0;
|
||||
|
||||
// Get all tables
|
||||
$query = "SHOW TABLES";
|
||||
$result = $db->doQuery($query);
|
||||
|
||||
while ($row = $db->doFetch($result)) {
|
||||
$table = array_values($row)[0];
|
||||
|
||||
// Optimize table
|
||||
$optimizeQuery = "OPTIMIZE TABLE `{$table}`";
|
||||
$db->doQuery($optimizeQuery);
|
||||
$optimizedTables++;
|
||||
}
|
||||
|
||||
return [
|
||||
'optimized_tables' => $optimizedTables,
|
||||
'message' => 'Database optimization completed'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Database optimization failed', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return [
|
||||
'error' => $e->getMessage(),
|
||||
'optimized_tables' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
430
f_jobs/NotificationJob.php
Normal file
430
f_jobs/NotificationJob.php
Normal file
@@ -0,0 +1,430 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| Enhanced Notification Job
|
||||
| Handles various types of notifications (email, push, in-app)
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
class NotificationJob extends BaseJob
|
||||
{
|
||||
/**
|
||||
* Handle notification sending
|
||||
* @param array $data Notification data
|
||||
* @return array Sending result
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$this->validateData($data, ['type', 'recipient']);
|
||||
|
||||
$type = $data['type'];
|
||||
$recipient = $data['recipient'];
|
||||
$subject = $data['subject'] ?? '';
|
||||
$message = $data['message'] ?? '';
|
||||
$templateData = $data['template_data'] ?? [];
|
||||
$priority = $data['priority'] ?? 'normal';
|
||||
|
||||
$this->logProgress('Processing notification', [
|
||||
'type' => $type,
|
||||
'recipient' => $recipient,
|
||||
'subject' => $subject,
|
||||
'priority' => $priority
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = [];
|
||||
|
||||
switch ($type) {
|
||||
case 'email':
|
||||
$result = $this->sendEmailNotification($recipient, $subject, $message, $templateData);
|
||||
break;
|
||||
|
||||
case 'push':
|
||||
$result = $this->sendPushNotification($recipient, $subject, $message, $templateData);
|
||||
break;
|
||||
|
||||
case 'in_app':
|
||||
$result = $this->sendInAppNotification($recipient, $subject, $message, $templateData);
|
||||
break;
|
||||
|
||||
case 'sms':
|
||||
$result = $this->sendSMSNotification($recipient, $message, $templateData);
|
||||
break;
|
||||
|
||||
case 'webhook':
|
||||
$result = $this->sendWebhookNotification($recipient, $data);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown notification type: {$type}");
|
||||
}
|
||||
|
||||
// Log notification result
|
||||
$this->logProgress('Notification sent', [
|
||||
'type' => $type,
|
||||
'recipient' => $recipient,
|
||||
'success' => $result['success'],
|
||||
'message_id' => $result['message_id'] ?? null
|
||||
]);
|
||||
|
||||
// Update notification status in database
|
||||
if (isset($data['notification_id'])) {
|
||||
$this->updateNotificationStatus($data['notification_id'], $result['success'] ? 'sent' : 'failed', $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Notification failed', [
|
||||
'type' => $type,
|
||||
'recipient' => $recipient,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
// Update notification status as failed
|
||||
if (isset($data['notification_id'])) {
|
||||
$this->updateNotificationStatus($data['notification_id'], 'failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email notification
|
||||
* @param string $recipient Email address
|
||||
* @param string $subject Email subject
|
||||
* @param string $message Email message
|
||||
* @param array $templateData Template data
|
||||
* @return array Sending result
|
||||
*/
|
||||
private function sendEmailNotification($recipient, $subject, $message, $templateData)
|
||||
{
|
||||
try {
|
||||
// Use existing email system or implement new one
|
||||
$emailSender = new VEmailSender();
|
||||
|
||||
// Process template if provided
|
||||
if (isset($templateData['template'])) {
|
||||
$message = $this->processEmailTemplate($templateData['template'], $templateData);
|
||||
}
|
||||
|
||||
$result = $emailSender->sendEmail($recipient, $subject, $message, [
|
||||
'html' => $templateData['html'] ?? true,
|
||||
'attachments' => $templateData['attachments'] ?? []
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => $result,
|
||||
'message_id' => $emailSender->getLastMessageId(),
|
||||
'type' => 'email'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'type' => 'email'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send push notification
|
||||
* @param string $recipient User ID or device token
|
||||
* @param string $title Notification title
|
||||
* @param string $message Notification message
|
||||
* @param array $templateData Additional data
|
||||
* @return array Sending result
|
||||
*/
|
||||
private function sendPushNotification($recipient, $title, $message, $templateData)
|
||||
{
|
||||
try {
|
||||
// Implement push notification logic
|
||||
$pushService = new VPushNotificationService();
|
||||
|
||||
$payload = [
|
||||
'title' => $title,
|
||||
'body' => $message,
|
||||
'data' => $templateData['data'] ?? [],
|
||||
'badge' => $templateData['badge'] ?? null,
|
||||
'sound' => $templateData['sound'] ?? 'default'
|
||||
];
|
||||
|
||||
$result = $pushService->sendNotification($recipient, $payload);
|
||||
|
||||
return [
|
||||
'success' => $result['success'],
|
||||
'message_id' => $result['message_id'] ?? null,
|
||||
'type' => 'push'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'type' => 'push'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send in-app notification
|
||||
* @param string $recipient User ID
|
||||
* @param string $title Notification title
|
||||
* @param string $message Notification message
|
||||
* @param array $templateData Additional data
|
||||
* @return array Sending result
|
||||
*/
|
||||
private function sendInAppNotification($recipient, $title, $message, $templateData)
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
|
||||
$notificationData = [
|
||||
'user_id' => $recipient,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'type' => $templateData['notification_type'] ?? 'general',
|
||||
'data' => json_encode($templateData['data'] ?? []),
|
||||
'read_status' => 0,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'expires_at' => $templateData['expires_at'] ?? date('Y-m-d H:i:s', strtotime('+30 days'))
|
||||
];
|
||||
|
||||
$notificationId = $db->doInsert('db_notifications', $notificationData);
|
||||
|
||||
// Send real-time notification if user is online
|
||||
$this->sendRealTimeNotification($recipient, $notificationData);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message_id' => $notificationId,
|
||||
'type' => 'in_app'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'type' => 'in_app'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMS notification
|
||||
* @param string $recipient Phone number
|
||||
* @param string $message SMS message
|
||||
* @param array $templateData Template data
|
||||
* @return array Sending result
|
||||
*/
|
||||
private function sendSMSNotification($recipient, $message, $templateData)
|
||||
{
|
||||
try {
|
||||
// Implement SMS service integration (Twilio, AWS SNS, etc.)
|
||||
$smsService = new VSMSService();
|
||||
|
||||
$result = $smsService->sendSMS($recipient, $message);
|
||||
|
||||
return [
|
||||
'success' => $result['success'],
|
||||
'message_id' => $result['message_id'] ?? null,
|
||||
'type' => 'sms'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'type' => 'sms'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send webhook notification
|
||||
* @param string $recipient Webhook URL
|
||||
* @param array $data Webhook data
|
||||
* @return array Sending result
|
||||
*/
|
||||
private function sendWebhookNotification($recipient, $data)
|
||||
{
|
||||
try {
|
||||
$payload = json_encode($data['payload'] ?? $data);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $recipient,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'User-Agent: EasyStream-Webhook/1.0'
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
throw new Exception("cURL error: {$error}");
|
||||
}
|
||||
|
||||
$success = $httpCode >= 200 && $httpCode < 300;
|
||||
|
||||
return [
|
||||
'success' => $success,
|
||||
'http_code' => $httpCode,
|
||||
'response' => $response,
|
||||
'type' => 'webhook'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'type' => 'webhook'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process email template
|
||||
* @param string $template Template name
|
||||
* @param array $data Template data
|
||||
* @return string Processed template
|
||||
*/
|
||||
private function processEmailTemplate($template, $data)
|
||||
{
|
||||
$templateFile = _FPATH . "f_templates/email/{$template}.tpl";
|
||||
|
||||
if (!file_exists($templateFile)) {
|
||||
return $data['message'] ?? '';
|
||||
}
|
||||
|
||||
$content = file_get_contents($templateFile);
|
||||
|
||||
// Simple template variable replacement
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_string($value) || is_numeric($value)) {
|
||||
$content = str_replace("{{$key}}", $value, $content);
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send real-time notification to online user
|
||||
* @param string $userId User ID
|
||||
* @param array $notificationData Notification data
|
||||
*/
|
||||
private function sendRealTimeNotification($userId, $notificationData)
|
||||
{
|
||||
try {
|
||||
// Implement WebSocket or Server-Sent Events notification
|
||||
// This is a placeholder for real-time notification system
|
||||
|
||||
$redis = VRedis::getInstance();
|
||||
if ($redis->isConnected()) {
|
||||
$channel = "user_notifications_{$userId}";
|
||||
$redis->publish($channel, json_encode($notificationData));
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Log but don't fail the job
|
||||
$this->logError('Real-time notification failed', [
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notification status in database
|
||||
* @param int $notificationId Notification ID
|
||||
* @param string $status Status
|
||||
* @param array $result Result data
|
||||
*/
|
||||
private function updateNotificationStatus($notificationId, $status, $result)
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
|
||||
$updateData = [
|
||||
'status' => $status,
|
||||
'sent_at' => date('Y-m-d H:i:s'),
|
||||
'result_data' => json_encode($result)
|
||||
];
|
||||
|
||||
$db->doUpdate('db_notification_queue', 'id', $updateData, $notificationId);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to update notification status', [
|
||||
'notification_id' => $notificationId,
|
||||
'status' => $status,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder classes for services that need to be implemented
|
||||
*/
|
||||
class VEmailSender
|
||||
{
|
||||
private $lastMessageId;
|
||||
|
||||
public function sendEmail($recipient, $subject, $message, $options = [])
|
||||
{
|
||||
// Implement actual email sending logic
|
||||
// This could use PHPMailer, SwiftMailer, or native mail()
|
||||
|
||||
$headers = "From: noreply@" . $_SERVER['HTTP_HOST'] . "\r\n";
|
||||
$headers .= "Reply-To: noreply@" . $_SERVER['HTTP_HOST'] . "\r\n";
|
||||
|
||||
if ($options['html'] ?? false) {
|
||||
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
|
||||
}
|
||||
|
||||
$result = mail($recipient, $subject, $message, $headers);
|
||||
$this->lastMessageId = uniqid('email_', true);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getLastMessageId()
|
||||
{
|
||||
return $this->lastMessageId;
|
||||
}
|
||||
}
|
||||
|
||||
class VPushNotificationService
|
||||
{
|
||||
public function sendNotification($recipient, $payload)
|
||||
{
|
||||
// Implement push notification service (Firebase, APNs, etc.)
|
||||
return [
|
||||
'success' => true,
|
||||
'message_id' => uniqid('push_', true)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class VSMSService
|
||||
{
|
||||
public function sendSMS($recipient, $message)
|
||||
{
|
||||
// Implement SMS service (Twilio, AWS SNS, etc.)
|
||||
return [
|
||||
'success' => true,
|
||||
'message_id' => uniqid('sms_', true)
|
||||
];
|
||||
}
|
||||
}
|
||||
139
f_jobs/SendEmailJob.php
Normal file
139
f_jobs/SendEmailJob.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| Send Email Job
|
||||
| Handles email sending in the background
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
class SendEmailJob extends BaseJob
|
||||
{
|
||||
/**
|
||||
* Handle email sending
|
||||
* @param array $data Email data
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$this->validateData($data, ['to', 'subject', 'message']);
|
||||
|
||||
$to = $data['to'];
|
||||
$subject = $data['subject'];
|
||||
$message = $data['message'];
|
||||
$from = $data['from'] ?? 'noreply@easystream.com';
|
||||
$headers = $data['headers'] ?? [];
|
||||
|
||||
$this->logProgress('Starting email send', [
|
||||
'to' => $to,
|
||||
'subject' => $subject,
|
||||
'from' => $from
|
||||
]);
|
||||
|
||||
try {
|
||||
// Prepare headers
|
||||
$emailHeaders = [
|
||||
'From: ' . $from,
|
||||
'Reply-To: ' . $from,
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'X-Mailer: EasyStream'
|
||||
];
|
||||
|
||||
// Add custom headers
|
||||
foreach ($headers as $header) {
|
||||
$emailHeaders[] = $header;
|
||||
}
|
||||
|
||||
$headerString = implode("\r\n", $emailHeaders);
|
||||
|
||||
// Send email
|
||||
$success = mail($to, $subject, $message, $headerString);
|
||||
|
||||
if ($success) {
|
||||
$this->logProgress('Email sent successfully', [
|
||||
'to' => $to,
|
||||
'subject' => $subject
|
||||
]);
|
||||
|
||||
// Log to database for tracking
|
||||
$this->logEmailToDatabase($to, $subject, 'sent');
|
||||
|
||||
return true;
|
||||
} else {
|
||||
throw new Exception('mail() function returned false');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to send email', [
|
||||
'to' => $to,
|
||||
'subject' => $subject,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
// Log failed email to database
|
||||
$this->logEmailToDatabase($to, $subject, 'failed', $e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log email to database for tracking
|
||||
* @param string $to Recipient
|
||||
* @param string $subject Subject
|
||||
* @param string $status Status (sent/failed)
|
||||
* @param string $error Error message if failed
|
||||
*/
|
||||
private function logEmailToDatabase($to, $subject, $status, $error = null)
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
|
||||
$emailLog = [
|
||||
'recipient' => $to,
|
||||
'subject' => $subject,
|
||||
'status' => $status,
|
||||
'error_message' => $error,
|
||||
'sent_at' => date('Y-m-d H:i:s'),
|
||||
'job_class' => get_class($this)
|
||||
];
|
||||
|
||||
// Create table if it doesn't exist
|
||||
$this->createEmailLogTable();
|
||||
|
||||
$db->doInsert('db_email_log', $emailLog);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to log email to database', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create email log table if it doesn't exist
|
||||
*/
|
||||
private function createEmailLogTable()
|
||||
{
|
||||
global $db;
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `db_email_log` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`recipient` varchar(255) NOT NULL,
|
||||
`subject` varchar(500) NOT NULL,
|
||||
`status` enum('sent','failed') NOT NULL,
|
||||
`error_message` text,
|
||||
`sent_at` datetime NOT NULL,
|
||||
`job_class` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_recipient` (`recipient`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_sent_at` (`sent_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
||||
|
||||
try {
|
||||
$db->Execute($sql);
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to create email log table', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
385
f_jobs/SendNotificationJob.php
Normal file
385
f_jobs/SendNotificationJob.php
Normal file
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| Send Notification Job
|
||||
| Handles sending notifications to users
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
class SendNotificationJob extends BaseJob
|
||||
{
|
||||
/**
|
||||
* Handle notification sending
|
||||
* @param array $data Notification data
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$this->validateData($data, ['user_id', 'type', 'message']);
|
||||
|
||||
$userId = $data['user_id'];
|
||||
$type = $data['type'];
|
||||
$message = $data['message'];
|
||||
$title = $data['title'] ?? 'EasyStream Notification';
|
||||
$actionUrl = $data['action_url'] ?? null;
|
||||
$metadata = $data['metadata'] ?? [];
|
||||
|
||||
$this->logProgress('Sending notification', [
|
||||
'user_id' => $userId,
|
||||
'type' => $type,
|
||||
'title' => $title
|
||||
]);
|
||||
|
||||
try {
|
||||
// Store notification in database
|
||||
$notificationId = $this->storeNotification($userId, $type, $title, $message, $actionUrl, $metadata);
|
||||
|
||||
// Send real-time notification if user is online
|
||||
$this->sendRealTimeNotification($userId, $notificationId, $type, $title, $message, $actionUrl);
|
||||
|
||||
// Send email notification if enabled for user
|
||||
$this->sendEmailNotification($userId, $type, $title, $message, $actionUrl);
|
||||
|
||||
// Send push notification if user has enabled it
|
||||
$this->sendPushNotification($userId, $type, $title, $message, $actionUrl);
|
||||
|
||||
$this->logProgress('Notification sent successfully', [
|
||||
'user_id' => $userId,
|
||||
'notification_id' => $notificationId,
|
||||
'type' => $type
|
||||
]);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to send notification', [
|
||||
'user_id' => $userId,
|
||||
'type' => $type,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store notification in database
|
||||
* @param int $userId User ID
|
||||
* @param string $type Notification type
|
||||
* @param string $title Title
|
||||
* @param string $message Message
|
||||
* @param string $actionUrl Action URL
|
||||
* @param array $metadata Metadata
|
||||
* @return int Notification ID
|
||||
*/
|
||||
private function storeNotification($userId, $type, $title, $message, $actionUrl, $metadata)
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Create table if it doesn't exist
|
||||
$this->createNotificationTable();
|
||||
|
||||
$notificationData = [
|
||||
'user_id' => $userId,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'action_url' => $actionUrl,
|
||||
'metadata' => json_encode($metadata),
|
||||
'is_read' => 0,
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$success = $db->doInsert('db_notifications', $notificationData);
|
||||
|
||||
if (!$success) {
|
||||
throw new Exception('Failed to store notification in database');
|
||||
}
|
||||
|
||||
// Get the inserted notification ID
|
||||
global $db as $adodb;
|
||||
return $adodb->Insert_ID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send real-time notification via WebSocket/Server-Sent Events
|
||||
* @param int $userId User ID
|
||||
* @param int $notificationId Notification ID
|
||||
* @param string $type Type
|
||||
* @param string $title Title
|
||||
* @param string $message Message
|
||||
* @param string $actionUrl Action URL
|
||||
*/
|
||||
private function sendRealTimeNotification($userId, $notificationId, $type, $title, $message, $actionUrl)
|
||||
{
|
||||
try {
|
||||
$redis = $this->getRedis();
|
||||
|
||||
$notificationData = [
|
||||
'id' => $notificationId,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'action_url' => $actionUrl,
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
// Store in Redis for real-time delivery
|
||||
$redis->lpush("notifications:user:{$userId}", json_encode($notificationData));
|
||||
|
||||
// Limit to last 50 notifications per user
|
||||
$redis->getRedis()->ltrim("notifications:user:{$userId}", 0, 49);
|
||||
|
||||
// Publish to notification channel for WebSocket delivery
|
||||
$redis->getRedis()->publish("notifications", json_encode([
|
||||
'user_id' => $userId,
|
||||
'notification' => $notificationData
|
||||
]));
|
||||
|
||||
$this->logProgress('Real-time notification queued', [
|
||||
'user_id' => $userId,
|
||||
'notification_id' => $notificationId
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to send real-time notification', [
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email notification if user has enabled it
|
||||
* @param int $userId User ID
|
||||
* @param string $type Type
|
||||
* @param string $title Title
|
||||
* @param string $message Message
|
||||
* @param string $actionUrl Action URL
|
||||
*/
|
||||
private function sendEmailNotification($userId, $type, $title, $message, $actionUrl)
|
||||
{
|
||||
try {
|
||||
// Check if user wants email notifications for this type
|
||||
if (!$this->userWantsEmailNotification($userId, $type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user email
|
||||
$userEmail = $this->getUserEmail($userId);
|
||||
if (!$userEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare email content
|
||||
$emailSubject = "EasyStream: {$title}";
|
||||
$emailMessage = $this->buildEmailTemplate($title, $message, $actionUrl);
|
||||
|
||||
// Queue email job
|
||||
$queue = new VQueue();
|
||||
$queue->enqueue('SendEmailJob', [
|
||||
'to' => $userEmail,
|
||||
'subject' => $emailSubject,
|
||||
'message' => $emailMessage,
|
||||
'from' => 'notifications@easystream.com'
|
||||
], 'email');
|
||||
|
||||
$this->logProgress('Email notification queued', [
|
||||
'user_id' => $userId,
|
||||
'email' => $userEmail,
|
||||
'type' => $type
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to queue email notification', [
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send push notification
|
||||
* @param int $userId User ID
|
||||
* @param string $type Type
|
||||
* @param string $title Title
|
||||
* @param string $message Message
|
||||
* @param string $actionUrl Action URL
|
||||
*/
|
||||
private function sendPushNotification($userId, $type, $title, $message, $actionUrl)
|
||||
{
|
||||
try {
|
||||
// Get user's push notification tokens
|
||||
$pushTokens = $this->getUserPushTokens($userId);
|
||||
|
||||
if (empty($pushTokens)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue push notification job for each token
|
||||
$queue = new VQueue();
|
||||
|
||||
foreach ($pushTokens as $token) {
|
||||
$queue->enqueue('SendPushNotificationJob', [
|
||||
'token' => $token,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'action_url' => $actionUrl,
|
||||
'user_id' => $userId
|
||||
], 'notifications');
|
||||
}
|
||||
|
||||
$this->logProgress('Push notifications queued', [
|
||||
'user_id' => $userId,
|
||||
'token_count' => count($pushTokens),
|
||||
'type' => $type
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to queue push notifications', [
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user wants email notifications for this type
|
||||
* @param int $userId User ID
|
||||
* @param string $type Notification type
|
||||
* @return bool
|
||||
*/
|
||||
private function userWantsEmailNotification($userId, $type)
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Check user notification preferences
|
||||
$preference = $db->singleFieldValue('db_user_preferences', 'email_notifications', 'user_id', $userId);
|
||||
|
||||
if ($preference === null) {
|
||||
return true; // Default to enabled
|
||||
}
|
||||
|
||||
$preferences = json_decode($preference, true);
|
||||
return isset($preferences[$type]) ? $preferences[$type] : true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
return true; // Default to enabled on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user email address
|
||||
* @param int $userId User ID
|
||||
* @return string|null Email address
|
||||
*/
|
||||
private function getUserEmail($userId)
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
return $db->singleFieldValue('db_accountuser', 'usr_email', 'usr_id', $userId);
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user push notification tokens
|
||||
* @param int $userId User ID
|
||||
* @return array Push tokens
|
||||
*/
|
||||
private function getUserPushTokens($userId)
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// This would need a push_tokens table
|
||||
// For now, return empty array
|
||||
return [];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build email template
|
||||
* @param string $title Title
|
||||
* @param string $message Message
|
||||
* @param string $actionUrl Action URL
|
||||
* @return string HTML email content
|
||||
*/
|
||||
private function buildEmailTemplate($title, $message, $actionUrl)
|
||||
{
|
||||
$actionButton = '';
|
||||
if ($actionUrl) {
|
||||
$actionButton = '<p style="text-align: center; margin: 30px 0;">
|
||||
<a href="' . htmlspecialchars($actionUrl) . '"
|
||||
style="background: #4A90E2; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||
View Details
|
||||
</a>
|
||||
</p>';
|
||||
}
|
||||
|
||||
return '
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>' . htmlspecialchars($title) . '</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1 style="color: #4A90E2;">EasyStream</h1>
|
||||
</div>
|
||||
|
||||
<h2 style="color: #333;">' . htmlspecialchars($title) . '</h2>
|
||||
|
||||
<div style="background: #f9f9f9; padding: 20px; border-radius: 5px; margin: 20px 0;">
|
||||
' . nl2br(htmlspecialchars($message)) . '
|
||||
</div>
|
||||
|
||||
' . $actionButton . '
|
||||
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
|
||||
|
||||
<p style="font-size: 12px; color: #666; text-align: center;">
|
||||
This is an automated message from EasyStream. Please do not reply to this email.
|
||||
</p>
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notification table if it doesn't exist
|
||||
*/
|
||||
private function createNotificationTable()
|
||||
{
|
||||
global $db;
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `db_notifications` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`type` varchar(50) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`message` text NOT NULL,
|
||||
`action_url` varchar(500) DEFAULT NULL,
|
||||
`metadata` text,
|
||||
`is_read` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime NOT NULL,
|
||||
`read_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_type` (`type`),
|
||||
KEY `idx_is_read` (`is_read`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
||||
|
||||
try {
|
||||
$db->Execute($sql);
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to create notification table', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
293
f_jobs/ThumbnailGenerationJob.php
Normal file
293
f_jobs/ThumbnailGenerationJob.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| Thumbnail Generation Job
|
||||
| Handles thumbnail generation for videos and images
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
class ThumbnailGenerationJob extends BaseJob
|
||||
{
|
||||
/**
|
||||
* Handle thumbnail generation
|
||||
* @param array $data Thumbnail generation data
|
||||
* @return array Generation result
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$this->validateData($data, ['file_key', 'file_path', 'file_type']);
|
||||
|
||||
$fileKey = $data['file_key'];
|
||||
$filePath = $data['file_path'];
|
||||
$fileType = $data['file_type'];
|
||||
$sizes = $data['sizes'] ?? ['small' => [160, 120], 'medium' => [320, 240], 'large' => [640, 480]];
|
||||
|
||||
$this->logProgress('Starting thumbnail generation', [
|
||||
'file_key' => $fileKey,
|
||||
'file_path' => $filePath,
|
||||
'file_type' => $fileType,
|
||||
'sizes' => array_keys($sizes)
|
||||
]);
|
||||
|
||||
try {
|
||||
if (!file_exists($filePath)) {
|
||||
throw new Exception("Source file not found: {$filePath}");
|
||||
}
|
||||
|
||||
$thumbnailDir = _FPATH . 'f_data/thumbnails/' . substr($fileKey, 0, 2) . '/' . $fileKey . '/';
|
||||
if (!is_dir($thumbnailDir)) {
|
||||
mkdir($thumbnailDir, 0755, true);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
if ($fileType === 'video') {
|
||||
$results = $this->generateVideoThumbnails($filePath, $thumbnailDir, $fileKey, $sizes);
|
||||
} elseif (in_array($fileType, ['image', 'photo'])) {
|
||||
$results = $this->generateImageThumbnails($filePath, $thumbnailDir, $fileKey, $sizes);
|
||||
} else {
|
||||
throw new Exception("Unsupported file type for thumbnail generation: {$fileType}");
|
||||
}
|
||||
|
||||
// Update database with thumbnail information
|
||||
$this->updateThumbnailRecord($fileKey, $results);
|
||||
|
||||
$this->logProgress('Thumbnail generation completed', [
|
||||
'file_key' => $fileKey,
|
||||
'thumbnails_generated' => count($results)
|
||||
]);
|
||||
|
||||
return [
|
||||
'file_key' => $fileKey,
|
||||
'thumbnails' => $results,
|
||||
'thumbnail_dir' => $thumbnailDir
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Thumbnail generation failed', [
|
||||
'file_key' => $fileKey,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate video thumbnails using FFmpeg
|
||||
* @param string $videoPath Video file path
|
||||
* @param string $outputDir Output directory
|
||||
* @param string $fileKey File key
|
||||
* @param array $sizes Thumbnail sizes
|
||||
* @return array Generation results
|
||||
*/
|
||||
private function generateVideoThumbnails($videoPath, $outputDir, $fileKey, $sizes)
|
||||
{
|
||||
$ffmpegPath = $this->findFFmpegPath();
|
||||
$results = [];
|
||||
|
||||
foreach ($sizes as $sizeName => $dimensions) {
|
||||
$width = $dimensions[0];
|
||||
$height = $dimensions[1];
|
||||
$outputFile = $outputDir . $fileKey . "_{$sizeName}.jpg";
|
||||
|
||||
// Generate thumbnail at 10% of video duration
|
||||
$command = sprintf(
|
||||
'%s -i %s -ss 00:00:10 -vframes 1 -vf "scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:color=black" -q:v 2 %s 2>&1',
|
||||
escapeshellarg($ffmpegPath),
|
||||
escapeshellarg($videoPath),
|
||||
$width,
|
||||
$height,
|
||||
$width,
|
||||
$height,
|
||||
escapeshellarg($outputFile)
|
||||
);
|
||||
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
exec($command, $output, $returnCode);
|
||||
|
||||
if ($returnCode === 0 && file_exists($outputFile)) {
|
||||
$results[$sizeName] = [
|
||||
'success' => true,
|
||||
'file' => $outputFile,
|
||||
'size' => filesize($outputFile),
|
||||
'width' => $width,
|
||||
'height' => $height
|
||||
];
|
||||
} else {
|
||||
$results[$sizeName] = [
|
||||
'success' => false,
|
||||
'error' => 'FFmpeg thumbnail generation failed',
|
||||
'output' => implode("\n", $output)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate image thumbnails using GD
|
||||
* @param string $imagePath Image file path
|
||||
* @param string $outputDir Output directory
|
||||
* @param string $fileKey File key
|
||||
* @param array $sizes Thumbnail sizes
|
||||
* @return array Generation results
|
||||
*/
|
||||
private function generateImageThumbnails($imagePath, $outputDir, $fileKey, $sizes)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
// Get image info
|
||||
$imageInfo = getimagesize($imagePath);
|
||||
if (!$imageInfo) {
|
||||
throw new Exception("Unable to read image information");
|
||||
}
|
||||
|
||||
$sourceWidth = $imageInfo[0];
|
||||
$sourceHeight = $imageInfo[1];
|
||||
$mimeType = $imageInfo['mime'];
|
||||
|
||||
// Create source image resource
|
||||
switch ($mimeType) {
|
||||
case 'image/jpeg':
|
||||
$sourceImage = imagecreatefromjpeg($imagePath);
|
||||
break;
|
||||
case 'image/png':
|
||||
$sourceImage = imagecreatefrompng($imagePath);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$sourceImage = imagecreatefromgif($imagePath);
|
||||
break;
|
||||
case 'image/webp':
|
||||
$sourceImage = imagecreatefromwebp($imagePath);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unsupported image format: {$mimeType}");
|
||||
}
|
||||
|
||||
if (!$sourceImage) {
|
||||
throw new Exception("Failed to create image resource");
|
||||
}
|
||||
|
||||
foreach ($sizes as $sizeName => $dimensions) {
|
||||
$targetWidth = $dimensions[0];
|
||||
$targetHeight = $dimensions[1];
|
||||
|
||||
// Calculate dimensions maintaining aspect ratio
|
||||
$ratio = min($targetWidth / $sourceWidth, $targetHeight / $sourceHeight);
|
||||
$newWidth = (int)($sourceWidth * $ratio);
|
||||
$newHeight = (int)($sourceHeight * $ratio);
|
||||
|
||||
// Create thumbnail
|
||||
$thumbnail = imagecreatetruecolor($targetWidth, $targetHeight);
|
||||
|
||||
// Handle transparency for PNG and GIF
|
||||
if ($mimeType === 'image/png' || $mimeType === 'image/gif') {
|
||||
imagealphablending($thumbnail, false);
|
||||
imagesavealpha($thumbnail, true);
|
||||
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
|
||||
imagefill($thumbnail, 0, 0, $transparent);
|
||||
} else {
|
||||
// Fill with white background for JPEG
|
||||
$white = imagecolorallocate($thumbnail, 255, 255, 255);
|
||||
imagefill($thumbnail, 0, 0, $white);
|
||||
}
|
||||
|
||||
// Calculate centering offsets
|
||||
$offsetX = (int)(($targetWidth - $newWidth) / 2);
|
||||
$offsetY = (int)(($targetHeight - $newHeight) / 2);
|
||||
|
||||
// Resize and copy
|
||||
imagecopyresampled(
|
||||
$thumbnail, $sourceImage,
|
||||
$offsetX, $offsetY, 0, 0,
|
||||
$newWidth, $newHeight, $sourceWidth, $sourceHeight
|
||||
);
|
||||
|
||||
// Save thumbnail
|
||||
$outputFile = $outputDir . $fileKey . "_{$sizeName}.jpg";
|
||||
|
||||
if (imagejpeg($thumbnail, $outputFile, 85)) {
|
||||
$results[$sizeName] = [
|
||||
'success' => true,
|
||||
'file' => $outputFile,
|
||||
'size' => filesize($outputFile),
|
||||
'width' => $targetWidth,
|
||||
'height' => $targetHeight
|
||||
];
|
||||
} else {
|
||||
$results[$sizeName] = [
|
||||
'success' => false,
|
||||
'error' => 'Failed to save thumbnail'
|
||||
];
|
||||
}
|
||||
|
||||
imagedestroy($thumbnail);
|
||||
}
|
||||
|
||||
imagedestroy($sourceImage);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update thumbnail record in database
|
||||
* @param string $fileKey File key
|
||||
* @param array $results Thumbnail generation results
|
||||
*/
|
||||
private function updateThumbnailRecord($fileKey, $results)
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
|
||||
$thumbnailData = [];
|
||||
foreach ($results as $sizeName => $result) {
|
||||
if ($result['success']) {
|
||||
$thumbnailData[$sizeName] = [
|
||||
'file' => basename($result['file']),
|
||||
'width' => $result['width'],
|
||||
'height' => $result['height'],
|
||||
'size' => $result['size']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$updateData = [
|
||||
'thumbnails_generated' => 1,
|
||||
'thumbnail_data' => json_encode($thumbnailData),
|
||||
'thumbnail_updated' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// Update appropriate table based on file type
|
||||
$db->doUpdate('db_videofiles', 'file_key', $updateData, $fileKey);
|
||||
|
||||
$this->logProgress('Thumbnail record updated', [
|
||||
'file_key' => $fileKey,
|
||||
'thumbnails' => array_keys($thumbnailData)
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to update thumbnail record', [
|
||||
'file_key' => $fileKey,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find FFmpeg path
|
||||
* @return string FFmpeg path
|
||||
*/
|
||||
private function findFFmpegPath()
|
||||
{
|
||||
$paths = ['/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg', 'ffmpeg'];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (shell_exec("which {$path}") || file_exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return 'ffmpeg'; // Fallback to PATH
|
||||
}
|
||||
}
|
||||
269
f_jobs/VideoProcessingJob.php
Normal file
269
f_jobs/VideoProcessingJob.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| Enhanced Video Processing Job
|
||||
| Handles video transcoding and processing in the background with VVideoProcessor
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
class VideoProcessingJob extends BaseJob
|
||||
{
|
||||
private $videoProcessor;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->videoProcessor = new VVideoProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle video processing using VVideoProcessor
|
||||
* @param array $data Video processing data
|
||||
* @return array Processing result
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$this->validateData($data, ['video_key', 'input_file']);
|
||||
|
||||
$videoKey = $data['video_key'];
|
||||
$inputFile = $data['input_file'];
|
||||
$options = [
|
||||
'formats' => $data['formats'] ?? ['1080p', '720p', '480p', '360p'],
|
||||
'generate_preview' => $data['generate_preview'] ?? true,
|
||||
'generate_hls' => $data['generate_hls'] ?? true
|
||||
];
|
||||
|
||||
$this->logProgress('Starting video processing', [
|
||||
'video_key' => $videoKey,
|
||||
'input_file' => $inputFile,
|
||||
'options' => $options
|
||||
]);
|
||||
|
||||
try {
|
||||
// Process video using enhanced VVideoProcessor
|
||||
$result = $this->videoProcessor->processVideo($inputFile, $videoKey, $options);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->logProgress('Video processing completed successfully', [
|
||||
'video_key' => $videoKey,
|
||||
'formats_processed' => array_keys($result['results']),
|
||||
'video_info' => $result['video_info']
|
||||
]);
|
||||
|
||||
// Send success notification to user
|
||||
$this->sendUserNotification($videoKey, 'processing_completed', $result);
|
||||
|
||||
return $result;
|
||||
} else {
|
||||
throw new Exception($result['error'] ?? 'Video processing failed');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Video processing failed', [
|
||||
'video_key' => $videoKey,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
// Send failure notification to user
|
||||
$this->sendUserNotification($videoKey, 'processing_failed', ['error' => $e->getMessage()]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process video to specific format
|
||||
* @param string $inputFile Input file path
|
||||
* @param string $outputDir Output directory
|
||||
* @param string $format Format (720p, 480p, etc.)
|
||||
* @param string $videoId Video ID
|
||||
* @return array Processing result
|
||||
*/
|
||||
private function processVideoFormat($inputFile, $outputDir, $format, $videoId)
|
||||
{
|
||||
$outputFile = $outputDir . '/' . $videoId . '_' . $format . '.mp4';
|
||||
|
||||
// Define format settings
|
||||
$formatSettings = [
|
||||
'1080p' => ['width' => 1920, 'height' => 1080, 'bitrate' => '5000k'],
|
||||
'720p' => ['width' => 1280, 'height' => 720, 'bitrate' => '2500k'],
|
||||
'480p' => ['width' => 854, 'height' => 480, 'bitrate' => '1000k'],
|
||||
'360p' => ['width' => 640, 'height' => 360, 'bitrate' => '750k'],
|
||||
'240p' => ['width' => 426, 'height' => 240, 'bitrate' => '400k']
|
||||
];
|
||||
|
||||
if (!isset($formatSettings[$format])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => "Unknown format: {$format}",
|
||||
'output_file' => null
|
||||
];
|
||||
}
|
||||
|
||||
$settings = $formatSettings[$format];
|
||||
|
||||
// Build FFmpeg command
|
||||
$ffmpegCmd = sprintf(
|
||||
'ffmpeg -i %s -vf "scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2" -c:v libx264 -b:v %s -c:a aac -b:a 128k -movflags +faststart %s 2>&1',
|
||||
escapeshellarg($inputFile),
|
||||
$settings['width'],
|
||||
$settings['height'],
|
||||
$settings['width'],
|
||||
$settings['height'],
|
||||
$settings['bitrate'],
|
||||
escapeshellarg($outputFile)
|
||||
);
|
||||
|
||||
$this->logProgress("Executing FFmpeg for {$format}", [
|
||||
'command' => $ffmpegCmd,
|
||||
'output_file' => $outputFile
|
||||
]);
|
||||
|
||||
// Execute FFmpeg
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
exec($ffmpegCmd, $output, $returnCode);
|
||||
|
||||
if ($returnCode === 0 && file_exists($outputFile)) {
|
||||
return [
|
||||
'success' => true,
|
||||
'output_file' => $outputFile,
|
||||
'file_size' => filesize($outputFile),
|
||||
'format' => $format
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'FFmpeg processing failed',
|
||||
'return_code' => $returnCode,
|
||||
'output' => implode("\n", $output),
|
||||
'output_file' => $outputFile
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate video thumbnail
|
||||
* @param string $inputFile Input file path
|
||||
* @param string $outputDir Output directory
|
||||
* @param string $videoId Video ID
|
||||
* @return array Generation result
|
||||
*/
|
||||
private function generateThumbnail($inputFile, $outputDir, $videoId)
|
||||
{
|
||||
$thumbnailFile = $outputDir . '/' . $videoId . '_thumb.jpg';
|
||||
|
||||
// Generate thumbnail at 10% of video duration
|
||||
$ffmpegCmd = sprintf(
|
||||
'ffmpeg -i %s -ss 00:00:10 -vframes 1 -vf "scale=320:240:force_original_aspect_ratio=decrease,pad=320:240:(ow-iw)/2:(oh-ih)/2" %s 2>&1',
|
||||
escapeshellarg($inputFile),
|
||||
escapeshellarg($thumbnailFile)
|
||||
);
|
||||
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
exec($ffmpegCmd, $output, $returnCode);
|
||||
|
||||
if ($returnCode === 0 && file_exists($thumbnailFile)) {
|
||||
return [
|
||||
'success' => true,
|
||||
'thumbnail_file' => $thumbnailFile,
|
||||
'file_size' => filesize($thumbnailFile)
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Thumbnail generation failed',
|
||||
'return_code' => $returnCode,
|
||||
'output' => implode("\n", $output)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to user about processing status
|
||||
* @param string $videoKey Video key
|
||||
* @param string $type Notification type
|
||||
* @param array $data Additional data
|
||||
*/
|
||||
private function sendUserNotification($videoKey, $type, $data = [])
|
||||
{
|
||||
try {
|
||||
// Get video and user information
|
||||
$db = $this->getDatabase();
|
||||
$query = "SELECT vf.*, au.usr_user, au.usr_email
|
||||
FROM db_videofiles vf
|
||||
JOIN db_accountuser au ON vf.usr_id = au.usr_id
|
||||
WHERE vf.file_key = ?";
|
||||
$result = $db->doQuery($query, [$videoKey]);
|
||||
$video = $db->doFetch($result);
|
||||
|
||||
if (!$video) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create notification
|
||||
$notification = [
|
||||
'user_id' => $video['usr_id'],
|
||||
'type' => 'video_' . $type,
|
||||
'title' => $this->getNotificationTitle($type),
|
||||
'message' => $this->getNotificationMessage($type, $video['file_title']),
|
||||
'data' => json_encode([
|
||||
'video_key' => $videoKey,
|
||||
'video_title' => $video['file_title'],
|
||||
'processing_data' => $data
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'read_status' => 0
|
||||
];
|
||||
|
||||
$db->doInsert('db_notifications', $notification);
|
||||
|
||||
$this->logProgress('User notification sent', [
|
||||
'video_key' => $videoKey,
|
||||
'user_id' => $video['usr_id'],
|
||||
'type' => $type
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logError('Failed to send user notification', [
|
||||
'video_key' => $videoKey,
|
||||
'type' => $type,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification title based on type
|
||||
* @param string $type Notification type
|
||||
* @return string Title
|
||||
*/
|
||||
private function getNotificationTitle($type)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'processing_completed':
|
||||
return 'Video Processing Complete';
|
||||
case 'processing_failed':
|
||||
return 'Video Processing Failed';
|
||||
default:
|
||||
return 'Video Update';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification message based on type
|
||||
* @param string $type Notification type
|
||||
* @param string $videoTitle Video title
|
||||
* @return string Message
|
||||
*/
|
||||
private function getNotificationMessage($type, $videoTitle)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'processing_completed':
|
||||
return "Your video '{$videoTitle}' has been processed successfully and is now available for viewing.";
|
||||
case 'processing_failed':
|
||||
return "Processing failed for your video '{$videoTitle}'. Please try uploading again or contact support.";
|
||||
default:
|
||||
return "Update for your video '{$videoTitle}'.";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user