mirror of
https://git.yylx.win/https://github.com/nielspeen/freescout-redis-driver.git
synced 2025-10-03 18:01:10 +08:00
first commit
This commit is contained in:
parent
cbafd7173e
commit
5af4cf83f8
15 changed files with 111 additions and 339 deletions
|
@ -1,124 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\CustomApp\Http\Controllers;
|
||||
|
||||
use App\Mailbox;
|
||||
use App\Conversation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class CustomAppController extends Controller
|
||||
{
|
||||
public function mailboxSettings($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
return view('customapp::mailbox_settings', [
|
||||
'settings' => [
|
||||
'customapp.callback_url' => \Option::get('customapp.callback_url')[(string)$id] ?? '',
|
||||
'customapp.secret_key' => \Option::get('customapp.secret_key')[(string)$id] ?? '',
|
||||
'customapp.signature_header' => \Option::get('customapp.signature_header')[(string)$id] ?? 'X-FREESCOUT-SIGNATURE',
|
||||
'customapp.title' => \Option::get('customapp.title')[(string)$id] ?? '',
|
||||
],
|
||||
'mailbox' => $mailbox
|
||||
]);
|
||||
}
|
||||
|
||||
public function mailboxSettingsSave($id, Request $request)
|
||||
{
|
||||
$settings = $request->settings ?: [];
|
||||
|
||||
$urls = \Option::get('customapp.url') ?: [];
|
||||
$secrets = \Option::get('customapp.secret') ?: [];
|
||||
|
||||
$urls[(string)$id] = $settings['customapp.callback_url'] ?? '';
|
||||
$secrets[(string)$id] = $settings['customapp.secret_key'] ?? '';
|
||||
$signatureHeaders[(string)$id] = $settings['customapp.signature_header'] ?? 'X-FREESCOUT-SIGNATURE';
|
||||
$titles[(string)$id] = $settings['customapp.title'] ?? '';
|
||||
|
||||
\Option::set('customapp.callback_url', $urls);
|
||||
\Option::set('customapp.secret_key', $secrets);
|
||||
\Option::set('customapp.signature_header', $signatureHeaders);
|
||||
\Option::set('customapp.title', $titles);
|
||||
|
||||
\Session::flash('flash_success_floating', __('Settings updated'));
|
||||
|
||||
return redirect()->route('mailboxes.customapp', ['id' => $id]);
|
||||
}
|
||||
|
||||
public function generateSignature(string $data, string $secret): string
|
||||
{
|
||||
return base64_encode(hash_hmac('sha1', $data, $secret, true));
|
||||
}
|
||||
|
||||
public function content(Request $request)
|
||||
{
|
||||
if(!auth()->check()) {
|
||||
return response()->json(['status' => 'error', 'msg' => 'Unauthorized']);
|
||||
}
|
||||
|
||||
$referrer = $request->headers->get('referer');
|
||||
|
||||
if ($referrer) {
|
||||
$referrer = explode('?', $referrer)[0];
|
||||
}
|
||||
|
||||
if (!is_array($referrerParts = explode('/', $referrer)) || !isset($referrerParts[4])) {
|
||||
return response()->json(['status' => 'error', 'msg' => 'Invalid referrer']);
|
||||
}
|
||||
|
||||
$conversationId = $referrerParts[4] ?? null;
|
||||
|
||||
if(!$conversation = Conversation::find($conversationId)) {
|
||||
return response()->json(['status' => 'error', 'msg' => 'Conversation not found']);
|
||||
}
|
||||
|
||||
if(!$mailbox = Mailbox::find($conversation->mailbox_id)) {
|
||||
return response()->json(['status' => 'error', 'msg' => 'Mailbox not found']);
|
||||
}
|
||||
|
||||
if(!$customer = $conversation->customer) {
|
||||
return response()->json(['status' => 'error', 'msg' => 'Customer not found']);
|
||||
}
|
||||
|
||||
$callbackUrl = \Option::get('customapp.callback_url')[(string)$mailbox->id] ?? '';
|
||||
$secretKey = \Option::get('customapp.secret_key')[(string)$mailbox->id] ?? '';
|
||||
$signatureHeader = \Option::get('customapp.signature_header')[(string)$mailbox->id] ?? 'X-FREESCOUT-SIGNATURE';
|
||||
$title = \Option::get('customapp.title')[(string)$mailbox->id] ?? 'Custom App';
|
||||
|
||||
if (!$callbackUrl) {
|
||||
return response()->json(['status' => 'error', 'msg' => 'Callback URL is not set']);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'customer' => [
|
||||
'id' => $customer->id,
|
||||
'email' => $customer->getMainEmail(),
|
||||
'emails' => $customer->emails->pluck('email')->toArray(),
|
||||
]
|
||||
];
|
||||
|
||||
$content = json_encode($payload);
|
||||
$signature = $this->generateSignature($content, $secretKey);
|
||||
|
||||
try {
|
||||
$client = new \GuzzleHttp\Client();
|
||||
$result = $client->post($callbackUrl, [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'text/html',
|
||||
$signatureHeader => $signature,
|
||||
],
|
||||
'body' => $content,
|
||||
]);
|
||||
$response = json_decode($result->getBody()->getContents(), true)['html'];
|
||||
} catch (\Exception $e) {
|
||||
$response = 'Callback error: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return response($response, 200, [
|
||||
'Content-Type' => 'text/html',
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<?php
|
||||
|
||||
Route::group(['middleware' => 'web', 'prefix' => \Helper::getSubdirectory(), 'namespace' => 'Modules\CustomApp\Http\Controllers'], function () {
|
||||
Route::get('/customapp/content', ['uses' => 'CustomAppController@content'])->name('customapp.content');
|
||||
|
||||
Route::get('/mailbox/customapp/{id}', ['uses' => 'CustomAppController@mailboxSettings', 'middleware' => ['auth', 'roles'], 'roles' => ['admin']])->name('mailboxes.customapp');
|
||||
Route::post('/mailbox/customapp/{id}', ['uses' => 'CustomAppController@mailboxSettingsSave', 'middleware' => ['auth', 'roles'], 'roles' => ['admin']])->name('mailboxes.customapp.save');
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\CustomApp\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class CustomAppServiceProvider extends ServiceProvider
|
||||
{
|
||||
private const MODULE_NAME = 'customapp';
|
||||
|
||||
public function boot()
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__ . '/../Resources/views', self::MODULE_NAME);
|
||||
$this->hooks();
|
||||
}
|
||||
|
||||
public function registerViews()
|
||||
{
|
||||
$viewPath = resource_path('views/modules/customapp');
|
||||
|
||||
$sourcePath = __DIR__ . '/../Resources/views';
|
||||
|
||||
$this->publishes([
|
||||
$sourcePath => $viewPath
|
||||
], 'views');
|
||||
|
||||
$this->loadViewsFrom(array_merge(array_map(function ($path) {
|
||||
return $path . '/modules/customapp';
|
||||
}, \Config::get('view.paths')), [$sourcePath]), 'customapp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Module hooks.
|
||||
*/
|
||||
public function hooks()
|
||||
{
|
||||
\Eventy::addFilter('javascripts', function ($javascripts) {
|
||||
$javascripts[] = \Module::getPublicPath('customapp') . '/js/customapp.js';
|
||||
return $javascripts;
|
||||
});
|
||||
|
||||
\Eventy::addAction('mailboxes.settings.menu', function ($mailbox) {
|
||||
if (auth()->user()->isAdmin()) {
|
||||
echo \View::make('customapp::partials/settings_menu', ['mailbox' => $mailbox])->render();
|
||||
}
|
||||
}, 34);
|
||||
|
||||
// Settings view.
|
||||
\Eventy::addFilter('settings.view', function ($view, $section) {
|
||||
if ($section != 'customapp') {
|
||||
return $view;
|
||||
} else {
|
||||
return 'customapp::settings';
|
||||
}
|
||||
}, 20, 2);
|
||||
|
||||
\Eventy::addAction('conversation.after_prev_convs', function ($customer, $conversation, $mailbox) {
|
||||
$url = \Option::get('customapp.callback_url')[(string)$mailbox->id] ?? '';
|
||||
$title = \Option::get('customapp.title')[(string)$mailbox->id] ?? 'Custom App';
|
||||
|
||||
if ($url != '') {
|
||||
echo \View::make(self::MODULE_NAME . '::partials/sidebar', ['title' => $title])->render();
|
||||
}
|
||||
}, -1, 3);
|
||||
}
|
||||
}
|
47
Providers/RedisDriverServiceProvider.php
Normal file
47
Providers/RedisDriverServiceProvider.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\RedisDriver\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\RedisDriver\Services\RedisOverrideService;
|
||||
|
||||
class RedisDriverServiceProvider extends ServiceProvider
|
||||
{
|
||||
private const MODULE_NAME = 'redisdriver';
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
// RedisOverrideService::override();
|
||||
$this->app->booting(function () {
|
||||
RedisOverrideService::override();
|
||||
});
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__ . '/../Resources/views', self::MODULE_NAME);
|
||||
$this->hooks();
|
||||
}
|
||||
|
||||
public function registerViews()
|
||||
{
|
||||
$viewPath = resource_path('views/modules/redisdriver');
|
||||
|
||||
$sourcePath = __DIR__ . '/../Resources/views';
|
||||
|
||||
$this->publishes([
|
||||
$sourcePath => $viewPath
|
||||
], 'views');
|
||||
|
||||
$this->loadViewsFrom(array_merge(array_map(function ($path) {
|
||||
return $path . '/modules/redisdriver';
|
||||
}, \Config::get('view.paths')), [$sourcePath]), 'redisdriver');
|
||||
}
|
||||
|
||||
/**
|
||||
* Module hooks.
|
||||
*/
|
||||
public function hooks()
|
||||
{
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1.4 MiB |
|
@ -1,10 +0,0 @@
|
|||
fetch('/customapp/content')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
document.getElementById('customapp-content').innerHTML = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading customapp content:', error);
|
||||
});
|
||||
|
||||
|
BIN
Public/redisdriver.png
Normal file
BIN
Public/redisdriver.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
28
README.md
28
README.md
|
@ -1,8 +1,26 @@
|
|||
# FreeScout Custom App
|
||||
# FreeScout Redis Driver
|
||||
|
||||
* Similar to Helpscout's Legacy App feature.
|
||||
* Fetches HTML from an external source to show in the sidebar of your conversations.
|
||||
Enable Redis caching for FreeScout.
|
||||
|
||||
# Limitations
|
||||
Inspired by #[4650](https://github.com/freescout-help-desk/freescout/issues/4650).
|
||||
|
||||
* Allows for only 1 Custom App per Mailbox
|
||||
## Installation
|
||||
|
||||
* Ensure Redis server is installed.
|
||||
* Ensure **phpredis** (not predis) driver for PHP is installed. (Both PHP-FPM and the PHP cli!)
|
||||
* Double-check your Redis settings in ```config/database.php```. It must use **phpredis**, not predis.
|
||||
* Add to your .env:
|
||||
```
|
||||
CACHE_DRIVER=redis
|
||||
CACHE_PREFIX=freescout
|
||||
QUEUE_DRIVER=redis
|
||||
# Session drivers in FreeScout appear to be broken or incompatible with PHP 8. Only file and database drivers work.
|
||||
# SESSION_DRIVER=redis
|
||||
```
|
||||
* Activate the Redis Driver module.
|
||||
* Run php artisan ```freescout:clear-cache```.
|
||||
|
||||
## FAQ
|
||||
|
||||
* Why phpredis?
|
||||
Using phpredis requires us to install a driver in PHP only. This removes the maintenance burden that comes with adding predis to FreeScout.
|
|
@ -1,26 +0,0 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title_full', 'Custom App'.' - '.$mailbox->name)
|
||||
|
||||
@section('sidebar')
|
||||
@include('partials/sidebar_menu_toggle')
|
||||
@include('mailboxes/sidebar_menu')
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="section-heading">
|
||||
Custom App
|
||||
</div>
|
||||
|
||||
@include('partials/flash_messages')
|
||||
|
||||
<div class="row-container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
@include('customapp::settings')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
|
@ -1 +0,0 @@
|
|||
<li @if (Route::is('mailboxes.customapp'))class="active" @endif><a href="{{ route('mailboxes.customapp', ['id'=>$mailbox->id]) }}"><i class="glyphicon glyphicon-cloud"></i> Custom App</a></li>
|
|
@ -1,20 +0,0 @@
|
|||
<div class="conv-sidebar-block">
|
||||
<div class="panel-group accordion accordion-empty">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" href=".collapse-custom-app">{{ $title }}
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="collapse-custom-app panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div id="customapp-content">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,67 +0,0 @@
|
|||
<form class="form-horizontal margin-top margin-bottom" method="POST" action="">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{{ __('Title') }}</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control input-sized-lg" name="settings[customapp.title]" value="{{ $settings['customapp.title'] }}">
|
||||
|
||||
<p class="form-help">
|
||||
{{ __('The title of the custom app. This is used to display the title of the custom app in the sidebar.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group{{ $errors->has('settings.customapp->url') ? ' has-error' : '' }}">
|
||||
<label class="col-sm-2 control-label">{{ __('Callback URL') }}</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group input-sized-lg">
|
||||
<input type="text" class="form-control input-sized-lg" name="settings[customapp.callback_url]" value="{{ old('settings') ? old('settings')['customapp.callback_url'] : $settings['customapp.callback_url'] }}">
|
||||
</div>
|
||||
|
||||
@include('partials/field_error', ['field'=>'settings.customapp->url'])
|
||||
|
||||
<p class="form-help">
|
||||
{{ __('Example') }}: https://crm.example.org/api/freescout/callback
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{{ __('Secret Key') }}</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control input-sized-lg" name="settings[customapp.secret_key]" value="{{ $settings['customapp.secret_key'] }}">
|
||||
|
||||
<p class="form-help">
|
||||
{{ __('The secret key used to generate a signature header. This can be used to verify the authenticity of the request.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="signature_header" class="col-sm-2 control-label">Signature Header</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<select id="signature_header" class="form-control input-sized" name="settings[customapp.signature_header]">
|
||||
<option value="X-FREESCOUT-SIGNATURE" {{ $settings['customapp.signature_header'] == 'X-FREESCOUT-SIGNATURE' ? 'selected' : '' }}>X-FREESCOUT-SIGNATURE</option>
|
||||
<option value="X-HELPSCOUT-SIGNATURE" {{ $settings['customapp.signature_header'] == 'X-HELPSCOUT-SIGNATURE' ? 'selected' : '' }}>X-HELPSCOUT-SIGNATURE</option>
|
||||
</select>
|
||||
|
||||
<p class="form-help">
|
||||
{{ __('Select the signature header to use. This is used to verify the authenticity of the request. Select X-HELPSCOUT-SIGNATURE if you are migrating from HelpScout.') }}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group margin-top margin-bottom">
|
||||
<div class="col-sm-6 col-sm-offset-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ __('Save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
28
Services/RedisOverrideService.php
Normal file
28
Services/RedisOverrideService.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\RedisDriver\Services;
|
||||
|
||||
class RedisOverrideService
|
||||
{
|
||||
public static function override() {
|
||||
|
||||
$models = [
|
||||
\App\Customer::class,
|
||||
\App\Email::class,
|
||||
\App\User::class,
|
||||
\App\MailboxUser::class,
|
||||
\App\Conversation::class,
|
||||
\App\Mailbox::class,
|
||||
];
|
||||
|
||||
foreach ( $models as $model_class ) {
|
||||
try {
|
||||
$model_class::retrieved( function ( $instance ) {
|
||||
$instance->rememberCacheDriver = 'redis';
|
||||
} );
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning( "Failed to patch Rememberable for {$model_class}: " . $e->getMessage() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "nielspeen/freescout-custom-app",
|
||||
"name": "nielspeen/freescout-redis-driver",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
|
@ -10,7 +10,7 @@
|
|||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Modules\\CustomApp\\Providers\\CustomAppServiceProvider"
|
||||
"Modules\\RedisDriver\\Providers\\RedisDriverServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\CustomApp\\": ""
|
||||
"Modules\\RedisDriver\\": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
module.json
19
module.json
|
@ -1,26 +1,27 @@
|
|||
{
|
||||
"name": "Custom App",
|
||||
"alias": "customapp",
|
||||
"description": "Custom App allows you to pull in external data to your Conversations.",
|
||||
"version": "1.0.2",
|
||||
"name": "Redis Driver",
|
||||
"alias": "redisdriver",
|
||||
"description": "Redis Driver for FreeScout.",
|
||||
"version": "1.0.0",
|
||||
"requiredAppVersion": "1.8.190",
|
||||
"detailsUrl": "https://github.com/nielspeen/freescout-custom-app",
|
||||
"detailsUrl": "https://github.com/nielspeen/freescout-redis-driver",
|
||||
"author": "Niels Peen",
|
||||
"authorUrl": "https://github.com/nielspeen",
|
||||
"license": "AGPL-3.0",
|
||||
"keywords": ["custom", "app", "external", "data"],
|
||||
"active": 1,
|
||||
"order": 0,
|
||||
"priority": -10000,
|
||||
"providers": [
|
||||
"Modules\\CustomApp\\Providers\\CustomAppServiceProvider"
|
||||
"Modules\\RedisDriver\\Providers\\RedisDriverServiceProvider"
|
||||
],
|
||||
"aliases": {},
|
||||
"files": [
|
||||
"start.php"
|
||||
],
|
||||
"requires": [],
|
||||
"img": "/modules/customapp/customapp.png",
|
||||
"latestVersionUrl": "https://raw.githubusercontent.com/nielspeen/freescout-custom-app/refs/heads/main/module.json",
|
||||
"latestVersionZipUrl": "https://github.com/nielspeen/freescout-custom-app/archive/refs/heads/main.zip"
|
||||
"img": "/modules/redisdriver/redisdriver.png",
|
||||
"latestVersionUrl": "https://raw.githubusercontent.com/nielspeen/freescout-redis-driver/refs/heads/main/module.json",
|
||||
"latestVersionZipUrl": "https://github.com/nielspeen/freescout-redis-driver/archive/refs/heads/main.zip"
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue