false, 'message' => 'No recipients provided.']; } $sections = (array) ($payload['sections'] ?? []); $links = array_values(array_unique(array_filter((array) ($payload['links'] ?? [])))); $parent = $payload['parent'] ?? null; $items = $this->buildItems($sections, $links); if (empty($items)) { Log::warning('WhatsappInvite: no invite items built.'); return ['ok' => false, 'message' => 'No invite links found.']; } $subject = count($items) > 1 ? 'Your WhatsApp Group Links (Multiple Classes)' : 'Your WhatsApp Group Link'; $lines = ['Assalamu Alaikum,', '', 'Here are your WhatsApp link(s):']; foreach ($items as $it) { $lines[] = ' - '.$it['class_section_name'].': '.$it['invite_link']; } $lines[] = ''; $lines[] = 'Jazakumu-llahu khayran.'; $bodyHtml = nl2br(implode("\n", $lines)); $ok = $this->emailService->send($to, $subject, $bodyHtml, 'notifications', $cc); $singleSectionId = count($items) === 1 ? ((int) ($items[0]['class_section_id'] ?? 0) ?: null) : null; $parentId = (int) ($parent['id'] ?? 0); if ($ok) { WhatsappInviteLog::logSuccess($parentId, implode(', ', $to), $singleSectionId, null); Log::info('Whatsapp invite sent.', ['to' => $to, 'cc' => $cc]); } else { WhatsappInviteLog::logFailure($parentId, implode(', ', $to), 'Email send failed', $singleSectionId, null); Log::error('Whatsapp invite failed.', ['to' => $to, 'cc' => $cc]); } return [ 'ok' => $ok, 'recipients' => count($to), ]; } private function buildItems(array $sections, array $links): array { $seen = []; $items = []; foreach ($sections as $section) { $sid = isset($section['class_section_id']) ? (int) $section['class_section_id'] : 0; $name = trim((string) ($section['class_section_name'] ?? ($sid ? ('Section ' . $sid) : 'Class'))); $url = trim((string) ($section['invite_link'] ?? '')); if ($url === '') { continue; } $key = $sid . '|' . $url; if (isset($seen[$key])) { continue; } $seen[$key] = true; $items[] = [ 'class_section_id' => $sid, 'class_section_name' => $name, 'invite_link' => $url, 'qr_src' => $this->generateQrImage($url), ]; } if (empty($items) && !empty($links)) { foreach ($links as $i => $url) { $url = trim((string) $url); if ($url === '') { continue; } $key = '0|' . $url; if (isset($seen[$key])) { continue; } $seen[$key] = true; $items[] = [ 'class_section_id' => 0, 'class_section_name' => 'Class Link ' . ($i + 1), 'invite_link' => $url, 'qr_src' => $this->generateQrImage($url), ]; } } return $items; } private function generateQrImage(string $inviteLink): string { if ($inviteLink === '') { return $this->qrFallback($inviteLink); } $dir = storage_path('app/qrcodes'); if (!is_dir($dir)) { if (!@mkdir($dir, 0775, true) && !is_dir($dir)) { Log::error('QR: cannot create dir', ['dir' => $dir]); return $this->qrFallback($inviteLink); } } if (!is_writable($dir)) { Log::error('QR: dir not writable', ['dir' => $dir]); return $this->qrFallback($inviteLink); } $filename = 'wa_' . md5($inviteLink) . '.png'; $path = $dir . DIRECTORY_SEPARATOR . $filename; if (!is_file($path)) { $v5BuilderClass = '\\Endroid\\QrCode\\Builder\\Builder'; $v5WriterClass = '\\Endroid\\QrCode\\Writer\\PngWriter'; $v5Encoding = '\\Endroid\\QrCode\\Encoding\\Encoding'; $writerClass = '\\Endroid\\QrCode\\Writer\\PngWriter'; $qrClass = '\\Endroid\\QrCode\\QrCode'; $chlQRCode = '\\chillerlan\\QRCode\\QRCode'; $chlQROptions = '\\chillerlan\\QRCode\\QROptions'; $chlQRMatrix = '\\chillerlan\\QRCode\\Data\\QRMatrix'; $lastErr = null; if (class_exists($v5BuilderClass) && class_exists($v5WriterClass)) { try { $builder = $v5BuilderClass::create() ->writer(new $v5WriterClass()) ->data($inviteLink); if (class_exists($v5Encoding)) { $builder->encoding(new $v5Encoding('UTF-8')); } $result = $builder->size(500)->margin(2)->build(); $result->saveToFile($path); } catch (\Throwable $e) { $lastErr = 'Endroid v5: ' . $e->getMessage(); } } if (!is_file($path) && class_exists($writerClass)) { try { if (class_exists($qrClass) && method_exists($qrClass, 'create')) { $qr = $qrClass::create($inviteLink); } else { $qr = class_exists($qrClass) ? new $qrClass($inviteLink) : null; } if ($qr) { if (method_exists($qr, 'setSize')) { $qr->setSize(500); } if (method_exists($qr, 'setMargin')) { $qr->setMargin(2); } $writer = new $writerClass(); $result = $writer->write($qr); if (method_exists($result, 'saveToFile')) { $result->saveToFile($path); } else { $raw = method_exists($result, 'getString') ? $result->getString() : (string) $result; file_put_contents($path, $raw); } } } catch (\Throwable $e) { $lastErr = 'Endroid v3/v4: ' . $e->getMessage(); } } if (!is_file($path) && class_exists($chlQRCode) && class_exists($chlQROptions)) { try { $optsArr = [ 'version' => 5, 'outputType' => constant($chlQRCode . '::OUTPUT_IMAGE_PNG'), 'scale' => 6, 'imageBase64' => false, 'quietzoneSize' => 2, ]; if (class_exists($chlQRMatrix) && defined($chlQRMatrix . '::ECC_H')) { $optsArr['eccLevel'] = constant($chlQRMatrix . '::ECC_H'); } $opts = new $chlQROptions($optsArr); $qrcode = new $chlQRCode($opts); $png = $qrcode->render($inviteLink); file_put_contents($path, $png); } catch (\Throwable $e) { $lastErr = 'chillerlan: ' . $e->getMessage(); } } if (!is_file($path) && $lastErr) { Log::error('QR generation failed', ['error' => $lastErr]); } } if (is_file($path)) { $data = @file_get_contents($path); if ($data !== false) { return 'data:image/png;base64,' . base64_encode($data); } } return $this->qrFallback($inviteLink); } private function qrFallback(string $inviteLink): string { $link = rawurlencode($inviteLink); return 'https://api.qrserver.com/v1/create-qr-code/?size=260x260&qzone=2&data=' . $link; } }