From faa7ea9e4f404743d28d6874b223f0fe051f958f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:49:37 +0000 Subject: [PATCH 1/7] Initial plan From 4661604cdca69e9725255f41458994e514e4bbe4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:00:14 +0000 Subject: [PATCH 2/7] Add complete FreeScout Knowledge Base API module with JavaScript and Python clients Co-authored-by: gabeparra <29493146+gabeparra@users.noreply.github.com> --- INSTALLATION.md | 316 +++++++++++ Modules/KnowledgeBaseAPI/Config/config.php | 20 + .../KnowledgeBaseAPIController.php | 532 ++++++++++++++++++ .../KnowledgeBaseAPIServiceProvider.php | 115 ++++ .../Providers/RouteServiceProvider.php | 53 ++ Modules/KnowledgeBaseAPI/README.md | 289 ++++++++++ Modules/KnowledgeBaseAPI/Routes/api.php | 39 ++ Modules/KnowledgeBaseAPI/composer.json | 28 + Modules/KnowledgeBaseAPI/module.json | 20 + README.md | 274 ++++++++- backend/GlobalDeskClient.py | 183 ++++-- backend/main.py | 60 +- frontend/demo.html | 500 ++++++++++++++++ frontend/knowledge-base-client.js | 432 ++++++++++++++ 14 files changed, 2801 insertions(+), 60 deletions(-) create mode 100644 INSTALLATION.md create mode 100644 Modules/KnowledgeBaseAPI/Config/config.php create mode 100644 Modules/KnowledgeBaseAPI/Http/Controllers/KnowledgeBaseAPIController.php create mode 100644 Modules/KnowledgeBaseAPI/Providers/KnowledgeBaseAPIServiceProvider.php create mode 100644 Modules/KnowledgeBaseAPI/Providers/RouteServiceProvider.php create mode 100644 Modules/KnowledgeBaseAPI/README.md create mode 100644 Modules/KnowledgeBaseAPI/Routes/api.php create mode 100644 Modules/KnowledgeBaseAPI/composer.json create mode 100644 Modules/KnowledgeBaseAPI/module.json create mode 100644 frontend/demo.html create mode 100644 frontend/knowledge-base-client.js diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..d517026 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,316 @@ +# Knowledge Base API Installation Guide + +This guide will walk you through installing and configuring the FreeScout Knowledge Base API module. + +## Prerequisites + +Before you begin, ensure you have: + +1. ✅ FreeScout installed and running (version 1.8.0 or higher) +2. ✅ Knowledge Base feature enabled in FreeScout +3. ✅ Admin access to your FreeScout installation +4. ✅ SSH/FTP access to your server (for file uploads) + +## Installation Steps + +### Step 1: Upload the Module + +There are two ways to install the module: + +#### Option A: Direct Copy + +1. Download or clone this repository +2. Copy the `Modules/KnowledgeBaseAPI` folder to your FreeScout's `Modules` directory + +```bash +# On your server +cd /path/to/freescout +cp -r /path/to/GlobalDeskModules/Modules/KnowledgeBaseAPI ./Modules/ +``` + +#### Option B: Symbolic Link (for development) + +If you're developing or want to keep the module updated via git: + +```bash +# On your server +cd /path/to/freescout/Modules +ln -s /path/to/GlobalDeskModules/Modules/KnowledgeBaseAPI ./KnowledgeBaseAPI +``` + +### Step 2: Set Permissions + +Ensure the web server can read the module files: + +```bash +cd /path/to/freescout +chown -R www-data:www-data Modules/KnowledgeBaseAPI +chmod -R 755 Modules/KnowledgeBaseAPI +``` + +(Replace `www-data` with your web server user if different, e.g., `apache`, `nginx`, etc.) + +### Step 3: Activate the Module + +1. Log in to your FreeScout admin panel +2. Navigate to **Manage → Modules** +3. Scroll down to find "KnowledgeBaseAPI" +4. Click the **Activate** button + +### Step 4: Verify Installation + +Test that the API is working: + +```bash +# Test the health endpoint (no authentication required) +curl https://your-freescout.com/api/kb/health + +# Expected response: +# {"status":"ok","module":"KnowledgeBaseAPI","version":"1.0.0"} +``` + +### Step 5: Configure the Module (Optional) + +Edit the configuration file if needed: + +```bash +nano /path/to/freescout/Modules/KnowledgeBaseAPI/Config/config.php +``` + +Available options: +- `enabled`: Enable/disable the API +- `rate_limit`: Requests per minute (default: 60) +- `require_auth`: Require authentication for protected endpoints +- `cors_origins`: Allowed CORS origins (default: '*') +- `include_content_in_list`: Include full article content in list responses + +### Step 6: Clear Cache + +Clear FreeScout's cache to ensure the module is properly loaded: + +```bash +cd /path/to/freescout +php artisan cache:clear +php artisan config:clear +php artisan route:clear +``` + +## Testing the API + +### Test Public Endpoints (No Authentication) + +```bash +# List categories +curl https://your-freescout.com/api/kb/public/categories + +# List articles +curl https://your-freescout.com/api/kb/public/articles + +# Search +curl "https://your-freescout.com/api/kb/public/search?q=password" +``` + +### Test Protected Endpoints (With API Key) + +```bash +# Get your API key from FreeScout: Profile → API Settings + +# List all categories (authenticated) +curl -H "X-FreeScout-API-Key: YOUR_API_KEY" \ + https://your-freescout.com/api/kb/categories + +# List all articles (authenticated) +curl -H "X-FreeScout-API-Key: YOUR_API_KEY" \ + https://your-freescout.com/api/kb/articles +``` + +## Using the JavaScript Client + +### 1. Include the JavaScript file + +```html + +``` + +### 2. Initialize and use + +```javascript +const client = new FreeScoutKBClient('https://your-freescout.com'); + +// Get categories +client.getCategories().then(categories => { + console.log('Categories:', categories); +}); + +// Search +client.search('password').then(results => { + console.log('Search results:', results); +}); +``` + +### 3. Use the complete widget + +```html +
+ + +``` + +## Using the Python Client + +### 1. Install dependencies + +```bash +cd backend +pip install -r requirements.txt +``` + +### 2. Configure environment + +Create a `.env` file: + +```env +FREESCOUT_BASE_URL=https://your-freescout.com +FREESCOUT_API_KEY=your_api_key_here +``` + +### 3. Use the client + +```python +from GlobalDeskClient import FreeScoutClient + +client = FreeScoutClient() + +# Get categories +categories = client.list_kb_categories(public=True) +print(f"Found {len(categories)} categories") + +# Get articles +articles = client.list_kb_articles(public=True, per_page=10) +print(f"Found {len(articles.get('data', []))} articles") + +# Search +results = client.search_kb_articles("password", public=True) +print(f"Found {len(results)} search results") +``` + +## Troubleshooting + +### Module doesn't appear in FreeScout + +**Solution:** +1. Check file permissions: `ls -la /path/to/freescout/Modules/KnowledgeBaseAPI` +2. Ensure `module.json` exists and is valid JSON +3. Clear cache: `php artisan cache:clear` +4. Check FreeScout logs: `storage/logs/laravel.log` + +### "Table kb_categories doesn't exist" error + +**Solution:** +This module requires a Knowledge Base feature/module to be installed in FreeScout that creates the necessary database tables. Check if you have: +- A Knowledge Base module enabled +- Database tables: `kb_categories` and `kb_articles` + +If not, you may need to install a Knowledge Base module first or create these tables manually. + +### API returns empty results + +**Solution:** +1. Check if you have any knowledge base articles created in FreeScout +2. Ensure articles are marked as "published" and "public" (for public endpoints) +3. Check the database: `SELECT * FROM kb_articles;` + +### CORS errors in JavaScript + +**Solution:** +1. Update `Config/config.php` in the module: + ```php + 'cors_origins' => ['https://yourdomain.com'], + ``` +2. Or allow all origins during development: + ```php + 'cors_origins' => ['*'], + ``` +3. Clear cache after changes + +### 401 Unauthorized errors + +**Solution:** +1. For protected endpoints, ensure you're sending the API key: + ``` + X-FreeScout-API-Key: your_api_key_here + ``` +2. Verify your API key is correct in FreeScout: Profile → API Settings +3. Use public endpoints if you don't need authentication + +## Uninstallation + +To remove the module: + +1. Deactivate the module in FreeScout admin panel +2. Delete the module directory: + ```bash + rm -rf /path/to/freescout/Modules/KnowledgeBaseAPI + ``` +3. Clear cache: + ```bash + php artisan cache:clear + php artisan config:clear + php artisan route:clear + ``` + +## Database Schema + +If you need to create the tables manually, here's the basic schema: + +```sql +CREATE TABLE kb_categories ( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + description TEXT, + slug VARCHAR(255), + parent_id INT NULL, + `order` INT DEFAULT 0, + visibility ENUM('public', 'private') DEFAULT 'public', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (parent_id) REFERENCES kb_categories(id) ON DELETE SET NULL +); + +CREATE TABLE kb_articles ( + id INT PRIMARY KEY AUTO_INCREMENT, + category_id INT NOT NULL, + title VARCHAR(255) NOT NULL, + slug VARCHAR(255), + content TEXT, + excerpt TEXT, + status ENUM('draft', 'published') DEFAULT 'draft', + visibility ENUM('public', 'private') DEFAULT 'public', + `order` INT DEFAULT 0, + views INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES kb_categories(id) ON DELETE CASCADE +); +``` + +## Getting Help + +- **GitHub Issues**: https://github.com/gabeparra/GlobalDeskModules/issues +- **FreeScout Forums**: https://freescout.net/community/ +- **Documentation**: See README.md files in each directory + +## Next Steps + +1. ✅ Install the module (you're here!) +2. 📝 Create some knowledge base articles in FreeScout +3. 🧪 Test the API endpoints +4. 🎨 Integrate the JavaScript widget into your website +5. 🚀 Start using the API in your applications + +Happy coding! 🎉 diff --git a/Modules/KnowledgeBaseAPI/Config/config.php b/Modules/KnowledgeBaseAPI/Config/config.php new file mode 100644 index 0000000..51dd8ea --- /dev/null +++ b/Modules/KnowledgeBaseAPI/Config/config.php @@ -0,0 +1,20 @@ + 'KnowledgeBaseAPI', + + // Enable or disable the API + 'enabled' => true, + + // API rate limiting (requests per minute) + 'rate_limit' => 60, + + // Require authentication for API access + 'require_auth' => true, + + // Allowed origins for CORS + 'cors_origins' => ['*'], + + // Include article content in list responses + 'include_content_in_list' => false, +]; diff --git a/Modules/KnowledgeBaseAPI/Http/Controllers/KnowledgeBaseAPIController.php b/Modules/KnowledgeBaseAPI/Http/Controllers/KnowledgeBaseAPIController.php new file mode 100644 index 0000000..e8f658c --- /dev/null +++ b/Modules/KnowledgeBaseAPI/Http/Controllers/KnowledgeBaseAPIController.php @@ -0,0 +1,532 @@ +json([ + 'status' => 'ok', + 'module' => 'KnowledgeBaseAPI', + 'version' => '1.0.0' + ]); + } + + /** + * List all knowledge base categories + * + * @param Request $request + * @return JsonResponse + */ + public function listCategories(Request $request): JsonResponse + { + try { + $categories = DB::table('kb_categories') + ->select('id', 'name', 'description', 'slug', 'parent_id', 'order', 'created_at', 'updated_at') + ->orderBy('order', 'asc') + ->orderBy('name', 'asc') + ->get(); + + // Build hierarchical structure + $categoriesTree = $this->buildCategoryTree($categories); + + return response()->json([ + 'success' => true, + 'data' => $categoriesTree, + 'count' => $categories->count() + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to fetch categories', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * Get a specific category by ID + * + * @param int $id + * @return JsonResponse + */ + public function getCategory(int $id): JsonResponse + { + try { + $category = DB::table('kb_categories') + ->where('id', $id) + ->first(); + + if (!$category) { + return response()->json([ + 'success' => false, + 'error' => 'Category not found' + ], 404); + } + + // Get articles in this category + $articles = DB::table('kb_articles') + ->where('category_id', $id) + ->where('status', 'published') + ->select('id', 'title', 'slug', 'excerpt', 'views', 'created_at', 'updated_at') + ->orderBy('order', 'asc') + ->orderBy('title', 'asc') + ->get(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'category' => $category, + 'articles' => $articles, + 'article_count' => $articles->count() + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to fetch category', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * List all knowledge base articles + * + * @param Request $request + * @return JsonResponse + */ + public function listArticles(Request $request): JsonResponse + { + try { + $query = DB::table('kb_articles') + ->where('status', 'published') + ->select('id', 'category_id', 'title', 'slug', 'excerpt', 'views', 'created_at', 'updated_at'); + + // Filter by category if provided + if ($request->has('category_id')) { + $query->where('category_id', $request->get('category_id')); + } + + // Pagination + $perPage = $request->get('per_page', 20); + $page = $request->get('page', 1); + + $articles = $query + ->orderBy('order', 'asc') + ->orderBy('title', 'asc') + ->skip(($page - 1) * $perPage) + ->take($perPage) + ->get(); + + $total = DB::table('kb_articles') + ->where('status', 'published') + ->when($request->has('category_id'), function($q) use ($request) { + return $q->where('category_id', $request->get('category_id')); + }) + ->count(); + + // Include category information + foreach ($articles as $article) { + $category = DB::table('kb_categories') + ->where('id', $article->category_id) + ->first(); + $article->category = $category; + } + + return response()->json([ + 'success' => true, + 'data' => $articles, + 'pagination' => [ + 'total' => $total, + 'per_page' => $perPage, + 'current_page' => $page, + 'total_pages' => ceil($total / $perPage) + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to fetch articles', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * Get a specific article by ID + * + * @param int $id + * @return JsonResponse + */ + public function getArticle(int $id): JsonResponse + { + try { + $article = DB::table('kb_articles') + ->where('id', $id) + ->where('status', 'published') + ->first(); + + if (!$article) { + return response()->json([ + 'success' => false, + 'error' => 'Article not found' + ], 404); + } + + // Get category information + $category = DB::table('kb_categories') + ->where('id', $article->category_id) + ->first(); + + // Increment view count + DB::table('kb_articles') + ->where('id', $id) + ->increment('views'); + + return response()->json([ + 'success' => true, + 'data' => [ + 'article' => $article, + 'category' => $category + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to fetch article', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * Search knowledge base articles + * + * @param Request $request + * @return JsonResponse + */ + public function search(Request $request): JsonResponse + { + try { + $query = $request->get('q', ''); + + if (empty($query)) { + return response()->json([ + 'success' => false, + 'error' => 'Search query is required' + ], 400); + } + + $articles = DB::table('kb_articles') + ->where('status', 'published') + ->where(function($q) use ($query) { + $q->where('title', 'like', '%' . $query . '%') + ->orWhere('content', 'like', '%' . $query . '%') + ->orWhere('excerpt', 'like', '%' . $query . '%'); + }) + ->select('id', 'category_id', 'title', 'slug', 'excerpt', 'views', 'created_at', 'updated_at') + ->orderBy('views', 'desc') + ->orderBy('title', 'asc') + ->limit(50) + ->get(); + + // Include category information + foreach ($articles as $article) { + $category = DB::table('kb_categories') + ->where('id', $article->category_id) + ->first(); + $article->category = $category; + } + + return response()->json([ + 'success' => true, + 'query' => $query, + 'data' => $articles, + 'count' => $articles->count() + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Search failed', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * List public categories (no authentication required) + * + * @return JsonResponse + */ + public function listPublicCategories(): JsonResponse + { + try { + $categories = DB::table('kb_categories') + ->where('visibility', 'public') + ->select('id', 'name', 'description', 'slug', 'parent_id', 'order') + ->orderBy('order', 'asc') + ->orderBy('name', 'asc') + ->get(); + + $categoriesTree = $this->buildCategoryTree($categories); + + return response()->json([ + 'success' => true, + 'data' => $categoriesTree, + 'count' => $categories->count() + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to fetch public categories', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * Get public category + * + * @param int $id + * @return JsonResponse + */ + public function getPublicCategory(int $id): JsonResponse + { + try { + $category = DB::table('kb_categories') + ->where('id', $id) + ->where('visibility', 'public') + ->first(); + + if (!$category) { + return response()->json([ + 'success' => false, + 'error' => 'Public category not found' + ], 404); + } + + $articles = DB::table('kb_articles') + ->where('category_id', $id) + ->where('status', 'published') + ->where('visibility', 'public') + ->select('id', 'title', 'slug', 'excerpt', 'views') + ->orderBy('order', 'asc') + ->orderBy('title', 'asc') + ->get(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'category' => $category, + 'articles' => $articles, + 'article_count' => $articles->count() + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to fetch public category', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * List public articles + * + * @param Request $request + * @return JsonResponse + */ + public function listPublicArticles(Request $request): JsonResponse + { + try { + $query = DB::table('kb_articles') + ->where('status', 'published') + ->where('visibility', 'public') + ->select('id', 'category_id', 'title', 'slug', 'excerpt', 'views'); + + if ($request->has('category_id')) { + $query->where('category_id', $request->get('category_id')); + } + + $perPage = $request->get('per_page', 20); + $page = $request->get('page', 1); + + $articles = $query + ->orderBy('order', 'asc') + ->orderBy('title', 'asc') + ->skip(($page - 1) * $perPage) + ->take($perPage) + ->get(); + + foreach ($articles as $article) { + $category = DB::table('kb_categories') + ->where('id', $article->category_id) + ->where('visibility', 'public') + ->first(); + $article->category = $category; + } + + $total = DB::table('kb_articles') + ->where('status', 'published') + ->where('visibility', 'public') + ->when($request->has('category_id'), function($q) use ($request) { + return $q->where('category_id', $request->get('category_id')); + }) + ->count(); + + return response()->json([ + 'success' => true, + 'data' => $articles, + 'pagination' => [ + 'total' => $total, + 'per_page' => $perPage, + 'current_page' => $page, + 'total_pages' => ceil($total / $perPage) + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to fetch public articles', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * Get public article + * + * @param int $id + * @return JsonResponse + */ + public function getPublicArticle(int $id): JsonResponse + { + try { + $article = DB::table('kb_articles') + ->where('id', $id) + ->where('status', 'published') + ->where('visibility', 'public') + ->first(); + + if (!$article) { + return response()->json([ + 'success' => false, + 'error' => 'Public article not found' + ], 404); + } + + $category = DB::table('kb_categories') + ->where('id', $article->category_id) + ->where('visibility', 'public') + ->first(); + + DB::table('kb_articles') + ->where('id', $id) + ->increment('views'); + + return response()->json([ + 'success' => true, + 'data' => [ + 'article' => $article, + 'category' => $category + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to fetch public article', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * Public search + * + * @param Request $request + * @return JsonResponse + */ + public function publicSearch(Request $request): JsonResponse + { + try { + $query = $request->get('q', ''); + + if (empty($query)) { + return response()->json([ + 'success' => false, + 'error' => 'Search query is required' + ], 400); + } + + $articles = DB::table('kb_articles') + ->where('status', 'published') + ->where('visibility', 'public') + ->where(function($q) use ($query) { + $q->where('title', 'like', '%' . $query . '%') + ->orWhere('content', 'like', '%' . $query . '%') + ->orWhere('excerpt', 'like', '%' . $query . '%'); + }) + ->select('id', 'category_id', 'title', 'slug', 'excerpt', 'views') + ->orderBy('views', 'desc') + ->orderBy('title', 'asc') + ->limit(50) + ->get(); + + foreach ($articles as $article) { + $category = DB::table('kb_categories') + ->where('id', $article->category_id) + ->where('visibility', 'public') + ->first(); + $article->category = $category; + } + + return response()->json([ + 'success' => true, + 'query' => $query, + 'data' => $articles, + 'count' => $articles->count() + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Public search failed', + 'message' => $e->getMessage() + ], 500); + } + } + + /** + * Build hierarchical category tree + * + * @param $categories + * @param int|null $parentId + * @return array + */ + private function buildCategoryTree($categories, ?int $parentId = null): array + { + $tree = []; + + foreach ($categories as $category) { + if ($category->parent_id == $parentId) { + $categoryArray = (array) $category; + $categoryArray['children'] = $this->buildCategoryTree($categories, $category->id); + $tree[] = $categoryArray; + } + } + + return $tree; + } +} diff --git a/Modules/KnowledgeBaseAPI/Providers/KnowledgeBaseAPIServiceProvider.php b/Modules/KnowledgeBaseAPI/Providers/KnowledgeBaseAPIServiceProvider.php new file mode 100644 index 0000000..e65363a --- /dev/null +++ b/Modules/KnowledgeBaseAPI/Providers/KnowledgeBaseAPIServiceProvider.php @@ -0,0 +1,115 @@ +registerConfig(); + $this->registerViews(); + $this->registerFactories(); + $this->loadMigrationsFrom(module_path('KnowledgeBaseAPI', 'Database/Migrations')); + $this->registerRoutes(); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $this->app->register(RouteServiceProvider::class); + } + + /** + * Register config. + * + * @return void + */ + protected function registerConfig() + { + $this->publishes([ + module_path('KnowledgeBaseAPI', 'Config/config.php') => config_path('knowledgebaseapi.php'), + ], 'config'); + $this->mergeConfigFrom( + module_path('KnowledgeBaseAPI', 'Config/config.php'), 'knowledgebaseapi' + ); + } + + /** + * Register views. + * + * @return void + */ + public function registerViews() + { + $viewPath = resource_path('views/modules/knowledgebaseapi'); + + $sourcePath = module_path('KnowledgeBaseAPI', 'Resources/views'); + + $this->publishes([ + $sourcePath => $viewPath + ], ['views', 'knowledgebaseapi-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), 'knowledgebaseapi'); + } + + /** + * Register an additional directory of factories. + * + * @return void + */ + public function registerFactories() + { + if (! app()->environment('production') && $this->app->runningInConsole()) { + app(Factory::class)->load(module_path('KnowledgeBaseAPI', 'Database/factories')); + } + } + + /** + * Register routes for the module. + * + * @return void + */ + protected function registerRoutes() + { + Route::group([ + 'middleware' => 'api', + 'prefix' => 'api', + ], function () { + $this->loadRoutesFrom(module_path('KnowledgeBaseAPI', 'Routes/api.php')); + }); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (\Config::get('view.paths') as $path) { + if (is_dir($path . '/modules/knowledgebaseapi')) { + $paths[] = $path . '/modules/knowledgebaseapi'; + } + } + return $paths; + } +} diff --git a/Modules/KnowledgeBaseAPI/Providers/RouteServiceProvider.php b/Modules/KnowledgeBaseAPI/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..14000e7 --- /dev/null +++ b/Modules/KnowledgeBaseAPI/Providers/RouteServiceProvider.php @@ -0,0 +1,53 @@ +mapApiRoutes(); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + * + * @return void + */ + protected function mapApiRoutes() + { + Route::prefix('api') + ->middleware('api') + ->namespace($this->moduleNamespace) + ->group(module_path('KnowledgeBaseAPI', 'Routes/api.php')); + } +} diff --git a/Modules/KnowledgeBaseAPI/README.md b/Modules/KnowledgeBaseAPI/README.md new file mode 100644 index 0000000..78856f0 --- /dev/null +++ b/Modules/KnowledgeBaseAPI/README.md @@ -0,0 +1,289 @@ +# Knowledge Base API Module for FreeScout + +This module exposes FreeScout's Knowledge Base through REST API endpoints, allowing external applications and JavaScript functions to access knowledge base articles and categories. + +## Features + +- ✅ RESTful API endpoints for knowledge base access +- ✅ List all categories (with hierarchical structure) +- ✅ Get category details with articles +- ✅ List and filter articles +- ✅ Get full article content +- ✅ Search functionality +- ✅ Public API endpoints (no authentication required) +- ✅ Protected API endpoints (requires authentication) +- ✅ Article view tracking +- ✅ Pagination support + +## Installation + +1. Copy the `KnowledgeBaseAPI` module folder to your FreeScout installation's `Modules` directory: + ```bash + cp -r Modules/KnowledgeBaseAPI /path/to/freescout/Modules/ + ``` + +2. Enable the module in FreeScout admin panel: + - Go to **Manage → Modules** + - Find "KnowledgeBaseAPI" module + - Click **Activate** + +3. The API endpoints will be immediately available + +## API Endpoints + +### Authenticated Endpoints + +These endpoints require FreeScout API authentication (X-FreeScout-API-Key header): + +#### Categories +- `GET /api/kb/categories` - List all categories +- `GET /api/kb/categories/{id}` - Get category details with articles + +#### Articles +- `GET /api/kb/articles` - List all articles + - Query params: `category_id`, `page`, `per_page` +- `GET /api/kb/articles/{id}` - Get article details + +#### Search +- `GET /api/kb/search?q={query}` - Search articles + +#### Health Check +- `GET /api/kb/health` - Check if API is working + +### Public Endpoints + +These endpoints don't require authentication: + +- `GET /api/kb/public/categories` - List public categories +- `GET /api/kb/public/categories/{id}` - Get public category +- `GET /api/kb/public/articles` - List public articles +- `GET /api/kb/public/articles/{id}` - Get public article +- `GET /api/kb/public/search?q={query}` - Search public articles + +## Usage Examples + +### JavaScript Fetch API + +```javascript +// Get all categories +async function getKBCategories() { + const response = await fetch('https://your-freescout.com/api/kb/public/categories', { + headers: { + 'Accept': 'application/json' + } + }); + const data = await response.json(); + return data.data; +} + +// Get articles from a category +async function getCategoryArticles(categoryId) { + const response = await fetch(`https://your-freescout.com/api/kb/public/categories/${categoryId}`, { + headers: { + 'Accept': 'application/json' + } + }); + const data = await response.json(); + return data.data.articles; +} + +// Get full article +async function getArticle(articleId) { + const response = await fetch(`https://your-freescout.com/api/kb/public/articles/${articleId}`, { + headers: { + 'Accept': 'application/json' + } + }); + const data = await response.json(); + return data.data.article; +} + +// Search articles +async function searchKB(query) { + const response = await fetch(`https://your-freescout.com/api/kb/public/search?q=${encodeURIComponent(query)}`, { + headers: { + 'Accept': 'application/json' + } + }); + const data = await response.json(); + return data.data; +} +``` + +### Python Client + +```python +from GlobalDeskClient import FreeScoutClient + +client = FreeScoutClient() + +# Get categories +categories = client._make_request('GET', '/kb/categories') + +# Get articles +articles = client._make_request('GET', '/kb/articles') + +# Search +results = client._make_request('GET', '/kb/search?q=password') + +# Get specific article +article = client._make_request('GET', '/kb/articles/1') +``` + +### jQuery Example + +```javascript +// Display knowledge base categories and articles +$.ajax({ + url: 'https://your-freescout.com/api/kb/public/categories', + method: 'GET', + dataType: 'json', + success: function(response) { + const categories = response.data; + categories.forEach(function(category) { + console.log(category.name); + // Load articles for this category + loadCategoryArticles(category.id); + }); + } +}); + +function loadCategoryArticles(categoryId) { + $.ajax({ + url: `https://your-freescout.com/api/kb/public/categories/${categoryId}`, + method: 'GET', + dataType: 'json', + success: function(response) { + const articles = response.data.articles; + articles.forEach(function(article) { + console.log(' -', article.title); + }); + } + }); +} +``` + +## Response Format + +All endpoints return JSON in this format: + +### Success Response +```json +{ + "success": true, + "data": { /* response data */ } +} +``` + +### Error Response +```json +{ + "success": false, + "error": "Error message", + "message": "Detailed error description" +} +``` + +### Article Object +```json +{ + "id": 1, + "category_id": 2, + "title": "How to reset password", + "slug": "how-to-reset-password", + "content": "Full article content...", + "excerpt": "Brief description...", + "views": 150, + "status": "published", + "visibility": "public", + "created_at": "2025-01-01 10:00:00", + "updated_at": "2025-01-15 14:30:00" +} +``` + +### Category Object +```json +{ + "id": 2, + "name": "Account Management", + "description": "Articles about managing your account", + "slug": "account-management", + "parent_id": null, + "order": 1, + "visibility": "public", + "children": [] +} +``` + +## Configuration + +Edit `Config/config.php` to customize: + +```php +'enabled' => true, // Enable/disable API +'rate_limit' => 60, // Requests per minute +'require_auth' => true, // Require auth for protected endpoints +'cors_origins' => ['*'], // CORS allowed origins +'include_content_in_list' => false, // Include full content in list responses +``` + +## Database Schema + +The module expects these tables to exist in FreeScout: + +### kb_categories +- id +- name +- description +- slug +- parent_id +- order +- visibility (public/private) +- created_at +- updated_at + +### kb_articles +- id +- category_id +- title +- slug +- content +- excerpt +- status (draft/published) +- visibility (public/private) +- order +- views +- created_at +- updated_at + +## Security + +- Protected endpoints require FreeScout API key authentication +- Public endpoints only expose articles marked as "public" +- Rate limiting prevents API abuse +- Input sanitization prevents SQL injection +- CORS headers can be configured + +## Troubleshooting + +### "Table kb_categories doesn't exist" +Make sure you have a Knowledge Base module installed in FreeScout that creates these tables. + +### "401 Unauthorized" +For protected endpoints, include the API key in the request header: +``` +X-FreeScout-API-Key: your_api_key_here +``` + +### CORS Issues +Update `cors_origins` in config.php to include your domain, or use '*' for all domains during development. + +## License + +MIT License + +## Support + +For issues and questions: +- GitHub Issues: https://github.com/gabeparra/GlobalDeskModules/issues +- FreeScout Community: https://freescout.net/community/ diff --git a/Modules/KnowledgeBaseAPI/Routes/api.php b/Modules/KnowledgeBaseAPI/Routes/api.php new file mode 100644 index 0000000..1654613 --- /dev/null +++ b/Modules/KnowledgeBaseAPI/Routes/api.php @@ -0,0 +1,39 @@ + ['auth:api']], function () { + // Knowledge Base Categories/Folders + Route::get('/kb/categories', 'KnowledgeBaseAPIController@listCategories'); + Route::get('/kb/categories/{id}', 'KnowledgeBaseAPIController@getCategory'); + + // Knowledge Base Articles + Route::get('/kb/articles', 'KnowledgeBaseAPIController@listArticles'); + Route::get('/kb/articles/{id}', 'KnowledgeBaseAPIController@getArticle'); + + // Search + Route::get('/kb/search', 'KnowledgeBaseAPIController@search'); + + // Public endpoint for health check (no auth required) + Route::get('/kb/health', 'KnowledgeBaseAPIController@health'); +}); + +// Public routes (if public knowledge base is enabled) +Route::group(['prefix' => 'kb/public'], function () { + Route::get('/categories', 'KnowledgeBaseAPIController@listPublicCategories'); + Route::get('/categories/{id}', 'KnowledgeBaseAPIController@getPublicCategory'); + Route::get('/articles', 'KnowledgeBaseAPIController@listPublicArticles'); + Route::get('/articles/{id}', 'KnowledgeBaseAPIController@getPublicArticle'); + Route::get('/search', 'KnowledgeBaseAPIController@publicSearch'); +}); diff --git a/Modules/KnowledgeBaseAPI/composer.json b/Modules/KnowledgeBaseAPI/composer.json new file mode 100644 index 0000000..0c343d7 --- /dev/null +++ b/Modules/KnowledgeBaseAPI/composer.json @@ -0,0 +1,28 @@ +{ + "name": "freescout/knowledgebaseapi", + "description": "Knowledge Base API Module for FreeScout", + "type": "freescout-module", + "keywords": ["freescout", "module", "knowledge base", "api"], + "license": "MIT", + "authors": [ + { + "name": "FreeScout Community", + "email": "support@freescout.net" + } + ], + "require": { + "php": ">=7.4" + }, + "autoload": { + "psr-4": { + "Modules\\KnowledgeBaseAPI\\": "" + } + }, + "extra": { + "laravel": { + "providers": [ + "Modules\\KnowledgeBaseAPI\\Providers\\KnowledgeBaseAPIServiceProvider" + ] + } + } +} diff --git a/Modules/KnowledgeBaseAPI/module.json b/Modules/KnowledgeBaseAPI/module.json new file mode 100644 index 0000000..a8574e5 --- /dev/null +++ b/Modules/KnowledgeBaseAPI/module.json @@ -0,0 +1,20 @@ +{ + "name": "KnowledgeBaseAPI", + "alias": "knowledgebaseapi", + "description": "Exposes FreeScout Knowledge Base through REST API endpoints", + "keywords": [ + "freescout", + "knowledge base", + "api", + "rest" + ], + "version": "1.0.0", + "active": 1, + "order": 1, + "providers": [ + "Modules\\KnowledgeBaseAPI\\Providers\\KnowledgeBaseAPIServiceProvider" + ], + "aliases": {}, + "files": [], + "requires": [] +} diff --git a/README.md b/README.md index f4c37b1..d9d980e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,275 @@ # GlobalDeskModules + +A collection of tools and modules for FreeScout, including a Knowledge Base API module that exposes the knowledge base through REST API endpoints for consumption by JavaScript and other applications. + +## 🚀 Features + +### Knowledge Base API Module (PHP/Laravel) +- ✅ RESTful API endpoints for FreeScout Knowledge Base +- ✅ Public and authenticated endpoints +- ✅ List categories with hierarchical structure +- ✅ Get articles by category +- ✅ Full-text search functionality +- ✅ Article view tracking +- ✅ Pagination support +- ✅ CORS support for JavaScript consumption + +### Python Client +- ✅ Easy-to-use Python client for FreeScout API +- ✅ Support for customers, tickets, conversations, and knowledge base +- ✅ Built-in methods for the new Knowledge Base API + +### JavaScript Client +- ✅ Modern JavaScript/ES6 client library +- ✅ Complete Knowledge Base widget implementation +- ✅ Interactive demo page +- ✅ Examples for fetch API, jQuery, and vanilla JS + +## 📦 Installation + +### 1. FreeScout Module Installation + +Copy the Knowledge Base API module to your FreeScout installation: + +```bash +# Copy the module to FreeScout's Modules directory +cp -r Modules/KnowledgeBaseAPI /path/to/freescout/Modules/ + +# Or create a symbolic link +ln -s /path/to/GlobalDeskModules/Modules/KnowledgeBaseAPI /path/to/freescout/Modules/ +``` + +Then activate the module in FreeScout: +1. Log in to FreeScout as admin +2. Go to **Manage → Modules** +3. Find "KnowledgeBaseAPI" in the list +4. Click **Activate** + +### 2. Python Client Setup + +```bash +cd backend + # Install dependencies pip install -r requirements.txt -# Set up your .env file with your FreeScout credentials -# Edit .env and add: -# FREESCOUT_BASE_URL=https://your-freescout-instance.com/api +# Set up environment variables +cp .env.example .env # If available, or create new .env file + +# Edit .env and add your FreeScout credentials: +# FREESCOUT_BASE_URL=https://your-freescout-instance.com # FREESCOUT_API_KEY=your_api_key_here +``` + +### 3. JavaScript Client Setup + +Simply include the JavaScript client in your HTML: + +```html + +``` + +Or view the demo page: +```bash +# Open frontend/demo.html in your browser +``` + +## 🔧 Usage + +### Python Client + +```python +from backend.GlobalDeskClient import FreeScoutClient + +# Initialize client (uses .env file) +client = FreeScoutClient() + +# Get all KB categories +categories = client.list_kb_categories(public=True) + +# Get articles from a category +articles = client.list_kb_articles(category_id=1, public=True) + +# Search articles +results = client.search_kb_articles("password reset", public=True) + +# Get a specific article +article = client.get_kb_article(article_id=5, public=True) +``` + +### JavaScript Client + +```javascript +// Initialize client +const client = new FreeScoutKBClient('https://your-freescout.com'); + +// Get categories +const categories = await client.getCategories(); + +// Get articles +const articles = await client.getArticles({ categoryId: 1, page: 1 }); + +// Search +const results = await client.search('password reset'); + +// Get specific article +const article = await client.getArticle(5); +``` + +### Complete Widget Example + +```javascript +// Initialize the complete widget +const kbWidget = new KnowledgeBaseWidget( + 'kb-container', // Container element ID + 'https://your-freescout.com' // FreeScout URL +); +``` + +See `frontend/demo.html` for a complete working example. + +## 📚 API Endpoints + +### Public Endpoints (No Authentication Required) + +``` +GET /api/kb/public/categories - List all public categories +GET /api/kb/public/categories/{id} - Get category with articles +GET /api/kb/public/articles - List all public articles +GET /api/kb/public/articles/{id} - Get specific article +GET /api/kb/public/search?q={query} - Search articles +``` + +### Authenticated Endpoints (API Key Required) + +``` +GET /api/kb/categories - List all categories +GET /api/kb/categories/{id} - Get category with articles +GET /api/kb/articles - List all articles +GET /api/kb/articles/{id} - Get specific article +GET /api/kb/search?q={query} - Search articles +GET /api/kb/health - Health check +``` + +## 📖 Documentation + +- **Module Documentation**: See [Modules/KnowledgeBaseAPI/README.md](Modules/KnowledgeBaseAPI/README.md) +- **JavaScript Examples**: See [frontend/knowledge-base-client.js](frontend/knowledge-base-client.js) +- **Interactive Demo**: Open [frontend/demo.html](frontend/demo.html) +- **Python Examples**: See [backend/GlobalDeskClient.py](backend/GlobalDeskClient.py) and [backend/main.py](backend/main.py) + +## 🧪 Testing + +### Test Python Client + +```bash +cd backend +python GlobalDeskClient.py +``` + +### Test JavaScript Client + +Open `frontend/demo.html` in your web browser and configure your FreeScout URL. + +## 🔐 Security + +- Protected endpoints require FreeScout API key authentication +- Public endpoints only expose articles marked as "public" visibility +- Input sanitization prevents SQL injection +- CORS headers can be configured in module config +- Rate limiting support + +## 🛠️ Configuration + +Module configuration is in `Modules/KnowledgeBaseAPI/Config/config.php`: + +```php +return [ + 'enabled' => true, // Enable/disable API + 'rate_limit' => 60, // Requests per minute + 'require_auth' => true, // Require auth for protected endpoints + 'cors_origins' => ['*'], // CORS allowed origins + 'include_content_in_list' => false, // Include full content in lists +]; +``` + +## 📋 Requirements + +### FreeScout Module +- PHP >= 7.4 +- Laravel (included with FreeScout) +- FreeScout >= 1.8.0 +- Knowledge Base feature enabled in FreeScout + +### Python Client +- Python >= 3.7 +- requests >= 2.31.0 +- python-dotenv >= 1.0.0 + +### JavaScript Client +- Modern browser with ES6 support +- Fetch API support (or polyfill) + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## 📄 License + +MIT License + +## 🐛 Troubleshooting + +### "Table kb_categories doesn't exist" +Make sure you have a Knowledge Base module installed in FreeScout that creates these tables. + +### "401 Unauthorized" +Include the API key in the request header for protected endpoints: +``` +X-FreeScout-API-Key: your_api_key_here +``` + +### CORS Issues +Update `cors_origins` in the module's config.php file to include your domain. + +### Module not showing in FreeScout +- Ensure the module is in the correct directory: `Modules/KnowledgeBaseAPI/` +- Check file permissions +- Clear FreeScout cache: `php artisan cache:clear` + +## 📞 Support + +- GitHub Issues: https://github.com/gabeparra/GlobalDeskModules/issues +- FreeScout Community: https://freescout.net/community/ + +## ✨ Examples + +See the [frontend/demo.html](frontend/demo.html) file for a complete working example with: +- Interactive KB widget +- Category browsing +- Article viewing +- Search functionality +- API endpoint testing + +## 🗂️ Project Structure -# Run the example -python GlobalDeskClient.py \ No newline at end of file +``` +GlobalDeskModules/ +├── Modules/ +│ └── KnowledgeBaseAPI/ # FreeScout PHP module +│ ├── Config/ +│ ├── Http/Controllers/ +│ ├── Providers/ +│ ├── Routes/ +│ ├── composer.json +│ ├── module.json +│ └── README.md +├── backend/ +│ ├── GlobalDeskClient.py # Python client +│ ├── main.py # Python examples +│ └── requirements.txt +├── frontend/ +│ ├── knowledge-base-client.js # JavaScript client +│ └── demo.html # Interactive demo +└── README.md +``` \ No newline at end of file diff --git a/backend/GlobalDeskClient.py b/backend/GlobalDeskClient.py index 5e1621a..812c8c8 100644 --- a/backend/GlobalDeskClient.py +++ b/backend/GlobalDeskClient.py @@ -269,52 +269,125 @@ def create_thread(self, conversation_id: int, body: str, return self._make_request('POST', f'/conversations/{conversation_id}/threads', data) - # Knowledge Base operations - def list_kb_folders(self) -> List[Dict]: - """List all knowledge base folders/categories""" + # Knowledge Base operations (using KnowledgeBaseAPI module) + def list_kb_categories(self, public: bool = True) -> List[Dict]: + """ + List all knowledge base categories + + Args: + public: Use public endpoint (no auth) if True, otherwise use authenticated endpoint + + Returns: + List of category dictionaries with hierarchical structure + """ try: - response = self._make_request('GET', '/folders') - return response.get('_embedded', {}).get('folders', []) + endpoint = '/kb/public/categories' if public else '/kb/categories' + response = self._make_request('GET', endpoint) + if response.get('success'): + return response.get('data', []) + return [] except Exception as e: - # Knowledge base may not be available via standard API - print(f"Note: Knowledge base folders endpoint may not be available: {e}") + print(f"Note: Knowledge base categories endpoint may not be available: {e}") return [] - def get_kb_folder(self, folder_id: int) -> Dict: - """Get a specific knowledge base folder""" - return self._make_request('GET', f'/folders/{folder_id}') + def get_kb_category(self, category_id: int, public: bool = True) -> Dict: + """ + Get a specific knowledge base category with its articles + + Args: + category_id: Category ID + public: Use public endpoint (no auth) if True + + Returns: + Dictionary with category and articles + """ + endpoint = f'/kb/public/categories/{category_id}' if public else f'/kb/categories/{category_id}' + response = self._make_request('GET', endpoint) + if response.get('success'): + return response.get('data', {}) + return {} - def list_kb_articles(self, folder_id: Optional[int] = None) -> List[Dict]: + def list_kb_articles(self, category_id: Optional[int] = None, + page: int = 1, per_page: int = 20, + public: bool = True) -> Dict: """ List knowledge base articles Args: - folder_id: Optional folder ID to filter articles + category_id: Optional category ID to filter articles + page: Page number for pagination + per_page: Items per page + public: Use public endpoint (no auth) if True + + Returns: + Dictionary with articles array and pagination info """ try: - endpoint = '/articles' - if folder_id: - endpoint += f'?folderId={folder_id}' + endpoint = '/kb/public/articles' if public else '/kb/articles' + params = [] + if category_id: + params.append(f'category_id={category_id}') + params.append(f'page={page}') + params.append(f'per_page={per_page}') + + if params: + endpoint += '?' + '&'.join(params) response = self._make_request('GET', endpoint) - return response.get('_embedded', {}).get('articles', []) + if response.get('success'): + return response + return {'data': [], 'pagination': {}} except Exception as e: - # Knowledge base may require a module or have different endpoints print(f"Note: Knowledge base articles endpoint may not be available: {e}") - return [] + return {'data': [], 'pagination': {}} - def get_kb_article(self, article_id: int) -> Dict: - """Get a specific knowledge base article""" - return self._make_request('GET', f'/articles/{article_id}') + def get_kb_article(self, article_id: int, public: bool = True) -> Dict: + """ + Get a specific knowledge base article + + Args: + article_id: Article ID + public: Use public endpoint (no auth) if True + + Returns: + Dictionary with article and category information + """ + endpoint = f'/kb/public/articles/{article_id}' if public else f'/kb/articles/{article_id}' + response = self._make_request('GET', endpoint) + if response.get('success'): + return response.get('data', {}) + return {} - def search_kb_articles(self, query: str) -> List[Dict]: - """Search knowledge base articles""" + def search_kb_articles(self, query: str, public: bool = True) -> List[Dict]: + """ + Search knowledge base articles + + Args: + query: Search query string + public: Use public endpoint (no auth) if True + + Returns: + List of matching articles + """ try: - response = self._make_request('GET', f'/articles?search={query}') - return response.get('_embedded', {}).get('articles', []) + endpoint = f'/kb/public/search' if public else f'/kb/search' + endpoint += f'?q={query}' + response = self._make_request('GET', endpoint) + if response.get('success'): + return response.get('data', []) + return [] except Exception as e: print(f"Note: Knowledge base search may not be available: {e}") return [] + + # Legacy KB methods (for backward compatibility) + def list_kb_folders(self) -> List[Dict]: + """List all knowledge base folders/categories (legacy method)""" + return self.list_kb_categories(public=True) + + def get_kb_folder(self, folder_id: int) -> Dict: + """Get a specific knowledge base folder (legacy method)""" + return self.get_kb_category(folder_id, public=True) # Example usage @@ -380,27 +453,57 @@ def search_kb_articles(self, query: str) -> List[Dict]: print() print("\n" + "="*60) - print("FREESCOUT KNOWLEDGE BASE") + print("FREESCOUT KNOWLEDGE BASE API (New Module)") print("="*60 + "\n") - # Try to access knowledge base - print("Fetching knowledge base folders...") - folders = client.list_kb_folders() - if folders: - print(f"Found {len(folders)} knowledge base folders:") - for folder in folders: - print(f" - {folder.get('name')} (ID: {folder.get('id')})") + # Try to access knowledge base using new API module + print("Fetching knowledge base categories (public API)...") + categories = client.list_kb_categories(public=True) + if categories: + print(f"Found {len(categories)} knowledge base categories:") + for category in categories[:5]: # Show first 5 + print(f" - {category.get('name')} (ID: {category.get('id')})") + if category.get('children'): + for child in category.get('children', []): + print(f" └─ {child.get('name')} (ID: {child.get('id')})") else: - print("No folders found or knowledge base may require a module.") + print("No categories found. Make sure the KnowledgeBaseAPI module is installed and activated.") - print("\nFetching knowledge base articles...") - articles = client.list_kb_articles() + print("\nFetching knowledge base articles (public API)...") + articles_response = client.list_kb_articles(public=True, per_page=5) + articles = articles_response.get('data', []) if articles: - print(f"Found {len(articles)} articles:") - for article in articles[:5]: # Show first 5 - print(f" - {article.get('title', 'Untitled')} (ID: {article.get('id')})") + print(f"Found articles (showing first 5):") + for article in articles: + print(f" - {article.get('title', 'Untitled')} (ID: {article.get('id')}, Views: {article.get('views', 0)})") + if article.get('category'): + print(f" Category: {article['category'].get('name')}") else: - print("No articles found. Knowledge base may require a specific module or different endpoints.") + print("No articles found. Make sure the KnowledgeBaseAPI module is installed.") + + # Test search + if articles: + print("\nTesting search functionality...") + search_results = client.search_kb_articles("help", public=True) + if search_results: + print(f"Found {len(search_results)} results for 'help':") + for result in search_results[:3]: # Show first 3 + print(f" - {result.get('title')}") + else: + print("No search results found.") + + # Get detailed article if available + if articles and len(articles) > 0: + first_article_id = articles[0].get('id') + print(f"\nFetching full details for article ID {first_article_id}...") + article_data = client.get_kb_article(first_article_id, public=True) + if article_data and article_data.get('article'): + article = article_data['article'] + print(f"Title: {article.get('title')}") + print(f"Excerpt: {article.get('excerpt', 'No excerpt')[:100]}...") + print(f"Views: {article.get('views', 0)}") + if article_data.get('category'): + print(f"Category: {article_data['category'].get('name')}") except Exception as e: print(f"Error: {e}") \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 757ad66..6760dde 100644 --- a/backend/main.py +++ b/backend/main.py @@ -42,27 +42,57 @@ def main(): print() print("\n" + "="*60) - print("FREESCOUT KNOWLEDGE BASE") + print("FREESCOUT KNOWLEDGE BASE API (New Module)") print("="*60 + "\n") - # Try to access knowledge base - print("Fetching knowledge base folders...") - folders = client.list_kb_folders() - if folders: - print(f"Found {len(folders)} knowledge base folders:") - for folder in folders: - print(f" - {folder.get('name')} (ID: {folder.get('id')})") + # Try to access knowledge base using new API module + print("Fetching knowledge base categories (public API)...") + categories = client.list_kb_categories(public=True) + if categories: + print(f"Found {len(categories)} knowledge base categories:") + for category in categories[:5]: # Show first 5 + print(f" - {category.get('name')} (ID: {category.get('id')})") + if category.get('children'): + for child in category.get('children', []): + print(f" └─ {child.get('name')} (ID: {child.get('id')})") else: - print("No folders found or knowledge base may require a module.") + print("No categories found. Make sure the KnowledgeBaseAPI module is installed and activated.") - print("\nFetching knowledge base articles...") - articles = client.list_kb_articles() + print("\nFetching knowledge base articles (public API)...") + articles_response = client.list_kb_articles(public=True, per_page=5) + articles = articles_response.get('data', []) if articles: - print(f"Found {len(articles)} articles:") - for article in articles[:5]: # Show first 5 - print(f" - {article.get('title', 'Untitled')} (ID: {article.get('id')})") + print(f"Found articles (showing first 5):") + for article in articles: + print(f" - {article.get('title', 'Untitled')} (ID: {article.get('id')}, Views: {article.get('views', 0)})") + if article.get('category'): + print(f" Category: {article['category'].get('name')}") else: - print("No articles found. Knowledge base may require a specific module or different endpoints.") + print("No articles found. Make sure the KnowledgeBaseAPI module is installed.") + + # Test search + if articles: + print("\nTesting search functionality...") + search_results = client.search_kb_articles("help", public=True) + if search_results: + print(f"Found {len(search_results)} results for 'help':") + for result in search_results[:3]: # Show first 3 + print(f" - {result.get('title')}") + else: + print("No search results found.") + + # Get detailed article if available + if articles and len(articles) > 0: + first_article_id = articles[0].get('id') + print(f"\nFetching full details for article ID {first_article_id}...") + article_data = client.get_kb_article(first_article_id, public=True) + if article_data and article_data.get('article'): + article = article_data['article'] + print(f"Title: {article.get('title')}") + print(f"Excerpt: {article.get('excerpt', 'No excerpt')[:100]}...") + print(f"Views: {article.get('views', 0)}") + if article_data.get('category'): + print(f"Category: {article_data['category'].get('name')}") if __name__ == '__main__': main() \ No newline at end of file diff --git a/frontend/demo.html b/frontend/demo.html new file mode 100644 index 0000000..dd0d453 --- /dev/null +++ b/frontend/demo.html @@ -0,0 +1,500 @@ + + + + + +Interactive demonstration of the Knowledge Base API endpoints
+ + +This widget provides a complete, interactive knowledge base interface.
+ +These endpoints don't require authentication:
+GET /api/kb/public/categories
+GET /api/kb/public/categories/{id}
+GET /api/kb/public/articles
+GET /api/kb/public/articles/{id}
+GET /api/kb/public/search?q={query}
+
+ These endpoints require an API key in the X-FreeScout-API-Key header:
+GET /api/kb/categories
+GET /api/kb/categories/{id}
+GET /api/kb/articles
+GET /api/kb/articles/{id}
+GET /api/kb/search?q={query}
+GET /api/kb/health
+
+ // Initialize client
+const client = new FreeScoutKBClient('https://your-freescout.com');
+
+// Get categories
+const categories = await client.getCategories();
+
+// Get articles
+const articles = await client.getArticles({ categoryId: 1, page: 1 });
+
+// Search
+const results = await client.search('password reset');
+
+// Get specific article
+const article = await client.getArticle(5);
+
+ {
+ "success": true,
+ "data": { /* response data */ }
+}
+