Example code
The code for support integrations can be found in:
app/Framework/Core/Support/TicketProvidersFor each ticket provider, you have a php class, for example the FreeScoutTicketProvider.php. To get started with a new ticket provider, you can just copy one and remove the code.
Example (FreeScout)
<?php
namespace App\Framework\Core\Support\TicketProviders;
use App\Enums\TicketDefaultStatuses;
use App\Framework\Core\Support\TicketProviders\Responses\TicketAnsweredResponse;
use App\Framework\Core\Support\TicketProviders\Responses\TicketCreatedResponse;
use App\Framework\Core\Support\TicketProviders\Responses\TicketDepartmentResponse;
use App\Framework\Core\Support\TicketProviders\Responses\TicketMessageAttachmentResponse;
use App\Framework\Core\Support\TicketProviders\Responses\TicketMessageResponse;
use App\Framework\Core\Support\TicketProviders\Responses\TicketResponse;
use App\Framework\Core\Support\TicketProviders\Responses\TicketStatusResponse;
use App\Framework\Core\Utils\HtmlToText;
use App\Models\Customer;
use App\Models\Domain\Domain;
use App\Models\ProductHosting;
use App\Models\SalesChannel;
use App\Models\Support\Ticket;
use App\Models\Support\TicketAttachment;
use App\Models\Support\TicketDepartment;
use Cache;
use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class FreeScoutTicketProvider extends SupportTicketProviderAbstract implements SupportTicketProviderInterface {
// Tickets, which where opened by the customer and need admin-response
public mixed $openStatusId = "active";
// Tickets, which where responded by the customer and need admin-response
public mixed $answeredStatusId = "active";
// Tickets, which where closed
public mixed $closedStatusId = "closed";
public function getStatuses(): Collection {
return collect([
new TicketStatusResponse("active", "Aktiv", "var(--bs-success)"),
new TicketStatusResponse("pending", "In Bearbeitung", "var(--bs-warning)"),
new TicketStatusResponse("closed", "Geschlossen", "var(--bs-danger)"),
new TicketStatusResponse("spam", "Spam", "var(--bs-secondary)"),
]);
}
public function getTickets(): Collection {
$response = $this->makeRequest("conversations", "GET", [
"sortField" => "updatedAt",
"sortOrder" => "desc",
"pageSize" => 150
]);
$tickets = [];
// dd(count($response->_embedded->conversations));
foreach ($response->_embedded->conversations as $conversation) {
if ($conversation->customer === null) {
continue;
}
$tickets[] = new TicketResponse(
$conversation->id,
$conversation->number,
$conversation->customer->id,
$conversation->customer->email,
$conversation->subject,
$conversation->status,
Carbon::parse($conversation->createdAt, "UTC")->setTimezone("Europe/Berlin"),
Carbon::parse($conversation->updatedAt, "UTC")->setTimezone("Europe/Berlin"),
$conversation->mailboxId,
null
);
}
return collect($tickets);
}
public function getTicketMessages(mixed $ticketId): Collection {
$response = $this->makeRequest("conversations/{$ticketId}");
$admins = $this->_getAdminMails();
$messages = [];
foreach ($response->_embedded->threads as $thread) {
if (!in_array($thread->type, ["message", "customer"])) {
continue;
}
$attachments = [];
$re = '/<img(?s).*src="(?<image_url>[A-Za-z0-9-_.:\/?=&;]*)"(?s).*alt="(?<image_name>[A-Za-z0-9-_.]*)">/m';
$str = $thread->body;
preg_match_all($re, $str, $matches, PREG_SET_ORDER, 0);
foreach ($matches as $index => $match) {
$match['image_url'] = str_replace("&", "&", $match['image_url']);
$attachments[] = new TicketMessageAttachmentResponse(
"INLINE-{$index}",
$match['image_name'],
-1,
$match['image_url'],
);
}
foreach ($thread->_embedded->attachments as $attachment) {
$attachments[] = new TicketMessageAttachmentResponse(
$attachment->id,
$attachment->fileName,
$attachment->size,
$attachment->fileUrl,
);
}
$adminId = $thread->createdBy->type === "user" ? ($admins->get($thread->createdBy->email, $admins->first())) : null;
// dump($thread);
// dump($thread->body);
// dump($thread->createdAt);
// dump(Carbon::parse($thread->createdAt)->addHours(2)->format("d.m.Y H:i:s"));
// dd("A");
$messages[] = new TicketMessageResponse(
$thread->id,
$thread->body,
Carbon::parse($thread->createdAt, "UTC")->setTimezone("Europe/Berlin"),
$adminId,
collect($attachments)
);
}
return collect($messages);
}
public function closeTicket(Ticket $ticket) {
$response = $this->makeRequest("conversations/{$ticket->ticket_provider_external_id}", "PUT", [
"status" => "closed",
"byUser" => $this->_getFreescoutUserId()
]);
}
public function createTicket(
SalesChannel $salesChannel,
?Customer $customer,
string $subject,
string $message,
mixed $product,
?TicketDepartment $department = null,
array $attachments = [],
?string $guestName = null,
?string $guestMail = null,
): TicketCreatedResponse {
$mailboxId = $department->provider_department_id["freescout"] ?? $this->provider->default_department_id;
$customerId = $this->getCustomerId($customer, $guestName, $guestMail);
$additionalTexts = [];
if($customer !== null) {
$customerViewUrl = $salesChannel->url . route('admin.customer.edit', $customer->id, false);
$additionalTexts = [
"Ticket aus hostware in Freescout erstellt",
"Verkaufskanal: {$salesChannel->name}",
"<a href='{$customerViewUrl}'>Kunde ansehen (#{$customer->customer_number})</a>"
];
if ($product !== null) {
$productRoute = "N-A";
$productText = "N/A";
if ($product instanceof Domain) {
$productRoute = route('admin.domain.view', $product->fqdn, false);
$productText = "{$product->fqdn} (Domain)";
}
if ($product instanceof ProductHosting) {
$productRoute = route('admin.hosting.hostings.view', $product->id, false);
$productText = "{$product->product->name} #{$product->product_hosting_number} (Hosting)";
}
$completeUrl = $salesChannel->url . $productRoute;
$additionalTexts[] = '<a href="' . $completeUrl . '">Verlinktes Produkt ansehen (' . $productText . ')</a>';
}
}
$threadAttachments = [];
foreach ($attachments as $attachment) {
$threadAttachments[] = [
'fileName' => $attachment->getClientOriginalName(),
'mimeType' => $attachment->getMimeType(),
'data' => base64_encode($attachment->getContent()),
];
}
$response = $this->makeRequest("conversations", "POST", [
"type" => "email",
"mailboxId" => $mailboxId,
"subject" => $subject,
"customer" => [
'id' => $customerId,
],
"threads" => [[
'text' => $message,
'type' => 'customer',
'customer' => [
'id' => $customerId,
],
"attachments" => $threadAttachments
], [
'text' => implode("<br>", $additionalTexts),
'type' => 'note',
'user' => $this->_getFreescoutUserId()
]],
"createdAt" => now()->setTimezone("UTC")->toISOString(),
"status" => "active"
]);
/**
* Get the attachment IDs
*/
$attachmentResponse = [];
foreach ($response->_embedded->threads[0]->_embedded->attachments as $file) {
$attachmentResponse[] = $file->id;
}
Log::debug("Freescout ticket creation response", ["res" => $response]);
return new TicketCreatedResponse(
$response->id,
$response->number,
$response->_embedded->threads[0]->id,
$attachmentResponse
);
}
public function answerTicket(Ticket $ticket, string $message, array $attachments): TicketAnsweredResponse {
$threadAttachments = [];
foreach ($attachments as $attachment) {
$threadAttachments[] = [
'fileName' => $attachment->getClientOriginalName(),
'mimeType' => $attachment->getMimeType(),
'data' => base64_encode($attachment->getContent()),
];
}
$customerId = $this->getCustomerId($ticket->customer);
$response = $this->makeRequest("conversations/{$ticket->ticket_provider_external_id}/threads", "POST", [
'text' => $message,
'type' => 'customer',
'customer' => [
'id' => $customerId,
],
"attachments" => $threadAttachments
]);
/**
* Get the attachment IDs
*/
$attachmentResponse = [];
foreach ($response->_embedded->attachments as $file) {
$attachmentResponse[] = $file->id;
}
return new TicketAnsweredResponse(
$response->id,
$attachmentResponse,
);
}
public function downloadAttachment(Ticket $ticket, TicketAttachment $attachment): BinaryFileResponse {
$ticketMessages = $this->getTicketMessages($ticket->ticket_provider_external_id);
/** @var TicketMessageAttachmentResponse $attachmentDetails */
$attachmentDetails = $ticketMessages->pluck('attachments')->flatten()->where('id', $attachment->file_path)->first();
$tempImage = tempnam(sys_get_temp_dir(), $attachment->name);
copy($attachmentDetails->url, $tempImage);
return response()->download($tempImage, $attachment->name);
}
public function getDepartments(): Collection {
$response = $this->makeRequest("mailboxes");
$result = collect();
foreach ($response->_embedded->mailboxes as $mailbox) {
$result->add(new TicketDepartmentResponse($mailbox->id, $mailbox->name));
}
return $result;
}
/**
* @throws GuzzleException
*/
private function makeRequest(string $url, string $method = "GET", array $payload = []) {
$client = new Client();
$data = [
'headers' => [
"X-FreeScout-API-Key" => $this->provider->api_key,
"Content-Type" => "application/json",
],
'json' => $payload,
'connect_timeout' => 10
];
if($method === "GET") {
$data['query'] = $payload;
}
$response = $client->request($method, "{$this->provider->hostname}/api/{$url}", $data);
return json_decode($response->getBody()->getContents());
}
private function getCustomerId(?Customer $customer, ?string $guestName = null, ?string $guestMail = null,) {
if($customer === null) {
/**
* Create a guest in freescout
*/
$allCustomers = $this->makeRequest("customers", "GET", [
"email" => $guestMail
]);
if (count($allCustomers->_embedded->customers) === 1) {
return $allCustomers->_embedded->customers[0]->id;
}
$response = $this->makeRequest("customers", "POST", [
"firstName" => $guestName,
"lastName" => "-",
"notes" => "Aus hostware als Gast erstellt",
"emails" => [[
'value' => $guestMail,
'type' => 'home',
]]
]);
return $response->id;
}
if (isset($customer->support_ticket_providers['freescout'])) {
return $customer->support_ticket_providers['freescout'];
}
/**
* Search existing customers for the email
*/
$allCustomers = $this->makeRequest("customers", "GET", [
"email" => $customer->email
]);
if (count($allCustomers->_embedded->customers) === 1) {
$customer->addTicketProviderId("freescout", $allCustomers->_embedded->customers[0]->id);
return $allCustomers->_embedded->customers[0]->id;
}
$firstAddress = $customer->firstAddress();
$customerViewUrl = $customer->salesChannel->url . route('admin.customer.edit', $customer->id, false);
$response = $this->makeRequest("customers", "POST", [
"firstName" => $customer->first_name,
"lastName" => $customer->last_name,
"phone" => $customer->phone,
"photoUrl" => null,
"jobTitle" => null,
"photoType" => null,
"address" => array(
'city' => $firstAddress->city,
'state' => '-',
'zip' => $firstAddress->zipcode,
'country' => $firstAddress->country,
'address' => $firstAddress->street_full,
),
"notes" => "Aus hostware importiert\n\n{$customerViewUrl}",
"company" => $customer->company,
"emails" => [[
'value' => $customer->email,
'type' => 'home',
]],
"phones" => [[
'value' => $customer->phone,
'type' => 'home',
]]
]);
$customer->addTicketProviderId("freescout", $response->id);
return $response->id;
}
private function _getFreescoutUserId() {
return Cache::remember("hw-freescout-systemuser-id", 3600, function () {
$hostwareUser = $this->makeRequest("users", "GET", [
"email" => "[email protected]"
]);
if (count($hostwareUser->_embedded->users) === 1) {
return $hostwareUser->_embedded->users[0]->id;
}
$response = $this->makeRequest("users", "POST", [
"firstName" => "hostware",
"lastName" => "System",
"email" => "[email protected]"
]);
return $response->id;
});
}
public function getTicketUrl(Ticket $ticket): string {
$ticketId = str_replace("FS", "", $ticket->ticket_number);
return "{$this->provider->hostname}/conversation/{$ticket->ticket_provider_external_id}";
return "{$this->provider->hostname}/conversation/{$ticketId}";
}
}Was this helpful?