freescout-github/Http/Controllers/GithubController.php
Jack Arturo ebf62ddccc fix: handle Option::get returning array instead of JSON string
FreeScout's Option::get() may return already-decoded arrays for
JSON-stored values. Now handle both string and array cases.
2025-11-24 22:44:47 +01:00

1084 lines
No EOL
41 KiB
PHP

<?php
namespace Modules\Github\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Github\Services\GithubApiClient;
use Modules\Github\Services\IssueContentGenerator;
use Modules\Github\Services\LabelAssignmentService;
use Modules\Github\Entities\GithubIssue;
use Modules\Github\Entities\GithubLabelMapping;
use App\Conversation;
use Modules\Github\Support\RepositoryCache;
class GithubController extends Controller
{
/**
* Normalize request values to boolean for older Laravel versions.
*
* @param mixed $value
* @param bool $default
* @return bool
*/
private function toBoolean($value, bool $default = false): bool
{
if (is_null($value)) {
return $default;
}
if (is_bool($value)) {
return $value;
}
if (is_string($value)) {
$value = strtolower(trim($value));
if ($value === '') {
return $default;
}
return in_array($value, ['1', 'true', 'on', 'yes'], true);
}
if (is_numeric($value)) {
return (int) $value === 1;
}
return $default;
}
/**
* Test GitHub connection
*/
public function testConnection(Request $request)
{
try {
$token = $request->input('token');
if (!$token) {
return response()->json([
'status' => 'error',
'message' => 'GitHub token is required'
]);
}
$result = GithubApiClient::testConnection($token);
if ($result['status'] === 'success') {
return response()->json([
'status' => 'success',
'message' => $result['message'],
'repositories' => $result['data']['repositories'] ?? []
]);
} else {
return response()->json([
'status' => 'error',
'message' => $result['message']
]);
}
} catch (\Exception $e) {
return response()->json([
'status' => 'error',
'message' => 'Connection test failed: ' . $e->getMessage()
]);
}
}
/**
* Get user repositories
*/
public function getRepositories(Request $request)
{
try {
if ($this->toBoolean($request->input('refresh'))) {
RepositoryCache::clear();
}
$result = RepositoryCache::getRepositories(true);
if ($result['status'] === 'success') {
return response()->json([
'status' => 'success',
'repositories' => $result['repositories'],
'source' => $result['source'] ?? 'cache',
'fetched_at' => $result['fetched_at'] ?? null,
]);
}
if ($result['status'] === 'throttled') {
return response()->json([
'status' => 'throttled',
'message' => $result['message'],
'retry_after' => $result['retry_after'] ?? 60,
], 429);
}
return response()->json([
'status' => 'error',
'message' => $result['message'] ?? 'Failed to load repositories',
], 400);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Get Repositories Error');
return response()->json([
'status' => 'error',
'message' => 'Failed to fetch repositories: ' . $e->getMessage()
]);
}
}
/**
* Refresh repository cache.
*/
public function refreshRepositories(): \Illuminate\Http\JsonResponse
{
try {
RepositoryCache::clear();
return response()->json([
'status' => 'success',
'message' => 'Repository cache cleared.',
]);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Refresh Repositories Error');
return response()->json([
'status' => 'error',
'message' => 'Failed to clear repository cache.',
], 500);
}
}
/**
* Search repositories with cache + throttled API fallback.
*/
public function searchRepositories(Request $request): \Illuminate\Http\JsonResponse
{
try {
$query = (string) $request->input('q', $request->input('term', ''));
$limit = (int) $request->input('limit', 20);
$result = RepositoryCache::search($query, $limit);
if ($result['status'] === 'success') {
$repositories = $result['repositories'];
$formatted = array_map(function ($repo) {
return [
'id' => $repo['full_name'],
'text' => $repo['full_name'],
'name' => $repo['name'] ?? $repo['full_name'],
'private' => $repo['private'] ?? false,
'has_issues' => $repo['has_issues'] ?? true,
];
}, $repositories);
return response()->json([
'status' => 'success',
'results' => $formatted,
'meta' => [
'count' => count($formatted),
'source' => $result['source'] ?? 'cache',
'fetched_at' => $result['fetched_at'] ?? null,
'throttled' => $result['throttled'] ?? false,
'retry_after' => $result['retry_after'] ?? null,
],
]);
}
if ($result['status'] === 'throttled') {
return response()->json([
'status' => 'throttled',
'message' => $result['message'],
'retry_after' => $result['retry_after'] ?? 60,
], 429);
}
$message = $result['message'] ?? 'Failed to search repositories.';
return response()->json([
'status' => 'error',
'message' => $message,
], 400);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Search Repositories Error');
return response()->json([
'status' => 'error',
'message' => 'Failed to search repositories: ' . $e->getMessage(),
], 500);
}
}
/**
* Get repository labels
*/
public function getLabels(Request $request, $repository)
{
try {
$repository = urldecode($repository);
$result = GithubApiClient::getLabels($repository);
if ($result['status'] === 'success') {
return response()->json([
'status' => 'success',
'data' => $result['data'] ?? $result['labels'] ?? []
]);
} else {
return response()->json([
'status' => 'error',
'message' => $result['message'] ?? 'Failed to load labels'
], 400);
}
} catch (\Exception $e) {
return response()->json([
'status' => 'error',
'message' => 'Failed to load labels: ' . $e->getMessage()
], 500);
}
}
/**
* Search GitHub issues
*/
public function searchIssues(Request $request)
{
$request->validate([
'repository' => 'required|string',
'query' => 'nullable|string',
'state' => 'nullable|string|in:open,closed,all',
'per_page' => 'nullable|integer|min:1|max:100'
]);
try {
$repository = $request->get('repository');
$query = $request->get('query', '');
$state = $request->get('state', 'open');
$per_page = $request->get('per_page', 20);
$result = GithubApiClient::searchIssues($repository, $query, $state, $per_page);
if ($result['status'] === 'success') {
return response()->json([
'status' => 'success',
'issues' => $result['data'] ?? []
]);
} else {
return response()->json([
'status' => 'error',
'message' => $result['message'] ?? 'Failed to search issues'
], 400);
}
} catch (\Exception $e) {
return response()->json([
'status' => 'error',
'message' => 'Failed to search issues: ' . $e->getMessage()
], 500);
}
}
/**
* Get issue details
*/
public function getIssueDetails(Request $request, $id)
{
$issue = GithubIssue::with('conversations')->find($id);
if (!$issue) {
return response()->json([
'status' => 'error',
'message' => 'Issue not found'
], 404);
}
return response()->json([
'status' => 'success',
'data' => $issue
]);
}
/**
* Refresh issue data from GitHub API
*/
public function refreshIssue(Request $request, $id)
{
$issue = GithubIssue::find($id);
if (!$issue) {
return response()->json([
'status' => 'error',
'message' => 'Issue not found'
], 404);
}
try {
// Fetch fresh data from GitHub API
$result = GithubApiClient::getIssue($issue->repository, $issue->number);
if ($result['status'] === 'success') {
// Update local issue with fresh data
$updatedIssue = GithubIssue::createOrUpdateFromGithub($result['data'], $issue->repository);
return response()->json([
'status' => 'success',
'message' => 'Issue refreshed successfully',
'data' => $updatedIssue
]);
} else {
return response()->json([
'status' => 'error',
'message' => $result['message'] ?? 'Failed to refresh issue from GitHub'
], 400);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Refresh Issue Error');
return response()->json([
'status' => 'error',
'message' => 'An error occurred while refreshing the issue'
], 500);
}
}
/**
* Refresh all issues for a conversation with intelligent caching
*/
public function refreshConversationIssues(Request $request)
{
$request->validate([
'conversation_id' => 'required|integer|exists:conversations,id'
]);
$conversation = \App\Conversation::findOrFail($request->get('conversation_id'));
// Permission check
try {
if (method_exists($conversation, 'userCanUpdate') && !$conversation->userCanUpdate()) {
return response()->json([
'status' => 'error',
'message' => 'You do not have permission to refresh issues for this conversation'
], 403);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Permission Check Error');
}
try {
$issues = GithubIssue::conversationLinkedIssues($conversation->id);
$refreshedIssues = [];
$cacheTime = now()->timestamp;
foreach ($issues as $issue) {
// Check if issue was recently refreshed (within 5 minutes)
$cacheKey = "github_issue_refresh_{$issue->id}";
$lastRefresh = \Cache::get($cacheKey, 0);
if ($cacheTime - $lastRefresh < 300) { // 5 minutes
// Use cached data
$refreshedIssues[] = $issue;
continue;
}
// Fetch fresh data from GitHub
$result = GithubApiClient::getIssue($issue->repository, $issue->number);
if ($result['status'] === 'success') {
$updatedIssue = GithubIssue::createOrUpdateFromGithub($result['data'], $issue->repository);
$refreshedIssues[] = $updatedIssue;
// Cache the refresh timestamp
\Cache::put($cacheKey, $cacheTime, 300); // Cache for 5 minutes
} else {
// If refresh fails, still include the existing issue
$refreshedIssues[] = $issue;
\Log::warning("GitHub: Failed to refresh issue {$issue->id}: " . ($result['message'] ?? 'Unknown error'));
}
}
return response()->json([
'status' => 'success',
'message' => 'Issues refreshed successfully',
'data' => $refreshedIssues
]);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Refresh Conversation Issues Error');
return response()->json([
'status' => 'error',
'message' => 'An error occurred while refreshing issues'
], 500);
}
}
/**
* Create new GitHub issue
*/
public function createIssue(Request $request)
{
// Increase execution time for AI generation (GPT-5 can be slow)
set_time_limit(90);
$request->validate([
'conversation_id' => 'required|integer|exists:conversations,id',
'repository' => 'required|string',
'title' => 'nullable|string|max:255',
'body' => 'nullable|string',
'labels' => 'nullable|array',
'assignees' => 'nullable|array',
'watchers' => 'nullable|array'
]);
$conversation = \App\Conversation::with('customer')->findOrFail($request->get('conversation_id'));
// Permission check
try {
if (method_exists($conversation, 'userCanUpdate') && !$conversation->userCanUpdate()) {
return response()->json([
'status' => 'error',
'message' => 'You do not have permission to create issues for this conversation'
], 403);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Permission Check Error');
}
$repository = $request->get('repository');
$title = $request->get('title');
$body = $request->get('body');
$labels = $request->get('labels', []) ?: [];
$assignees = $request->get('assignees', []) ?: [];
$watchers = $request->get('watchers', []) ?: [];
try {
// Check global settings for auto-generation
$aiEnabled = !empty(\Option::get('github.ai_service')) && !empty(\Option::get('github.ai_api_key'));
// Check if auto-assign labels is enabled (if any allowed labels are configured)
$allowedLabelsJson = \Option::get('github.allowed_labels', '[]');
$allowedLabels = is_array($allowedLabelsJson) ? $allowedLabelsJson : (json_decode($allowedLabelsJson, true) ?: []);
$autoAssignLabels = !empty($allowedLabels);
// Auto-generate content if AI is enabled and fields are empty
if ($aiEnabled && (empty($title) || empty($body))) {
try {
// Use allowed labels from settings instead of fetching from API
$allowedLabelsJson = \Option::get('github.allowed_labels', '[]');
$availableLabels = is_array($allowedLabelsJson) ? $allowedLabelsJson : (json_decode($allowedLabelsJson, true) ?: []);
$contentGenerator = new IssueContentGenerator();
$generatedContent = $contentGenerator->generateContent($conversation, $availableLabels);
$title = $title ?: $generatedContent['title'];
$body = $body ?: $generatedContent['body'];
// Use AI-suggested labels if auto-assign is enabled and no labels provided
if ($autoAssignLabels && empty($labels) && isset($generatedContent['suggested_labels']) && is_array($generatedContent['suggested_labels'])) {
$labels = $generatedContent['suggested_labels'];
\Helper::log('github_controller', 'Using AI-suggested labels: ' . implode(', ', $labels));
}
} catch (\Exception $e) {
// Log the error but return it to frontend for display
\Helper::log('github_controller_error', 'AI content generation failed: ' . $e->getMessage());
return response()->json([
'status' => 'error',
'message' => $e->getMessage()
], 400);
}
}
// Note: Label assignment is now handled above in the AI content generation
// to avoid duplicate AI calls. Labels are already assigned from generatedContent['suggested_labels']
// Create the issue (pass watchers for @mention auto-subscription)
$result = GithubApiClient::createIssue($repository, $title, $body, $labels, $assignees, $watchers);
if ($result['status'] === 'success') {
// Link the issue to the conversation
$issue = $result['issue'];
$issue->linkToConversation($conversation->id);
// Add system note to conversation
\App\Thread::create($conversation, \App\Thread::TYPE_NOTE, "GitHub issue created: <a href=\"{$result['data']['html_url']}\" target=\"_blank\">#{$result['data']['number']} {$result['data']['title']}</a>", [
'created_by_user_id' => auth()->id(),
'source_via' => \App\Thread::PERSON_USER
]);
return response()->json([
'status' => 'success',
'message' => 'Issue created successfully',
'data' => $result['data'],
'issue' => $issue
]);
} else {
return response()->json($result, 400);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Create Issue Error');
return response()->json([
'status' => 'error',
'message' => 'An error occurred while creating the issue'
], 500);
}
}
/**
* Link existing GitHub issue to conversation
*/
public function linkIssue(Request $request)
{
$request->validate([
'conversation_id' => 'required|integer|exists:conversations,id',
'repository' => 'required|string',
'issue_number' => 'required|integer'
]);
$conversation = \App\Conversation::with('customer')->findOrFail($request->get('conversation_id'));
// Permission check
try {
if (method_exists($conversation, 'userCanUpdate') && !$conversation->userCanUpdate()) {
return response()->json([
'status' => 'error',
'message' => 'You do not have permission to link issues to this conversation'
], 403);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Permission Check Error');
}
$repository = $request->get('repository');
$issueNumber = $request->get('issue_number');
try {
// Get issue from GitHub
$result = GithubApiClient::getIssue($repository, $issueNumber);
if ($result['status'] === 'success') {
// Create or update local issue
$issue = GithubIssue::createOrUpdateFromGithub($result['data'], $repository);
// Link to conversation
$linked = $issue->linkToConversation($conversation->id);
if ($linked) {
// Add system note to conversation
\App\Thread::create($conversation, \App\Thread::TYPE_NOTE, "GitHub issue linked: <a href=\"{$issue->html_url}\" target=\"_blank\">#{$issue->number} {$issue->title}</a>", [
'created_by_user_id' => auth()->id(),
'source_via' => \App\Thread::PERSON_USER
]);
return response()->json([
'status' => 'success',
'message' => 'Issue linked successfully',
'issue' => $issue
]);
} else {
return response()->json([
'status' => 'error',
'message' => 'Issue is already linked to this conversation'
], 400);
}
} else {
return response()->json($result, 400);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Link Issue Error');
return response()->json([
'status' => 'error',
'message' => 'An error occurred while linking the issue'
], 500);
}
}
/**
* Unlink GitHub issue from conversation
*/
public function unlinkIssue(Request $request)
{
$request->validate([
'conversation_id' => 'required|integer|exists:conversations,id',
'issue_id' => 'required|integer'
]);
$conversation = \App\Conversation::with('customer')->findOrFail($request->get('conversation_id'));
// Permission check
try {
if (method_exists($conversation, 'userCanUpdate') && !$conversation->userCanUpdate()) {
return response()->json([
'status' => 'error',
'message' => 'You do not have permission to unlink issues from this conversation'
], 403);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Permission Check Error');
}
$issue = GithubIssue::findOrFail($request->get('issue_id'));
try {
$unlinked = $issue->unlinkFromConversation($conversation->id);
if ($unlinked) {
// Add system note to conversation
\App\Thread::create($conversation, \App\Thread::TYPE_NOTE, "GitHub issue unlinked: #{$issue->number} {$issue->title}", [
'created_by_user_id' => auth()->id(),
'source_via' => \App\Thread::PERSON_USER
]);
return response()->json([
'status' => 'success',
'message' => 'Issue unlinked successfully'
]);
} else {
return response()->json([
'status' => 'error',
'message' => 'Issue is not linked to this conversation'
], 400);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Unlink Issue Error');
return response()->json([
'status' => 'error',
'message' => 'An error occurred while unlinking the issue'
], 500);
}
}
/**
* Get label mappings for repository
*/
public function getLabelMappings(Request $request)
{
$request->validate([
'repository' => 'required|string'
]);
$repository = $request->get('repository');
$mappings = GithubLabelMapping::getRepositoryMappings($repository);
return response()->json([
'status' => 'success',
'data' => $mappings
]);
}
/**
* Save label mappings for repository
*/
public function saveLabelMappings(Request $request)
{
$request->validate([
'repository' => 'required|string',
'mappings' => 'nullable|array',
'mappings.*.freescout_tag' => 'required_with:mappings|string',
'mappings.*.github_label' => 'required_with:mappings|string',
'mappings.*.confidence_threshold' => 'nullable|numeric|min:0|max:1'
]);
$repository = $request->get('repository');
$mappings = collect($request->get('mappings', []))
->map(function ($mapping) {
return [
'freescout_tag' => trim($mapping['freescout_tag'] ?? ''),
'github_label' => trim($mapping['github_label'] ?? ''),
'confidence_threshold' => isset($mapping['confidence_threshold'])
? (float) $mapping['confidence_threshold']
: 0.80,
];
})
->filter(function ($mapping) {
return !empty($mapping['freescout_tag']) && !empty($mapping['github_label']);
})
->values();
try {
$activeTags = $mappings->pluck('freescout_tag')->unique()->all();
if (empty($activeTags)) {
GithubLabelMapping::where('repository', $repository)->delete();
} else {
GithubLabelMapping::where('repository', $repository)
->whereNotIn('freescout_tag', $activeTags)
->delete();
}
foreach ($mappings as $mapping) {
GithubLabelMapping::createOrUpdateMapping(
$mapping['freescout_tag'],
$mapping['github_label'],
$repository,
$mapping['confidence_threshold'] ?? 0.80
);
}
return response()->json([
'status' => 'success',
'message' => 'Label mappings saved successfully',
'data' => GithubLabelMapping::getRepositoryMappings($repository)
]);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Save Label Mappings Error');
return response()->json([
'status' => 'error',
'message' => 'An error occurred while saving label mappings'
], 500);
}
}
/**
* Handle GitHub webhook
*/
public function webhook(Request $request)
{
// Verify webhook signature if secret is configured
$secret = \Option::get('github.webhook_secret');
if ($secret) {
$signature = $request->header('X-Hub-Signature-256');
$payload = $request->getContent();
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expectedSignature, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
}
$event = $request->header('X-GitHub-Event');
$payload = $request->json()->all();
try {
// Handle different webhook events
switch ($event) {
case 'issues':
$result = GithubApiClient::handleWebhook($payload);
break;
case 'ping':
return response()->json(['message' => 'pong']);
default:
return response()->json(['message' => 'Event not handled'], 200);
}
return response()->json($result);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Webhook Error');
return response()->json(['error' => 'Internal server error'], 500);
}
}
/**
* Save GitHub settings from settings page
*/
public function saveSettings(Request $request)
{
try {
// Basic validation to prevent fatal errors
if (!$request->has('settings')) {
return redirect()->back()->with('error', 'No settings data received.');
}
$settings = $request->input('settings', []);
// Validate settings is an array
if (!is_array($settings)) {
return redirect()->back()->with('error', 'Invalid settings format.');
}
$allowed = [
'github.token',
'github.default_repository',
'github.webhook_secret',
'github.organizations',
'github.ai_service',
'github.ai_api_key',
'github.openai_model',
'github.ai_prompt_template',
'github.manual_template',
'github.create_remote_link',
'github.allowed_labels',
];
// Handle user mappings separately (with defensive coding)
try {
$userMappings = $request->input('user_mappings', []);
$cleanedMappings = [];
if (is_array($userMappings)) {
foreach ($userMappings as $userId => $mapping) {
// Skip if mapping is not an array (defensive)
if (!is_array($mapping)) {
continue;
}
$githubUsername = isset($mapping['github_username']) ? trim($mapping['github_username']) : '';
if (!empty($githubUsername)) {
$cleanedMappings[$userId] = [
'user_id' => (int) $userId,
'name' => isset($mapping['name']) ? $mapping['name'] : '',
'github_username' => $githubUsername,
];
}
}
}
\Option::set('github.user_mappings', json_encode($cleanedMappings));
} catch (\Exception $e) {
\Helper::log('github_settings', 'Error saving user mappings: ' . $e->getMessage());
}
foreach ($allowed as $key) {
try {
if (array_key_exists($key, $settings)) {
$value = $settings[$key];
// Handle special cases
if ($key === 'github.create_remote_link') {
// Checkbox values: if not set, set to 0
$value = $value ? 1 : 0;
} elseif ($key === 'github.allowed_labels') {
// Array of allowed labels - store as JSON
if (is_array($value)) {
// Validate that all values are strings and not empty
$cleanedValue = [];
foreach ($value as $label) {
if (is_string($label) && !empty(trim($label))) {
$cleanedValue[] = trim($label);
}
}
$value = json_encode(array_values($cleanedValue));
} else {
$value = '[]';
}
}
\Option::set($key, $value);
} else if ($key === 'github.create_remote_link') {
// Unchecked checkbox
\Option::set($key, 0);
} elseif ($key === 'github.allowed_labels') {
// No labels selected - store empty array
\Option::set($key, '[]');
}
} catch (\Exception $e) {
\Helper::log('github_settings', 'Error setting ' . $key . ': ' . $e->getMessage());
// Continue with other settings even if one fails
}
}
return redirect()->back()->with('success', __('Settings saved.'));
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Settings Save Error');
return redirect()->back()->with('error', 'Failed to save settings: ' . $e->getMessage());
}
}
/**
* Generate AI content for issue
*/
public function generateContent(Request $request)
{
// Increase execution time for AI generation (GPT-5 can be slow)
set_time_limit(90);
try {
$request->validate([
'conversation_id' => 'required|integer|exists:conversations,id'
]);
} catch (\Illuminate\Validation\ValidationException $e) {
return response()->json([
'status' => 'error',
'message' => 'Invalid conversation ID',
'errors' => $e->errors()
], 422);
}
try {
$conversationId = $request->get('conversation_id');
$conversation = \App\Conversation::with(['customer', 'threads'])->find($conversationId);
if (!$conversation) {
return response()->json([
'status' => 'error',
'message' => 'Conversation not found'
], 404);
}
} catch (\Exception $e) {
\Log::error('[GitHub] Error loading conversation', [
'error' => $e->getMessage(),
'conversation_id' => $request->get('conversation_id')
]);
return response()->json([
'status' => 'error',
'message' => 'Error loading conversation'
], 500);
}
// Permission check
try {
if (method_exists($conversation, 'userCanUpdate') && !$conversation->userCanUpdate()) {
return response()->json([
'status' => 'error',
'message' => 'You do not have permission to access this conversation'
], 403);
}
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Permission Check Error');
}
try {
// Use allowed labels from settings instead of fetching from API
$allowedLabelsJson = \Option::get('github.allowed_labels', '[]');
$availableLabels = is_array($allowedLabelsJson) ? $allowedLabelsJson : (json_decode($allowedLabelsJson, true) ?: []);
$contentGenerator = new IssueContentGenerator();
$generatedContent = $contentGenerator->generateContent($conversation, $availableLabels);
return response()->json([
'status' => 'success',
'data' => $generatedContent
]);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Generate Content Error');
// Return the actual error message for API errors, generic message for others
$errorMessage = 'Failed to generate content';
if (strpos($e->getMessage(), 'Failed to generate content:') === 0) {
$errorMessage = $e->getMessage();
} else {
$errorMessage .= ': ' . $e->getMessage();
}
return response()->json([
'status' => 'error',
'message' => $errorMessage
], 500);
}
}
/**
* Get user mappings for the watchers dropdown
* Returns ALL active FreeScout users with their GitHub mappings (if any)
*/
public function getUserMappings()
{
try {
$rawMappings = \Option::get('github.user_mappings', '{}');
$userMappings = is_array($rawMappings) ? $rawMappings : (json_decode($rawMappings, true) ?: []);
$currentUserId = auth()->id();
// Get ALL active FreeScout users and include their GitHub mappings
$users = \App\User::where('status', \App\User::STATUS_ACTIVE)->orderBy('first_name')->get();
$mappings = [];
foreach ($users as $user) {
$githubUsername = $userMappings[$user->id]['github_username'] ?? '';
// Only include users who have a GitHub username mapped
if (!empty($githubUsername)) {
$mappings[] = [
'user_id' => (int) $user->id,
'name' => $user->getFullName(),
'github_username' => $githubUsername,
'is_current_user' => ((int) $user->id === $currentUserId),
];
}
}
return response()->json([
'status' => 'success',
'data' => $mappings,
'current_user_id' => $currentUserId,
]);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Get User Mappings Error');
return response()->json([
'status' => 'error',
'message' => 'Failed to load user mappings: ' . $e->getMessage()
], 500);
}
}
/**
* Test token and show detailed API responses
*/
public function testToken()
{
try {
$results = [];
// Test 1: Basic user info
$userResponse = GithubApiClient::apiCall('user');
$results['user'] = $userResponse;
// Test 2: User repositories
$userReposResponse = GithubApiClient::apiCall('user/repos', [
'per_page' => 10,
'type' => 'all'
]);
$results['user_repos'] = [
'status' => $userReposResponse['status'],
'count' => $userReposResponse['status'] === 'success' ? count($userReposResponse['data']) : 0,
'repos' => $userReposResponse['status'] === 'success' ? array_map(function($repo) {
return ['full_name' => $repo['full_name'], 'private' => $repo['private']];
}, array_slice($userReposResponse['data'], 0, 5)) : []
];
// Test 3: Organizations
$orgsResponse = GithubApiClient::apiCall('user/orgs');
$results['organizations'] = [
'status' => $orgsResponse['status'],
'count' => $orgsResponse['status'] === 'success' ? count($orgsResponse['data']) : 0,
'orgs' => $orgsResponse['status'] === 'success' ? array_map(function($org) {
return $org['login'];
}, $orgsResponse['data']) : []
];
// Test 4: Specific org repos (verygoodplugins)
$vgpReposResponse = GithubApiClient::apiCall('orgs/verygoodplugins/repos', [
'per_page' => 10,
'type' => 'all'
]);
$results['verygoodplugins_repos'] = [
'status' => $vgpReposResponse['status'],
'message' => $vgpReposResponse['message'] ?? null,
'count' => $vgpReposResponse['status'] === 'success' ? count($vgpReposResponse['data']) : 0,
'repos' => $vgpReposResponse['status'] === 'success' ? array_map(function($repo) {
return ['full_name' => $repo['full_name'], 'private' => $repo['private']];
}, array_slice($vgpReposResponse['data'], 0, 5)) : []
];
// Test 5: Check specific repo
$wpFusionResponse = GithubApiClient::apiCall('repos/verygoodplugins/wp-fusion');
$results['wp_fusion_direct'] = [
'status' => $wpFusionResponse['status'],
'message' => $wpFusionResponse['message'] ?? null,
'exists' => $wpFusionResponse['status'] === 'success',
'private' => $wpFusionResponse['status'] === 'success' ? $wpFusionResponse['data']['private'] : null,
'has_issues' => $wpFusionResponse['status'] === 'success' ? $wpFusionResponse['data']['has_issues'] : null
];
return response()->json($results, 200, [], JSON_PRETTY_PRINT);
} catch (\Exception $e) {
return response()->json([
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
], 500);
}
}
}