Can now get channels!

- Inject LoggerInterface across WebhookController, TalkService, and ImageCleanup
- Replace TalkService.getAvailableTalkRooms() OCS API calls with direct database queries
- Add getBaseUrl() helper to resolve localhost/SSRF constraints for internal API calls
- Update buildRichObject() to generate public link shares for file attachments
- Add saveBotPassword, debug, and debugTables endpoints to WebhookController
- Separate frontend assets (JS/CSS) from adminSettings template
- Add composer.json and autoload files for PSR-4 autoloading
- Update Admin settings to use TemplateResponse and register app icons
This commit is contained in:
kyle
2026-06-11 10:21:23 -07:00
parent 3bba599133
commit 3f51a8ca48
11 changed files with 832 additions and 360 deletions
+144 -19
View File
@@ -10,15 +10,24 @@ use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Middleware\Security\CSRF\Token;
use OCP\AppFramework\Middleware\Security\CSRF\TokenStore;
use OCP\AppFramework\Utility\IControllerMethodReflector;
use OCP\Http\Client\IClientService;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\AppFramework\Controller\Attribute\AdminRequired;
use Psr\Log\LoggerInterface;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\SubAdminRequired;
class WebhookController extends Controller {
private TalkService $talkService;
private LoggerInterface $logger;
public function __construct(IRequest $request, TalkService $talkService) {
public function __construct(IRequest $request, TalkService $talkService, LoggerInterface $logger) {
// Debug: write to file to bypass output buffering
@file_put_contents('/tmp/ncdebug.txt', date('c') . " WebhookController constructed\n", FILE_APPEND);
parent::__construct('ncdiscordhook', $request);
$this->talkService = $talkService;
$this->logger = $logger;
}
/**
@@ -26,6 +35,7 @@ class WebhookController extends Controller {
*
* URL: POST /apps/ncdiscordhook/webhook/{roomToken}/{authToken}
*/
#[NoCSRFRequired]
public function receive(string $roomToken, string $authToken): DataResponse {
// Validate auth token
if (!$this->talkService->validateAuthToken($roomToken, $authToken)) {
@@ -37,7 +47,7 @@ class WebhookController extends Controller {
}
// Parse Discord JSON payload
$body = $this->request->getContent();
$body = file_get_contents('php://input');
$data = @json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
return new DataResponse(
@@ -88,7 +98,10 @@ class WebhookController extends Controller {
$filePath = $this->talkService->uploadImage($roomToken, $filename, $imageData['data'], $imageData['mimeType']);
if ($filePath !== null) {
$richObjects[] = $this->talkService->buildRichObject($filePath, $imageData['mimeType'], $roomToken);
$richObj = $this->talkService->buildRichObject($filePath, $imageData['mimeType'], $roomToken);
if ($richObj !== null) {
$richObjects[] = $richObj;
}
}
}
}
@@ -112,14 +125,37 @@ class WebhookController extends Controller {
);
}
/**
* Save bot password from the settings UI.
*
* URL: POST /apps/ncdiscordhook/save-bot-password
*/
#[AdminRequired]
#[NoCSRFRequired]
public function saveBotPassword(): DataResponse {
$body = file_get_contents('php://input');
$data = @json_decode($body, true);
if (!is_array($data) || empty($data['bot_password'])) {
return new DataResponse(
['error' => 'Invalid data'],
Http::STATUS_BAD_REQUEST,
);
}
$this->talkService->setBotPassword($data['bot_password']);
return new DataResponse(['status' => 'ok']);
}
/**
* Save configuration from the settings UI.
*
* URL: POST /apps/ncdiscordhook/save-config
*/
@AdminRequired
#[AdminRequired]
#[NoCSRFRequired]
public function saveConfig(): DataResponse {
$body = $this->request->getContent();
$body = file_get_contents('php://input');
$config = @json_decode($body, true);
if (!is_array($config)) {
return new DataResponse(
@@ -134,27 +170,116 @@ class WebhookController extends Controller {
}
/**
* Get available Talk rooms.
* Debug endpoint to verify the app is working.
*
* URL: GET /apps/ncdiscordhook/rooms
* URL: GET /apps/ncdiscordhook/debug
*/
@AdminRequired
public function getRooms(): DataResponse {
$rooms = $this->talkService->getAvailableTalkRooms();
$configured = $this->talkService->getRooms();
#[NoCSRFRequired]
public function debug(): DataResponse {
$info = [
'app_enabled' => \OC_App::isEnabled('ncdiscordhook'),
'user' => \OC_User::getUser(),
'user_is_admin' => \OC_User::isAdminUser(\OC_User::getUser()),
'bot_user' => null,
'has_bot_password' => false,
];
// Mark which rooms are already configured
try {
$bot = $this->talkService->getBotUser();
$info['bot_user'] = $bot ? $bot->getUID() : null;
$info['has_bot_password'] = $this->talkService->hasBotPassword();
} catch (\Exception $e) {
$info['bot_error'] = $e->getMessage();
}
return new DataResponse($info);
}
/**
* Diagnostic endpoint to discover Talk table names.
*
* URL: GET /apps/ncdiscordhook/debug-tables
*/
#[NoCSRFRequired]
public function debugTables(): DataResponse {
$result = [];
foreach ($rooms as $room) {
$token = $room['token'] ?? $room['roomId'] ?? '';
$name = $room['displayName'] ?? $room['name'] ?? '';
$result[] = [
'token' => $token,
'name' => $name,
'configured' => isset($configured[$token]),
try {
$conn = $this->talkService->getDbConnection();
$pdo = $conn->getConnection()->getWrappedConnection();
// List all tables that could be Talk-related
$tables = $pdo->query("
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_type = 'BASE TABLE'
AND table_schema NOT IN ('pg_catalog', 'information_schema')
AND (table_name LIKE 'talk%' OR table_name LIKE 'spreed%'
OR table_name LIKE '%room%' OR table_name LIKE '%attendee%'
OR table_name LIKE '%conversation%')
ORDER BY table_schema, table_name
")->fetchAll();
$result['tables'] = $tables;
// Check Talk app config
$talkPrefix = \OC::$server->get(\OCP\IConfig::class)->getAppValue('spreed', 'databaseprefix', '');
$sysPrefix = \OC::$server->get(\OCP\IConfig::class)->getSystemValueString('dbtableprefix', '');
$result['sysPrefix'] = $sysPrefix;
$result['talkPrefix'] = $talkPrefix;
// Check each candidate with tableExists
$candidates = [
$talkPrefix . 'talk_rooms',
$sysPrefix . 'talk_rooms',
'talk_rooms',
$talkPrefix . 'talk_attendees',
$sysPrefix . 'talk_attendees',
'talk_attendees',
];
$result['tableExists_results'] = [];
foreach ($candidates as $c) {
$result['tableExists_results'][$c] = $conn->tableExists($c);
}
} catch (\Exception $e) {
$result['error'] = $e->getMessage();
}
return new DataResponse($result);
}
/**
* Get available Talk rooms.
*
* URL: GET /apps/ncdiscordhook/rooms
*/
#[AdminRequired]
#[NoCSRFRequired]
public function getRooms(): DataResponse {
try {
$rooms = $this->talkService->getAvailableTalkRooms();
if (!is_array($rooms)) {
$rooms = [];
}
$configured = $this->talkService->getRooms();
// Mark which rooms are already configured
$result = [];
foreach ($rooms as $token => $name) {
$result[] = [
'token' => $token,
'name' => $name !== '' ? $name : $token,
'configured' => isset($configured[$token]),
];
}
return new DataResponse($result);
} catch (\Exception $e) {
$this->logger->error('NCdiscordhook getRooms failed: ' . $e->getMessage(), ['app' => 'ncdiscordhook', 'exception' => (string)$e]);
return new DataResponse(
['error' => 'Server error: ' . $e->getMessage()],
Http::STATUS_INTERNAL_SERVER_ERROR,
);
}
}
}
+5 -1
View File
@@ -7,20 +7,24 @@ use OCP\BackgroundJob\IJob;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
class ImageCleanup implements IJob {
private TalkService $talkService;
private IRootFolder $rootFolder;
private IUserManager $userManager;
private LoggerInterface $logger;
public function __construct(
TalkService $talkService,
IRootFolder $rootFolder,
IUserManager $userManager,
LoggerInterface $logger,
) {
$this->talkService = $talkService;
$this->rootFolder = $rootFolder;
$this->userManager = $userManager;
$this->logger = $logger;
}
public function run($argument = null): void {
@@ -41,7 +45,7 @@ class ImageCleanup implements IJob {
$count = $this->talkService->purgeOldImages();
if ($count > 0) {
\OC::$server->getLogger()->info(
$this->logger->info(
'NCdiscordhook: purged ' . $count . ' old image files',
['app' => 'ncdiscordhook'],
);
+277 -50
View File
@@ -10,10 +10,13 @@ use OCP\Files\IRootFolder;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\Security\Crypto\DefaultCrypto;
use OCP\Security\ICrypto;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Share\IManager;
use Psr\Log\LoggerInterface;
class TalkService {
private const APP_ID = 'ncdiscordhook';
@@ -21,28 +24,37 @@ class TalkService {
private IClient $client;
private IConfig $config;
private IDBConnection $db;
private IRootFolder $rootFolder;
private DefaultCrypto $crypto;
private ICrypto $crypto;
private IURLGenerator $urlGenerator;
private IUserManager $userManager;
private IRequest $request;
private IManager $shareManager;
private LoggerInterface $logger;
public function __construct(
IClientService $clientService,
IConfig $config,
IDBConnection $db,
IRootFolder $rootFolder,
DefaultCrypto $crypto,
ICrypto $crypto,
IURLGenerator $urlGenerator,
IUserManager $userManager,
IRequest $request,
IManager $shareManager,
LoggerInterface $logger,
) {
$this->client = $clientService->newClient();
$this->config = $config;
$this->db = $db;
$this->rootFolder = $rootFolder;
$this->crypto = $crypto;
$this->urlGenerator = $urlGenerator;
$this->userManager = $userManager;
$this->request = $request;
$this->shareManager = $shareManager;
$this->logger = $logger;
}
// ── Bot password ──────────────────────────────────────────────
@@ -71,6 +83,13 @@ class TalkService {
return $this->userManager->get('talk-bot');
}
/**
* Get the raw DB connection (for diagnostic use).
*/
public function getDbConnection(): IDBConnection {
return $this->db;
}
// ── Retention ─────────────────────────────────────────────────
public function getRetentionDays(): int {
@@ -81,6 +100,58 @@ class TalkService {
$this->config->setAppValue(self::APP_ID, 'retention_days', (string) $days);
}
// ── Base URL ──────────────────────────────────────────────────
/**
* Get the server base URL for internal API calls.
* Falls back to overwrite.cli.url when overwritewebroot is not set.
* When the URL resolves to localhost, falls back to the request host
* or trusted_domains — the HTTP client blocks localhost for SSRF safety.
*/
private function getBaseUrl(): string {
$overwritten = $this->config->getSystemValueString('overwritewebroot', '');
if ($overwritten !== '') {
return rtrim($overwritten, '/');
}
$cliUrl = $this->config->getSystemValueString('overwrite.cli.url', '');
if ($cliUrl === '') {
return '';
}
$parsed = parse_url($cliUrl);
$scheme = $parsed['scheme'] ?? 'https';
$host = $parsed['host'] ?? '';
$port = $parsed['port'] ?? '';
$path = rtrim($parsed['path'] ?? '', '/');
$portStr = $port !== '' ? ':' . $port : '';
$baseUrl = $scheme . '://' . $host . $portStr . $path;
// HTTP client blocks localhost — fall back to request host or trusted_domains
if (in_array(strtolower($host), ['localhost', '127.0.0.1', '::1'], true)) {
$requestHost = $this->request->getServerHost();
if ($requestHost !== '') {
// Extract port from Host header (e.g. "example.com:443" or "example.com")
$hostHeader = $this->request->getHeader('Host');
$requestPort = 0;
if (str_contains($hostHeader, ':')) {
$parts = explode(':', $hostHeader);
$requestPort = (int) end($parts);
}
$basePort = parse_url($baseUrl, PHP_URL_PORT);
$requestPortStr = $requestPort !== 0 && $requestPort !== $basePort
? ':' . $requestPort
: '';
return $scheme . '://' . $requestHost . $requestPortStr . $path;
}
// Last resort: trusted_domains
$trusted = $this->config->getSystemValue('trusted_domains', []);
if (!empty($trusted[0])) {
return $scheme . '://' . $trusted[0] . $path;
}
}
return $baseUrl;
}
// ── Rooms ─────────────────────────────────────────────────────
/**
@@ -100,33 +171,154 @@ class TalkService {
}
/**
* Get available Talk rooms via API.
* Get available Talk rooms via database query.
*
* Queries the Talk DB directly to find rooms where the bot is a participant.
* Bypasses the OCS API which does not accept app passwords for auth.
* Returns rooms as [token => displayName] pairs.
*/
public function getAvailableTalkRooms(): array {
$baseUrl = rtrim($this->config->getSystemValueString('overwritewebroot', ''), '/');
$url = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/room';
try {
$response = $this->client->get($url, [
'auth' => 'basic',
'basic' => [
$this->config->getSystemValueString('adminuser', 'admin'),
$this->config->getSystemValueString('adminpass', ''),
],
'headers' => [
'OCS-Expect' => '100',
],
]);
$data = json_decode($response->getBody(), true);
if (isset($data['ocs']['data'])) {
return $data['ocs']['data'];
}
} catch (\Exception $e) {
// Log but don't fail — rooms list is optional
$botUser = $this->getBotUser();
if (!$botUser) {
return [];
}
return [];
$botUid = $botUser->getUID();
// Detect table names by querying PostgreSQL information_schema directly.
// We cannot rely on tableExists() — it has case-sensitivity issues with PostgreSQL
// where it may match a differently-cased table name but raw SQL still fails.
$roomTable = $this->detectTalkTableFromCatalog('talk_rooms', 'spreed_room');
$participantTable = $this->detectTalkTableFromCatalog('talk_attendees', 'spreed_participant');
if ($roomTable === null || $participantTable === null) {
$sysPrefix = $this->config->getSystemValueString('dbtableprefix', '');
$talkPrefix = $this->config->getAppValue('spreed', 'databaseprefix', $sysPrefix);
$this->logger->warning('NCdiscordhook: Talk tables not found', [
'app' => self::APP_ID,
'sysPrefix' => $sysPrefix,
'talkPrefix' => $talkPrefix,
]);
return [];
}
// Find rooms where the bot is a participant.
// Use raw SQL — query builder's expr() methods re-parse table names and double-prefix them.
$sql = 'SELECT "' . $roomTable . '".token, "' . $roomTable . '".name, "' . $roomTable . '".type
FROM "' . $roomTable . '"
JOIN "' . $participantTable . '" ON "' . $participantTable . '".room_id = "' . $roomTable . '".id
WHERE "' . $participantTable . '".actor_type = :actorType
AND "' . $participantTable . '".actor_id = :actorId
AND "' . $roomTable . '".type <> :excludeType';
$result = $this->db->executeQuery($sql, [
'actorType' => 'users',
'actorId' => $botUid,
'excludeType' => -1, // exclude UNKNOWN type
]);
$this->logger->info('NCdiscordhook: getAvailableTalkRooms', [
'app' => self::APP_ID,
'bot_user' => $botUid,
'room_table' => $roomTable,
'participant_table' => $participantTable,
]);
try {
$rooms = [];
while ($row = $result->fetch()) {
$token = $row['token'];
$name = $row['name'] !== '' ? $row['name'] : $token;
$rooms[$token] = $name;
}
$result->closeCursor();
$this->logger->info('NCdiscordhook: found ' . count($rooms) . ' rooms', [
'app' => self::APP_ID,
'rooms' => array_keys($rooms),
]);
return $rooms;
} catch (\Exception $e) {
$this->logger->error('NCdiscordhook: room listing exception', [
'app' => self::APP_ID,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
return [];
}
}
/**
* Detect which Talk table name variant exists in the database.
*
* Queries PostgreSQL information_schema directly to find actual table names,
* then verifies by checking column presence. Avoids tableExists() which has
* case-sensitivity issues with PostgreSQL.
*
* Talk app has its own database prefix (spreed.appconfig.databaseprefix),
* separate from the main dbtableprefix.
*/
private function detectTalkTableFromCatalog(string $newName, string $oldName): ?string {
$sysPrefix = $this->config->getSystemValueString('dbtableprefix', '');
$talkPrefix = $this->config->getAppValue('spreed', 'databaseprefix', $sysPrefix);
// Build candidate list ordered by likelihood
$candidates = [
$talkPrefix . $newName,
$sysPrefix . $newName,
$talkPrefix . $oldName,
$sysPrefix . $oldName,
$newName,
$oldName,
];
// Deduplicate preserving order
$seen = [];
$unique = [];
foreach ($candidates as $name) {
if (!isset($seen[$name])) {
$seen[$name] = true;
$unique[] = $name;
}
}
// Query information_schema to find which candidates actually exist as tables
try {
$result = $this->db->executeQuery(
'SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\' AND table_type = \'BASE TABLE\' AND table_name IN (' . implode(',', array_map(fn($n) => '\'' . $n . '\'', $unique)) . ')',
);
$found = [];
while ($row = $result->fetch()) {
$found[] = $row['table_name'];
}
$result->closeCursor();
} catch (\Exception $e) {
$this->logger->warning('NCdiscordhook: information_schema query failed', [
'app' => self::APP_ID,
'error' => $e->getMessage(),
]);
return null;
}
// Try executing a lightweight query on each candidate to verify it's usable.
// This avoids the PostgreSQL case-sensitivity mismatch where tableExists()
// may match a differently-cased table but raw SQL fails.
foreach ($unique as $table) {
try {
$testResult = $this->db->executeQuery(
'SELECT 1 FROM "' . $table . '" LIMIT 1',
);
$testResult->closeCursor();
return $table;
} catch (\Exception $e) {
// Table exists in catalog but query failed — skip to next candidate
continue;
}
}
return null;
}
// ── Auth tokens ───────────────────────────────────────────────
@@ -274,7 +466,7 @@ class TalkService {
return ['data' => $body, 'mimeType' => $mimeType];
} catch (\Exception $e) {
\OC::$server->getLogger()->error('Failed to download image: ' . $url, ['app' => self::APP_ID]);
$this->logger->error('Failed to download image: ' . $url, ['app' => self::APP_ID]);
return null;
}
}
@@ -291,8 +483,8 @@ class TalkService {
try {
$userFolder = $this->rootFolder->getUserFolder($bot->getUID());
$imagesDir = $userFolder->getFolder(self::IMAGES_DIR);
$roomDir = $imagesDir->getFolder($roomToken);
$imagesDir = $userFolder->getFolder(self::IMAGES_DIR, true);
$roomDir = $imagesDir->getFolder($roomToken, true);
// Avoid path traversal
$safeFilename = basename($filename);
@@ -300,29 +492,60 @@ class TalkService {
return self::IMAGES_DIR . '/' . $roomToken . '/' . $safeFilename;
} catch (\Exception $e) {
\OC::$server->getLogger()->error('Failed to upload image: ' . $e->getMessage(), ['app' => self::APP_ID]);
$this->logger->error('Failed to upload image: ' . $e->getMessage(), ['app' => self::APP_ID]);
return null;
}
}
/**
* Build rich object data for a Talk message from an uploaded file path.
* Creates a public link share so Talk can resolve the rich object.
*/
public function buildRichObject(string $filePath, string $mimeType, string $roomToken): array {
$baseUrl = rtrim($this->config->getSystemValueString('overwritewebroot', ''), '/');
$serverUrl = $this->config->getSystemValueString('overwrite.cli.url', 'https://example.com');
public function buildRichObject(string $filePath, string $mimeType, string $roomToken): ?array {
$bot = $this->userManager->get('talk-bot');
if (!$bot) {
return null;
}
return [
'type' => 'file',
'source' => 'share',
'fileId' => basename($filePath),
'sourceType' => 'file',
'mimetype' => $mimeType,
'title' => basename($filePath),
'description' => 'Webhook image attachment',
'thumbnailReady' => true,
'fileTarget' => '/' . $filePath,
];
try {
$userFolder = $this->rootFolder->getUserFolder($bot->getUID());
$file = $userFolder->get($filePath);
if (!$file || !$file->isReadable()) {
return null;
}
$share = $this->shareManager->newShare();
$share->setNode($file)
->setPermissions(\OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_DOWNLOAD)
->setType(\OCP\Constants::SHARE_TYPE_LINK)
->setName(basename($filePath));
$share = $this->shareManager->createShare($share);
$linkShare = $this->shareManager->getShareById($share->getId());
$serverUrl = $this->config->getSystemValueString('overwrite.cli.url', 'https://example.com');
return [
'rich_object' => [
'id' => '',
'elements' => [
[
'type' => 'file',
'id' => $linkShare->getToken(),
'name' => basename($filePath),
'mimetype' => $mimeType,
'thumbnailReady' => true,
'fileTarget' => '/' . $filePath,
'path' => basename($filePath),
],
],
],
'source' => 'file',
];
} catch (\Exception $e) {
$this->logger->error('Failed to build rich object: ' . $e->getMessage(), ['app' => self::APP_ID]);
return null;
}
}
// ── Talk Chat API ─────────────────────────────────────────────
@@ -344,17 +567,17 @@ class TalkService {
): bool {
$bot = $this->userManager->get('talk-bot');
if (!$bot) {
\OC::$server->getLogger()->error('talk-bot user not found', ['app' => self::APP_ID]);
$this->logger->error('talk-bot user not found', ['app' => self::APP_ID]);
return false;
}
$botPassword = $this->getBotPassword();
if (!$botPassword) {
\OC::$server->getLogger()->error('Bot password not configured', ['app' => self::APP_ID]);
$this->logger->error('Bot password not configured', ['app' => self::APP_ID]);
return false;
}
$baseUrl = rtrim($this->config->getSystemValueString('overwritewebroot', ''), '/');
$baseUrl = $this->getBaseUrl();
$url = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/chat/' . rawurlencode($roomToken);
// Build form body
@@ -364,7 +587,11 @@ class TalkService {
];
if (!empty($richObjects)) {
$bodyParts['richObjects'] = json_encode($richObjects);
$indexed = [];
foreach ($richObjects as $index => $richObj) {
$indexed['file-' . $index] = $richObj;
}
$bodyParts['richObjects'] = $indexed;
}
$body = http_build_query($bodyParts, '', '&', PHP_QUERY_RFC3986);
@@ -384,7 +611,7 @@ class TalkService {
$httpCode = $response->getStatusCode();
return $httpCode === 200;
} catch (\Exception $e) {
\OC::$server->getLogger()->error('Failed to post to Talk: ' . $e->getMessage(), ['app' => self::APP_ID]);
$this->logger->error('Failed to post to Talk: ' . $e->getMessage(), ['app' => self::APP_ID]);
return false;
}
}
@@ -408,7 +635,7 @@ class TalkService {
$imagesDir = $userFolder->getFolder(self::IMAGES_DIR);
return $this->purgeFolder($imagesDir, $cutoff);
} catch (\Exception $e) {
\OC::$server->getLogger()->error('Image cleanup failed: ' . $e->getMessage(), ['app' => self::APP_ID]);
$this->logger->error('Image cleanup failed: ' . $e->getMessage(), ['app' => self::APP_ID]);
return 0;
}
}
+15 -8
View File
@@ -3,6 +3,7 @@
namespace OCA\NCdiscordhook\Settings;
use OCA\NCdiscordhook\Service\TalkService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IL10N;
use OCP\Settings\ISettings;
@@ -15,7 +16,10 @@ class Admin implements ISettings {
$this->l10n = $l10n;
}
public function getForm(): string {
public function getForm(): TemplateResponse {
\OCP\Util::addStyle('ncdiscordhook', 'adminSettings');
\OCP\Util::addScript('ncdiscordhook', 'settings');
$params = [
'hasBotPassword' => $this->talkService->hasBotPassword(),
'retentionDays' => $this->talkService->getRetentionDays(),
@@ -23,12 +27,8 @@ class Admin implements ISettings {
'authTokens' => $this->talkService->getAuthTokens(),
];
$template = \OC::$server->get(\OCP\ITemplate\ITemplateFactory::class)->load('ncdiscordhook', 'adminSettings');
foreach ($params as $key => $value) {
$template->assign($key, $value);
}
return $template->fetchPage();
$response = new TemplateResponse('ncdiscordhook', 'adminSettings', $params);
return $response;
}
public function getPriority(): int {
@@ -36,6 +36,13 @@ class Admin implements ISettings {
}
public function getSection(): string {
return 'server';
return 'additional';
}
public function getIcons(): array {
$urlGenerator = \OC::$server->get(\OCP\IURLGenerator::class);
return [
$urlGenerator->imagePath('ncdiscordhook', 'app.svg'),
];
}
}