├── Http ├── Controllers │ └── SidebarWebhookController.php └── routes.php ├── LICENSE ├── Providers └── SidebarWebhookServiceProvider.php ├── Public └── js │ ├── laroute.js │ └── module.js ├── README.md ├── Resources └── views │ ├── mailbox_settings.blade.php │ ├── partials │ ├── settings_menu.blade.php │ └── sidebar.blade.php │ └── settings.blade.php ├── composer.json ├── module.json ├── sidebar-with-title.png ├── sidebar-without-title.png └── start.php /Http/Controllers/SidebarWebhookController.php: -------------------------------------------------------------------------------- 1 | [ 23 | 'sidebarwebhook.url' => \Option::get('sidebarwebhook.url')[(string)$id] ?? '', 24 | 'sidebarwebhook.secret' => \Option::get('sidebarwebhook.secret')[(string)$id] ?? '', 25 | ], 26 | 'mailbox' => $mailbox 27 | ]); 28 | } 29 | 30 | public function mailboxSettingsSave($id, Request $request) 31 | { 32 | $mailbox = Mailbox::findOrFail($id); 33 | 34 | $settings = $request->settings ?: []; 35 | 36 | $urls = \Option::get('sidebarwebhook.url') ?: []; 37 | $secrets = \Option::get('sidebarwebhook.secret') ?: []; 38 | 39 | $urls[(string)$id] = $settings['sidebarwebhook.url'] ?? ''; 40 | $secrets[(string)$id] = $settings['sidebarwebhook.secret'] ?? ''; 41 | 42 | \Option::set('sidebarwebhook.url', $urls); 43 | \Option::set('sidebarwebhook.secret', $secrets); 44 | 45 | \Session::flash('flash_success_floating', __('Settings updated')); 46 | 47 | return redirect()->route('mailboxes.sidebarwebhook', ['id' => $id]); 48 | } 49 | 50 | /** 51 | * Ajax controller. 52 | */ 53 | public function ajax(Request $request) 54 | { 55 | $response = [ 56 | 'status' => 'error', 57 | 'msg' => '', // this is error message 58 | ]; 59 | 60 | switch ($request->action) { 61 | 62 | case 'loadSidebar': 63 | // mailbox_id and customer_id are required. 64 | if (!$request->mailbox_id || !$request->conversation_id) { 65 | $response['msg'] = 'Missing required parameters'; 66 | break; 67 | } 68 | 69 | try { 70 | $mailbox = Mailbox::findOrFail($request->mailbox_id); 71 | $conversation = Conversation::findOrFail($request->conversation_id); 72 | $customer = $conversation->customer; 73 | } catch (\Exception $e) { 74 | $response['msg'] = 'Invalid mailbox or customer'; 75 | break; 76 | } 77 | 78 | $url = \Option::get('sidebarwebhook.url')[(string)$mailbox->id] ?? ''; 79 | $secret = \Option::get('sidebarwebhook.secret')[(string)$mailbox->id] ?? ''; 80 | if (!$url) { 81 | $response['msg'] = 'Webhook URL is not set'; 82 | break; 83 | } 84 | 85 | $payload = [ 86 | 'customerId' => $customer->id, 87 | 'customerEmail' => $customer->getMainEmail(), 88 | 'customerEmails' => $customer->emails->pluck('email')->toArray(), 89 | 'customerPhones' => $customer->getPhones(), 90 | 'conversationSubject' => $conversation->getSubject(), 91 | 'conversationType' => $conversation->getTypeName(), 92 | 'mailboxId' => $mailbox->id, 93 | 'secret' => empty($secret) ? '' : $secret, 94 | ]; 95 | 96 | try { 97 | $client = new \GuzzleHttp\Client(); 98 | $result = $client->post($url, [ 99 | 'headers' => [ 100 | 'Content-Type' => 'application/json', 101 | 'Accept' => 'text/html', 102 | ], 103 | 'body' => json_encode($payload), 104 | ]); 105 | $response['html'] = $result->getBody()->getContents(); 106 | $response['status'] = 'success'; 107 | } catch (\Exception $e) { 108 | $response['msg'] = 'Webhook error: ' . $e->getMessage(); 109 | break; 110 | } 111 | 112 | break; 113 | 114 | default: 115 | $response['msg'] = 'Unknown action'; 116 | break; 117 | } 118 | 119 | if ($response['status'] == 'error' && empty($response['msg'])) { 120 | $response['msg'] = 'Unknown error occured'; 121 | } 122 | 123 | return \Response::json($response); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Http/routes.php: -------------------------------------------------------------------------------- 1 | 'web', 'prefix' => \Helper::getSubdirectory(), 'namespace' => 'Modules\SidebarWebhook\Http\Controllers'], function () { 4 | Route::post('/sidebarwebhook/ajax', ['uses' => 'SidebarWebhookController@ajax', 'laroute' => true])->name('sidebarwebhook.ajax'); 5 | 6 | Route::get('/mailbox/sidebarwebhook/{id}', ['uses' => 'SidebarWebhookController@mailboxSettings', 'middleware' => ['auth', 'roles'], 'roles' => ['admin']])->name('mailboxes.sidebarwebhook'); 7 | Route::post('/mailbox/sidebarwebhook/{id}', ['uses' => 'SidebarWebhookController@mailboxSettingsSave', 'middleware' => ['auth', 'roles'], 'roles' => ['admin']])->name('mailboxes.sidebarwebhook.save'); 8 | }); 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021–2024 William Entriken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Providers/SidebarWebhookServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__ . '/../Resources/views', self::MODULE_NAME); 14 | $this->hooks(); 15 | } 16 | 17 | public function registerViews() 18 | { 19 | $viewPath = resource_path('views/modules/sidebarwebhook'); 20 | 21 | $sourcePath = __DIR__ . '/../Resources/views'; 22 | 23 | $this->publishes([ 24 | $sourcePath => $viewPath 25 | ], 'views'); 26 | 27 | $this->loadViewsFrom(array_merge(array_map(function ($path) { 28 | return $path . '/modules/sidebarwebhook'; 29 | }, \Config::get('view.paths')), [$sourcePath]), 'sidebarwebhook'); 30 | } 31 | 32 | /** 33 | * Module hooks. 34 | */ 35 | public function hooks() 36 | { 37 | \Eventy::addFilter('javascripts', function ($javascripts) { 38 | $javascripts[] = \Module::getPublicPath('sidebarwebhook') . '/js/laroute.js'; 39 | $javascripts[] = \Module::getPublicPath('sidebarwebhook') . '/js/module.js'; 40 | return $javascripts; 41 | }); 42 | 43 | \Eventy::addAction('mailboxes.settings.menu', function ($mailbox) { 44 | if (auth()->user()->isAdmin()) { 45 | echo \View::make('sidebarwebhook::partials/settings_menu', ['mailbox' => $mailbox])->render(); 46 | } 47 | }, 34); 48 | 49 | // Settings view. 50 | \Eventy::addFilter('settings.view', function ($view, $section) { 51 | if ($section != 'sidebarwebhook') { 52 | return $view; 53 | } else { 54 | return 'sidebarwebhook::settings'; 55 | } 56 | }, 20, 2); 57 | 58 | \Eventy::addAction('conversation.after_prev_convs', function ($customer, $conversation, $mailbox) { 59 | $url = \Option::get('sidebarwebhook.url')[(string)$mailbox->id] ?? ''; 60 | 61 | if ($url != '') { 62 | echo \View::make(self::MODULE_NAME . '::partials/sidebar', [])->render(); 63 | } 64 | }, -1, 3); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Public/js/laroute.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var module_routes = [{ 3 | "uri": "sidebarwebhook\/ajax", 4 | "name": "sidebarwebhook.ajax" 5 | }]; 6 | 7 | if (typeof(laroute) != "undefined") { 8 | laroute.add_routes(module_routes); 9 | } else { 10 | contole.log('laroute not initialized, can not add module routes:'); 11 | contole.log(module_routes); 12 | } 13 | })(); 14 | 15 | -------------------------------------------------------------------------------- /Public/js/module.js: -------------------------------------------------------------------------------- 1 | function swh_load_content() { 2 | $('#swh-title').html(''); 3 | $('#swh-title').parent().addClass('hide'); 4 | $('#swh-content').addClass('hide'); 5 | $('#swh-loader').removeClass('hide'); 6 | 7 | fsAjax({ 8 | action: 'loadSidebar', 9 | mailbox_id: getGlobalAttr('mailbox_id'), 10 | conversation_id: getGlobalAttr('conversation_id') 11 | }, 12 | laroute.route('sidebarwebhook.ajax'), 13 | function(response) { 14 | if (typeof(response.status) != "undefined" && response.status == 'success' && typeof(response.html) != "undefined" && response.html) { 15 | $('#swh-content').html(response.html); 16 | 17 | // Find a element inside the response and display it 18 | title = $('#swh-content').find('title').first().text(); 19 | 20 | if (title) { 21 | $('#swh-title').html(title); 22 | $('#swh-title').parent().removeClass('hide'); 23 | } 24 | 25 | $('#swh-loader').addClass('hide'); 26 | $('#swh-content').removeClass('hide'); 27 | } else { 28 | showAjaxError(response); 29 | } 30 | }, true 31 | ); 32 | } 33 | 34 | $(document).ready(function() { 35 | // If we're not actually viewing a conversation, don't try to do anything. 36 | if (typeof(getGlobalAttr('mailbox_id')) == "undefined" || typeof(getGlobalAttr('conversation_id')) == "undefined") { 37 | return; 38 | } 39 | 40 | // If we don't have the #swh-content element, the server doesn't have a configured webhook URL. 41 | if ($('#swh-content').length == 0) { 42 | return; 43 | } 44 | 45 | swh_load_content(); 46 | 47 | $('.swh-refresh').click(function(e) { 48 | e.preventDefault(); 49 | swh_load_content(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeScout Sidebar Webhook 2 | 3 | Sidebar Webhook asynchronously injects HTML from your server into conversation sidebars. 4 | 5 | This screenshot shows what it does: you can load any content on a per-customer, per-message basis from your own web server, asynchronously, every time a conversation is loaded on the screen. 6 | 7 | In this picture, the kittens and text were loaded from an external server based on the customer's email address. 8 | 9 | ![with title](sidebar-with-title.png) 10 | ![without title](sidebar-without-title.png) 11 | 12 | ## Use cases 13 | 14 | - Directly link to your customer management system 15 | - Show details about order status live from your fulfillment system 16 | - Quickly ship changes to your FreeScout system UI without updating modules 17 | - Connect to backends using PHP/Node.js/Ruby/Perl/Rust/Go/Bash/Haskell and even Java 18 | 19 | ## Installation 20 | 21 | These instructions assume you installed FreeScout using the [recommended process](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide), the "one-click install" or the "interactive installation bash-script", and you are viewing this page using a macOS or Ubuntu system. 22 | 23 | Other installations are possible, but not supported here. 24 | 25 | 1. Download the [latest release of FreeScout Sidebar Webhook](https://github.com/fulldecent/freescout-sidebar-webhook/releases). 26 | 2. Unzip the file locally. 27 | 3. Copy the folder into your server using SFTP. (ℹ️ Folder is renamed in this process.) 28 | ```sh 29 | scp -r ~/Downloads/freescout-sidebar-webhook root@freescout.example.com:/var/www/html/Modules/SidebarWebhook/ 30 | ``` 31 | 4. SSH into the server and update permissions on that folder. 32 | ```sh 33 | chown -R www-data:www-data /var/www/html/Modules/SidebarWebhook/ 34 | ``` 35 | 5. Access your admin modules page like https://freescout.example.com/modules/list. 36 | 6. Find **Sidebar Webhook** and click ACTIVATE. 37 | 7. Configure the webhook URL in the mailbox settings. The webhook secret is optional and will be sent as part of the payload if set. 38 | 8. After everything works, purchase a license code by sending USD 10 at https://www.paypal.com/paypalme/fulldecent/10usd 39 | 40 | ## Your webhook server 41 | 42 | Your webhook server will receive a POST request with this kind of JSON body: 43 | ```json 44 | { 45 | "customerId": 123, 46 | "conversationSubject": "Testing this sidebar", 47 | "conversationType": "Phone", 48 | "customerEmail": "hello@example.com", 49 | "customerEmails": [ 50 | "hello@example.com", 51 | "welcome@example.com" 52 | ], 53 | "customerPhones": [{ 54 | "n": "", 55 | "type": 1, 56 | "value": "" 57 | }], 58 | "mailboxId": 1, 59 | "secret": "0C7DA918-E72C-47B2-923B-0C5BB6A6104E" 60 | } 61 | ``` 62 | 63 | Your webhook server shall respond with content to be injected into the sidebar. The document should be a complete, well-formed HTML document like so: 64 | 65 | ```html 66 | <html> 67 | <body> 68 | <h1>Hello world</h1> 69 | </body> 70 | </html> 71 | ``` 72 | 73 | You can optionally set a title in the document and it will be used as the panel title in the sidebar: 74 | 75 | ```html 76 | <html> 77 | <head> 78 | <title>My panel title 79 | 80 | 81 |

Hello world

82 | 83 | 84 | ``` 85 | Setting CORS headers is not required, as the document is requested by the FreeScout server (not by the user's browser). 86 | 87 | ## Project scope 88 | 89 | Our goal is to have a very simple module to allow vast extensibility in the conversation sidebar. 90 | 91 | Anything that makes it simpler (removes unneded code) or more extensible for most people (adding a couple post parameters in `boot()`) will be a welcome improvement. 92 | 93 | ## Troubleshooting 94 | 95 | Hints 96 | 97 | * > Class "Modules\SidebarWebhook\Providers\SidebarWebhookServiceProvider" not found 98 | 99 | Did you rename the folder as per step 3? 100 | 101 | If something is not working, please try these steps so we can see what's wrong. 102 | 103 | 1. Update FreeScout to the latest version (even if the new version doesn't have any relevant changes, the process of updating can sometimes fix problems that would prevent freescout-sidebar-webhook from running). 104 | 2. Use `chown -r` to ensure the module has the same owner/permissions as other files in your FreeScout installation. 105 | 3. Try to disable and reenable freescout-sidebare-webhook from your system/modules page. 106 | 4. To confirm the module file is actually activated and readable you might add a line like this above the `private const MODULE_NAME` line. The code to add is: `file_put_contents("/tmp/sidebartmp", "is running");` And then you can confirm it is running by seeing if that file is created when you load the page. 107 | 5. Next check for system logs. It will be helpful to note any warnings, errors or notices as they may instruct where the problem is coming from. 108 | 6. Check your PHP version, is it a version supported by FreeScout? 109 | 110 | After you have checked all these things, please create an issue and detail how you tried each of these steps. 111 | 112 | ## Inspiration 113 | 114 | * This project was inspired by [Sidebar API](https://github.com/scout-devs/SidebarApi). 115 | -------------------------------------------------------------------------------- /Resources/views/mailbox_settings.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title_full', 'Sidebar Webhook'.' - '.$mailbox->name) 4 | 5 | @section('sidebar') 6 | @include('partials/sidebar_menu_toggle') 7 | @include('mailboxes/sidebar_menu') 8 | @endsection 9 | 10 | @section('content') 11 | 12 |
13 | Sidebar Webhook 14 |
15 | 16 | @include('partials/flash_messages') 17 | 18 |
19 |
20 |
21 | @include('sidebarwebhook::settings') 22 |
23 |
24 |
25 | 26 | @endsection 27 | -------------------------------------------------------------------------------- /Resources/views/partials/settings_menu.blade.php: -------------------------------------------------------------------------------- 1 |
  • Sidebar Webhook
  • 2 | -------------------------------------------------------------------------------- /Resources/views/partials/sidebar.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    5 |
    6 |
    7 |
    8 |
    9 | 10 |
    11 | 14 |
    15 |
    16 |
    17 | -------------------------------------------------------------------------------- /Resources/views/settings.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | {{ csrf_field() }} 3 | 4 |
    5 | 6 | 7 |
    8 |
    9 | 10 |
    11 | 12 | @include('partials/field_error', ['field'=>'settings.sidebarwebhook->url']) 13 | 14 |

    15 | {{ __('Example') }}: https://example.org/webhook 16 |

    17 |
    18 |
    19 | 20 |
    21 | 22 | 23 |
    24 | 25 | 26 |

    27 | {{ __('You can choose an arbitrary string. This string will be sent as a parameter to authenticate requests.') }} 28 |

    29 |
    30 |
    31 | 32 |
    33 |
    34 | 37 |
    38 |
    39 |
    40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fulldecent/freescout-sidebar-webhook", 3 | "description": "", 4 | "authors": [ 5 | { 6 | "name": "William Entriken", 7 | "email": "github.com@phor.net" 8 | } 9 | ], 10 | "extra": { 11 | "laravel": { 12 | "providers": [ 13 | "Modules\\SidebarWebhook\\Providers\\SidebarWebhookServiceProvider" 14 | ], 15 | "aliases": { 16 | } 17 | } 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Modules\\SidebarWebhook\\": "" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sidebar Webhook", 3 | "alias": "sidebarwebhook", 4 | "description": "Sidebar Webhook asynchronously injects HTML from your server into conversation sidebars.", 5 | "version": "3.0.0", 6 | "detailsUrl": "https://github.com/fulldecent/freescout-sidebar-webhook", 7 | "author": "William Entriken", 8 | "providers": [ 9 | "Modules\\SidebarWebhook\\Providers\\SidebarWebhookServiceProvider" 10 | ], 11 | "requires": [], 12 | "files": [ 13 | "start.php" 14 | ], 15 | "latestVersionUrl": "https://raw.githubusercontent.com/fulldecent/freescout-sidebar-webhook/refs/heads/main/module.json", 16 | "latestVersionZipUrl": "https://github.com/fulldecent/freescout-sidebar-webhook/archive/refs/heads/main.zip" 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sidebar-with-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulldecent/freescout-sidebar-webhook/fd7354a69d4fb992de67556a53213ef1ca213a99/sidebar-with-title.png -------------------------------------------------------------------------------- /sidebar-without-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulldecent/freescout-sidebar-webhook/fd7354a69d4fb992de67556a53213ef1ca213a99/sidebar-without-title.png -------------------------------------------------------------------------------- /start.php: -------------------------------------------------------------------------------- 1 | routesAreCached()) { 16 | require __DIR__ . '/Http/routes.php'; 17 | } 18 | --------------------------------------------------------------------------------