diff --git a/files/acp/database/install_dev.hanashi.wsc.discord-api.php b/files/acp/database/install_dev.hanashi.wsc.discord-api.php index fbafcba..e256252 100644 --- a/files/acp/database/install_dev.hanashi.wsc.discord-api.php +++ b/files/acp/database/install_dev.hanashi.wsc.discord-api.php @@ -2,12 +2,14 @@ use wcf\system\database\table\column\BigintDatabaseTableColumn; use wcf\system\database\table\column\BlobDatabaseTableColumn; +use wcf\system\database\table\column\IntDatabaseTableColumn; use wcf\system\database\table\column\NotNullInt10DatabaseTableColumn; use wcf\system\database\table\column\ObjectIdDatabaseTableColumn; use wcf\system\database\table\column\TextDatabaseTableColumn; use wcf\system\database\table\column\TinyintDatabaseTableColumn; use wcf\system\database\table\column\VarcharDatabaseTableColumn; use wcf\system\database\table\DatabaseTable; +use wcf\system\database\table\index\DatabaseTableForeignKey; use wcf\system\database\table\index\DatabaseTablePrimaryIndex; use wcf\system\database\table\PartialDatabaseTable; @@ -55,10 +57,19 @@ VarcharDatabaseTableColumn::create('publicKey') ->length(100), NotNullInt10DatabaseTableColumn::create('botTime'), + IntDatabaseTableColumn::create('webhookIconID') + ->length(10), ]) ->indices([ DatabaseTablePrimaryIndex::create() ->columns(['botID']), + ]) + ->foreignKeys([ + DatabaseTableForeignKey::create() + ->columns(['webhookIconID']) + ->referencedTable('wcf1_file') + ->referencedColumns(['fileID']) + ->onDelete('SET NULL'), ]), // wcf1_discord_webhook diff --git a/files/images/discord_webhook/.htaccess b/files/images/discord_webhook/.htaccess deleted file mode 100644 index 8d2f256..0000000 --- a/files/images/discord_webhook/.htaccess +++ /dev/null @@ -1 +0,0 @@ -deny from all diff --git a/files/lib/acp/form/DiscordBotAddForm.class.php b/files/lib/acp/form/DiscordBotAddForm.class.php index a30b60d..b13910d 100644 --- a/files/lib/acp/form/DiscordBotAddForm.class.php +++ b/files/lib/acp/form/DiscordBotAddForm.class.php @@ -7,10 +7,10 @@ use wcf\form\AbstractFormBuilderForm; use wcf\system\discord\DiscordApi; use wcf\system\form\builder\container\FormContainer; +use wcf\system\form\builder\field\FileProcessorFormField; use wcf\system\form\builder\field\IntegerFormField; use wcf\system\form\builder\field\PasswordFormField; use wcf\system\form\builder\field\TextFormField; -use wcf\system\form\builder\field\UploadFormField; use wcf\system\form\builder\field\validation\FormFieldValidationError; use wcf\system\form\builder\field\validation\FormFieldValidator; @@ -110,13 +110,12 @@ protected function createForm() ->required() ->maximumLength(50) ->value(PAGE_TITLE), - UploadFormField::create('webhookIcon') + FileProcessorFormField::create('webhookIconID') + ->objectType('dev.hanashi.wsc.discord.webhook.avatar') ->label('wcf.acp.discordBotAdd.webhookIcon') ->description('wcf.acp.discordBotAdd.webhookIcon.description') - ->maximum(1) - ->imageOnly() - ->maximumFilesize(8000000) - ->setAcceptableFiles(['image/jpeg', 'image/png', 'image/gif']), + ->singleFileUpload() + ->bigPreview(), ]), FormContainer::create('oauth2Settings') ->label('wcf.acp.discordBotAdd.oauth2Settings') diff --git a/files/lib/acp/form/DiscordBotEditForm.class.php b/files/lib/acp/form/DiscordBotEditForm.class.php index c842d52..883c7ea 100644 --- a/files/lib/acp/form/DiscordBotEditForm.class.php +++ b/files/lib/acp/form/DiscordBotEditForm.class.php @@ -7,6 +7,7 @@ use wcf\data\discord\bot\DiscordBot; use wcf\http\Helper; use wcf\system\exception\IllegalLinkException; +use wcf\system\form\builder\field\FileProcessorFormField; class DiscordBotEditForm extends DiscordBotAddForm { @@ -38,4 +39,16 @@ public function readParameters() throw new IllegalLinkException(); } } + + #[Override] + protected function createForm() + { + parent::createForm(); + + $webhookIconFormField = $this->form->getNodeById('webhookIconID'); + \assert($webhookIconFormField instanceof FileProcessorFormField); + $webhookIconFormField->context([ + 'botID' => $this->formObject->botID, + ]); + } } diff --git a/files/lib/bootstrap/dev.hanashi.wsc.discord-api.php b/files/lib/bootstrap/dev.hanashi.wsc.discord-api.php index 8bad4df..725d164 100644 --- a/files/lib/bootstrap/dev.hanashi.wsc.discord-api.php +++ b/files/lib/bootstrap/dev.hanashi.wsc.discord-api.php @@ -5,11 +5,13 @@ use wcf\acp\page\DiscordWebhookListPage; use wcf\event\acp\dashboard\box\PHPExtensionCollecting; use wcf\event\acp\menu\item\ItemCollecting; +use wcf\event\worker\RebuildWorkerCollecting; use wcf\system\event\EventHandler; use wcf\system\menu\acp\AcpMenuItem; use wcf\system\request\LinkHandler; use wcf\system\style\FontAwesomeIcon; use wcf\system\WCF; +use wcf\system\worker\DiscordWebhookAvatarRebuildDataWorker; return static function (): void { EventHandler::getInstance()->register(ItemCollecting::class, static function (ItemCollecting $event) { @@ -68,4 +70,11 @@ static function (PHPExtensionCollecting $event) { $event->register('sodium'); } ); + + EventHandler::getInstance()->register( + RebuildWorkerCollecting::class, + static function (RebuildWorkerCollecting $event) { + $event->register(DiscordWebhookAvatarRebuildDataWorker::class, 0); + } + ); }; diff --git a/files/lib/data/discord/bot/DiscordBot.class.php b/files/lib/data/discord/bot/DiscordBot.class.php index 0ac7158..0b54126 100644 --- a/files/lib/data/discord/bot/DiscordBot.class.php +++ b/files/lib/data/discord/bot/DiscordBot.class.php @@ -3,6 +3,7 @@ namespace wcf\data\discord\bot; use wcf\data\DatabaseObject; +use wcf\data\file\File; use wcf\system\cache\builder\DiscordGuildChannelCacheBuilder; use wcf\system\discord\DiscordApi; @@ -25,6 +26,7 @@ * @property-read string|null $clientSecret * @property-read string|null $publicKey * @property-read int $botTime + * @property-read int|null $webhookIconID */ final class DiscordBot extends DatabaseObject { @@ -40,6 +42,8 @@ final class DiscordBot extends DatabaseObject protected DiscordApi $discordApi; + protected ?File $file; + public function getDiscordApi(): DiscordApi { if (!isset($this->discordApi)) { @@ -53,9 +57,9 @@ public function getWebhookIconUploadFileLocations(): array { $files = []; - $filename = \sprintf('%simages/discord_webhook/%s.png', WCF_DIR, $this->botID); - if (\file_exists($filename)) { - $files[] = $filename; + if ($this->webhookIconID !== null) { + $file = new File($this->webhookIconID); + $files[] = $file->getPathname(); } return $files; @@ -68,4 +72,27 @@ public function getCachedDiscordChannel() 'botToken' => $this->botToken, ]); } + + public function getWebhookAvatar(): ?File + { + if ($this->webhookIconID === null) { + return null; + } + + if (!isset($this->file)) { + $this->file = new File($this->webhookIconID); + } + + return $this->file; + } + + public function getWebhookAvatarData(): ?string + { + $file = $this->getWebhookAvatar(); + if ($file === null) { + return null; + } + + return 'data:' . $file->mimeType . ';base64,' . \base64_encode(\file_get_contents($file->getPathname())); + } } diff --git a/files/lib/data/discord/bot/DiscordBotAction.class.php b/files/lib/data/discord/bot/DiscordBotAction.class.php index b6dc1f0..16662e1 100644 --- a/files/lib/data/discord/bot/DiscordBotAction.class.php +++ b/files/lib/data/discord/bot/DiscordBotAction.class.php @@ -7,7 +7,6 @@ use wcf\system\cache\builder\DiscordGuildChannelsCacheBuilder; use wcf\system\exception\AJAXException; use wcf\system\exception\PermissionDeniedException; -use wcf\system\file\upload\UploadFile; use wcf\system\WCF; /** @@ -47,13 +46,7 @@ public function create() unset($this->parameters['data']['useApplicationCommands']); } - $discordBot = parent::create(); - - if (isset($this->parameters['webhookIcon']) && \is_array($this->parameters['webhookIcon'])) { - $this->processWebhookIcon($discordBot->botID); - } - - return $discordBot; + return parent::create(); } #[Override] @@ -67,34 +60,6 @@ public function update() } parent::update(); - - foreach ($this->getObjects() as $object) { - if (isset($this->parameters['webhookIcon']) && \is_array($this->parameters['webhookIcon'])) { - if ($this->parameters['webhookIcon'] === '') { - $filename = \sprintf('%simages/discord_webhook/%s.png', WCF_DIR, $object->botID); - if (\file_exists($filename)) { - \unlink($filename); - } - } else { - $this->processWebhookIcon($object->botID); - } - } - } - } - - #[Override] - public function delete() - { - $returnValues = parent::delete(); - - foreach ($this->getObjects() as $object) { - $filename = \sprintf('%simages/discord_webhook/%s.png', WCF_DIR, $object->botID); - if (\file_exists($filename)) { - \unlink($filename); - } - } - - return $returnValues; } #[Override] @@ -103,23 +68,6 @@ protected function resetCache() DiscordGuildChannelsCacheBuilder::getInstance()->reset(); } - /** - * verarbeitet hochgeladenes Icon - * - * @param mixed $botID - * @return void - */ - protected function processWebhookIcon(int $botID) - { - $iconFile = \reset($this->parameters['webhookIcon']); - if ($iconFile instanceof UploadFile && !$iconFile->isProcessed()) { - $filename = \sprintf('%simages/discord_webhook/%s.png', WCF_DIR, $botID); - - \rename($iconFile->getLocation(), $filename); - $iconFile->setProcessed($filename); - } - } - /** * validiert die Methode getBotToken * diff --git a/files/lib/system/discord/WebhookHandler.class.php b/files/lib/system/discord/WebhookHandler.class.php index a8c48cc..e4b5006 100644 --- a/files/lib/system/discord/WebhookHandler.class.php +++ b/files/lib/system/discord/WebhookHandler.class.php @@ -8,7 +8,6 @@ use wcf\data\discord\webhook\DiscordWebhookList; use wcf\system\cache\runtime\DiscordBotRuntimeCache; use wcf\system\SingletonFactory; -use wcf\util\FileUtil; final class WebhookHandler extends SingletonFactory { @@ -21,12 +20,7 @@ public function saveWebhooks(int $botID, array $channelIDs, string $usageBy) } $api = $bot->getDiscordApi(); - $avatar = null; - $avatarFile = \sprintf('%simages/discord_webhook/%s.png', WCF_DIR, $botID); - if (\file_exists($avatarFile)) { - $mimeType = FileUtil::getMimeType($avatarFile); - $avatar = 'data:' . $mimeType . ';base64,' . \base64_encode(\file_get_contents($avatarFile)); - } + $avatar = $bot->getWebhookAvatarData(); $list = new DiscordWebhookList(); $list->getConditionBuilder()->add('botID = ?', [$bot->botID]); diff --git a/files/lib/system/discord/type/WebhookChannelMultiSelectDiscordType.class.php b/files/lib/system/discord/type/WebhookChannelMultiSelectDiscordType.class.php index 495eb4e..7f2dee2 100644 --- a/files/lib/system/discord/type/WebhookChannelMultiSelectDiscordType.class.php +++ b/files/lib/system/discord/type/WebhookChannelMultiSelectDiscordType.class.php @@ -6,7 +6,6 @@ use wcf\data\discord\webhook\DiscordWebhookAction; use wcf\data\discord\webhook\DiscordWebhookList; use wcf\system\exception\UserInputException; -use wcf\util\FileUtil; class WebhookChannelMultiSelectDiscordType extends ChannelMultiSelectDiscordType { @@ -67,12 +66,7 @@ public function validate($newValue) if (!isset($webhookChannelIDs[$botID]) || !\in_array($channelID, $webhookChannelIDs[$botID])) { $discordApi = $discordBots[$botID]->getDiscordApi(); - $avatar = null; - $avatarFile = \sprintf('%simages/discord_webhook/%s.png', WCF_DIR, $botID); - if (\file_exists($avatarFile)) { - $mimeType = FileUtil::getMimeType($avatarFile); - $avatar = 'data:' . $mimeType . ';base64,' . \base64_encode(\file_get_contents($avatarFile)); - } + $avatar = $discordBots[$botID]->getWebhookAvatarData(); $response = $discordApi->createWebhook($channelID, $discordBots[$botID]->webhookName, $avatar); if (!$response['error']) { $action = new DiscordWebhookAction([], 'create', [ diff --git a/files/lib/system/discord/type/WebhookChannelSelectDiscordType.class.php b/files/lib/system/discord/type/WebhookChannelSelectDiscordType.class.php index 7628c26..14db61f 100644 --- a/files/lib/system/discord/type/WebhookChannelSelectDiscordType.class.php +++ b/files/lib/system/discord/type/WebhookChannelSelectDiscordType.class.php @@ -6,7 +6,6 @@ use wcf\data\discord\webhook\DiscordWebhookAction; use wcf\data\discord\webhook\DiscordWebhookList; use wcf\system\exception\UserInputException; -use wcf\util\FileUtil; class WebhookChannelSelectDiscordType extends ChannelSelectDiscordType { @@ -63,12 +62,7 @@ public function validate($newValue, ?int $maxChannels = null) if (!isset($webhookChannelIDs[$botID]) || !\in_array($channelID, $webhookChannelIDs[$botID])) { $discordApi = $discordBots[$botID]->getDiscordApi(); - $avatar = null; - $avatarFile = \sprintf('%simages/discord_webhook/%s.png', WCF_DIR, $botID); - if (\file_exists($avatarFile)) { - $mimeType = FileUtil::getMimeType($avatarFile); - $avatar = 'data:' . $mimeType . ';base64,' . \base64_encode(\file_get_contents($avatarFile)); - } + $avatar = $discordBots[$botID]->getWebhookAvatarData(); $response = $discordApi->createWebhook($channelID, $discordBots[$botID]->webhookName, $avatar); if (!$response['error']) { $action = new DiscordWebhookAction([], 'create', [ diff --git a/files/lib/system/file/DiscordWebhookAvatarFileProcessor.class.php b/files/lib/system/file/DiscordWebhookAvatarFileProcessor.class.php new file mode 100644 index 0000000..5f53def --- /dev/null +++ b/files/lib/system/file/DiscordWebhookAvatarFileProcessor.class.php @@ -0,0 +1,117 @@ +getBot($context); + if ($bot === null) { + return FileProcessorPreflightResult::InvalidContext; + } + } + + if (!FileUtil::endsWithAllowedExtension($filename, $this->getAllowedFileExtensions($context))) { + return FileProcessorPreflightResult::FileExtensionNotPermitted; + } + + return FileProcessorPreflightResult::Passed; + } + + #[Override] + public function validateUpload(File $file): void + { + $imageData = @\getimagesize($file->getPathname()); + if ($imageData === false) { + throw new UserInputException('file', 'noImage'); + } + if ($imageData[0] < 128 || $imageData[1] < 128) { + throw new UserInputException('file', 'tooSmall'); + } + } + + #[Override] + public function adopt(File $file, array $context): void + { + $bot = $this->getBot($context); + + if ($bot !== null) { + (new DiscordBotEditor($bot))->update([ + 'webhookIconID' => $file->fileID, + ]); + } + } + + #[Override] + public function canDelete(File $file): bool + { + return WCF::getSession()->getPermission('admin.discord.canManageConnection'); + } + + #[Override] + public function canDownload(File $file): bool + { + return WCF::getSession()->getPermission('admin.discord.canManageConnection'); + } + + #[Override] + public function delete(array $fileIDs, array $thumbnailIDs): void + { + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add('webhookIconID IN (?)', [$fileIDs]); + + $sql = "UPDATE wcf1_discord_bot + SET webhookIconID = NULL + {$conditionBuilder}"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute($conditionBuilder->getParameters()); + } + + #[Override] + public function getObjectTypeName(): string + { + return 'dev.hanashi.wsc.discord.webhook.avatar'; + } + + #[Override] + public function getAllowedFileExtensions(array $context): array + { + return [ + 'jpg', + 'jpeg', + 'png', + 'gif', + ]; + } + + #[Override] + public function getMaximumSize(array $context): ?int + { + return 8000000; + } + + private function getBot(array $context): ?DiscordBot + { + $botID = $context['botID'] ?? null; + if ($botID === null) { + return null; + } + + return DiscordBotRuntimeCache::getInstance()->getObject($botID); + } +} diff --git a/files/lib/system/worker/DiscordWebhookAvatarRebuildDataWorker.class.php b/files/lib/system/worker/DiscordWebhookAvatarRebuildDataWorker.class.php new file mode 100644 index 0000000..027a228 --- /dev/null +++ b/files/lib/system/worker/DiscordWebhookAvatarRebuildDataWorker.class.php @@ -0,0 +1,57 @@ +objectList)) { + return; + } + + foreach ($this->objectList as $bot) { + $avatarFile = \sprintf('%simages/discord_webhook/%s.png', WCF_DIR, $bot->botID); + if (!\file_exists($avatarFile)) { + continue; + } + + $editor = new DiscordBotEditor($bot); + + $file = FileEditor::createFromExistingFile( + $avatarFile, + $bot->botID . '.png', + 'dev.hanashi.wsc.discord.webhook.avatar' + ); + + if ($file === null) { + continue; + } + + $editor->update([ + 'webhookIconID' => $file->fileID, + ]); + } + } + + #[Override] + protected function initObjectList() + { + parent::initObjectList(); + + $this->objectList->getConditionBuilder()->add('webhookIconID IS NULL'); + } +} diff --git a/language/de.xml b/language/de.xml index b385e6d..a38b40e 100644 --- a/language/de.xml +++ b/language/de.xml @@ -117,6 +117,10 @@ Discord-Bot erledigen.]]> + + + + diff --git a/language/en.xml b/language/en.xml index 0cef2dd..3d618e2 100644 --- a/language/en.xml +++ b/language/en.xml @@ -117,6 +117,10 @@ Go the Discord bot.]]> + + + + diff --git a/objectType.xml b/objectType.xml new file mode 100644 index 0000000..332796f --- /dev/null +++ b/objectType.xml @@ -0,0 +1,10 @@ + + + + + dev.hanashi.wsc.discord.webhook.avatar + com.woltlab.wcf.file + wcf\system\file\DiscordWebhookAvatarFileProcessor + + + diff --git a/package.xml b/package.xml index 6840234..beb06da 100644 --- a/package.xml +++ b/package.xml @@ -6,8 +6,8 @@ Discord-API API to cummincate with Discord. API um mit Discord zu kommunizieren. - 2.7.0 - 2024-08-31 + 2.7.1 + 2024-10-03 https://creativecommons.org/publicdomain/zero/1.0/deed.en @@ -15,7 +15,7 @@ https://hanashi.dev/ - com.woltlab.wcf + com.woltlab.wcf com.woltlab.wcf @@ -27,17 +27,25 @@ + acp/database/install_dev.hanashi.wsc.discord-api.php + + + + + acp/database/install_dev.hanashi.wsc.discord-api.php + + acp/database/install_dev.hanashi.wsc.discord-api.php