├── 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 | 
10 | 
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 |
67 |
68 | Hello world
69 |
70 |
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 |
77 |
78 | 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 |
17 |
--------------------------------------------------------------------------------
/Resources/views/settings.blade.php:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------