Add f7push webhook for support replies and ticket deep links.
OCS endpoint for support.f7cloud.ru, notification notifier, and ?ticket= URL opening in the client UI.
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<name>f7support</name>
|
<name>f7support</name>
|
||||||
<summary>Support ticket client for F7cloud (F7cloud-compatible)</summary>
|
<summary>Support ticket client for F7cloud (F7cloud-compatible)</summary>
|
||||||
<description>f7support client app for creating and viewing support tickets.</description>
|
<description>f7support client app for creating and viewing support tickets.</description>
|
||||||
<version>0.1.8</version>
|
<version>0.1.9</version>
|
||||||
<licence>AGPL</licence>
|
<licence>AGPL</licence>
|
||||||
<author>f7support team</author>
|
<author>f7support team</author>
|
||||||
<namespace>F7Support</namespace>
|
<namespace>F7Support</namespace>
|
||||||
|
|||||||
+8
-1
@@ -4,5 +4,12 @@ return [
|
|||||||
'routes' => [
|
'routes' => [
|
||||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||||
],
|
],
|
||||||
|
'ocs' => [
|
||||||
|
[
|
||||||
|
'name' => 'push_api#notify',
|
||||||
|
'url' => '/api/v{version}/push',
|
||||||
|
'verb' => 'POST',
|
||||||
|
'requirements' => ['version' => '1'],
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1589,6 +1589,10 @@
|
|||||||
.catch((e) => showError(e.message))
|
.catch((e) => showError(e.message))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
scheduleTicketsBoardPolling();
|
scheduleTicketsBoardPolling();
|
||||||
|
const ticketFromUrl = new URLSearchParams(window.location.search).get("ticket");
|
||||||
|
if (ticketFromUrl) {
|
||||||
|
void tryOpenTicket(ticketFromUrl.trim());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\F7Support\AppInfo;
|
||||||
|
|
||||||
|
use OCA\F7Support\Notification\Notifier;
|
||||||
|
use OCP\AppFramework\App;
|
||||||
|
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||||
|
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||||
|
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||||
|
|
||||||
|
class Application extends App implements IBootstrap {
|
||||||
|
public const APP_ID = 'f7support';
|
||||||
|
|
||||||
|
public function __construct(array $urlParams = []) {
|
||||||
|
parent::__construct(self::APP_ID, $urlParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(IRegistrationContext $context): void {
|
||||||
|
$context->registerNotifierService(Notifier::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(IBootContext $context): void {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace OCA\F7Support;
|
|
||||||
|
|
||||||
use OCP\AppFramework\App;
|
|
||||||
|
|
||||||
class Application extends App {
|
|
||||||
public const APP_ID = 'f7support';
|
|
||||||
|
|
||||||
public function __construct(array $urlParams = []) {
|
|
||||||
parent::__construct(self::APP_ID, $urlParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -50,6 +50,10 @@ class PageController extends Controller {
|
|||||||
// Отключить вызовы с клиента: occ config:app:set f7support client_read_receipts --value=0
|
// Отключить вызовы с клиента: occ config:app:set f7support client_read_receipts --value=0
|
||||||
$clientReadReceipts = $this->config->getAppValue('f7support', 'client_read_receipts', '0');
|
$clientReadReceipts = $this->config->getAppValue('f7support', 'client_read_receipts', '0');
|
||||||
|
|
||||||
|
// Push webhook (support.f7cloud.ru → f7push): POST /ocs/v2.php/apps/f7support/api/v1/push
|
||||||
|
// Secret: occ config:app:get f7push api_secret (or f7support push_webhook_secret)
|
||||||
|
// occ config:app:set f7support push_enabled --value=yes
|
||||||
|
|
||||||
// Периодический GET /api/client/tickets (мс): has_unread и отпечаток карточки; WebSocket только у открытого тикета.
|
// Периодический GET /api/client/tickets (мс): has_unread и отпечаток карточки; WebSocket только у открытого тикета.
|
||||||
// По умолчанию 3000 (3 с). Пустая строка в конфиге трактуется как 3000. Явное «0» отключает опрос. Иначе 3000…120000. occ: f7support tickets_poll_ms
|
// По умолчанию 3000 (3 с). Пустая строка в конфиге трактуется как 3000. Явное «0» отключает опрос. Иначе 3000…120000. occ: f7support tickets_poll_ms
|
||||||
$rawPoll = trim((string) $this->config->getAppValue('f7support', 'tickets_poll_ms', '3000'));
|
$rawPoll = trim((string) $this->config->getAppValue('f7support', 'tickets_poll_ms', '3000'));
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\F7Support\Controller;
|
||||||
|
|
||||||
|
use OCA\F7Support\Service\ConfigService;
|
||||||
|
use OCA\F7Support\Service\PushBridgeService;
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
|
use OCP\AppFramework\OCSController;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook for support.f7cloud.ru: notify F7cloud user about a new support reply.
|
||||||
|
*
|
||||||
|
* Header: X-F7-Support-Push-Secret or X-F7-Push-Secret
|
||||||
|
* JSON: { userId?, email?, ticketNumber, body?, messagePreview?, ticketSubject? }
|
||||||
|
*/
|
||||||
|
class PushApiController extends OCSController {
|
||||||
|
public function __construct(
|
||||||
|
string $appName,
|
||||||
|
IRequest $request,
|
||||||
|
private ConfigService $config,
|
||||||
|
private PushBridgeService $pushBridge,
|
||||||
|
private IUserManager $userManager,
|
||||||
|
) {
|
||||||
|
parent::__construct($appName, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notify(): DataResponse {
|
||||||
|
if (!$this->authorize()) {
|
||||||
|
return new DataResponse(['error' => 'forbidden'], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->request->getParams();
|
||||||
|
$userId = trim((string)($params['userId'] ?? $params['user'] ?? ''));
|
||||||
|
if ($userId === '') {
|
||||||
|
$email = trim((string)($params['email'] ?? $params['clientEmail'] ?? ''));
|
||||||
|
if ($email !== '') {
|
||||||
|
$user = $this->userManager->getByEmail($email);
|
||||||
|
if ($user !== null) {
|
||||||
|
$userId = $user->getUID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticketNumber = trim((string)($params['ticketNumber'] ?? $params['ticket'] ?? ''));
|
||||||
|
$preview = trim((string)($params['body'] ?? $params['messagePreview'] ?? $params['preview'] ?? ''));
|
||||||
|
$ticketSubject = trim((string)($params['ticketSubject'] ?? $params['subject'] ?? ''));
|
||||||
|
|
||||||
|
if ($userId === '' || $ticketNumber === '') {
|
||||||
|
return new DataResponse(
|
||||||
|
['error' => 'userId (or email) and ticketNumber required'],
|
||||||
|
Http::STATUS_BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->userManager->get($userId) === null) {
|
||||||
|
return new DataResponse(['error' => 'user not found'], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queued = $this->pushBridge->notifyNewSupportMessage(
|
||||||
|
$userId,
|
||||||
|
$ticketNumber,
|
||||||
|
$preview,
|
||||||
|
$ticketSubject !== '' ? $ticketSubject : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new DataResponse([
|
||||||
|
'success' => $queued,
|
||||||
|
'queued' => $queued,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function authorize(): bool {
|
||||||
|
$header = $this->request->getHeader('X-F7-Support-Push-Secret');
|
||||||
|
if ($header === '') {
|
||||||
|
$header = $this->request->getHeader('X-F7-Push-Secret');
|
||||||
|
}
|
||||||
|
if ($header === '') {
|
||||||
|
$header = (string)$this->request->getParam('secret', '');
|
||||||
|
}
|
||||||
|
return $this->config->verifyPushWebhookSecret($header);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\F7Support\Notification;
|
||||||
|
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\L10N\IFactory;
|
||||||
|
use OCP\Notification\IManager as INotificationManager;
|
||||||
|
use OCP\Notification\INotification;
|
||||||
|
use OCP\Notification\INotifier;
|
||||||
|
use OCP\Notification\UnknownNotificationException;
|
||||||
|
|
||||||
|
class Notifier implements INotifier {
|
||||||
|
public function __construct(
|
||||||
|
private IFactory $l10nFactory,
|
||||||
|
private IURLGenerator $urlGenerator,
|
||||||
|
private INotificationManager $notificationManager,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getID(): string {
|
||||||
|
return 'f7support';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return $this->l10nFactory->get('f7support')->t('Support');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepare(INotification $notification, string $languageCode): INotification {
|
||||||
|
if ($notification->getApp() !== 'f7support') {
|
||||||
|
throw new UnknownNotificationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($notification->getSubject() !== 'support_reply') {
|
||||||
|
throw new UnknownNotificationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $notification->getSubjectParameters();
|
||||||
|
$ticketNumber = (string)($params['ticketNumber'] ?? '');
|
||||||
|
$ticketSubject = trim((string)($params['ticketSubject'] ?? ''));
|
||||||
|
$preview = trim((string)($params['preview'] ?? ''));
|
||||||
|
|
||||||
|
if ($preview === '') {
|
||||||
|
$msgParams = $notification->getMessageParameters();
|
||||||
|
$preview = trim((string)($msgParams['preview'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ticketSubject !== '') {
|
||||||
|
$title = 'Заявка ' . $ticketNumber . ': ' . $ticketSubject;
|
||||||
|
} else {
|
||||||
|
$title = 'Заявка ' . $ticketNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($preview === '') {
|
||||||
|
$preview = 'Новый ответ от поддержки';
|
||||||
|
}
|
||||||
|
|
||||||
|
$icon = $this->urlGenerator->getAbsoluteURL(
|
||||||
|
$this->urlGenerator->imagePath('f7support', 'app.svg')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->notificationManager->isPreparingPushNotification()) {
|
||||||
|
$notification->setParsedSubject($title . "\n" . $preview);
|
||||||
|
} else {
|
||||||
|
$notification->setParsedSubject($title);
|
||||||
|
$notification->setParsedMessage($preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
$notification->setIcon($icon);
|
||||||
|
|
||||||
|
return $notification;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\F7Support\Service;
|
||||||
|
|
||||||
|
use OCP\IConfig;
|
||||||
|
|
||||||
|
class ConfigService {
|
||||||
|
public const APP_ID = 'f7support';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private IConfig $config,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPushEnabled(): bool {
|
||||||
|
return $this->config->getAppValue(self::APP_ID, 'push_enabled', 'yes') === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPushWebhookSecret(): string {
|
||||||
|
return trim($this->config->getAppValue(self::APP_ID, 'push_webhook_secret', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyPushWebhookSecret(?string $provided): bool {
|
||||||
|
if ($provided === null || $provided === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$own = $this->getPushWebhookSecret();
|
||||||
|
if ($own !== '' && hash_equals($own, $provided)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$f7pushSecret = trim($this->config->getAppValue('f7push', 'api_secret', ''));
|
||||||
|
if ($f7pushSecret !== '' && hash_equals($f7pushSecret, $provided)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\F7Support\Service;
|
||||||
|
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\Notification\IManager as INotificationManager;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class PushBridgeService {
|
||||||
|
private const PREVIEW_MAX = 240;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private INotificationManager $notificationManager,
|
||||||
|
private IURLGenerator $urlGenerator,
|
||||||
|
private ConfigService $config,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notifyNewSupportMessage(
|
||||||
|
string $userId,
|
||||||
|
string $ticketNumber,
|
||||||
|
string $messagePreview,
|
||||||
|
?string $ticketSubject = null,
|
||||||
|
): bool {
|
||||||
|
if (!$this->config->isPushEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$preview = $this->truncate($messagePreview);
|
||||||
|
if ($preview === '') {
|
||||||
|
$preview = 'Новый ответ от поддержки';
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = trim((string)$ticketSubject);
|
||||||
|
$link = $this->urlGenerator->linkToRouteAbsolute('f7support.page.index')
|
||||||
|
. '?ticket=' . rawurlencode($ticketNumber);
|
||||||
|
|
||||||
|
$notification = $this->notificationManager->createNotification();
|
||||||
|
$notification
|
||||||
|
->setApp('f7support')
|
||||||
|
->setUser($userId)
|
||||||
|
->setDateTime(new \DateTime())
|
||||||
|
->setObject('ticket', $ticketNumber)
|
||||||
|
->setSubject('support_reply', [
|
||||||
|
'ticketNumber' => $ticketNumber,
|
||||||
|
'ticketSubject' => $subject,
|
||||||
|
'preview' => $preview,
|
||||||
|
])
|
||||||
|
->setMessage('support_reply', [
|
||||||
|
'preview' => $preview,
|
||||||
|
])
|
||||||
|
->setLink($link);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->notificationManager->notify($notification);
|
||||||
|
$this->logger->debug('f7support: push notification queued', [
|
||||||
|
'app' => 'f7support',
|
||||||
|
'user' => $userId,
|
||||||
|
'ticket' => $ticketNumber,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logger->warning('f7support: failed to queue push notification', [
|
||||||
|
'app' => 'f7support',
|
||||||
|
'user' => $userId,
|
||||||
|
'ticket' => $ticketNumber,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function truncate(string $text): string {
|
||||||
|
$text = trim(preg_replace('/\s+/u', ' ', $text) ?? $text);
|
||||||
|
if ($text === '' || mb_strlen($text) <= self::PREVIEW_MAX) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
return mb_substr($text, 0, self::PREVIEW_MAX - 1) . '…';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user