From f490722b92ade67b73b534b7320495679d5a03aa Mon Sep 17 00:00:00 2001 From: itsAzaria Date: Wed, 18 Feb 2026 23:40:53 +0000 Subject: [PATCH] First pass at message listener --- app/Events/MessageListener.php | 150 +++++++++++++++++++++++++++++++-- app/Models/Config.php | 2 + app/Services/Pastecord.php | 36 ++++++++ 3 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 app/Services/Pastecord.php diff --git a/app/Events/MessageListener.php b/app/Events/MessageListener.php index bd246eb..5c4a83d 100644 --- a/app/Events/MessageListener.php +++ b/app/Events/MessageListener.php @@ -5,6 +5,7 @@ namespace App\Events; use Discord\Discord; use Discord\Parts\Channel\Message; use Discord\WebSockets\Event as Events; +use Illuminate\Support\Str; use Laracord\Events\Event; use finfo; @@ -22,23 +23,154 @@ class MessageListener extends Event */ public function handle(Message $message, Discord $discord) { - if ($message->member->user->bot) { + if ($message->author?->bot || $message->member?->permissions?->manage_messages) { + // return; + } + + $files = []; + $codeBlocks = []; + $deletedMimeTypes = []; + $shouldDeleteOriginal = false; + + $content = $message->content ?? ''; + + // contains code block + if (Str::contains($content, '```')) { + $maxCodeBlockSize = \App\Models\Config::get(\App\Models\Config::MAX_CODEBLOCK_SIZE, 1000); + + foreach ($this->extractCodeBlocksWithLanguage($content) as $block) { + $isTooLarge = Str::length($block['code']) > $maxCodeBlockSize; + + $codeBlocks[] = [ + 'content' => $block['code'], + 'should_upload' => $isTooLarge, + 'language' => $block['language'], + 'url' => null, + ]; + + if ($isTooLarge) { + $shouldDeleteOriginal = true; + } + } + } + + + + + foreach ($message->attachments as $attachment) { + $fileContents = file_get_contents($attachment->url); + $mimeType = (new finfo(FILEINFO_MIME_TYPE))->buffer($fileContents); + + $mimeRule = \App\Models\Mime::where('mime', $mimeType)->first(); + + if (!$mimeRule) { + // Disallowed MIME type + $shouldDeleteOriginal = true; + $deletedMimeTypes[] = $mimeType; + continue; + } + + // Allowed, check if upload is required + if ($mimeRule->handling === 'UPLOAD') { + $files[] = [ + 'name' => $attachment->filename, + 'file' => $fileContents, + 'mime' => $mimeType, + 'url' => null, + ]; + $shouldDeleteOriginal = true; + } + } + + if (!$shouldDeleteOriginal) { return; } - $attachments = $message->attachments; + $message->delete(); - + $hasUploadableContent = count($files) > 0 || collect($codeBlocks)->where('should_upload', true)->isNotEmpty(); + + if (!$hasUploadableContent) { + $mimeList = $deletedMimeTypes ? implode(', ', $deletedMimeTypes) : 'unknown/unsupported'; + $message->channel->sendMessage( + "Hey <@{$message->member->id}>, your message contained disallowed content (MIME types: {$mimeList}) and has been deleted." + ); + return; + } - $message->attachments->map(function ($attachment) use ($message) { - $file = file_get_contents($attachment->url); + $pastecord = new \App\Services\Pastecord(); - $finfo = new finfo(FILEINFO_MIME_TYPE); - $mimeType = $finfo->buffer($file); + foreach ($codeBlocks as &$block) { + if ($block['should_upload']) { + $block['url'] = $pastecord->upload($block['content']); + } + } - $message->reply('You sent an attachment with mime type ' . $mimeType . ' and filename ' . $attachment->filename); - }); + foreach ($files as &$file) { + $file['url'] = $pastecord->upload($file['file']); + } + + $responseLines = ["Hey <@{$message->member->id}>, your file(s) and/or code block(s) have been uploaded to Pastecord:\n"]; + + $count = 0; + foreach ($codeBlocks as &$block) { + $lang = $block['language'] ? "{$block['language']}" : ''; + if ($block['should_upload']) { + $responseLines[] = "- **Code Block[{$count}]: **" . ($block['url'] ?? 'Failed to upload'); + } else { + $responseLines[] = "- **Code Block[{$count}]: ** (not uploaded, below is the content)\n```{$lang}\n{$block['content']}\n```"; + } + + $count++; + } + + foreach ($files as $file) { + $responseLines[] = "- **File:** {$file['name']}: " . ($file['url'] ?? 'Failed to upload'); + } + + $strippedContent = trim($this->stripCodeBlocks($content)); + if ($strippedContent) { + $responseLines[] = "\n**Message Content:**\n```" . $strippedContent . "```"; + } + + $message->channel->sendMessage(implode("\n", $responseLines)); + + $loggingChannelId = \App\Models\Config::get(\App\Models\Config::LOGGING_CHANNEL_ID); + if (!$loggingChannelId) { + return; + } + + $loggingChannel = $this->discord()->getChannel($loggingChannelId); + if (!$loggingChannel) { + return; + } + + $loggingChannel->sendMessage( + "Message from <@{$message->member->id}> in <#{$message->channel_id}> was deleted due to disallowed content. Uploaded content:\n\n" . + implode("\n", $responseLines) + ); + } + + function extractCodeBlocksWithLanguage(string $string): array + { + $codeBlocks = []; + + if (preg_match_all('/```(\w+)?\s*(.*?)```/s', $string, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $codeBlocks[] = [ + 'language' => $match[1] ?? '', + 'code' => trim($match[2]) + ]; + } + } + + return $codeBlocks; + } + + private function stripCodeBlocks(string $string): string + { + return preg_replace('/```(?:\w+)?\s*.*?```/s', '', $string); } } diff --git a/app/Models/Config.php b/app/Models/Config.php index 74ba3f4..161488b 100644 --- a/app/Models/Config.php +++ b/app/Models/Config.php @@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model; class Config extends Model { + const MAX_CODEBLOCK_SIZE = 'MAX_CODEBLOCK_SIZE'; + const LOGGING_CHANNEL_ID = 'LOGGING_CHANNEL_ID'; protected $fillable = ['key', 'value']; public static function get($key, $default = null) diff --git a/app/Services/Pastecord.php b/app/Services/Pastecord.php new file mode 100644 index 0000000..177424d --- /dev/null +++ b/app/Services/Pastecord.php @@ -0,0 +1,36 @@ +client = new \GuzzleHttp\Client([ + 'base_uri' => self::API_BASE_URL, + 'timeout' => 5.0, + ]); + } + public function upload(string $content): string + { + try { + // post with content in the body not form nor json + $response = $this->client->post('/documents', [ + 'body' => $content, + 'headers' => [ + 'Content-Type' => 'text/plain', + ], + ]); + + $data = json_decode($response->getBody(), true); + + return self::API_BASE_URL . '/' . $data['key']; + } catch (\Exception $e) { + return null; + } + } +}