diff --git a/.env.ci b/.env.ci
index cf2b0fa4..e5008277 100644
--- a/.env.ci
+++ b/.env.ci
@@ -47,4 +47,9 @@ HCAPTCHA_SITEKEY=
HCAPTCHA_SECRET=
SLACK_CONTACT_WEBHOOK=
-EVENT_DEBUG_LOGGING_ENABLED=1
\ No newline at end of file
+SLACK_BOT_TOKEN=
+SLACK_SIGNING_SECRET=
+SLACK_CLIENT_ID=
+SLACK_CLIENT_SECRET=
+
+EVENT_DEBUG_LOGGING_ENABLED=1
diff --git a/.env.docker b/.env.docker
index 7a378215..6feb83b1 100644
--- a/.env.docker
+++ b/.env.docker
@@ -10,7 +10,7 @@ FORCE_SSL=false
LOG_CHANNEL=single
WWWGROUP=1337
LARAVEL_SAIL=1
-PHP_RUNTIME=8.1
+PHP_RUNTIME=8.3
# PHP_RUNTIME=7.4
DB_CONNECTION=mysql
@@ -61,6 +61,11 @@ HCAPTCHA_SITEKEY=
HCAPTCHA_SECRET=
SLACK_CONTACT_WEBHOOK=
+SLACK_BOT_TOKEN=
+SLACK_SIGNING_SECRET=
+SLACK_CLIENT_ID=
+SLACK_CLIENT_SECRET=
+
# The private Eventbrite API key / token for importing events
EVENTBRITE_PRIVATE_TOKEN=
@@ -88,4 +93,4 @@ EVENT_IMPORTER_MEETUP_GRAPHQL_PRIVATE_KEY_PATH=
# in the past will be returned. That is, start_date = {today's date - EVENTS_API_DEFAULT_DAYS}
EVENTS_API_DEFAULT_DAYS=1
-EVENT_DEBUG_LOGGING_ENABLED=1
\ No newline at end of file
+EVENT_DEBUG_LOGGING_ENABLED=1
diff --git a/.env.example b/.env.example
index f1320222..a879f8de 100644
--- a/.env.example
+++ b/.env.example
@@ -42,7 +42,7 @@ REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
-# using Mailtrap (a fake SMTP) will not actually send emails
+# using Mailtrap (a fake SMTP) will not actually send emails
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
@@ -57,6 +57,11 @@ ORGS_API_DOMAIN='https://data.openupstate.org'
GOOGLE_TAG_MANAGER=
+SLACK_BOT_TOKEN=
+SLACK_SIGNING_SECRET=
+SLACK_CLIENT_ID=
+SLACK_CLIENT_SECRET=
+
HCAPTCHA_SITEKEY=
HCAPTCHA_SECRET=
SLACK_CONTACT_WEBHOOK=
diff --git a/.env.testing b/.env.testing
index 18cd309e..b19e14e3 100644
--- a/.env.testing
+++ b/.env.testing
@@ -47,4 +47,9 @@ HCAPTCHA_SITEKEY=
HCAPTCHA_SECRET=
SLACK_CONTACT_WEBHOOK=
-EVENT_DEBUG_LOGGING_ENABLED=1
\ No newline at end of file
+SLACK_BOT_TOKEN=
+SLACK_SIGNING_SECRET=
+SLACK_CLIENT_ID=
+SLACK_CLIENT_SECRET=
+
+EVENT_DEBUG_LOGGING_ENABLED=1
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 91870516..220b2831 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -31,12 +31,12 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: 18
+ node-version: 24
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
- php-version: 8.1
+ php-version: 8.3
extensions: :php_psr
- name: copy environment variables to .env
diff --git a/.gitignore b/.gitignore
index a6df0c15..6ada9b35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ npm-debug.log
yarn-error.log
.env
database/database.sqlite*
+app-modules/slack-events-bot/slackbot-manifest.dev.json
# IDE helper files. These can be auto generated when running composer update
/_ide_helper.php
@@ -34,4 +35,4 @@ public/build/
*.pem
# Scribe cache and temporary storage
-.scribe
\ No newline at end of file
+.scribe
diff --git a/.tool-versions b/.tool-versions
index d400008e..cbf8b46a 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,3 +1,3 @@
-nodejs 18.17.1
-php 8.1.27
-yarn 1.22.19
+nodejs 24.3.0
+php 8.3.6
+yarn 1.22.22
diff --git a/Dockerfile b/Dockerfile
index a3893ea4..a13b51df 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM serversideup/php:8.1-fpm-nginx
+FROM serversideup/php:8.3-fpm-nginx
# Install additional packages
USER root
diff --git a/app-modules/slack-events-bot/README.md b/app-modules/slack-events-bot/README.md
new file mode 100644
index 00000000..34af7545
--- /dev/null
+++ b/app-modules/slack-events-bot/README.md
@@ -0,0 +1,128 @@
+# Slack Events Bot Module
+
+A Laravel module that posts HackGreenville events from the database to configured Slack channels.
+
+## Installation
+
+This module is automatically loaded as it's in the `app-modules` directory.
+
+## Configuration
+
+Add the following environment variables to your `.env` file:
+
+```env
+SLACK_BOT_TOKEN=xoxb-your-bot-token
+SLACK_SIGNING_SECRET=your-signing-secret
+SLACK_CLIENT_ID=your-client-id
+SLACK_CLIENT_SECRET=your-client-secret
+```
+
+## Setting up Slack Credentials for Testing
+
+To obtain the necessary Slack credentials for testing (SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET), you'll need to create a Slack app and configure it. Follow these steps:
+
+1. Run `cp app-modules/slack-events-bot/slackbot-manifest.json app-modules/slack-events-bot/slackbot-manifest.dev.json` to create a copy version of the manifest. More information about Slack bot manifests can be found on their documentation [here](https://docs.slack.dev/app-manifests/). Modify the values in `slackbot-manifest.dev.json` to match your public endpoint. If you don't have a public endpoint, you may need to create one with [ngrok](https://ngrok.com/).
+
+2. Go to [api.slack.com/apps](https://api.slack.com/apps) and click "Create New App". Then, upload your development manifest to get a head start.
+
+3. Get the following environment variables and add them to your `.env` file: `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`
+
+4. Next, navigate to your public endpoint and add `/slack/install` at the end. You should see a `Add to Slack` button displayed if it worked correctly!
+
+5. Go through the flow to connect the bot to a server and channel accordingly.
+
+6. In your Slack workspace, try doing the `/check_api`, `/add_channel`, and `/remove_channel` commands.
+
+7. The slackbot should work correctly!
+
+## Publishing Configuration
+
+To publish the configuration file:
+
+```bash
+php artisan vendor:publish --tag=slack-events-bot-config
+```
+
+## Running Migrations
+
+The migrations will run automatically with:
+
+```bash
+php artisan migrate
+```
+
+## Available Commands
+
+```bash
+# Manually check for events and update Slack messages
+php artisan slack:check-events
+
+# Delete old messages (default: 90 days)
+php artisan slack:delete-old-messages
+php artisan slack:delete-old-messages --days=60
+```
+
+## Scheduled Tasks
+
+The module automatically schedules:
+- Event check: Every hour
+- Old message cleanup: Daily
+
+Make sure your Laravel scheduler is running:
+
+```bash
+php artisan schedule:work
+```
+
+## Slack Commands
+
+The bot supports the following slash commands:
+
+- `/add_channel` - Add the current channel to receive event updates (admin only)
+- `/remove_channel` - Remove the current channel from receiving updates (admin only)
+- `/check_api` - Manually trigger an event check (rate limited to once per 15 minutes per workspace)
+
+## Routes
+
+- `GET /slack/install` - Display Slack installation button
+- `GET /slack/auth` - OAuth callback for Slack
+- `POST /slack/events` - Webhook endpoint for Slack events and commands
+
+## Features
+
+- Posts weekly event summaries to configured Slack channels
+- Automatically updates messages when events change
+- Handles message chunking for large event lists
+- Rate limiting for manual checks
+- Admin-only channel management
+- OAuth installation flow
+- Automatic cleanup of old messages
+- Direct database integration (no API calls needed)
+
+## How It Works
+
+1. The bot queries the Event model directly every hour for new/updated events
+2. Events are filtered to show published events from 1 day ago to 14 days ahead
+3. Events are grouped by week (Sunday to Saturday)
+4. Messages are posted/updated in configured Slack channels
+5. If a week has many events, they're split across multiple messages
+6. Messages for the current week and next week (5 days early) are maintained
+
+## Configuration Options
+
+The module can be configured in `config/slack-events-bot.php`:
+
+- `days_to_look_back` - How many days in the past to include events (default: 1)
+- `days_to_look_ahead` - How many days in the future to include events (default: 14)
+- `max_message_character_length` - Maximum characters per Slack message (default: 3000)
+- `check_api_cooldown_minutes` - Cooldown period for manual checks (default: 15)
+- `old_messages_retention_days` - Days to keep old messages (default: 90)
+
+## Migration from Python
+
+This module is a Laravel port of the original Python slack-events-bot, now refactored to use the Event model directly instead of making API calls. This provides:
+
+- Better performance (no HTTP overhead)
+- Real-time data (no API caching delays)
+- Tighter integration with the application
+- Easier maintenance and debugging
diff --git a/app-modules/slack-events-bot/composer.json b/app-modules/slack-events-bot/composer.json
new file mode 100644
index 00000000..53f4b16d
--- /dev/null
+++ b/app-modules/slack-events-bot/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "hack-greenville/slack-events-bot",
+ "description": "Slack bot that relays information from HackGreenville Labs' Events API to Slack channels",
+ "type": "library",
+ "license": "MIT",
+ "version": "1.0",
+ "autoload": {
+ "psr-4": {
+ "HackGreenville\\SlackEventsBot\\": "src/",
+ "HackGreenville\\SlackEventsBot\\Database\\Factories\\": "./database/factories/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "HackGreenville\\SlackEventsBot\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "HackGreenville\\SlackEventsBot\\Providers\\SlackEventsBotServiceProvider"
+ ]
+ }
+ }
+}
diff --git a/app-modules/slack-events-bot/config/slack-events-bot.php b/app-modules/slack-events-bot/config/slack-events-bot.php
new file mode 100644
index 00000000..f0520bcc
--- /dev/null
+++ b/app-modules/slack-events-bot/config/slack-events-bot.php
@@ -0,0 +1,60 @@
+ env('SLACK_BOT_TOKEN'),
+ 'signing_secret' => env('SLACK_SIGNING_SECRET'),
+ 'client_id' => env('SLACK_CLIENT_ID'),
+ 'client_secret' => env('SLACK_CLIENT_SECRET'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Scopes
+ |--------------------------------------------------------------------------
+ */
+ 'scopes' => [
+ 'chat:write',
+ 'chat:write.public',
+ 'commands',
+ 'incoming-webhook',
+ 'users:read',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Message Configuration
+ |--------------------------------------------------------------------------
+ */
+ 'max_message_character_length' => 3000,
+ 'header_buffer_length' => 61,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cooldown Configuration
+ |--------------------------------------------------------------------------
+ */
+ 'check_api_cooldown_minutes' => 15,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Database Configuration
+ |--------------------------------------------------------------------------
+ */
+ 'old_messages_retention_days' => 90,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Event Configuration
+ |--------------------------------------------------------------------------
+ */
+ 'days_to_look_back' => 1,
+ 'days_to_look_ahead' => 14,
+];
diff --git a/app-modules/slack-events-bot/database/factories/SlackChannelFactory.php b/app-modules/slack-events-bot/database/factories/SlackChannelFactory.php
new file mode 100644
index 00000000..6ab5da0c
--- /dev/null
+++ b/app-modules/slack-events-bot/database/factories/SlackChannelFactory.php
@@ -0,0 +1,20 @@
+ $this->faker->unique()->regexify('[C][A-Z0-9]{10}'),
+ 'slack_workspace_id' => SlackWorkspace::factory(),
+ ];
+ }
+}
diff --git a/app-modules/slack-events-bot/database/factories/SlackMessageFactory.php b/app-modules/slack-events-bot/database/factories/SlackMessageFactory.php
new file mode 100644
index 00000000..8dee6023
--- /dev/null
+++ b/app-modules/slack-events-bot/database/factories/SlackMessageFactory.php
@@ -0,0 +1,24 @@
+ Carbon::now()->startOfWeek(),
+ 'message' => $this->faker->sentence,
+ 'message_timestamp' => now()->timestamp . '.' . $this->faker->randomNumber(6, true),
+ 'sequence_position' => $this->faker->numberBetween(0, 10),
+ 'channel_id' => SlackChannel::factory(),
+ ];
+ }
+}
diff --git a/app-modules/slack-events-bot/database/factories/SlackWorkspaceFactory.php b/app-modules/slack-events-bot/database/factories/SlackWorkspaceFactory.php
new file mode 100644
index 00000000..67655920
--- /dev/null
+++ b/app-modules/slack-events-bot/database/factories/SlackWorkspaceFactory.php
@@ -0,0 +1,31 @@
+ $this->faker->unique()->regexify('[T][A-Z0-9]{10}'),
+ 'team_name' => $this->faker->company,
+ 'access_token' => $this->faker->sha256,
+ 'bot_user_id' => $this->faker->unique()->regexify('[U][A-Z0-9]{10}'),
+ ];
+ }
+}
diff --git a/app-modules/slack-events-bot/database/migrations/2024_01_01_000001_create_slack_channels_table.php b/app-modules/slack-events-bot/database/migrations/2024_01_01_000001_create_slack_channels_table.php
new file mode 100644
index 00000000..5cdecece
--- /dev/null
+++ b/app-modules/slack-events-bot/database/migrations/2024_01_01_000001_create_slack_channels_table.php
@@ -0,0 +1,23 @@
+id();
+ $table->string('slack_channel_id')->unique();
+ $table->timestamps();
+
+ $table->index('slack_channel_id');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('slack_channels');
+ }
+};
diff --git a/app-modules/slack-events-bot/database/migrations/2024_01_01_000002_create_slack_messages_table.php b/app-modules/slack-events-bot/database/migrations/2024_01_01_000002_create_slack_messages_table.php
new file mode 100644
index 00000000..ae6faa16
--- /dev/null
+++ b/app-modules/slack-events-bot/database/migrations/2024_01_01_000002_create_slack_messages_table.php
@@ -0,0 +1,28 @@
+id();
+ $table->dateTime('week');
+ $table->string('message_timestamp');
+ $table->text('message');
+ $table->integer('sequence_position')->default(0);
+ $table->foreignId('channel_id')->constrained('slack_channels')->onDelete('cascade');
+ $table->timestamps();
+
+ $table->index('week');
+ $table->index(['channel_id', 'week']);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('slack_messages');
+ }
+};
diff --git a/app-modules/slack-events-bot/database/migrations/2024_01_01_000003_create_slack_cooldowns_table.php b/app-modules/slack-events-bot/database/migrations/2024_01_01_000003_create_slack_cooldowns_table.php
new file mode 100644
index 00000000..25267b3c
--- /dev/null
+++ b/app-modules/slack-events-bot/database/migrations/2024_01_01_000003_create_slack_cooldowns_table.php
@@ -0,0 +1,26 @@
+id();
+ $table->string('accessor')->comment('Unique identifier from whomever is accessing the resource');
+ $table->string('resource')->comment('Unique identifier for whatever is rate-limited');
+ $table->dateTime('expires_at')->comment('When the accessor will be allowed to access the resource again');
+ $table->timestamps();
+
+ $table->unique(['accessor', 'resource']);
+ $table->index(['accessor', 'resource']);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('slack_cooldowns');
+ }
+};
diff --git a/app-modules/slack-events-bot/database/migrations/2025_07_08_232704_add_slack_workspace_id_to_slack_channels_table.php b/app-modules/slack-events-bot/database/migrations/2025_07_08_232704_add_slack_workspace_id_to_slack_channels_table.php
new file mode 100644
index 00000000..d43e4a07
--- /dev/null
+++ b/app-modules/slack-events-bot/database/migrations/2025_07_08_232704_add_slack_workspace_id_to_slack_channels_table.php
@@ -0,0 +1,27 @@
+string('slack_workspace_id')->nullable()->after('channel_id');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('slack_channels', function (Blueprint $table) {
+ $table->dropColumn('slack_workspace_id');
+ });
+ }
+};
diff --git a/app-modules/slack-events-bot/database/migrations/2025_07_08_233636_add_domain_to_slack_workspaces_table.php b/app-modules/slack-events-bot/database/migrations/2025_07_08_233636_add_domain_to_slack_workspaces_table.php
new file mode 100644
index 00000000..6541a81e
--- /dev/null
+++ b/app-modules/slack-events-bot/database/migrations/2025_07_08_233636_add_domain_to_slack_workspaces_table.php
@@ -0,0 +1,27 @@
+string('domain')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('slack_workspaces', function (Blueprint $table) {
+ $table->dropColumn('domain');
+ });
+ }
+};
diff --git a/app-modules/slack-events-bot/routes/api.php b/app-modules/slack-events-bot/routes/api.php
new file mode 100644
index 00000000..bd4707ca
--- /dev/null
+++ b/app-modules/slack-events-bot/routes/api.php
@@ -0,0 +1,9 @@
+middleware(ValidateSlackRequest::class)
+ ->name('events');
diff --git a/app-modules/slack-events-bot/routes/web.php b/app-modules/slack-events-bot/routes/web.php
new file mode 100644
index 00000000..76ac51f6
--- /dev/null
+++ b/app-modules/slack-events-bot/routes/web.php
@@ -0,0 +1,7 @@
+name('install');
+Route::get('/auth', [SlackController::class, 'auth'])->name('auth');
diff --git a/app-modules/slack-events-bot/slackbot-manifest.json b/app-modules/slack-events-bot/slackbot-manifest.json
new file mode 100644
index 00000000..814358a1
--- /dev/null
+++ b/app-modules/slack-events-bot/slackbot-manifest.json
@@ -0,0 +1,58 @@
+{
+ "display_information": {
+ "name": "HackGreenvilleAPIBot",
+ "description": "Allow Greenville APIs and tools to interact with the HackGreenville Slack.",
+ "background_color": "#006341",
+ "long_description": "Posts invite requests, contact form, and events from the HackGreenville site and local API / tools to HG Slack channels. See https://github.com/hackgvl for more details."
+ },
+ "features": {
+ "app_home": {
+ "home_tab_enabled": true,
+ "messages_tab_enabled": true,
+ "messages_tab_read_only_enabled": false
+ },
+ "bot_user": {
+ "display_name": "HackGreenville APIs",
+ "always_online": true
+ },
+ "slash_commands": [
+ {
+ "command": "/add_channel",
+ "url": "https://hackgreenville.com/slack/events",
+ "description": "Posts the app in the Slack channel",
+ "should_escape": false
+ },
+ {
+ "command": "/remove_channel",
+ "url": "https://hackgreenville.com/slack/events",
+ "description": "Removes the app from this slack channel",
+ "should_escape": false
+ },
+ {
+ "command": "/check_api",
+ "url": "https://hackgreenville.com/slack/events",
+ "description": "Manually checks OpenData API for events",
+ "should_escape": false
+ }
+ ]
+ },
+ "oauth_config": {
+ "redirect_urls": [
+ "https://hackgreenville.com/slack/auth"
+ ],
+ "scopes": {
+ "bot": [
+ "chat:write",
+ "chat:write.public",
+ "commands",
+ "incoming-webhook",
+ "users:read"
+ ]
+ }
+ },
+ "settings": {
+ "org_deploy_enabled": false,
+ "socket_mode_enabled": false,
+ "token_rotation_enabled": false
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Console/Commands/DeleteOldMessagesCommand.php b/app-modules/slack-events-bot/src/Console/Commands/DeleteOldMessagesCommand.php
new file mode 100644
index 00000000..6f0cb572
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Console/Commands/DeleteOldMessagesCommand.php
@@ -0,0 +1,33 @@
+option('days');
+ $this->info("Deleting messages and cooldowns older than {$days} days...");
+
+ try {
+ $this->databaseService->deleteOldMessages($days);
+ $this->info('Old messages and cooldowns deleted successfully!');
+ return self::SUCCESS;
+ } catch (Exception $e) {
+ $this->error('Error deleting old messages and cooldowns: ' . $e->getMessage());
+ return self::FAILURE;
+ }
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Exceptions/UnsafeMessageSpilloverException.php b/app-modules/slack-events-bot/src/Exceptions/UnsafeMessageSpilloverException.php
new file mode 100644
index 00000000..30a241d3
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Exceptions/UnsafeMessageSpilloverException.php
@@ -0,0 +1,10 @@
+ $state]);
+
+ $clientId = config('slack-events-bot.client_id');
+ $scopes = implode(',', config('slack-events-bot.scopes'));
+
+ $url = "https://slack.com/oauth/v2/authorize?" . http_build_query([
+ 'client_id' => $clientId,
+ 'scope' => $scopes,
+ 'state' => $state,
+ ]);
+
+ $html = <<
+
+
+ HTML;
+
+ return response($html);
+ }
+
+ public function auth(Request $request): Response
+ {
+ $code = $request->get('code');
+ $state = $request->get('state');
+ $error = $request->get('error');
+
+ if ($error) {
+ return response("Something is wrong with the installation (error: {$error})", 400);
+ }
+
+ $sessionState = session('slack_oauth_state');
+
+ if ($code && $sessionState === $state) {
+ session()->forget('slack_oauth_state');
+
+ $response = Http::asForm()->post('https://slack.com/api/oauth.v2.access', [
+ 'client_id' => config('slack-events-bot.client_id'),
+ 'client_secret' => config('slack-events-bot.client_secret'),
+ 'code' => $code,
+ ]);
+
+ $jsonResponse = $response->json();
+
+ if ($response->successful() && ($jsonResponse['ok'] ?? false)) {
+ $this->databaseService->createOrUpdateWorkspace($jsonResponse);
+ return response('The HackGreenville API bot has been installed successfully!');
+ }
+
+ $error = $jsonResponse['error'] ?? 'unknown_error';
+
+ if ($error === 'invalid_code') {
+ Log::warning('Slack OAuth failed with invalid_code. This may be due to a user re-trying the auth flow or the code expiring.', [
+ 'response' => $jsonResponse,
+ ]);
+ return response('The authorization code is invalid or has expired. Please try installing the app again.', 400);
+ }
+
+ Log::error('Slack OAuth failed', ['error' => $error, 'response' => $jsonResponse]);
+ return response("Something is wrong with the installation (error: {$error})", 400);
+ }
+
+ return response('Invalid state. Your session may have expired or you tried to use an old authorization link. Please try installing the app again.', 400);
+ }
+
+ public function events(Request $request): Response
+ {
+ $payload = $request->all();
+
+ // Handle URL verification challenge
+ if (isset($payload['type']) && $payload['type'] === 'url_verification') {
+ return response($payload['challenge']);
+ }
+
+ // Handle slash commands
+ if (isset($payload['command'])) {
+ return $this->handleSlashCommand($payload);
+ }
+
+ // Handle other events
+ if (isset($payload['event'])) {
+ // Process events asynchronously if needed
+ Log::info('Received Slack event', $payload);
+ }
+
+ return response('', 200);
+ }
+
+ private function handleSlashCommand(array $payload): Response
+ {
+ Log::info('Handling slash command', ['payload' => $payload]);
+
+ $command = $payload['command'];
+ $userId = $payload['user_id'];
+ $channelId = $payload['channel_id'];
+ $teamId = $payload['team_id'];
+ $teamDomain = $payload['team_domain'] ?? null;
+
+ $originalCommand = $command;
+
+ // Normalize dev commands to match production commands
+ if (true || app()->isLocal() && str_starts_with($command, '/dev_')) {
+ $command = str_replace('/dev_', '/', $command);
+ Log::info('Normalized dev command', ['original' => $originalCommand, 'normalized' => $command]);
+ }
+
+ switch ($command) {
+ case '/add_channel':
+ Log::info('Executing /add_channel command', ['user_id' => $userId, 'channel_id' => $channelId]);
+ if ( ! $this->authService->isAdmin($userId, $teamId)) {
+ Log::warning('/add_channel failed: user is not an admin', ['user_id' => $userId]);
+ return response('You must be a workspace admin in order to run `/add_channel`');
+ }
+
+ try {
+ $this->databaseService->addChannel($channelId, $teamId);
+ Log::info('Successfully added channel', ['channel_id' => $channelId]);
+ return response('Added channel to slack events bot 👍');
+ } catch (Exception $e) {
+ Log::error('/add_channel failed: could not add channel', [
+ 'channel_id' => $channelId,
+ 'exception' => $e->getMessage(),
+ ]);
+ return response('Slack events bot has already been activated for this channel');
+ }
+
+ case '/remove_channel':
+ Log::info('Executing /remove_channel command', ['user_id' => $userId, 'channel_id' => $channelId]);
+ if ( ! $this->authService->isAdmin($userId, $teamId)) {
+ Log::warning('/remove_channel failed: user is not an admin', ['user_id' => $userId]);
+ return response('You must be a workspace admin in order to run `/remove_channel`');
+ }
+
+ try {
+ $this->databaseService->removeChannel($channelId);
+ Log::info('Successfully removed channel', ['channel_id' => $channelId]);
+ return response('Removed channel from slack events bot 👍');
+ } catch (Exception $e) {
+ Log::error('/remove_channel failed: could not remove channel', [
+ 'channel_id' => $channelId,
+ 'exception' => $e->getMessage(),
+ ]);
+ return response('Slack events bot is not activated for this channel');
+ }
+
+ case '/check_api':
+ Log::info('Executing /check_api command', ['user_id' => $userId, 'team_domain' => $teamDomain]);
+ // Check cooldown
+ if (false && ! app()->isLocal() && $teamDomain) {
+ $expiryTime = $this->databaseService->getCooldownExpiryTime($teamDomain, 'check_api');
+
+ if ($expiryTime && $expiryTime->isFuture()) {
+ Log::info('/check_api command on cooldown', [
+ 'team_domain' => $teamDomain,
+ 'expires_at' => $expiryTime->toIso8601String(),
+ ]);
+ return response(
+ 'This command has been run recently and is on a cooldown period. ' .
+ 'Please try again in a little while!'
+ );
+ }
+
+ $cooldownMinutes = config('slack-events-bot.check_api_cooldown_minutes');
+ // Set new cooldown
+ $this->databaseService->createCooldown(
+ $teamDomain,
+ 'check_api',
+ $cooldownMinutes
+ );
+ Log::info('/check_api cooldown created', [
+ 'team_domain' => $teamDomain,
+ 'duration_minutes' => $cooldownMinutes,
+ ]);
+ }
+
+ // Run API check asynchronously
+ CheckEventsApi::dispatch();
+
+ Log::info('/check_api command successful, dispatched job.', ['user_id' => $userId]);
+ return response('Checking api for events 👍');
+
+ default:
+ Log::warning('Unknown slash command received', ['command' => $originalCommand, 'payload' => $payload]);
+ return response('Unknown command', 400);
+ }
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Http/Middleware/ValidateSlackRequest.php b/app-modules/slack-events-bot/src/Http/Middleware/ValidateSlackRequest.php
new file mode 100644
index 00000000..9438fa48
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Http/Middleware/ValidateSlackRequest.php
@@ -0,0 +1,23 @@
+authService->validateSlackRequest($request)) {
+ return response('Unauthorized', 401);
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Jobs/CheckEventsApi.php b/app-modules/slack-events-bot/src/Jobs/CheckEventsApi.php
new file mode 100644
index 00000000..01117284
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Jobs/CheckEventsApi.php
@@ -0,0 +1,41 @@
+handlePostingToSlack();
+ Log::info('Finished CheckEventsApi job.');
+ } catch (Throwable $e) {
+ Log::error('CheckEventsApi job failed with an exception.', [
+ 'exception' => get_class($e),
+ 'message' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]);
+ throw $e;
+ }
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Models/SlackChannel.php b/app-modules/slack-events-bot/src/Models/SlackChannel.php
new file mode 100644
index 00000000..f3e0cb70
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Models/SlackChannel.php
@@ -0,0 +1,34 @@
+hasMany(SlackMessage::class, 'channel_id');
+ }
+
+ public function workspace(): BelongsTo
+ {
+ return $this->belongsTo(SlackWorkspace::class, 'slack_workspace_id');
+ }
+
+ protected static function newFactory()
+ {
+ return SlackChannelFactory::new();
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Models/SlackCooldown.php b/app-modules/slack-events-bot/src/Models/SlackCooldown.php
new file mode 100644
index 00000000..dfca6d62
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Models/SlackCooldown.php
@@ -0,0 +1,18 @@
+ 'datetime',
+ ];
+}
diff --git a/app-modules/slack-events-bot/src/Models/SlackMessage.php b/app-modules/slack-events-bot/src/Models/SlackMessage.php
new file mode 100644
index 00000000..fca1a5d6
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Models/SlackMessage.php
@@ -0,0 +1,35 @@
+ 'datetime',
+ ];
+
+ public function channel(): BelongsTo
+ {
+ return $this->belongsTo(SlackChannel::class, 'channel_id');
+ }
+
+ protected static function newFactory()
+ {
+ return SlackMessageFactory::new();
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Models/SlackWorkspace.php b/app-modules/slack-events-bot/src/Models/SlackWorkspace.php
new file mode 100644
index 00000000..5452e2a8
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Models/SlackWorkspace.php
@@ -0,0 +1,52 @@
+attributes['access_token'] = Crypt::encryptString($value);
+ }
+
+ /**
+ * Decrypt the access token when getting it.
+ */
+ public function getAccessTokenAttribute(string $value): string
+ {
+ try {
+ return Crypt::decryptString($value);
+ } catch (DecryptException $e) {
+ Log::warning('Could not decrypt access token for workspace. The token might be stored in plain text.', [
+ 'workspace_id' => $this->id,
+ 'team_id' => $this->team_id,
+ ]);
+
+ return $value;
+ }
+ }
+
+ protected static function newFactory()
+ {
+ return SlackWorkspaceFactory::new();
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Providers/SlackEventsBotServiceProvider.php b/app-modules/slack-events-bot/src/Providers/SlackEventsBotServiceProvider.php
new file mode 100644
index 00000000..e6124e14
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Providers/SlackEventsBotServiceProvider.php
@@ -0,0 +1,71 @@
+mergeConfigFrom(
+ __DIR__ . '/../../config/slack-events-bot.php',
+ 'slack-events-bot'
+ );
+
+ // Register services
+ $this->app->singleton(AuthService::class);
+ $this->app->singleton(BotService::class);
+ $this->app->singleton(DatabaseService::class);
+ $this->app->singleton(EventService::class);
+ $this->app->singleton(MessageBuilderService::class);
+ }
+
+ public function boot(): void
+ {
+ $this->loadRoutes();
+
+ // Register commands
+ if ($this->app->runningInConsole()) {
+ $this->commands([
+ DeleteOldMessagesCommand::class,
+ ]);
+ }
+
+ $this->loadMigrationsFrom($this->moduleDir . '/database/migrations');
+
+ // Schedule tasks
+ $this->callAfterResolving(Schedule::class, function (Schedule $schedule) {
+ $schedule->command('slack:delete-old-messages')->daily();
+ $schedule->job(CheckEventsApi::class)->hourly();
+ });
+ }
+
+ protected function loadRoutes(): void
+ {
+ Route::prefix(config('slack-events-bot.route_prefix', 'slack'))
+ ->name('slack.')
+ ->group(function () {
+ Route::middleware('web')
+ ->group("{$this->moduleDir}/routes/web.php");
+
+ Route::middleware('api')
+ ->group("{$this->moduleDir}/routes/api.php");
+ });
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Services/AuthService.php b/app-modules/slack-events-bot/src/Services/AuthService.php
new file mode 100644
index 00000000..118abeed
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Services/AuthService.php
@@ -0,0 +1,70 @@
+first();
+
+ if ( ! $workspace) {
+ throw new RuntimeException("Workspace with team ID {$teamId} not found.");
+ }
+
+ $response = Http::withToken($workspace->access_token)
+ ->get('https://slack.com/api/users.info', [
+ 'user' => $userId,
+ ]);
+
+ return $response->json();
+ }
+
+ public function isAdmin(string $userId, string $teamId): bool
+ {
+ $userInfo = $this->getUserInfo($userId, $teamId);
+
+ return $userInfo['user']['is_admin'] ?? false;
+ }
+
+ public function validateSlackRequest(Request $request): bool
+ {
+ $timestamp = $request->header('X-Slack-Request-Timestamp');
+ $signature = $request->header('X-Slack-Signature');
+
+ if ( ! $timestamp || ! $signature) {
+ Log::warning('Slack request validation failed: Missing timestamp or signature.');
+ return false;
+ }
+
+ // Check for possible replay attacks (5 minutes)
+ if (abs(time() - (int)$timestamp) > 60 * 5) {
+ Log::warning('Slack request validation failed: Timestamp expired (replay attack?).', [
+ 'request_timestamp' => $timestamp,
+ ]);
+ return false;
+ }
+
+ // Verify signature
+ $signingSecret = config('slack-events-bot.signing_secret');
+ if (empty($signingSecret)) {
+ Log::error('Slack request validation failed: Signing secret is not configured.');
+ return false; // Fail safe
+ }
+ $sigBasestring = 'v0:' . $timestamp . ':' . $request->getContent();
+ $mySignature = 'v0=' . hash_hmac('sha256', $sigBasestring, $signingSecret);
+
+ if ( ! hash_equals($mySignature, $signature)) {
+ Log::warning('Slack request validation failed: Signature mismatch.');
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Services/BotService.php b/app-modules/slack-events-bot/src/Services/BotService.php
new file mode 100644
index 00000000..ba55614f
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Services/BotService.php
@@ -0,0 +1,257 @@
+databaseService->getSlackChannels();
+ $existingMessages = $this->databaseService->getMessages($week);
+
+ $messageDetails = [];
+ foreach ($channels as $channel) {
+ $channelId = $channel->slack_channel_id;
+ $messageDetails[$channelId] = [];
+ $channelExistingMessages = $existingMessages->where('channel.slack_channel_id', $channelId);
+
+ foreach ($channelExistingMessages as $existingMessage) {
+ $position = $existingMessage->sequence_position;
+ $messageDetails[$channelId][$position] = [
+ 'timestamp' => $existingMessage->message_timestamp,
+ 'message_text' => $existingMessage->message,
+ ];
+ }
+ }
+
+ foreach ($channels as $channel) {
+ $slackChannelId = $channel->slack_channel_id;
+ $token = $channel->workspace->access_token;
+ try {
+ foreach ($messages as $msgIdx => $msg) {
+ $msgText = $msg['text'];
+ $msgBlocks = $msg['blocks'];
+
+ $existingMsgDetail = $messageDetails[$slackChannelId][$msgIdx] ?? null;
+
+ if (
+ ! $existingMsgDetail
+ || $msgText !== $existingMsgDetail['message_text']
+ ) {
+ if ( ! $existingMsgDetail) {
+ if ($this->isUnsafeToSpillover(
+ count($existingMessages->where('channel.slack_channel_id', $slackChannelId)),
+ count($messages),
+ $week,
+ $slackChannelId
+ )) {
+ throw new UnsafeMessageSpilloverException;
+ }
+
+ Log::info("Posting new message " . ($msgIdx + 1) . " for week {$week->format('F j')} in {$slackChannelId}");
+
+ $slackResponse = $this->postNewMessage($slackChannelId, $msgBlocks, $msgText, $token);
+
+ $this->databaseService->createMessage(
+ $week,
+ $msgText,
+ $slackResponse['ts'],
+ $slackChannelId,
+ $msgIdx
+ );
+ } else { // An existing message needs to be updated
+ if ($this->isUnsafeToSpillover(
+ count($existingMessages->where('channel.slack_channel_id', $slackChannelId)),
+ count($messages),
+ $week,
+ $slackChannelId
+ )) {
+ throw new UnsafeMessageSpilloverException;
+ }
+
+ Log::info(
+ "Updating message " . ($msgIdx + 1) . " for week {$week->format('F j')} " .
+ "in {$slackChannelId} due to text content change."
+ );
+
+ $timestamp = $existingMsgDetail['timestamp'];
+
+ $slackResponse = Http::withToken($token)
+ ->post('https://slack.com/api/chat.update', [
+ 'ts' => $timestamp,
+ 'channel' => $slackChannelId,
+ 'blocks' => $msgBlocks,
+ 'text' => $msgText,
+ ]);
+
+ $json = $slackResponse->json();
+
+ if ( ! $slackResponse->successful() || ! ($json['ok'] ?? false)) {
+ $error = $json['error'] ?? 'unknown_error';
+ Log::error("Failed to update message {$timestamp} in Slack channel {$slackChannelId}", [
+ 'error' => $error,
+ 'response' => $json,
+ ]);
+ throw new Exception("Slack API error when updating message: {$error}");
+ }
+
+ $this->databaseService->updateMessage(
+ $week,
+ $msgText,
+ $timestamp,
+ $slackChannelId
+ );
+ }
+ } else {
+ Log::info(
+ "Message " . ($msgIdx + 1) . " for week of {$week->format('F j')} " .
+ "in {$slackChannelId} hasn't changed, not updating"
+ );
+ }
+ }
+ // Handle deletion of messages if the new set has fewer messages
+ $currentMessageCountForChannel = count($messageDetails[$slackChannelId] ?? []);
+ for ($i = count($messages); $i < $currentMessageCountForChannel; $i++) {
+ $timestampToDelete = $messageDetails[$slackChannelId][$i]['timestamp'] ?? null;
+ if ($timestampToDelete) {
+ Log::info("Deleting old message (sequence_position " . ($i) . ") for week of {$week->format('F j')} in {$slackChannelId}. No longer needed.");
+ Http::withToken($token)
+ ->post('https://slack.com/api/chat.delete', [
+ 'channel' => $slackChannelId,
+ 'ts' => $timestampToDelete,
+ ]);
+ $this->databaseService->deleteMessage($slackChannelId, $timestampToDelete);
+ }
+ }
+
+ } catch (UnsafeMessageSpilloverException $e) {
+ Log::error(
+ "Cannot update messages for {$week->format('m/d/Y')} for channel {$slackChannelId}. " .
+ "New events have caused the number of messages needed to increase, " .
+ "but the next week's post has already been sent. Cannot resize. " . "Existing message count: " . count($existingMessages->where('channel.slack_channel_id', $slackChannelId)) . " --- New message count: " . count($messages)
+ );
+ throw $e;
+ }
+ }
+ }
+
+ public function parseEventsForWeek(Collection $events, Carbon $weekStart): void
+ {
+ $eventBlocks = $this->messageBuilderService->buildEventBlocks($events);
+ $chunkedMessages = $this->messageBuilderService->chunkMessages($eventBlocks, $weekStart);
+
+ $this->postOrUpdateMessages($weekStart, $chunkedMessages);
+ }
+
+ public function handlePostingToSlack(): void
+ {
+ $weekStart = now()->copy()->startOfWeek();
+
+ $events = $this->getEventsForWeek($weekStart);
+
+ if ($events->isEmpty()) {
+ Log::info("No upcoming events found for the week of {$weekStart->format('F j')}. Cleaning up any existing messages for this week.");
+ $this->deleteMessagesForWeek($weekStart);
+ } else {
+ $this->parseEventsForWeek($events, $weekStart);
+ }
+ }
+
+ protected function getEventsForWeek(Carbon $weekStart): Collection
+ {
+ return Event::query()
+ ->with(['venue.state', 'organization'])
+ ->published()
+ ->whereBetween('active_at', [
+ $weekStart,
+ $weekStart->copy()->endOfWeek(),
+ ])
+ ->oldest('active_at')
+ ->get();
+ }
+
+ protected function deleteMessagesForWeek(Carbon $week): void
+ {
+ $messagesToDelete = $this->databaseService->getMessages($week);
+
+ if ($messagesToDelete->isEmpty()) {
+ return;
+ }
+
+ foreach ($messagesToDelete as $message) {
+ $token = $message->channel->workspace->access_token;
+ Log::info("Deleting stale message {$message->message_timestamp} in channel {$message->channel->slack_channel_id}.");
+ Http::withToken($token)
+ ->post('https://slack.com/api/chat.delete', [
+ 'channel' => $message->channel->slack_channel_id,
+ 'ts' => $message->message_timestamp,
+ ]);
+ // We don't care if the deletion fails on Slack's end (e.g., message already deleted),
+ // we still want to remove it from our database.
+ }
+
+ $this->databaseService->deleteMessagesForWeek($week);
+ }
+
+ private function isUnsafeToSpillover(
+ int $existingMessagesLength,
+ int $newMessagesLength,
+ Carbon $week,
+ string $slackChannelId
+ ): bool {
+ if ($newMessagesLength > $existingMessagesLength && $existingMessagesLength > 0) {
+ $latestMessage = $this->databaseService->getMostRecentMessageForChannel($slackChannelId);
+
+ if ( ! $latestMessage) {
+ return false;
+ }
+
+ $latestMessageWeek = Carbon::parse($latestMessage['week']);
+
+ // If the latest message is for a more recent week then it is unsafe
+ // to add new messages. We cannot place new messages before older, existing ones.
+ return $latestMessageWeek->greaterThan($week);
+ }
+
+ return false;
+ }
+
+ private function postNewMessage(string $slackChannelId, array $msgBlocks, string $msgText, string $token): array
+ {
+ $slackResponse = Http::withToken($token)
+ ->post('https://slack.com/api/chat.postMessage', [
+ 'channel' => $slackChannelId,
+ 'blocks' => $msgBlocks,
+ 'text' => $msgText,
+ 'unfurl_links' => false,
+ 'unfurl_media' => false,
+ ]);
+
+ $json = $slackResponse->json();
+
+ if ( ! $slackResponse->successful() || ! ($json['ok'] ?? false)) {
+ $error = $json['error'] ?? 'unknown_error';
+ Log::error("Failed to post new message to Slack channel {$slackChannelId}", [
+ 'error' => $error,
+ 'response' => $json,
+ ]);
+ throw new Exception("Slack API error when posting message: {$error}");
+ }
+
+ return $json;
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Services/DatabaseService.php b/app-modules/slack-events-bot/src/Services/DatabaseService.php
new file mode 100644
index 00000000..020d2ae1
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Services/DatabaseService.php
@@ -0,0 +1,173 @@
+firstOrFail();
+
+ return SlackMessage::create([
+ 'week' => $week->toDateTimeString(),
+ 'message' => $message,
+ 'message_timestamp' => $messageTimestamp,
+ 'channel_id' => $channel->id,
+ 'sequence_position' => $sequencePosition,
+ ]);
+ }
+
+ public function updateMessage(
+ Carbon $week,
+ string $message,
+ string $messageTimestamp,
+ string $slackChannelId
+ ): int {
+ $channel = SlackChannel::where('slack_channel_id', $slackChannelId)->firstOrFail();
+
+ return SlackMessage::whereDate('week', $week->toDateString())
+ ->where('message_timestamp', $messageTimestamp)
+ ->where('channel_id', $channel->id)
+ ->update(['message' => $message]);
+ }
+
+ public function getMessages(Carbon $week): Collection
+ {
+ return SlackMessage::with('channel.workspace')
+ ->whereDate('week', $week->toDateString())
+ ->orderBy('channel_id')
+ ->orderBy('sequence_position')
+ ->get();
+ }
+
+ public function getMostRecentMessageForChannel(string $slackChannelId): ?array
+ {
+ $channel = SlackChannel::where('slack_channel_id', $slackChannelId)->first();
+
+ if ( ! $channel) {
+ return null;
+ }
+
+ $message = SlackMessage::where('channel_id', $channel->id)
+ ->orderBy('week', 'desc')
+ ->orderBy('sequence_position', 'desc')
+ ->first();
+
+ if ( ! $message) {
+ return null;
+ }
+
+ return [
+ 'week' => $message->week->toIso8601String(),
+ 'message' => $message->message,
+ 'message_timestamp' => $message->message_timestamp,
+ ];
+ }
+
+ public function getSlackChannels(): Collection
+ {
+ return SlackChannel::with('workspace')->get();
+ }
+
+ public function addChannel(string $slackChannelId, string $teamId): SlackChannel
+ {
+ $workspace = SlackWorkspace::where('team_id', $teamId)->firstOrFail();
+
+ return SlackChannel::firstOrCreate(
+ ['slack_channel_id' => $slackChannelId],
+ ['slack_workspace_id' => $workspace->id]
+ );
+ }
+
+ public function removeChannel(string $slackChannelId): int
+ {
+ return SlackChannel::where('slack_channel_id', $slackChannelId)->delete();
+ }
+
+ public function deleteMessagesForWeek(Carbon $week): int
+ {
+ return SlackMessage::whereDate('week', $week->toDateString())->delete();
+ }
+
+ /**
+ * Deletes a specific message by its Slack channel ID and timestamp.
+ * This is used for cleaning up individual messages no longer needed.
+ *
+ * @param string $slackChannelId The Slack channel ID.
+ * @param string $messageTimestamp The Slack message timestamp (ts).
+ * @return int The number of deleted records.
+ */
+ public function deleteMessage(string $slackChannelId, string $messageTimestamp): int
+ {
+ $channel = SlackChannel::where('slack_channel_id', $slackChannelId)->first();
+
+ if ( ! $channel) {
+ Log::warning("Attempted to delete message in non-existent channel: {$slackChannelId}");
+ return 0;
+ }
+
+ return SlackMessage::where('channel_id', $channel->id)
+ ->where('message_timestamp', $messageTimestamp)
+ ->delete();
+ }
+
+
+ public function deleteOldMessages(int $daysBack = 90): void
+ {
+ $cutoffDate = Carbon::now()->subDays($daysBack);
+
+ // Delete old messages
+ SlackMessage::whereDate('week', '<', $cutoffDate->startOfWeek()->toDateString())
+ ->delete();
+
+ // Delete old cooldowns
+ SlackCooldown::where('expires_at', '<', $cutoffDate)->delete();
+ }
+
+ public function createCooldown(string $accessor, string $resource, int $cooldownMinutes): SlackCooldown
+ {
+ return SlackCooldown::updateOrCreate(
+ [
+ 'accessor' => $accessor,
+ 'resource' => $resource,
+ ],
+ [
+ 'expires_at' => Carbon::now()->addMinutes($cooldownMinutes),
+ ]
+ );
+ }
+
+ public function getCooldownExpiryTime(string $accessor, string $resource): ?Carbon
+ {
+ $cooldown = SlackCooldown::where('accessor', $accessor)
+ ->where('resource', $resource)
+ ->first();
+
+ return $cooldown?->expires_at;
+ }
+
+ public function createOrUpdateWorkspace(array $data): SlackWorkspace
+ {
+ return SlackWorkspace::updateOrCreate(
+ ['team_id' => $data['team']['id']],
+ [
+ 'team_name' => $data['team']['name'],
+ 'access_token' => $data['access_token'],
+ 'bot_user_id' => $data['bot_user_id'],
+ ]
+ );
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Services/EventService.php b/app-modules/slack-events-bot/src/Services/EventService.php
new file mode 100644
index 00000000..c630c9a4
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Services/EventService.php
@@ -0,0 +1,69 @@
+event_name);
+ if (empty($eventName)) {
+ $eventName = 'Untitled Event';
+ }
+ $limitedEventName = Str::limit($eventName, 150);
+
+ return [
+ [
+ 'type' => 'header',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => $limitedEventName,
+ ],
+ ],
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => Str::limit($event->description, 250),
+ ],
+ 'fields' => [
+ ['type' => 'mrkdwn', 'text' => '*' . Str::limit($event->organization->title) . '*'],
+ ['type' => 'mrkdwn', 'text' => '<' . $event->url . '|*Link* :link:>'],
+ ['type' => 'mrkdwn', 'text' => '*Status*'],
+ ['type' => 'mrkdwn', 'text' => $this->printStatus($event->status)],
+ ['type' => 'mrkdwn', 'text' => '*Location*'],
+ ['type' => 'mrkdwn', 'text' => $event->venue ? $event->venue->fullAddress() : 'No location'],
+ ['type' => 'mrkdwn', 'text' => '*Time*'],
+ ['type' => 'plain_text', 'text' => $event->active_at->format('F j, Y g:i A T')],
+ ],
+ ],
+ ];
+ }
+
+ public function generateText(Event $event): string
+ {
+ return sprintf(
+ "%s\nOrganization: %s\nDescription: %s\nLink: %s\nStatus: %s\nLocation: %s\nTime: %s",
+ Str::limit($event->event_name, 250),
+ Str::limit($event->organization->title, 250),
+ Str::limit($event->description, 250),
+ $event['url'],
+ $this->printStatus($event->status),
+ $event->venue?->name ?? 'No location',
+ $event->active_at->format('F j, Y g:i A T')
+ );
+ }
+
+ private function printStatus(string $status): string
+ {
+ return match ($status) {
+ 'upcoming' => 'Upcoming ✅',
+ 'past' => 'Past ✔',
+ 'cancelled' => 'Cancelled ❌',
+ default => ucfirst($status),
+ };
+ }
+}
diff --git a/app-modules/slack-events-bot/src/Services/MessageBuilderService.php b/app-modules/slack-events-bot/src/Services/MessageBuilderService.php
new file mode 100644
index 00000000..43a41e9b
--- /dev/null
+++ b/app-modules/slack-events-bot/src/Services/MessageBuilderService.php
@@ -0,0 +1,145 @@
+ $events A collection of Event models.
+ * @return Collection A collection where each item represents an event's blocks and text for Slack.
+ */
+ public function buildEventBlocks(Collection $events): Collection
+ {
+ return $events
+ ->map(fn (Event $event) => $this->buildSingleEventBlock($event))
+ ->filter()
+ ->values();
+ }
+
+ /**
+ * Chunks a collection of event blocks into multiple Slack messages.
+ *
+ * @param Collection $eventBlocks A collection of arrays, each containing 'blocks', 'text', and 'text_length'.
+ * @param Carbon $weekStart The start of the week for header generation.
+ * @return array An array of message arrays, each containing 'blocks' and 'text'.
+ */
+ public function chunkMessages(Collection $eventBlocks, Carbon $weekStart): array
+ {
+ $messagesNeeded = $this->totalMessagesNeeded($eventBlocks);
+ $maxLength = config('slack-events-bot.max_message_character_length');
+
+ $messages = [];
+ $initialHeader = $this->buildHeader($weekStart, 1, $messagesNeeded);
+
+ $blocks = $initialHeader['blocks'];
+ $text = $initialHeader['text'];
+
+ foreach ($eventBlocks as $event) {
+ // Event can be safely added to existing message
+ if (($event['text_length'] + mb_strlen($text)) < $maxLength) {
+ $blocks = array_merge($blocks, $event['blocks']);
+ $text .= $event['text'];
+ continue;
+ }
+
+ // Save message and then start a new one
+ $messages[] = ['blocks' => $blocks, 'text' => $text];
+
+ $newHeader = $this->buildHeader($weekStart, count($messages) + 1, $messagesNeeded);
+ $blocks = array_merge($newHeader['blocks'], $event['blocks']);
+ $text = $newHeader['text'] . $event['text'];
+ }
+
+ // Add whatever is left as a new message
+ $messages[] = ['blocks' => $blocks, 'text' => $text];
+
+ return $messages;
+ }
+
+ /**
+ * Builds the header block and text for a Slack message.
+ *
+ * @param Carbon $weekStart The start of the week.
+ * @param int $index The current message index (e.g., 1 of 3).
+ * @param int $total The total number of messages.
+ * @return array An array containing 'blocks', 'text', and 'text_length' for the header.
+ */
+ private function buildHeader(Carbon $weekStart, int $index, int $total): array
+ {
+ $text = sprintf(
+ "HackGreenville Events for the week of %s - %d of %d\n\n===\n\n",
+ $weekStart->format('F j'),
+ $index,
+ $total
+ );
+
+ return [
+ 'blocks' => [
+ [
+ 'type' => 'header',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => sprintf(
+ 'HackGreenville Events for the week of %s - %d of %d',
+ $weekStart->format('F j'),
+ $index,
+ $total
+ ),
+ ],
+ ],
+ ['type' => 'divider'],
+ ],
+ 'text' => $text,
+ 'text_length' => mb_strlen($text),
+ ];
+ }
+
+ /**
+ * Builds a single event's blocks and text for inclusion in a Slack message.
+ *
+ * @param Event $event The Event model.
+ * @return array|null An array containing 'blocks', 'text', and 'text_length' for the event, or null if the event should be skipped.
+ */
+ private function buildSingleEventBlock(Event $event): ?array
+ {
+ $text = $this->eventService->generateText($event) . "\n\n";
+
+ if (empty(mb_trim($text))) { // Check if text is empty or just whitespace
+ return null;
+ }
+
+ return [
+ 'blocks' => array_merge($this->eventService->generateBlocks($event), [['type' => 'divider']]),
+ 'text' => $text,
+ 'text_length' => mb_strlen($text),
+ ];
+ }
+
+ /**
+ * Calculates the total number of Slack messages needed for the given event blocks.
+ *
+ * @param Collection $eventBlocks A collection of arrays, each containing 'blocks', 'text', and 'text_length'.
+ * @return int The total number of messages required.
+ */
+ private function totalMessagesNeeded(Collection $eventBlocks): int
+ {
+ $maxLength = config('slack-events-bot.max_message_character_length');
+ $headerBuffer = config('slack-events-bot.header_buffer_length');
+
+ $totalLength = $eventBlocks->sum('text_length');
+ $messagesNeeded = (int) ceil($totalLength / ($maxLength - $headerBuffer));
+
+ // Ensure total count is at least 1 if we're going to post anything
+ return max(1, $messagesNeeded);
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Console/Commands/DeleteOldMessagesCommandTest.php b/app-modules/slack-events-bot/tests/Console/Commands/DeleteOldMessagesCommandTest.php
new file mode 100644
index 00000000..49140457
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Console/Commands/DeleteOldMessagesCommandTest.php
@@ -0,0 +1,53 @@
+mock(DatabaseService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('deleteOldMessages')->once()->with(90);
+ });
+
+ $this->artisan('slack:delete-old-messages')
+ ->expectsOutput('Deleting messages and cooldowns older than 90 days...')
+ ->expectsOutput('Old messages and cooldowns deleted successfully!')
+ ->assertSuccessful();
+ }
+
+ /** @test */
+ public function it_deletes_old_messages_with_custom_days_option(): void
+ {
+ $this->mock(DatabaseService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('deleteOldMessages')->once()->with(30);
+ });
+
+ $this->artisan('slack:delete-old-messages', ['--days' => 30])
+ ->expectsOutput('Deleting messages and cooldowns older than 30 days...')
+ ->expectsOutput('Old messages and cooldowns deleted successfully!')
+ ->assertSuccessful();
+ }
+
+ /** @test */
+ public function it_handles_exceptions_gracefully(): void
+ {
+ $this->mock(DatabaseService::class, function (MockInterface $mock) {
+ $mock->shouldReceive('deleteOldMessages')->once()->with(90)->andThrow(new Exception('Test exception'));
+ });
+
+ $this->artisan('slack:delete-old-messages')
+ ->expectsOutput('Deleting messages and cooldowns older than 90 days...')
+ ->expectsOutput('Error deleting old messages and cooldowns: Test exception')
+ ->assertFailed();
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Feature/SchedulingTest.php b/app-modules/slack-events-bot/tests/Feature/SchedulingTest.php
new file mode 100644
index 00000000..8be9e14c
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Feature/SchedulingTest.php
@@ -0,0 +1,32 @@
+app->make(Schedule::class);
+
+ $event = collect($schedule->events())->first(fn ($event) => property_exists($event, 'command') && str_contains($event->command, 'slack:delete-old-messages'));
+
+ $this->assertNotNull($event, 'The delete old messages command is not scheduled.');
+ $this->assertEquals('0 0 * * *', $event->expression);
+ }
+
+ /** @test */
+ public function it_schedules_the_check_events_api_job_hourly()
+ {
+ $schedule = $this->app->make(Schedule::class);
+
+ $event = collect($schedule->events())->first(fn ($event) => $event instanceof \Illuminate\Console\Scheduling\CallbackEvent &&
+ str_contains($event->description, 'CheckEventsApi'));
+
+ $this->assertNotNull($event, 'The check events api job is not scheduled.');
+ $this->assertEquals('0 * * * *', $event->expression);
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Feature/SlackEventsBotTest.php b/app-modules/slack-events-bot/tests/Feature/SlackEventsBotTest.php
new file mode 100644
index 00000000..a29fec49
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Feature/SlackEventsBotTest.php
@@ -0,0 +1,568 @@
+databaseService = app(DatabaseService::class);
+ $this->eventService = app(EventService::class);
+ $this->messageBuilderService = app(MessageBuilderService::class);
+
+ // Mock AuthService to control isAdmin behavior in tests
+ $this->authService = $this->mock(AuthService::class);
+ $this->authService->shouldReceive('validateSlackRequest')->andReturn(true); // Allow middleware to pass
+ $this->app->instance(AuthService::class, $this->authService);
+
+ // Set up config values that the controller relies on
+ config()->set('slack-events-bot.client_id', 'test_client_id');
+ config()->set('slack-events-bot.client_secret', 'test_client_secret');
+ config()->set('slack-events-bot.scopes', ['commands', 'chat:write']);
+ config()->set('slack-events-bot.check_api_cooldown_minutes', 15); // For cooldown tests
+ config()->set('slack-events-bot.max_message_character_length', 3000); // For chunking tests
+ }
+
+ /**
+ * Test the /slack/install route.
+ * Verifies that the route returns an HTML response with the "Add to Slack" button
+ * and sets a session state.
+ */
+ public function test_install_route()
+ {
+ $response = $this->get('/slack/install');
+
+ $response->assertStatus(200);
+ $response->assertSee('Add to Slack');
+ $response->assertSee('https://slack.com/oauth/v2/authorize?');
+ $response->assertSee('client_id=test_client_id');
+ $response->assertSee('scope=commands%2Cchat%3Awrite');
+
+ // Assert that a session state was set
+ $response->assertSessionHas('slack_oauth_state');
+ }
+
+ /**
+ * Test the /slack/auth route for a successful OAuth callback.
+ * Mocks the Slack API response and verifies workspace creation.
+ */
+ public function test_auth_route_success()
+ {
+ $code = 'test_code';
+ $state = uniqid('slack_', true);
+ Session::put('slack_oauth_state', $state);
+
+ // Mock the HTTP call to Slack's oauth.v2.access endpoint
+ Http::fake([
+ 'https://slack.com/api/oauth.v2.access' => Http::response([
+ 'ok' => true,
+ 'access_token' => 'xoxb-test-token',
+ 'team' => ['id' => 'T123', 'name' => 'Test Team'],
+ 'authed_user' => ['id' => 'U123'],
+ 'bot_user_id' => 'BU123', // Corrected bot_user_id location
+ 'incoming_webhook' => ['channel' => 'C123', 'channel_id' => 'C123', 'configuration_url' => 'http://example.com'],
+ ], 200),
+ 'https://slack.com/api/users.info' => Http::response([
+ 'ok' => true,
+ 'user' => ['is_admin' => true],
+ ], 200),
+ ]);
+
+ $response = $this->get("/slack/auth?code={$code}&state={$state}");
+
+ $response->assertStatus(200);
+ $response->assertSee('The HackGreenville API bot has been installed successfully!');
+
+ // Assert that the session state was forgotten
+ $response->assertSessionMissing('slack_oauth_state');
+
+ // Assert that the workspace was created in the database
+ $workspace = SlackWorkspace::where('team_id', 'T123')->first();
+ $this->assertNotNull($workspace);
+ $this->assertEquals('T123', $workspace->team_id);
+ $this->assertEquals('Test Team', $workspace->team_name);
+ $this->assertEquals('xoxb-test-token', $workspace->access_token);
+ }
+
+ /**
+ * Test the /slack/auth route when the state parameter is invalid.
+ */
+ public function test_auth_route_invalid_state()
+ {
+ $code = 'test_code';
+ $validState = uniqid('slack_', true);
+ $invalidState = 'wrong_state';
+ Session::put('slack_oauth_state', $validState);
+
+ $response = $this->get("/slack/auth?code={$code}&state={$invalidState}");
+
+ $response->assertStatus(400);
+ $response->assertSee('Invalid state. Your session may have expired or you tried to use an old authorization link.');
+ $response->assertSessionHas('slack_oauth_state', $validState); // State should still be there if it didn't match
+ }
+
+ /**
+ * Test the /slack/auth route when Slack's API returns an error.
+ */
+ public function test_auth_route_slack_error()
+ {
+ $code = 'test_code';
+ $state = uniqid('slack_', true);
+ Session::put('slack_oauth_state', $state);
+
+ // Mock the HTTP call to Slack's oauth.v2.access endpoint to return an error
+ Http::fake([
+ 'https://slack.com/api/oauth.v2.access' => Http::response([
+ 'ok' => false,
+ 'error' => 'invalid_code',
+ ], 200),
+ ]);
+
+ $response = $this->get("/slack/auth?code={$code}&state={$state}");
+
+ $response->assertStatus(400);
+ $response->assertSee('The authorization code is invalid or has expired.');
+ $response->assertSessionMissing('slack_oauth_state'); // Should still forget the state
+ }
+
+ /**
+ * Test the /slack/events route for URL verification.
+ */
+ public function test_events_url_verification()
+ {
+ $challenge = $this->faker->uuid;
+ $payload = [
+ 'token' => 'test_token',
+ 'challenge' => $challenge,
+ 'type' => 'url_verification',
+ ];
+
+ $response = $this->postJson('/slack/events', $payload);
+
+ $response->assertStatus(200);
+ $response->assertContent($challenge);
+ }
+
+ /**
+ * Test the /slack/events route for the /add_channel command as an admin.
+ */
+ public function test_events_add_channel_command_as_admin()
+ {
+ $workspace = SlackWorkspace::factory()->create(['team_id' => 'T123']);
+ $channelId = 'C1234567890';
+ $userId = 'U_ADMIN';
+
+ // Mock isAdmin to return true
+ $this->authService->shouldReceive('isAdmin')
+ ->once()
+ ->with($userId, $workspace->team_id)
+ ->andReturn(true);
+
+ $payload = [
+ 'token' => 'test_token',
+ 'team_id' => $workspace->team_id,
+ 'channel_id' => $channelId,
+ 'user_id' => $userId,
+ 'command' => '/add_channel',
+ 'text' => '',
+ 'response_url' => 'http://example.com/response',
+ 'trigger_id' => 'trigger_id',
+ ];
+
+ $response = $this->postJson('/slack/events', $payload);
+
+ $response->assertStatus(200);
+ $response->assertSee('Added channel to slack events bot 👍');
+ $this->assertDatabaseHas('slack_channels', [
+ 'slack_channel_id' => $channelId,
+ 'slack_workspace_id' => $workspace->id,
+ ]);
+ }
+
+ /**
+ * Test the /slack/events route for the /add_channel command when not an admin.
+ */
+ public function test_events_add_channel_command_not_admin()
+ {
+ $workspace = SlackWorkspace::factory()->create(['team_id' => 'T123']);
+ $channelId = 'C1234567890';
+ $userId = 'U_NON_ADMIN';
+
+ // Mock isAdmin to return false
+ $this->authService->shouldReceive('isAdmin')
+ ->once()
+ ->with($userId, $workspace->team_id)
+ ->andReturn(false);
+
+ $payload = [
+ 'token' => 'test_token',
+ 'team_id' => $workspace->team_id,
+ 'channel_id' => $channelId,
+ 'user_id' => $userId,
+ 'command' => '/add_channel',
+ 'text' => '',
+ 'response_url' => 'http://example.com/response',
+ 'trigger_id' => 'trigger_id',
+ ];
+
+ $response = $this->postJson('/slack/events', $payload);
+
+ $response->assertStatus(200);
+ $response->assertSee('You must be a workspace admin in order to run `/add_channel`');
+ $this->assertDatabaseMissing('slack_channels', [
+ 'slack_channel_id' => $channelId,
+ ]);
+ }
+
+ /**
+ * Test the /slack/events route for the /remove_channel command as an admin.
+ */
+ public function test_events_remove_channel_command_as_admin()
+ {
+ $workspace = SlackWorkspace::factory()->create(['team_id' => 'T123']);
+ $channel = $this->databaseService->addChannel('C1234567890', $workspace->team_id);
+ $userId = 'U_ADMIN';
+
+ // Mock isAdmin to return true
+ $this->authService->shouldReceive('isAdmin')
+ ->once()
+ ->with($userId, $workspace->team_id)
+ ->andReturn(true);
+
+ $payload = [
+ 'token' => 'test_token',
+ 'team_id' => $workspace->team_id,
+ 'channel_id' => $channel->slack_channel_id,
+ 'user_id' => $userId,
+ 'command' => '/remove_channel',
+ 'text' => '',
+ 'response_url' => 'http://example.com/response',
+ 'trigger_id' => 'trigger_id',
+ ];
+
+ $response = $this->postJson('/slack/events', $payload);
+
+ $response->assertStatus(200);
+ $response->assertSee('Removed channel from slack events bot 👍');
+ $this->assertDatabaseMissing('slack_channels', [
+ 'slack_channel_id' => $channel->slack_channel_id,
+ ]);
+ }
+
+ /**
+ * Test the /slack/events route for the /remove_channel command when not an admin.
+ */
+ public function test_events_remove_channel_command_not_admin()
+ {
+ $workspace = SlackWorkspace::factory()->create(['team_id' => 'T123']);
+ $channel = $this->databaseService->addChannel('C1234567890', $workspace->team_id);
+ $userId = 'U_NON_ADMIN';
+
+ // Mock isAdmin to return false
+ $this->authService->shouldReceive('isAdmin')
+ ->once()
+ ->with($userId, $workspace->team_id)
+ ->andReturn(false);
+
+ $payload = [
+ 'token' => 'test_token',
+ 'team_id' => $workspace->team_id,
+ 'channel_id' => $channel->slack_channel_id,
+ 'user_id' => $userId,
+ 'command' => '/remove_channel',
+ 'text' => '',
+ 'response_url' => 'http://example.com/response',
+ 'trigger_id' => 'trigger_id',
+ ];
+
+ $response = $this->postJson('/slack/events', $payload);
+
+ $response->assertStatus(200);
+ $response->assertSee('You must be a workspace admin in order to run `/remove_channel`');
+ $this->assertDatabaseHas('slack_channels', [ // Should still be in DB
+ 'slack_channel_id' => $channel->slack_channel_id,
+ ]);
+ }
+
+ /**
+ * Test the /slack/events route for the /check_api command.
+ * Verifies that the job is dispatched and a success message is returned.
+ */
+ public function test_events_check_api_command()
+ {
+ Queue::fake(); // Prevent actual job dispatch
+ SlackWorkspace::factory()->create(['team_id' => 'T123', 'team_name' => 'Test Team', 'access_token' => 'xoxb-test-token', 'domain' => 'testdomain']);
+
+ $userId = 'U123';
+ $teamId = 'T123';
+ $teamDomain = 'testdomain';
+
+ // Mock isAdmin as true for simplicity, though not strictly required for /check_api
+ $this->authService->shouldReceive('isAdmin')
+ ->andReturn(true);
+
+ $payload = [
+ 'token' => 'test_token',
+ 'team_id' => $teamId,
+ 'channel_id' => 'C456',
+ 'user_id' => $userId,
+ 'command' => '/check_api',
+ 'team_domain' => $teamDomain,
+ ];
+
+ $response = $this->postJson('/slack/events', $payload);
+
+ $response->assertStatus(200);
+ $response->assertSee('Checking api for events 👍');
+
+ // Assert that the CheckEventsApi job was dispatched
+ Queue::assertPushed(CheckEventsApi::class);
+
+ // Assert cooldown was created
+ $this->assertNotNull($this->databaseService->getCooldownExpiryTime($teamDomain, 'check_api'));
+ }
+
+ /**
+ * Test the /slack/events route for the /check_api command when on cooldown.
+ */
+ public function test_events_check_api_command_cooldown()
+ {
+ Queue::fake();
+ SlackWorkspace::factory()->create(['team_id' => 'T123', 'team_name' => 'Test Team', 'access_token' => 'xoxb-test-token', 'domain' => 'testdomain']);
+
+ $userId = 'U123';
+ $teamId = 'T123';
+ $teamDomain = 'testdomain';
+
+ // Create a cooldown that is in the future
+ $this->databaseService->createCooldown($teamDomain, 'check_api', 15); // 15 minutes from now
+
+ // Mock isAdmin as true for simplicity
+ $this->authService->shouldReceive('isAdmin')
+ ->andReturn(true);
+
+ $payload = [
+ 'token' => 'test_token',
+ 'team_id' => $teamId,
+ 'channel_id' => 'C456',
+ 'user_id' => $userId,
+ 'command' => '/check_api',
+ 'team_domain' => $teamDomain,
+ ];
+
+ $response = $this->postJson('/slack/events', $payload);
+
+ $response->assertStatus(200);
+ $response->assertSee('This command has been run recently and is on a cooldown period.');
+
+ // Assert that the CheckEventsApi job was NOT dispatched
+ Queue::assertNotPushed(CheckEventsApi::class);
+ }
+
+ /**
+ * Test the /slack/events route for an unknown slash command.
+ */
+ public function test_events_unknown_command()
+ {
+ $payload = [
+ 'token' => 'test_token',
+ 'team_id' => 'T123',
+ 'channel_id' => 'C456',
+ 'user_id' => 'U123',
+ 'command' => '/unknown_command',
+ 'text' => '',
+ 'response_url' => 'http://example.com/response',
+ 'trigger_id' => 'trigger_id',
+ ];
+
+ $response = $this->postJson('/slack/events', $payload);
+
+ $response->assertStatus(400);
+ $response->assertSee('Unknown command');
+ }
+
+ // Original tests (unmodified as they test service logic directly, not HTTP endpoints)
+
+ public function test_can_add_and_remove_channel()
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channelId = 'C1234567890';
+ $this->databaseService->addChannel($channelId, $workspace->team_id);
+ $this->assertDatabaseHas('slack_channels', ['slack_channel_id' => $channelId]);
+
+ $channels = $this->databaseService->getSlackChannels();
+ $this->assertTrue($channels->contains(fn ($channel) => $channel->slack_channel_id === $channelId));
+
+ $this->databaseService->removeChannel($channelId);
+ $this->assertDatabaseMissing('slack_channels', ['slack_channel_id' => $channelId]);
+ }
+
+ public function test_can_create_and_get_messages()
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channelId = 'C1234567890';
+ $channel = $this->databaseService->addChannel($channelId, $workspace->team_id);
+ $this->assertDatabaseHas('slack_channels', ['slack_channel_id' => $channelId]);
+
+ $week = Carbon::now()->startOfWeek();
+ $message = 'Test message content';
+ $timestamp = '1234567890.123456';
+ $sequencePosition = 0;
+
+ $this->databaseService->createMessage($week, $message, $timestamp, $channelId, $sequencePosition);
+
+ $this->assertDatabaseHas('slack_messages', [
+ 'message' => $message,
+ 'message_timestamp' => $timestamp,
+ 'week' => $week->toDateTimeString(),
+ 'sequence_position' => $sequencePosition,
+ 'channel_id' => $channel->id,
+ ]);
+
+ $messages = $this->databaseService->getMessages($week);
+ $this->assertCount(1, $messages);
+ $this->assertEquals($message, $messages->first()->message);
+ $this->assertEquals($channelId, $messages->first()->channel->slack_channel_id);
+ }
+
+ public function test_event_parsing()
+ {
+ $state = State::factory()->create(['abbr' => 'SC']);
+
+ $organization = Org::factory()->create(['title' => 'Test Group']);
+ $venue = Venue::factory()->create([
+ 'name' => 'Test Venue',
+ 'address' => '123 Main St',
+ 'city' => 'Greenville',
+ 'state_id' => $state->id,
+ 'zipcode' => '29601',
+ ]);
+ $event = Event::factory()->create([
+ 'event_name' => 'Test Event',
+ 'description' => 'This is a test event description',
+ 'uri' => 'https://example.com/event',
+ 'event_uuid' => $this->faker->uuid,
+ 'active_at' => Carbon::now()->addDays(7),
+ 'organization_id' => $organization->id,
+ 'venue_id' => $venue->id,
+ ]);
+
+ $event->load('organization', 'venue');
+
+ $blocks = $this->eventService->generateBlocks($event);
+ $this->assertIsArray($blocks);
+ $this->assertEquals('header', $blocks[0]['type']);
+ $this->assertEquals('section', $blocks[1]['type']);
+ $this->assertEquals('Test Event', $blocks[0]['text']['text']);
+ $this->assertArrayHasKey('type', $blocks[1]['text']);
+ $this->assertArrayHasKey('text', $blocks[1]['text']);
+ $this->assertStringContainsString('This is a test event description', $blocks[1]['text']['text']);
+
+ $text = $this->eventService->generateText($event);
+ $this->assertStringContainsString('Test Event', $text);
+ $this->assertStringContainsString('Test Group', $text);
+ $this->assertStringContainsString('Test Venue', $text);
+ $this->assertStringContainsString('https://example.com/event', $text);
+ $this->assertStringContainsString('Upcoming ✅', $text);
+ }
+
+ public function test_cooldown_functionality()
+ {
+ $accessor = 'test-workspace';
+ $resource = 'check_api';
+
+ // No cooldown initially
+ $expiry = $this->databaseService->getCooldownExpiryTime($accessor, $resource);
+ $this->assertNull($expiry);
+
+ // Create cooldown
+ $this->databaseService->createCooldown($accessor, $resource, 15);
+
+ // Check cooldown exists and is in future
+ $expiry = $this->databaseService->getCooldownExpiryTime($accessor, $resource);
+ $this->assertNotNull($expiry);
+ $this->assertTrue($expiry->isFuture());
+ }
+
+ public function test_message_chunking()
+ {
+ $weekStart = Carbon::now()->startOfWeek();
+ $state = State::factory()->create(['abbr' => 'SC']);
+
+ $eventsData = [];
+ for ($i = 0; $i < 10; $i++) {
+ $organization = Org::factory()->create(['title' => "Group {$i}"]);
+ $venue = Venue::factory()->create([
+ 'name' => "Venue {$i}",
+ 'address' => "{$i} Main St",
+ 'city' => 'Greenville',
+ 'state_id' => $state->id,
+ 'zipcode' => '29601',
+ ]);
+
+ $event = Event::factory()->create([
+ 'event_name' => "Event {$i} with a very long title that takes up space",
+ 'description' => str_repeat("This is a long description. ", 10),
+ 'uri' => "https://example.com/event-{$i}",
+ 'active_at' => $weekStart->copy()->addDays($i % 7),
+ 'organization_id' => $organization->id,
+ 'venue_id' => $venue->id,
+ 'event_uuid' => $this->faker->uuid,
+ ]);
+
+ $event->load('organization', 'venue');
+
+ $eventsData[] = $event;
+ }
+
+ $eventsCollection = collect($eventsData);
+
+ $eventBlocks = $this->messageBuilderService->buildEventBlocks($eventsCollection);
+ $this->assertInstanceOf(Collection::class, $eventBlocks);
+ $this->assertGreaterThan(0, $eventBlocks->count());
+
+ $chunkedMessages = $this->messageBuilderService->chunkMessages($eventBlocks, $weekStart);
+ $this->assertIsArray($chunkedMessages);
+ $this->assertGreaterThan(0, count($chunkedMessages));
+
+ // Verify each message has required structure
+ foreach ($chunkedMessages as $message) {
+ $this->assertArrayHasKey('blocks', $message);
+ $this->assertArrayHasKey('text', $message);
+ $this->assertLessThan(
+ config('slack-events-bot.max_message_character_length'),
+ mb_strlen($message['text'])
+ );
+ }
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Jobs/CheckEventsApiTest.php b/app-modules/slack-events-bot/tests/Jobs/CheckEventsApiTest.php
new file mode 100644
index 00000000..9ef9a2ca
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Jobs/CheckEventsApiTest.php
@@ -0,0 +1,89 @@
+shouldReceive('handlePostingToSlack')
+ ->once()
+ ->andReturnNull(); // Simulate successful execution
+
+ // Bind the mock to the service container
+ $this->app->instance(BotService::class, $botServiceMock);
+
+ // Expect specific log messages
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Executing CheckEventsApi job.');
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Finished CheckEventsApi job.');
+
+ // Dispatch the job
+ CheckEventsApi::dispatchSync();
+ }
+
+ /**
+ * Test that the job handles exceptions from BotService::handlePostingToSlack()
+ * by logging the error and re-throwing the exception.
+ */
+ public function test_job_handles_exceptions()
+ {
+ // Create a dummy exception
+ $exception = new Exception('Simulated BotService error');
+
+ // Mock the BotService to throw an exception
+ $botServiceMock = Mockery::mock(BotService::class);
+ $botServiceMock->shouldReceive('handlePostingToSlack')
+ ->once()
+ ->andThrow($exception);
+
+ // Bind the mock to the service container
+ $this->app->instance(BotService::class, $botServiceMock);
+
+ // Expect specific log messages
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Executing CheckEventsApi job.');
+
+ Log::shouldReceive('error')
+ ->once()
+ ->withArgs(fn ($message, $context) => str_contains($message, 'CheckEventsApi job failed with an exception.') &&
+ $context['exception'] === get_class($exception) &&
+ $context['message'] === $exception->getMessage() &&
+ $context['file'] === $exception->getFile() &&
+ $context['line'] === $exception->getLine());
+
+ // Expect the exception to be re-thrown
+ $this->expectException(Throwable::class);
+ $this->expectExceptionMessage('Simulated BotService error');
+
+ // Dispatch the job
+ CheckEventsApi::dispatchSync();
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Models/SlackWorkspaceTest.php b/app-modules/slack-events-bot/tests/Models/SlackWorkspaceTest.php
new file mode 100644
index 00000000..e5a8a1d7
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Models/SlackWorkspaceTest.php
@@ -0,0 +1,63 @@
+access_token = $plainTextToken;
+
+ // The attribute should be encrypted, so it should not be the plain text token
+ $this->assertNotEquals($plainTextToken, $workspace->getAttributes()['access_token']);
+ // And it should be decryptable back to the original
+ $this->assertEquals($plainTextToken, Crypt::decryptString($workspace->getAttributes()['access_token']));
+ }
+
+ /** @test */
+ public function it_decrypts_access_token_on_get()
+ {
+ $workspace = new SlackWorkspace;
+ $plainTextToken = 'fake-token-abcdefghijklmnopqrstuvwxyz';
+ $encryptedToken = Crypt::encryptString($plainTextToken);
+
+ // Manually set the encrypted token to simulate it coming from the database
+ $workspace->setRawAttributes(['access_token' => $encryptedToken]);
+
+ // When getting the attribute, it should be decrypted
+ $this->assertEquals($plainTextToken, $workspace->access_token);
+ }
+
+ /** @test */
+ public function it_handles_decrypt_exception_gracefully()
+ {
+ $invalidToken = 'not-a-valid-encrypted-string';
+
+ // Manually set an invalid token and team_id to simulate it coming from the database
+ $workspace = new SlackWorkspace;
+ $workspace->setRawAttributes([
+ 'access_token' => $invalidToken,
+ 'team_id' => 'T12345', // Set team_id here
+ ]);
+
+ Log::shouldReceive('warning')
+ ->once()
+ ->with('Could not decrypt access token for workspace. The token might be stored in plain text.', Mockery::subset([
+ 'workspace_id' => null, // Expect null for a new model
+ 'team_id' => 'T12345',
+ ]));
+
+ // When getting the attribute, it should return the original invalid token
+ // and log a warning.
+ $this->assertEquals($invalidToken, $workspace->access_token);
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Services/AuthServiceTest.php b/app-modules/slack-events-bot/tests/Services/AuthServiceTest.php
new file mode 100644
index 00000000..0268cbe1
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Services/AuthServiceTest.php
@@ -0,0 +1,197 @@
+authService = $this->app->make(AuthService::class);
+ }
+
+ /**
+ * Test getUserInfo method.
+ */
+ public function test_get_user_info()
+ {
+ $teamId = 'T123';
+ $userId = 'U123';
+ $accessToken = 'xoxb-test-token';
+
+ // Mock Http facade
+ Http::shouldReceive('withToken')
+ ->with($accessToken)
+ ->andReturnSelf();
+
+ Http::shouldReceive('get')
+ ->with('https://slack.com/api/users.info', ['user' => $userId])
+ ->andReturn(new \Illuminate\Http\Client\Response(new \GuzzleHttp\Psr7\Response(200, [], json_encode(['ok' => true, 'user' => ['id' => $userId, 'is_admin' => true]]))));
+
+ SlackWorkspace::factory()->create([
+ 'team_id' => $teamId,
+ 'access_token' => $accessToken,
+ ]);
+
+ $userInfo = $this->authService->getUserInfo($userId, $teamId);
+
+ $this->assertIsArray($userInfo);
+ $this->assertTrue($userInfo['ok']);
+ $this->assertEquals($userId, $userInfo['user']['id']);
+ }
+
+ public function test_get_user_info_workspace_not_found()
+ {
+ $teamId = 'T123';
+ $userId = 'U123';
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage("Workspace with team ID {$teamId} not found.");
+
+ $this->authService->getUserInfo($userId, $teamId);
+ }
+
+ public function test_get_user_info_slack_api_error()
+ {
+ $teamId = 'T123';
+ $userId = 'U123';
+ $accessToken = 'xoxb-test-token';
+
+ // Mock Http facade to return an error
+ Http::shouldReceive('withToken')
+ ->with($accessToken)
+ ->andReturnSelf();
+
+ Http::shouldReceive('get')
+ ->with('https://slack.com/api/users.info', ['user' => $userId])
+ ->andReturn(new \Illuminate\Http\Client\Response(new \GuzzleHttp\Psr7\Response(200, [], json_encode(['ok' => false, 'error' => 'user_not_found']))));
+
+ SlackWorkspace::factory()->create([
+ 'team_id' => $teamId,
+ 'access_token' => $accessToken,
+ ]);
+
+ $userInfo = $this->authService->getUserInfo($userId, $teamId);
+
+ $this->assertIsArray($userInfo);
+ $this->assertFalse($userInfo['ok']);
+ $this->assertEquals('user_not_found', $userInfo['error']);
+ }
+
+ /**
+ * Test isAdmin method.
+ */
+ public function test_is_admin()
+ {
+ $userId = 'U123';
+ $teamId = 'T123';
+
+ // Test case: User is admin
+ $this->authService = Mockery::mock(AuthService::class . '[getUserInfo]');
+ $this->authService->shouldReceive('getUserInfo')
+ ->once()
+ ->with($userId, $teamId)
+ ->andReturn(['ok' => true, 'user' => ['is_admin' => true]]);
+
+ $this->assertTrue($this->authService->isAdmin($userId, $teamId));
+
+ // Test case: User is not admin
+ $this->authService = Mockery::mock(AuthService::class . '[getUserInfo]');
+ $this->authService->shouldReceive('getUserInfo')
+ ->once()
+ ->with($userId, $teamId)
+ ->andReturn(['ok' => true, 'user' => ['is_admin' => false]]);
+
+ $this->assertFalse($this->authService->isAdmin($userId, $teamId));
+
+ // Test case: User info not available (e.g., 'user' key missing or 'is_admin' missing)
+ $this->authService = Mockery::mock(AuthService::class . '[getUserInfo]');
+ $this->authService->shouldReceive('getUserInfo')
+ ->once()
+ ->with($userId, $teamId)
+ ->andReturn(['ok' => true, 'user' => []]); // Missing 'is_admin'
+
+ $this->assertFalse($this->authService->isAdmin($userId, $teamId));
+
+ $this->authService = Mockery::mock(AuthService::class . '[getUserInfo]');
+ $this->authService->shouldReceive('getUserInfo')
+ ->once()
+ ->with($userId, $teamId)
+ ->andReturn(['ok' => true]); // Missing 'user' key
+
+ $this->assertFalse($this->authService->isAdmin($userId, $teamId));
+ }
+
+ /**
+ * Test validateSlackRequest method.
+ */
+ public function test_validate_slack_request()
+ {
+ $signingSecret = 'test_signing_secret';
+ config(['slack-events-bot.signing_secret' => $signingSecret]);
+
+ // Test case: Valid request
+ $timestamp = time();
+ $body = 'token=gIkuvaNzCQz2PNYwY2TBCJg2&team_id=T0001&team_domain=example&channel_id=C2147483705&channel_name=fun&user_id=U2147483697&user_name=steve&command=/weather&text=94070&response_url=https://hooks.slack.com/commands/1234/5678&trigger_id=1334522460.139236018';
+ $signature = 'v0=' . hash_hmac('sha256', 'v0:' . $timestamp . ':' . $body, $signingSecret);
+
+ $request = Request::create('/slack/events', 'POST', [], [], [], [
+ 'HTTP_X_SLACK_REQUEST_TIMESTAMP' => $timestamp,
+ 'HTTP_X_SLACK_SIGNATURE' => $signature,
+ ], $body);
+
+ $this->assertTrue($this->authService->validateSlackRequest($request));
+
+ // Test case: Missing timestamp or signature
+ Log::shouldReceive('warning')->once()->with('Slack request validation failed: Missing timestamp or signature.');
+ $request = Request::create('/slack/events', 'POST', [], [], [], [], $body);
+ $this->assertFalse($this->authService->validateSlackRequest($request));
+
+ // Test case: Timestamp expired (replay attack)
+ Log::shouldReceive('warning')->once()->with('Slack request validation failed: Timestamp expired (replay attack?).', Mockery::any());
+ $timestamp = time() - (60 * 6); // 6 minutes ago
+ $signature = 'v0=' . hash_hmac('sha256', 'v0:' . $timestamp . ':' . $body, $signingSecret);
+ $request = Request::create('/slack/events', 'POST', [], [], [], [
+ 'HTTP_X_SLACK_REQUEST_TIMESTAMP' => $timestamp,
+ 'HTTP_X_SLACK_SIGNATURE' => $signature,
+ ], $body);
+ $this->assertFalse($this->authService->validateSlackRequest($request));
+
+ // Test case: Signing secret not configured
+ Log::shouldReceive('error')->once()->with('Slack request validation failed: Signing secret is not configured.');
+ config(['slack-events-bot.signing_secret' => null]);
+ $timestamp = time();
+ $signature = 'v0=' . hash_hmac('sha256', 'v0:' . $timestamp . ':' . $body, ''); // Empty secret
+ $request = Request::create('/slack/events', 'POST', [], [], [], [
+ 'HTTP_X_SLACK_REQUEST_TIMESTAMP' => $timestamp,
+ 'HTTP_X_SLACK_SIGNATURE' => $signature,
+ ], $body);
+ $this->assertFalse($this->authService->validateSlackRequest($request));
+ config(['slack-events-bot.signing_secret' => $signingSecret]); // Reset for other tests
+
+ // Test case: Signature mismatch
+ Log::shouldReceive('warning')->once()->with('Slack request validation failed: Signature mismatch.');
+ $timestamp = time();
+ $invalidSignature = 'v0=' . hash_hmac('sha256', 'v0:' . $timestamp . ':' . 'invalid_body', $signingSecret);
+ $request = Request::create('/slack/events', 'POST', [], [], [], [
+ 'HTTP_X_SLACK_REQUEST_TIMESTAMP' => $timestamp,
+ 'HTTP_X_SLACK_SIGNATURE' => $invalidSignature,
+ ], $body);
+ $this->assertFalse($this->authService->validateSlackRequest($request));
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Services/BotServiceTest.php b/app-modules/slack-events-bot/tests/Services/BotServiceTest.php
new file mode 100644
index 00000000..b5246cf7
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Services/BotServiceTest.php
@@ -0,0 +1,446 @@
+databaseServiceMock = Mockery::mock(DatabaseService::class);
+ $this->messageBuilderServiceMock = Mockery::mock(MessageBuilderService::class);
+
+ $this->botService = new BotService(
+ $this->databaseServiceMock,
+ $this->messageBuilderServiceMock
+ );
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ parent::tearDown();
+ }
+
+ /**
+ * Test handlePostingToSlack when no events are found.
+ */
+ public function test_handle_posting_to_slack_no_events()
+ {
+ $this->refreshApplication();
+
+ $this->botService = Mockery::mock(BotService::class . '[getEventsForWeek, deleteMessagesForWeek]', [
+ $this->databaseServiceMock,
+ $this->messageBuilderServiceMock
+ ])->makePartial()->shouldAllowMockingProtectedMethods();
+
+ // Expect getEventsForWeek to be called and return an empty collection
+ $this->botService->shouldReceive('getEventsForWeek')
+ ->once()
+ ->andReturn(new Collection);
+
+ // Expect deleteMessagesForWeek to be called
+ $this->botService->shouldReceive('deleteMessagesForWeek')
+ ->once();
+
+ // Expect Log::info for no events found
+ Log::shouldReceive('info')
+ ->once()
+ ->withArgs(fn ($message) => str_contains($message, 'No upcoming events found for the week of'));
+
+ // Call the real handlePostingToSlack method on the botService instance
+ $this->botService->handlePostingToSlack();
+
+ $this->assertTrue(true); // Assert that the test ran without issues
+ }
+
+ /**
+ * Test handlePostingToSlack when events are found.
+ */
+ public function test_handle_posting_to_slack_with_events()
+ {
+ $events = new Collection([Mockery::mock(Event::class)]);
+
+ $eventMock = Mockery::mock(Event::class);
+ $queryBuilderMock = Mockery::mock(\Illuminate\Database\Eloquent\Builder::class);
+
+ $eventMock->shouldReceive('newQuery')->andReturn($queryBuilderMock);
+ $queryBuilderMock->shouldReceive('with')->andReturnSelf();
+ $queryBuilderMock->shouldReceive('published')->andReturnSelf();
+ $queryBuilderMock->shouldReceive('whereBetween')->andReturnSelf();
+ $queryBuilderMock->shouldReceive('oldest')->andReturnSelf();
+ $queryBuilderMock->shouldReceive('get')->andReturn($events);
+
+ $this->app->instance(Event::class, $eventMock);
+
+ // Expect messageBuilderService methods to be called
+ $this->messageBuilderServiceMock->shouldReceive('buildEventBlocks')
+ ->once()
+ ->with(Mockery::any()) // Use Mockery::any() for argument matching
+ ->andReturn(new Collection); // Return a Collection
+
+ $this->messageBuilderServiceMock->shouldReceive('chunkMessages')
+ ->once()
+ ->with(Mockery::any(), Mockery::any())
+ ->andReturn([]);
+
+ // Expect postOrUpdateMessages to be called
+ $this->databaseServiceMock->shouldReceive('getSlackChannels')
+ ->once()
+ ->andReturn(new Collection);
+
+ $this->databaseServiceMock->shouldReceive('getMessages')
+ ->once()
+ ->andReturn(new Collection);
+
+ // Expect Log::info for posting/updating messages (from postOrUpdateMessages)
+ Log::shouldReceive('info')
+ ->zeroOrMoreTimes();
+
+ $this->botService->handlePostingToSlack();
+
+ $this->assertTrue(true); // Assert that the test ran without issues
+ }
+
+ /**
+ * Test parseEventsForWeek method.
+ */
+ public function test_parse_events_for_week()
+ {
+ $events = new Collection([Mockery::mock(Event::class)]);
+ $weekStart = Carbon::now()->startOfWeek();
+ $eventBlocks = new Collection([['type' => 'section', 'text' => ['type' => 'mrkdwn', 'text' => 'Event 1']]]);
+ $chunkedMessages = [['text' => 'Message 1', 'blocks' => []]];
+
+ $this->messageBuilderServiceMock->shouldReceive('buildEventBlocks')
+ ->once()
+ ->with(Mockery::any())
+ ->andReturn($eventBlocks);
+
+ $this->messageBuilderServiceMock->shouldReceive('chunkMessages')
+ ->once()
+ ->with(Mockery::any(), Mockery::any())
+ ->andReturn($chunkedMessages);
+
+ // Mock dependencies for postOrUpdateMessages
+ $this->databaseServiceMock->shouldReceive('getSlackChannels')
+ ->once()
+ ->andReturn(new Collection);
+
+ $this->databaseServiceMock->shouldReceive('getMessages')
+ ->once()
+ ->andReturn(new Collection);
+
+ Log::shouldReceive('info')
+ ->zeroOrMoreTimes();
+
+ $this->botService->parseEventsForWeek($events, $weekStart);
+
+ $this->assertTrue(true); // Assert that the test ran without issues
+ }
+
+ /**
+ * Test postOrUpdateMessages - posting a new message.
+ */
+ public function test_post_or_update_messages_post_new()
+ {
+ $week = Carbon::now()->startOfWeek();
+ $messages = [['text' => 'New message', 'blocks' => []]];
+ $channels = new Collection([
+ (object)['slack_channel_id' => 'C123', 'workspace' => (object)['access_token' => 'xoxb-token']],
+ ]);
+ $existingMessages = new Collection;
+
+ $this->databaseServiceMock->shouldReceive('getSlackChannels')
+ ->once()
+ ->andReturn($channels);
+
+ $this->databaseServiceMock->shouldReceive('getMessages')
+ ->once()
+ ->with($week)
+ ->andReturn($existingMessages);
+
+ // Mock isUnsafeToSpillover indirectly by mocking getMostRecentMessageForChannel
+ $this->databaseServiceMock->shouldReceive('getMostRecentMessageForChannel')
+ ->zeroOrMoreTimes()
+ ->andReturn(null); // Simulate safe spillover
+
+ Log::shouldReceive('info')
+ ->once()
+ ->withArgs(fn ($message) => str_contains($message, 'Posting new message'));
+
+ Http::shouldReceive('withToken')
+ ->once()
+ ->with('xoxb-token')
+ ->andReturnSelf();
+
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('successful')->andReturn(true);
+ $mockResponse->shouldReceive('json')->andReturn(['ok' => true, 'ts' => '123.456']);
+
+ Http::shouldReceive('post')
+ ->once()
+ ->with('https://slack.com/api/chat.postMessage', Mockery::any())
+ ->andReturn($mockResponse);
+
+ $this->databaseServiceMock->shouldReceive('createMessage')
+ ->once()
+ ->with($week, 'New message', '123.456', 'C123', 0);
+
+ $this->botService->postOrUpdateMessages($week, $messages);
+
+ $this->assertTrue(true); // Assert that the test ran without issues
+ }
+
+ /**
+ * Test postOrUpdateMessages - updating an existing message.
+ */
+ public function test_post_or_update_messages_update_existing()
+ {
+ $week = Carbon::now()->startOfWeek();
+ $messages = [['text' => 'Updated message', 'blocks' => []]];
+ $channels = new Collection([
+ (object)['slack_channel_id' => 'C123', 'workspace' => (object)['access_token' => 'xoxb-token']],
+ ]);
+ $existingMessages = new Collection([
+ (object)[
+ 'channel' => (object)['slack_channel_id' => 'C123'],
+ 'sequence_position' => 0,
+ 'message_timestamp' => '123.456',
+ 'message' => 'Old message',
+ ],
+ ]);
+
+ $this->databaseServiceMock->shouldReceive('getSlackChannels')
+ ->once()
+ ->andReturn($channels);
+
+ $this->databaseServiceMock->shouldReceive('getMessages')
+ ->once()
+ ->with($week)
+ ->andReturn($existingMessages);
+
+ // Mock isUnsafeToSpillover indirectly
+ $this->databaseServiceMock->shouldReceive('getMostRecentMessageForChannel')
+ ->zeroOrMoreTimes()
+ ->andReturn(null);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->withArgs(fn ($message) => str_contains($message, 'Updating message'));
+
+ Http::shouldReceive('withToken')
+ ->once()
+ ->with('xoxb-token')
+ ->andReturnSelf();
+
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('successful')->andReturn(true);
+ $mockResponse->shouldReceive('json')->andReturn(['ok' => true]);
+
+ Http::shouldReceive('post')
+ ->once()
+ ->with('https://slack.com/api/chat.update', Mockery::any())
+ ->andReturn($mockResponse);
+
+ $this->databaseServiceMock->shouldReceive('updateMessage')
+ ->once()
+ ->with($week, 'Updated message', '123.456', 'C123');
+
+ $this->botService->postOrUpdateMessages($week, $messages);
+
+ $this->assertTrue(true); // Assert that the test ran without issues
+ }
+
+ /**
+ * Test postOrUpdateMessages - deleting an old message.
+ */
+ public function test_post_or_update_messages_delete_old()
+ {
+ $week = Carbon::now()->startOfWeek();
+ $messages = []; // No new messages
+ $channels = new Collection([
+ (object)['slack_channel_id' => 'C123', 'workspace' => (object)['access_token' => 'xoxb-token']],
+ ]);
+ $existingMessages = new Collection([
+ (object)[
+ 'channel' => (object)['slack_channel_id' => 'C123'],
+ 'sequence_position' => 0,
+ 'message_timestamp' => '123.456',
+ 'message' => 'Old message to be deleted',
+ ],
+ ]);
+
+ $this->databaseServiceMock->shouldReceive('getSlackChannels')
+ ->once()
+ ->andReturn($channels);
+
+ $this->databaseServiceMock->shouldReceive('getMessages')
+ ->once()
+ ->with($week)
+ ->andReturn($existingMessages);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->withArgs(fn ($message) => str_contains($message, 'Deleting old message'));
+
+ Http::shouldReceive('withToken')
+ ->once()
+ ->with('xoxb-token')
+ ->andReturnSelf();
+
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('successful')->andReturn(true);
+ $mockResponse->shouldReceive('json')->andReturn(['ok' => true]); // Mock json() even if not used directly
+
+ Http::shouldReceive('post')
+ ->once()
+ ->with('https://slack.com/api/chat.delete', Mockery::any())
+ ->andReturn($mockResponse);
+
+ $this->databaseServiceMock->shouldReceive('deleteMessage')
+ ->once()
+ ->with('C123', '123.456');
+
+ $this->botService->postOrUpdateMessages($week, $messages);
+
+ $this->assertTrue(true); // Assert that the test ran without issues
+ }
+
+ /**
+ * Test postOrUpdateMessages - UnsafeMessageSpilloverException is thrown.
+ */
+ public function test_post_or_update_messages_unsafe_spillover_exception()
+ {
+ // Http::fake() to prevent real HTTP calls
+ Http::fake([
+ 'https://slack.com/api/chat.update' => Http::response(['ok' => true], 200),
+ 'https://slack.com/api/chat.postMessage' => Http::response(['ok' => true, 'ts' => '123.456'], 200),
+ ]);
+
+ $week = Carbon::now()->startOfWeek();
+ // Change messages to have more items than existingMessages
+ $messages = [['text' => 'New message 1', 'blocks' => []], ['text' => 'New message 2', 'blocks' => []]];
+ $channels = new Collection([
+ (object)['slack_channel_id' => 'C123', 'workspace' => (object)['access_token' => 'xoxb-token']],
+ ]);
+ $existingMessages = new Collection([
+ (object)[
+ 'channel' => (object)['slack_channel_id' => 'C123'],
+ 'sequence_position' => 0,
+ 'message_timestamp' => '123.456',
+ 'message' => 'Old message',
+ ],
+ ]);
+
+ $this->databaseServiceMock->shouldReceive('getSlackChannels')
+ ->once()
+ ->andReturn($channels);
+
+ $this->databaseServiceMock->shouldReceive('getMessages')
+ ->once()
+ ->with($week)
+ ->andReturn($existingMessages);
+
+ // Mock isUnsafeToSpillover to return true
+ $this->databaseServiceMock->shouldReceive('getMostRecentMessageForChannel')
+ ->once()
+ ->andReturn(['week' => $week->copy()->addWeek()->toDateString()]);
+
+ // Ensure updateMessage is NOT called
+ $this->databaseServiceMock->shouldNotReceive('updateMessage');
+
+ Log::shouldReceive('info')
+ ->zeroOrMoreTimes(); // Add this expectation
+
+ Log::shouldReceive('error')
+ ->once()
+ ->withArgs(fn ($message) => str_contains($message, 'Cannot update messages for') &&
+ str_contains($message, 'New events have caused the number of messages needed to increase'));
+
+ $this->expectException(UnsafeMessageSpilloverException::class);
+
+ $this->botService->postOrUpdateMessages($week, $messages);
+ }
+
+ /**
+ * Test postOrUpdateMessages - Slack API error when updating.
+ */
+ public function test_post_or_update_messages_slack_api_error_update()
+ {
+ $week = Carbon::now()->startOfWeek();
+ $messages = [['text' => 'Updated message', 'blocks' => []]];
+ $channels = new Collection([
+ (object)['slack_channel_id' => 'C123', 'workspace' => (object)['access_token' => 'xoxb-token']],
+ ]);
+ $existingMessages = new Collection([
+ (object)[
+ 'channel' => (object)['slack_channel_id' => 'C123'],
+ 'sequence_position' => 0,
+ 'message_timestamp' => '123.456',
+ 'message' => 'Old message',
+ ],
+ ]);
+
+ $this->databaseServiceMock->shouldReceive('getSlackChannels')
+ ->once()
+ ->andReturn($channels);
+
+ $this->databaseServiceMock->shouldReceive('getMessages')
+ ->once()
+ ->with($week)
+ ->andReturn($existingMessages);
+
+ // Mock isUnsafeToSpillover indirectly
+ $this->databaseServiceMock->shouldReceive('getMostRecentMessageForChannel')
+ ->zeroOrMoreTimes()
+ ->andReturn(null);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->withArgs(fn ($message) => str_contains($message, 'Updating message'));
+
+ Http::shouldReceive('withToken')
+ ->once()
+ ->with('xoxb-token')
+ ->andReturnSelf();
+
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('successful')->andReturn(false);
+ $mockResponse->shouldReceive('json')->andReturn(['ok' => false, 'error' => 'update_failed']);
+
+ Http::shouldReceive('post')
+ ->once()
+ ->with('https://slack.com/api/chat.update', Mockery::any())
+ ->andReturn($mockResponse);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->withArgs(fn ($message, $context) => str_contains($message, 'Failed to update message') &&
+ $context['error'] === 'update_failed');
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Slack API error when updating message: update_failed');
+
+ $this->botService->postOrUpdateMessages($week, $messages);
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Services/DatabaseServiceTest.php b/app-modules/slack-events-bot/tests/Services/DatabaseServiceTest.php
new file mode 100644
index 00000000..a140263b
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Services/DatabaseServiceTest.php
@@ -0,0 +1,476 @@
+databaseService = new DatabaseService;
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ parent::tearDown();
+ }
+
+ /** @test */
+ public function it_creates_a_message(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ $week = Carbon::now();
+ $message = 'Test message';
+ $messageTimestamp = '12345.6789';
+ $slackChannelId = $channel->slack_channel_id;
+ $sequencePosition = 1;
+
+ $slackMessage = $this->databaseService->createMessage(
+ $week,
+ $message,
+ $messageTimestamp,
+ $slackChannelId,
+ $sequencePosition
+ );
+
+ $this->assertInstanceOf(SlackMessage::class, $slackMessage);
+ $this->assertDatabaseHas('slack_messages', [
+ 'week' => $week->toDateTimeString(),
+ 'message' => $message,
+ 'message_timestamp' => $messageTimestamp,
+ 'channel_id' => $channel->id,
+ 'sequence_position' => $sequencePosition,
+ ]);
+ }
+
+ /** @test */
+ public function it_updates_a_message(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ $week = Carbon::now();
+ $oldMessage = 'Old message';
+ $newMessage = 'New message';
+ $messageTimestamp = '12345.6789';
+ $slackChannelId = $channel->slack_channel_id;
+ $sequencePosition = 1;
+
+ SlackMessage::create([
+ 'week' => $week->toDateTimeString(),
+ 'message' => $oldMessage,
+ 'message_timestamp' => $messageTimestamp,
+ 'channel_id' => $channel->id,
+ 'sequence_position' => $sequencePosition,
+ ]);
+
+ $updatedRows = $this->databaseService->updateMessage(
+ $week,
+ $newMessage,
+ $messageTimestamp,
+ $slackChannelId
+ );
+
+ $this->assertEquals(1, $updatedRows);
+ $this->assertDatabaseHas('slack_messages', [
+ 'week' => $week->toDateTimeString(),
+ 'message' => $newMessage,
+ 'message_timestamp' => $messageTimestamp,
+ 'channel_id' => $channel->id,
+ ]);
+ $this->assertDatabaseMissing('slack_messages', [
+ 'message' => $oldMessage,
+ ]);
+ }
+
+ /** @test */
+ public function it_gets_messages_for_a_week(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel1 = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+ $channel2 = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ $week = Carbon::now();
+
+ SlackMessage::create([
+ 'week' => $week->toDateTimeString(),
+ 'message' => 'Message 1',
+ 'message_timestamp' => '1',
+ 'channel_id' => $channel1->id,
+ 'sequence_position' => 1,
+ ]);
+ SlackMessage::create([
+ 'week' => $week->toDateTimeString(),
+ 'message' => 'Message 2',
+ 'message_timestamp' => '2',
+ 'channel_id' => $channel2->id,
+ 'sequence_position' => 2,
+ ]);
+ SlackMessage::create([
+ 'week' => Carbon::now()->subWeek()->toDateTimeString(), // Different week
+ 'message' => 'Message 3',
+ 'message_timestamp' => '3',
+ 'channel_id' => $channel1->id,
+ 'sequence_position' => 3,
+ ]);
+
+ $messages = $this->databaseService->getMessages($week);
+
+ $this->assertCount(2, $messages);
+ $this->assertEquals('Message 1', $messages->first()->message);
+ $this->assertEquals('Message 2', $messages->last()->message);
+ }
+
+ /** @test */
+ public function it_gets_most_recent_message_for_channel(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ SlackMessage::create([
+ 'week' => Carbon::now()->subDays(2)->toDateTimeString(),
+ 'message' => 'Old message',
+ 'message_timestamp' => '1',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 1,
+ ]);
+ $expectedMessage = SlackMessage::create([
+ 'week' => Carbon::now()->toDateTimeString(),
+ 'message' => 'New message',
+ 'message_timestamp' => '2',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 2,
+ ]);
+
+ $mostRecentMessage = $this->databaseService->getMostRecentMessageForChannel($channel->slack_channel_id);
+
+ $this->assertIsArray($mostRecentMessage);
+ $this->assertEquals($expectedMessage->message, $mostRecentMessage['message']);
+ $this->assertEquals($expectedMessage->message_timestamp, $mostRecentMessage['message_timestamp']);
+ }
+
+ /** @test */
+ public function it_returns_null_if_channel_not_found_for_most_recent_message(): void
+ {
+ $mostRecentMessage = $this->databaseService->getMostRecentMessageForChannel('non-existent-channel');
+ $this->assertNull($mostRecentMessage);
+ }
+
+ /** @test */
+ public function it_returns_null_if_no_messages_for_channel(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ $mostRecentMessage = $this->databaseService->getMostRecentMessageForChannel($channel->slack_channel_id);
+ $this->assertNull($mostRecentMessage);
+ }
+
+ /** @test */
+ public function it_gets_slack_channels(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ SlackChannel::factory()->count(3)->create(['slack_workspace_id' => $workspace->id]);
+
+ $channels = $this->databaseService->getSlackChannels();
+
+ $this->assertCount(3, $channels);
+ $this->assertInstanceOf(SlackChannel::class, $channels->first());
+ }
+
+ /** @test */
+ public function it_adds_a_channel(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $slackChannelId = 'C12345';
+ $teamId = $workspace->team_id;
+
+ $channel = $this->databaseService->addChannel($slackChannelId, $teamId);
+
+ $this->assertInstanceOf(SlackChannel::class, $channel);
+ $this->assertDatabaseHas('slack_channels', [
+ 'slack_channel_id' => $slackChannelId,
+ 'slack_workspace_id' => $workspace->id,
+ ]);
+ }
+
+ /** @test */
+ public function it_removes_a_channel(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ $deletedRows = $this->databaseService->removeChannel($channel->slack_channel_id);
+
+ $this->assertEquals(1, $deletedRows);
+ $this->assertDatabaseMissing('slack_channels', [
+ 'id' => $channel->id,
+ ]);
+ }
+
+ /** @test */
+ public function it_deletes_messages_for_a_week(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ $weekToDelete = Carbon::now();
+ $otherWeek = Carbon::now()->subWeek();
+
+ SlackMessage::create([
+ 'week' => $weekToDelete->toDateTimeString(),
+ 'message' => 'Message 1',
+ 'message_timestamp' => '1',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 1,
+ ]);
+ SlackMessage::create([
+ 'week' => $weekToDelete->toDateTimeString(),
+ 'message' => 'Message 2',
+ 'message_timestamp' => '2',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 2,
+ ]);
+ SlackMessage::create([
+ 'week' => Carbon::now()->subWeek()->toDateTimeString(), // Different week
+ 'message' => 'Message 3',
+ 'message_timestamp' => '3',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 1,
+ ]);
+
+ $deletedRows = $this->databaseService->deleteMessagesForWeek($weekToDelete);
+
+ $this->assertEquals(2, $deletedRows);
+ $this->assertDatabaseMissing('slack_messages', [
+ 'message' => 'Message 1',
+ ]);
+ $this->assertDatabaseMissing('slack_messages', [
+ 'message' => 'Message 2',
+ ]);
+ $this->assertDatabaseHas('slack_messages', [
+ 'message' => 'Message 3',
+ ]);
+ }
+
+ /** @test */
+ public function it_deletes_a_specific_message(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ $messageToDelete = SlackMessage::create([
+ 'week' => Carbon::now()->toDateTimeString(),
+ 'message' => 'Message to delete',
+ 'message_timestamp' => '123',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 1,
+ ]);
+ SlackMessage::create([
+ 'week' => Carbon::now()->toDateTimeString(),
+ 'message' => 'Another message',
+ 'message_timestamp' => '456',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 2,
+ ]);
+
+ $deletedRows = $this->databaseService->deleteMessage($channel->slack_channel_id, $messageToDelete->message_timestamp);
+
+ $this->assertEquals(1, $deletedRows);
+ $this->assertDatabaseMissing('slack_messages', [
+ 'id' => $messageToDelete->id,
+ ]);
+ $this->assertDatabaseHas('slack_messages', [
+ 'message_timestamp' => '456',
+ ]);
+ }
+
+ /** @test */
+ public function it_handles_deleting_message_in_non_existent_channel(): void
+ {
+ Log::shouldReceive('warning')
+ ->once()
+ ->with('Attempted to delete message in non-existent channel: non-existent-channel');
+
+ $deletedRows = $this->databaseService->deleteMessage('non-existent-channel', '123');
+
+ $this->assertEquals(0, $deletedRows);
+ }
+
+ /** @test */
+ public function it_deletes_old_messages_and_cooldowns(): void
+ {
+ $workspace = SlackWorkspace::factory()->create();
+ $channel = SlackChannel::factory()->create(['slack_workspace_id' => $workspace->id]);
+
+ // Create messages
+ SlackMessage::create([
+ 'week' => Carbon::now()->subDays(100)->toDateTimeString(),
+ 'message' => 'Old message 1',
+ 'message_timestamp' => '1',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 1,
+ ]);
+ SlackMessage::create([
+ 'week' => Carbon::now()->subDays(50)->toDateTimeString(),
+ 'message' => 'Recent message 1',
+ 'message_timestamp' => '2',
+ 'channel_id' => $channel->id,
+ 'sequence_position' => 2,
+ ]);
+
+ // Create cooldowns
+ SlackCooldown::create([
+ 'accessor' => 'user1',
+ 'resource' => 'resource1',
+ 'expires_at' => Carbon::now()->subDays(100),
+ ]);
+ SlackCooldown::create([
+ 'accessor' => 'user2',
+ 'resource' => 'resource2',
+ 'expires_at' => Carbon::now()->addDays(10),
+ ]);
+
+ $this->databaseService->deleteOldMessages(90);
+
+ $this->assertDatabaseMissing('slack_messages', [
+ 'message' => 'Old message 1',
+ ]);
+ $this->assertDatabaseHas('slack_messages', [
+ 'message' => 'Recent message 1',
+ ]);
+ $this->assertDatabaseMissing('slack_cooldowns', [
+ 'accessor' => 'user1',
+ ]);
+ $this->assertDatabaseHas('slack_cooldowns', [
+ 'accessor' => 'user2',
+ ]);
+ }
+
+ /** @test */
+ public function it_creates_or_updates_a_cooldown(): void
+ {
+ $accessor = 'user1';
+ $resource = 'resource1';
+ $cooldownMinutes = 5;
+
+ // Create new cooldown
+ $cooldown = $this->databaseService->createCooldown($accessor, $resource, $cooldownMinutes);
+
+ $this->assertInstanceOf(SlackCooldown::class, $cooldown);
+ $this->assertDatabaseHas('slack_cooldowns', [
+ 'accessor' => $accessor,
+ 'resource' => $resource,
+ ]);
+
+ // Update existing cooldown
+ $newCooldownMinutes = 10;
+ $updatedCooldown = $this->databaseService->createCooldown($accessor, $resource, $newCooldownMinutes);
+
+ $this->assertInstanceOf(SlackCooldown::class, $updatedCooldown);
+ $this->assertEquals($cooldown->id, $updatedCooldown->id);
+ $this->assertDatabaseHas('slack_cooldowns', [
+ 'accessor' => $accessor,
+ 'resource' => $resource,
+ 'expires_at' => $updatedCooldown->expires_at,
+ ]);
+ }
+
+ /** @test */
+ public function it_gets_cooldown_expiry_time(): void
+ {
+ $accessor = 'user1';
+ $resource = 'resource1';
+ $expiresAt = Carbon::now()->addMinutes(10);
+
+ SlackCooldown::create([
+ 'accessor' => $accessor,
+ 'resource' => $resource,
+ 'expires_at' => $expiresAt,
+ ]);
+
+ $expiryTime = $this->databaseService->getCooldownExpiryTime($accessor, $resource);
+
+ $this->assertInstanceOf(Carbon::class, $expiryTime);
+ $this->assertEquals($expiresAt->toDateTimeString(), $expiryTime->toDateTimeString());
+ }
+
+ /** @test */
+ public function it_returns_null_for_non_existent_cooldown_expiry_time(): void
+ {
+ $expiryTime = $this->databaseService->getCooldownExpiryTime('non-existent', 'resource');
+ $this->assertNull($expiryTime);
+ }
+
+ /** @test */
+ public function it_creates_or_updates_a_workspace(): void
+ {
+ $data = [
+ 'team' => [
+ 'id' => 'T123',
+ 'name' => 'Test Team',
+ ],
+ 'access_token' => 'xoxb-test-token',
+ 'bot_user_id' => 'B123',
+ ];
+
+ // Create new workspace
+ $workspace = $this->databaseService->createOrUpdateWorkspace($data);
+
+ $this->assertInstanceOf(SlackWorkspace::class, $workspace);
+ $this->assertDatabaseHas('slack_workspaces', [
+ 'team_id' => 'T123',
+ 'team_name' => 'Test Team',
+ 'bot_user_id' => 'B123',
+ ]);
+ // Retrieve the created workspace to check the decrypted token
+ $createdWorkspace = SlackWorkspace::where('team_id', 'T123')->first();
+ $this->assertEquals('xoxb-test-token', $createdWorkspace->access_token);
+
+
+ // Update existing workspace
+ $updatedData = [
+ 'team' => [
+ 'id' => 'T123',
+ 'name' => 'Updated Team',
+ ],
+ 'access_token' => 'xoxb-updated-token',
+ 'bot_user_id' => 'B456',
+ ];
+
+ $updatedWorkspace = $this->databaseService->createOrUpdateWorkspace($updatedData);
+
+ $this->assertInstanceOf(SlackWorkspace::class, $updatedWorkspace);
+ $this->assertEquals($workspace->id, $updatedWorkspace->id);
+ $this->assertDatabaseHas('slack_workspaces', [
+ 'team_id' => 'T123',
+ 'team_name' => 'Updated Team',
+ 'bot_user_id' => 'B456',
+ ]);
+ // Retrieve the updated workspace to check the decrypted token
+ $retrievedUpdatedWorkspace = SlackWorkspace::where('team_id', 'T123')->first();
+ $this->assertEquals('xoxb-updated-token', $retrievedUpdatedWorkspace->access_token);
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Services/EventServiceTest.php b/app-modules/slack-events-bot/tests/Services/EventServiceTest.php
new file mode 100644
index 00000000..d3bd08e3
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Services/EventServiceTest.php
@@ -0,0 +1,310 @@
+eventService = new EventService;
+ }
+
+ /** @test */
+ public function it_generates_blocks_for_an_event(): void
+ {
+ // Create related models for the Event
+ $organization = Org::factory()->create(['title' => 'Test Organization']);
+ $state = \App\Models\State::factory()->create(['name' => 'South Carolina', 'abbr' => 'SC']); // Create a State model
+ $venue = Venue::factory()->create([
+ 'name' => 'Test Venue',
+ 'address' => '123 Main St',
+ 'city' => 'Greenville',
+ 'state_id' => $state->id, // Use state_id
+ 'zipcode' => '29601' // Use zipcode instead of zip
+ ]);
+
+ $event = Event::factory()->create([
+ 'event_name' => 'My Awesome Event',
+ 'description' => 'This is a very interesting description for my awesome event.',
+ 'organization_id' => $organization->id,
+ 'venue_id' => $venue->id,
+ 'uri' => 'https://example.com/event',
+ 'active_at' => now()->addDays(7),
+ ]);
+
+ $blocks = $this->eventService->generateBlocks($event);
+
+ $this->assertIsArray($blocks);
+ $this->assertCount(2, $blocks); // Header and Section
+
+ // Assert Header Block
+ $this->assertEquals('header', $blocks[0]['type']);
+ $this->assertEquals('plain_text', $blocks[0]['text']['type']);
+ $this->assertEquals('My Awesome Event', $blocks[0]['text']['text']);
+
+ // Assert Section Block
+ $this->assertEquals('section', $blocks[1]['type']);
+ $this->assertEquals('plain_text', $blocks[1]['text']['type']);
+ $this->assertEquals('This is a very interesting description for my awesome event.', $blocks[1]['text']['text']);
+ $this->assertIsArray($blocks[1]['fields']);
+ $this->assertCount(8, $blocks[1]['fields']); // 4 pairs of mrkdwn/plain_text
+
+ // Assert fields content
+ $this->assertEquals('*Test Organization*', $blocks[1]['fields'][0]['text']); // Organization
+ $this->assertEquals('', $blocks[1]['fields'][1]['text']); // Link
+ $this->assertEquals('*Status*', $blocks[1]['fields'][2]['text']); // Status Label
+ $this->assertEquals('Upcoming ✅', $blocks[1]['fields'][3]['text']); // Status Value
+ $this->assertEquals('*Location*', $blocks[1]['fields'][4]['text']); // Location Label
+ $this->assertEquals('Test Venue - 123 Main St Greenville, SC 29601', $blocks[1]['fields'][5]['text']); // Location Value
+ $this->assertEquals('*Time*', $blocks[1]['fields'][6]['text']); // Time Label
+ $this->assertEquals($event->active_at->format('F j, Y g:i A T'), $blocks[1]['fields'][7]['text']); // Time Value
+ }
+
+ /** @test */
+ public function it_generates_blocks_with_limited_text(): void
+ {
+ $longEventName = str_repeat('a', 200);
+ $longDescription = str_repeat('b', 300);
+
+ $organization = Org::factory()->create(['title' => 'Test Organization']);
+ $state = \App\Models\State::factory()->create(['name' => 'South Carolina', 'abbr' => 'SC']);
+ $venue = Venue::factory()->create([
+ 'name' => 'Test Venue',
+ 'address' => '123 Main St',
+ 'city' => 'Greenville',
+ 'state_id' => $state->id,
+ 'zipcode' => '29601',
+ ]);
+
+ $event = Event::factory()->create([
+ 'event_name' => $longEventName,
+ 'description' => $longDescription,
+ 'organization_id' => $organization->id,
+ 'venue_id' => $venue->id,
+ 'uri' => 'https://example.com/event',
+ 'active_at' => now()->addDays(7),
+ ]);
+
+ $blocks = $this->eventService->generateBlocks($event);
+
+ $this->assertEquals(153, mb_strlen($blocks[0]['text']['text']));
+ $this->assertStringEndsWith('...', $blocks[0]['text']['text']);
+ $this->assertEquals(253, mb_strlen($blocks[1]['text']['text']));
+ $this->assertStringEndsWith('...', $blocks[1]['text']['text']);
+ }
+
+ /** @test */
+ public function it_generates_blocks_with_no_venue(): void
+ {
+ $organization = Org::factory()->create(['title' => 'Test Organization']);
+
+ $event = Event::factory()->create([
+ 'event_name' => 'Event Without Venue',
+ 'description' => 'Description for event without venue.',
+ 'organization_id' => $organization->id,
+ 'venue_id' => null,
+ 'uri' => 'https://example.com/event',
+ 'active_at' => now()->addDays(7),
+ ]);
+
+ $blocks = $this->eventService->generateBlocks($event);
+
+ $this->assertEquals('No location', $blocks[1]['fields'][5]['text']);
+ }
+
+ /** @test */
+ public function it_generates_blocks_with_empty_event_name(): void
+ {
+ $organization = Org::factory()->create(['title' => 'Test Organization']);
+
+ $event = Event::factory()->create([
+ 'event_name' => '',
+ 'description' => 'Description for event with empty name.',
+ 'organization_id' => $organization->id,
+ 'uri' => 'https://example.com/event',
+ 'active_at' => now()->addDays(7),
+ ]);
+
+ $blocks = $this->eventService->generateBlocks($event);
+
+ $this->assertEquals('Untitled Event', $blocks[0]['text']['text']);
+ }
+
+ /** @test */
+ public function it_generates_text_for_an_event(): void
+ {
+ $organization = Org::factory()->create(['title' => 'Test Organization']);
+ $state = \App\Models\State::factory()->create(['name' => 'South Carolina', 'abbr' => 'SC']);
+ $venue = Venue::factory()->create([
+ 'name' => 'Test Venue',
+ 'address' => '123 Main St',
+ 'city' => 'Greenville',
+ 'state_id' => $state->id,
+ 'zipcode' => '29601',
+ ]);
+
+ $event = Event::factory()->create([
+ 'event_name' => 'My Awesome Event',
+ 'description' => 'This is a very interesting description for my awesome event.',
+ 'organization_id' => $organization->id,
+ 'venue_id' => $venue->id,
+ 'uri' => 'https://example.com/event',
+ 'active_at' => now()->addDays(7),
+ ]);
+
+ $expectedText = sprintf(
+ "%s\nOrganization: %s\nDescription: %s\nLink: %s\nStatus: %s\nLocation: %s\nTime: %s",
+ 'My Awesome Event',
+ 'Test Organization',
+ 'This is a very interesting description for my awesome event.',
+ 'https://example.com/event',
+ 'Upcoming ✅',
+ 'Test Venue',
+ $event->active_at->format('F j, Y g:i A T')
+ );
+
+ $generatedText = $this->eventService->generateText($event);
+
+ $this->assertEquals($expectedText, $generatedText);
+ }
+
+ /** @test */
+ public function it_generates_text_with_limited_text(): void
+ {
+ $longEventName = str_repeat('x', 300);
+ $longOrgTitle = str_repeat('y', 255);
+ $longDescription = str_repeat('z', 300);
+
+ $organization = Org::factory()->create(['title' => $longOrgTitle]);
+
+ $event = Event::factory()->create([
+ 'event_name' => $longEventName,
+ 'description' => $longDescription,
+ 'organization_id' => $organization->id,
+ 'venue_id' => null,
+ 'uri' => 'https://example.com/event',
+ 'active_at' => now()->addDays(7),
+ ]);
+
+ $generatedText = $this->eventService->generateText($event);
+
+ $expectedEventName = mb_substr($longEventName, 0, 250) . '...';
+ $expectedOrgTitle = mb_substr($longOrgTitle, 0, 250) . '...';
+ $expectedDescription = mb_substr($longDescription, 0, 250) . '...';
+
+ $expectedText = sprintf(
+ "%s\nOrganization: %s\nDescription: %s\nLink: %s\nStatus: %s\nLocation: %s\nTime: %s",
+ $expectedEventName,
+ $expectedOrgTitle,
+ $expectedDescription,
+ 'https://example.com/event',
+ 'Upcoming ✅',
+ 'No location',
+ $event->active_at->format('F j, Y g:i A T')
+ );
+
+ $this->assertEquals($expectedText, $generatedText);
+ }
+
+ /** @test */
+ public function it_generates_text_with_no_venue(): void
+ {
+ $organization = Org::factory()->create(['title' => 'Test Organization']);
+
+ $event = Event::factory()->create([
+ 'event_name' => 'Event Without Venue',
+ 'description' => 'Description for event without venue.',
+ 'organization_id' => $organization->id,
+ 'venue_id' => null,
+ 'uri' => 'https://example.com/event',
+ 'active_at' => now()->addDays(7),
+ ]);
+
+ $expectedText = sprintf(
+ "%s\nOrganization: %s\nDescription: %s\nLink: %s\nStatus: %s\nLocation: %s\nTime: %s",
+ 'Event Without Venue',
+ 'Test Organization',
+ 'Description for event without venue.',
+ 'https://example.com/event',
+ 'Upcoming ✅',
+ 'No location',
+ $event->active_at->format('F j, Y g:i A T')
+ );
+
+ $generatedText = $this->eventService->generateText($event);
+
+ $this->assertEquals($expectedText, $generatedText);
+ }
+
+ /** @test */
+ public function it_generates_text_for_past_event(): void
+ {
+ $organization = Org::factory()->create(['title' => 'Test Organization']);
+ $event = Event::factory()->create([
+ 'event_name' => 'Past Event',
+ 'description' => 'Description for past event.',
+ 'organization_id' => $organization->id,
+ 'uri' => 'https://example.com/past-event',
+ 'active_at' => now()->subDays(7),
+ 'venue_id' => null,
+ ]);
+
+ $expectedText = sprintf(
+ "%s\nOrganization: %s\nDescription: %s\nLink: %s\nStatus: %s\nLocation: %s\nTime: %s",
+ 'Past Event',
+ 'Test Organization',
+ 'Description for past event.',
+ 'https://example.com/past-event',
+ 'Past ✔',
+ 'No location',
+ $event->active_at->format('F j, Y g:i A T')
+ );
+
+ $generatedText = $this->eventService->generateText($event);
+
+ $this->assertEquals($expectedText, $generatedText);
+ }
+
+ /** @test */
+ public function it_generates_text_for_cancelled_event(): void
+ {
+ $organization = Org::factory()->create(['title' => 'Test Organization']);
+ $event = Event::factory()->create([
+ 'event_name' => 'Cancelled Event',
+ 'description' => 'Description for cancelled event.',
+ 'organization_id' => $organization->id,
+ 'uri' => 'https://example.com/cancelled-event',
+ 'active_at' => now()->addDays(7),
+ 'cancelled_at' => now(),
+ 'venue_id' => null,
+ ]);
+
+ $expectedText = sprintf(
+ "%s\nOrganization: %s\nDescription: %s\nLink: %s\nStatus: %s\nLocation: %s\nTime: %s",
+ 'Cancelled Event',
+ 'Test Organization',
+ 'Description for cancelled event.',
+ 'https://example.com/cancelled-event',
+ 'Cancelled ❌',
+ 'No location',
+ $event->active_at->format('F j, Y g:i A T')
+ );
+
+ $generatedText = $this->eventService->generateText($event);
+
+ $this->assertEquals($expectedText, $generatedText);
+ }
+}
diff --git a/app-modules/slack-events-bot/tests/Services/MessageBuilderServiceTest.php b/app-modules/slack-events-bot/tests/Services/MessageBuilderServiceTest.php
new file mode 100644
index 00000000..9ed5892b
--- /dev/null
+++ b/app-modules/slack-events-bot/tests/Services/MessageBuilderServiceTest.php
@@ -0,0 +1,124 @@
+ 'Event 1', 'description' => 'Description 1']);
+ $event2 = new Event(['name' => 'Event 2', 'description' => 'Description 2']);
+ $events = Collection::make([$event1, $event2]);
+
+ $eventServiceMock->shouldReceive('generateText')
+ ->with($event1)
+ ->andReturn('Event 1 Text');
+ $eventServiceMock->shouldReceive('generateBlocks')
+ ->with($event1)
+ ->andReturn([['type' => 'section', 'text' => ['type' => 'mrkdwn', 'text' => 'Event 1 Block']]]);
+
+ $eventServiceMock->shouldReceive('generateText')
+ ->with($event2)
+ ->andReturn('Event 2 Text');
+ $eventServiceMock->shouldReceive('generateBlocks')
+ ->with($event2)
+ ->andReturn([['type' => 'section', 'text' => ['type' => 'mrkdwn', 'text' => 'Event 2 Block']]]);
+
+ $result = $messageBuilderService->buildEventBlocks($events);
+
+ $this->assertCount(2, $result);
+ $this->assertEquals('Event 1 Text' . "\n\n", $result[0]['text']);
+ $this->assertStringContainsString('Event 1 Block', json_encode($result[0]['blocks']));
+ $this->assertEquals('Event 2 Text' . "\n\n", $result[1]['text']);
+ $this->assertStringContainsString('Event 2 Block', json_encode($result[1]['blocks']));
+ }
+
+ /** @test */
+ public function it_chunks_messages_correctly()
+ {
+ $eventServiceMock = Mockery::mock(EventService::class);
+ $messageBuilderService = new MessageBuilderService($eventServiceMock);
+
+ // Set a small max character length for testing chunking
+ config(['slack-events-bot.max_message_character_length' => 100]);
+ config(['slack-events-bot.header_buffer_length' => 50]); // A reasonable buffer for header
+
+ $weekStart = Carbon::parse('2025-07-07'); // Monday
+
+ $eventBlocks = Collection::make([
+ ['blocks' => [['type' => 'section', 'text' => ['text' => 'Event 1']]], 'text' => 'Event 1 Text' . "\n\n", 'text_length' => mb_strlen('Event 1 Text' . "\n\n")],
+ ['blocks' => [['type' => 'section', 'text' => ['text' => 'Event 2']]], 'text' => 'Event 2 Text' . "\n\n", 'text_length' => mb_strlen('Event 2 Text' . "\n\n")],
+ ['blocks' => [['type' => 'section', 'text' => ['text' => 'Event 3']]], 'text' => 'Event 3 Text' . "\n\n", 'text_length' => mb_strlen('Event 3 Text' . "\n\n")],
+ ['blocks' => [['type' => 'section', 'text' => ['text' => 'Event 4']]], 'text' => 'Event 4 Text' . "\n\n", 'text_length' => mb_strlen('Event 4 Text' . "\n\n")],
+ ]);
+
+ $messages = $messageBuilderService->chunkMessages($eventBlocks, $weekStart);
+
+ // Based on the small max_message_character_length, we expect multiple messages.
+ // The exact number depends on the header length and event text lengths.
+ // Let's assume each event text is around 15 chars, and header is around 50 chars.
+ // Max content length per message = 100 - 50 = 50 chars.
+ // Each event text is 15 chars. So, 3 events per message (3 * 15 = 45).
+ // With 4 events, we should get 2 messages.
+ $this->assertGreaterThanOrEqual(2, count($messages));
+
+ // Verify the first message
+ $this->assertStringContainsString('HackGreenville Events for the week of July 7 - 1 of', $messages[0]['text']);
+ $this->assertStringContainsString('Event 1 Text', $messages[0]['text']);
+ $this->assertStringContainsString('Event 2 Text', $messages[0]['text']);
+
+ // Verify the second message
+ $this->assertStringContainsString('HackGreenville Events for the week of July 7 - 2 of', $messages[1]['text']);
+ $this->assertStringContainsString('Event 3 Text', $messages[1]['text']);
+ $this->assertStringContainsString('Event 4 Text', $messages[1]['text']);
+ }
+
+ /** @test */
+ public function it_filters_out_null_event_blocks()
+ {
+ $eventServiceMock = Mockery::mock(EventService::class);
+ $messageBuilderService = new MessageBuilderService($eventServiceMock);
+
+ $event1 = new Event(['name' => 'Event 1']);
+ $event2 = new Event(['name' => 'Event 2']);
+ $events = Collection::make([$event1, $event2]);
+
+ $eventServiceMock->shouldReceive('generateText')
+ ->with($event1)
+ ->andReturn('Event 1 Text');
+ $eventServiceMock->shouldReceive('generateBlocks')
+ ->with($event1)
+ ->andReturn([['type' => 'section', 'text' => ['type' => 'mrkdwn', 'text' => 'Event 1 Block']]]);
+
+ // Simulate an event that should be skipped (e.g., generateBlocks returns null or empty)
+ $eventServiceMock->shouldReceive('generateText')
+ ->with($event2)
+ ->andReturn(''); // Empty text will cause buildSingleEventBlock to return null
+ $eventServiceMock->shouldReceive('generateBlocks')
+ ->with($event2)
+ ->andReturn([]); // Empty blocks will cause buildSingleEventBlock to return null
+
+ $result = $messageBuilderService->buildEventBlocks($events);
+
+ $this->assertCount(1, $result);
+ $this->assertEquals('Event 1 Text' . "\n\n", $result[0]['text']);
+ }
+}
diff --git a/app-modules/slack-events-bot/vendor/autoload.php b/app-modules/slack-events-bot/vendor/autoload.php
new file mode 100644
index 00000000..d225b9b0
--- /dev/null
+++ b/app-modules/slack-events-bot/vendor/autoload.php
@@ -0,0 +1,22 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier
+ * @author Jordi Boggiano
+ * @see https://www.php-fig.org/psr/psr-0/
+ * @see https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ /** @var \Closure(string):void */
+ private static $includeFile;
+
+ /** @var string|null */
+ private $vendorDir;
+
+ // PSR-4
+ /**
+ * @var array>
+ */
+ private $prefixLengthsPsr4 = array();
+ /**
+ * @var array>
+ */
+ private $prefixDirsPsr4 = array();
+ /**
+ * @var list
+ */
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ /**
+ * List of PSR-0 prefixes
+ *
+ * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+ *
+ * @var array>>
+ */
+ private $prefixesPsr0 = array();
+ /**
+ * @var list
+ */
+ private $fallbackDirsPsr0 = array();
+
+ /** @var bool */
+ private $useIncludePath = false;
+
+ /**
+ * @var array
+ */
+ private $classMap = array();
+
+ /** @var bool */
+ private $classMapAuthoritative = false;
+
+ /**
+ * @var array
+ */
+ private $missingClasses = array();
+
+ /** @var string|null */
+ private $apcuPrefix;
+
+ /**
+ * @var array
+ */
+ private static $registeredLoaders = array();
+
+ /**
+ * @param string|null $vendorDir
+ */
+ public function __construct($vendorDir = null)
+ {
+ $this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
+ }
+
+ /**
+ * @return array>
+ */
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+ }
+
+ return array();
+ }
+
+ /**
+ * @return array>
+ */
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ /**
+ * @return list
+ */
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ /**
+ * @return list
+ */
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ /**
+ * @return array Array of classname => path
+ */
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ *
+ * @return void
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @return void
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 base directories
+ *
+ * @return void
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ *
+ * @return void
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ *
+ * @return void
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ *
+ * @return void
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ *
+ * @return void
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+ if (null === $this->vendorDir) {
+ return;
+ }
+
+ if ($prepend) {
+ self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+ } else {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ self::$registeredLoaders[$this->vendorDir] = $this;
+ }
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ *
+ * @return void
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+
+ if (null !== $this->vendorDir) {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ }
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return true|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ $includeFile = self::$includeFile;
+ $includeFile($file);
+
+ return true;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Returns the currently registered loaders keyed by their corresponding vendor directories.
+ *
+ * @return array
+ */
+ public static function getRegisteredLoaders()
+ {
+ return self::$registeredLoaders;
+ }
+
+ /**
+ * @param string $class
+ * @param string $ext
+ * @return string|false
+ */
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath . '\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = \Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
+}
diff --git a/app-modules/slack-events-bot/vendor/composer/LICENSE b/app-modules/slack-events-bot/vendor/composer/LICENSE
new file mode 100644
index 00000000..f27399a0
--- /dev/null
+++ b/app-modules/slack-events-bot/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/app-modules/slack-events-bot/vendor/composer/autoload_classmap.php b/app-modules/slack-events-bot/vendor/composer/autoload_classmap.php
new file mode 100644
index 00000000..0fb0a2c1
--- /dev/null
+++ b/app-modules/slack-events-bot/vendor/composer/autoload_classmap.php
@@ -0,0 +1,10 @@
+ $vendorDir . '/composer/InstalledVersions.php',
+);
diff --git a/app-modules/slack-events-bot/vendor/composer/autoload_namespaces.php b/app-modules/slack-events-bot/vendor/composer/autoload_namespaces.php
new file mode 100644
index 00000000..15a2ff3a
--- /dev/null
+++ b/app-modules/slack-events-bot/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+ array($baseDir . '/database/seeders'),
+ 'HackGreenville\\SlackEventsBot\\Database\\Factories\\' => array($baseDir . '/database/factories'),
+ 'HackGreenville\\SlackEventsBot\\' => array($baseDir . '/src'),
+);
diff --git a/app-modules/slack-events-bot/vendor/composer/autoload_real.php b/app-modules/slack-events-bot/vendor/composer/autoload_real.php
new file mode 100644
index 00000000..657c4a9b
--- /dev/null
+++ b/app-modules/slack-events-bot/vendor/composer/autoload_real.php
@@ -0,0 +1,36 @@
+register(true);
+
+ return $loader;
+ }
+}
diff --git a/app-modules/slack-events-bot/vendor/composer/autoload_static.php b/app-modules/slack-events-bot/vendor/composer/autoload_static.php
new file mode 100644
index 00000000..cb1c9742
--- /dev/null
+++ b/app-modules/slack-events-bot/vendor/composer/autoload_static.php
@@ -0,0 +1,46 @@
+
+ array (
+ 'HackGreenville\\SlackEventsBot\\Database\\Seeders\\' => 47,
+ 'HackGreenville\\SlackEventsBot\\Database\\Factories\\' => 49,
+ 'HackGreenville\\SlackEventsBot\\' => 30,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'HackGreenville\\SlackEventsBot\\Database\\Seeders\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/database/seeders',
+ ),
+ 'HackGreenville\\SlackEventsBot\\Database\\Factories\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/database/factories',
+ ),
+ 'HackGreenville\\SlackEventsBot\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/src',
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInit8c95e1f6ebe5c8c5503a12dcb01d68c6::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInit8c95e1f6ebe5c8c5503a12dcb01d68c6::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInit8c95e1f6ebe5c8c5503a12dcb01d68c6::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/app/Http/Controllers/CalendarFeedController.php b/app/Http/Controllers/CalendarFeedController.php
index e07b4e4f..c47315f3 100644
--- a/app/Http/Controllers/CalendarFeedController.php
+++ b/app/Http/Controllers/CalendarFeedController.php
@@ -23,7 +23,8 @@ public function index(CalendarFeedRequest $request)
->when(
value: $request->validOrganizations()->isNotEmpty(),
callback: function (Builder $query) use ($request) {
- $query->orderByFieldSequence('id', $request->validOrganizations()->pluck('id')->toArray())
+ $query->whereIn('id', $request->validOrganizations()->pluck('id')->toArray())
+ ->orderByFieldSequence('id', $request->validOrganizations()->pluck('id')->toArray())
->orderBy('title');
},
default: fn (Builder $query) => $query->orderBy('title')
diff --git a/app/Models/Venue.php b/app/Models/Venue.php
index 0bbf2111..3e88b45b 100644
--- a/app/Models/Venue.php
+++ b/app/Models/Venue.php
@@ -71,7 +71,14 @@ class Venue extends Model
public function fullAddress()
{
- return "{$this->name} - {$this->address} {$this->city}, {$this->state} {$this->zipcode}";
+ $location = collect([$this->city, $this->state?->abbr])
+ ->filter()
+ ->join(', ');
+
+ return collect([
+ "{$this->name} - {$this->address}",
+ mb_trim("{$location} {$this->zipcode}"),
+ ])->filter()->join(' ');
}
diff --git a/composer.json b/composer.json
index cd2850ec..ffe05828 100644
--- a/composer.json
+++ b/composer.json
@@ -9,7 +9,7 @@
"license": "MIT",
"type": "project",
"require": {
- "php": "^8.1",
+ "php": "^8.3",
"ext-json": "*",
"filament/filament": "^3.3",
"firebase/php-jwt": "^6.10",
@@ -18,6 +18,7 @@
"guzzlehttp/guzzle": "^7.0.1",
"hack-greenville/api": "*",
"hack-greenville/event-importer": "*",
+ "hack-greenville/slack-events-bot": "*",
"internachi/modular": "^2.0",
"knuckleswtf/scribe": "^5.2",
"laravel/framework": "^10.0",
diff --git a/composer.lock b/composer.lock
index 704d3a2f..53e7499d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,879 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "74defb137e7f4e7bd9a1a4a97b6d7153",
+ "content-hash": "bdb361a695b7785b5da943de2b5bec1d",
"packages": [
- {
- "name": "amphp/amp",
- "version": "v3.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/amp.git",
- "reference": "138801fb68cfc9c329da8a7b39d01ce7291ee4b0"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/amp/zipball/138801fb68cfc9c329da8a7b39d01ce7291ee4b0",
- "reference": "138801fb68cfc9c329da8a7b39d01ce7291ee4b0",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1",
- "revolt/event-loop": "^1 || ^0.2"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "phpunit/phpunit": "^9",
- "psalm/phar": "5.23.1"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions.php",
- "src/Future/functions.php",
- "src/Internal/functions.php"
- ],
- "psr-4": {
- "Amp\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Bob Weinand",
- "email": "bobwei9@hotmail.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- },
- {
- "name": "Daniel Lowrey",
- "email": "rdlowrey@php.net"
- }
- ],
- "description": "A non-blocking concurrency framework for PHP applications.",
- "homepage": "https://amphp.org/amp",
- "keywords": [
- "async",
- "asynchronous",
- "awaitable",
- "concurrency",
- "event",
- "event-loop",
- "future",
- "non-blocking",
- "promise"
- ],
- "support": {
- "issues": "https://github.com/amphp/amp/issues",
- "source": "https://github.com/amphp/amp/tree/v3.0.2"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-05-10T21:37:46+00:00"
- },
- {
- "name": "amphp/byte-stream",
- "version": "v2.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/byte-stream.git",
- "reference": "daa00f2efdbd71565bf64ffefa89e37542addf93"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/byte-stream/zipball/daa00f2efdbd71565bf64ffefa89e37542addf93",
- "reference": "daa00f2efdbd71565bf64ffefa89e37542addf93",
- "shasum": ""
- },
- "require": {
- "amphp/amp": "^3",
- "amphp/parser": "^1.1",
- "amphp/pipeline": "^1",
- "amphp/serialization": "^1",
- "amphp/sync": "^2",
- "php": ">=8.1",
- "revolt/event-loop": "^1 || ^0.2.3"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "amphp/phpunit-util": "^3",
- "phpunit/phpunit": "^9",
- "psalm/phar": "5.22.1"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions.php",
- "src/Internal/functions.php"
- ],
- "psr-4": {
- "Amp\\ByteStream\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "A stream abstraction to make working with non-blocking I/O simple.",
- "homepage": "https://amphp.org/byte-stream",
- "keywords": [
- "amp",
- "amphp",
- "async",
- "io",
- "non-blocking",
- "stream"
- ],
- "support": {
- "issues": "https://github.com/amphp/byte-stream/issues",
- "source": "https://github.com/amphp/byte-stream/tree/v2.1.1"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-02-17T04:49:38+00:00"
- },
- {
- "name": "amphp/cache",
- "version": "v2.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/cache.git",
- "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c",
- "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c",
- "shasum": ""
- },
- "require": {
- "amphp/amp": "^3",
- "amphp/serialization": "^1",
- "amphp/sync": "^2",
- "php": ">=8.1",
- "revolt/event-loop": "^1 || ^0.2"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "amphp/phpunit-util": "^3",
- "phpunit/phpunit": "^9",
- "psalm/phar": "^5.4"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Amp\\Cache\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- },
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Daniel Lowrey",
- "email": "rdlowrey@php.net"
- }
- ],
- "description": "A fiber-aware cache API based on Amp and Revolt.",
- "homepage": "https://amphp.org/cache",
- "support": {
- "issues": "https://github.com/amphp/cache/issues",
- "source": "https://github.com/amphp/cache/tree/v2.0.1"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-04-19T03:38:06+00:00"
- },
- {
- "name": "amphp/dns",
- "version": "v2.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/dns.git",
- "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/dns/zipball/758266b0ea7470e2e42cd098493bc6d6c7100cf7",
- "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7",
- "shasum": ""
- },
- "require": {
- "amphp/amp": "^3",
- "amphp/byte-stream": "^2",
- "amphp/cache": "^2",
- "amphp/parser": "^1",
- "amphp/windows-registry": "^1.0.1",
- "daverandom/libdns": "^2.0.2",
- "ext-filter": "*",
- "php": ">=8.1",
- "revolt/event-loop": "^1 || ^0.2"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "amphp/phpunit-util": "^3",
- "phpunit/phpunit": "^9",
- "psalm/phar": "5.20"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions.php"
- ],
- "psr-4": {
- "Amp\\Dns\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Chris Wright",
- "email": "addr@daverandom.com"
- },
- {
- "name": "Daniel Lowrey",
- "email": "rdlowrey@php.net"
- },
- {
- "name": "Bob Weinand",
- "email": "bobwei9@hotmail.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- },
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- }
- ],
- "description": "Async DNS resolution for Amp.",
- "homepage": "https://github.com/amphp/dns",
- "keywords": [
- "amp",
- "amphp",
- "async",
- "client",
- "dns",
- "resolve"
- ],
- "support": {
- "issues": "https://github.com/amphp/dns/issues",
- "source": "https://github.com/amphp/dns/tree/v2.2.0"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-06-02T19:54:12+00:00"
- },
- {
- "name": "amphp/parallel",
- "version": "v2.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/parallel.git",
- "reference": "9777db1460d1535bc2a843840684fb1205225b87"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/parallel/zipball/9777db1460d1535bc2a843840684fb1205225b87",
- "reference": "9777db1460d1535bc2a843840684fb1205225b87",
- "shasum": ""
- },
- "require": {
- "amphp/amp": "^3",
- "amphp/byte-stream": "^2",
- "amphp/cache": "^2",
- "amphp/parser": "^1",
- "amphp/pipeline": "^1",
- "amphp/process": "^2",
- "amphp/serialization": "^1",
- "amphp/socket": "^2",
- "amphp/sync": "^2",
- "php": ">=8.1",
- "revolt/event-loop": "^1"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "amphp/phpunit-util": "^3",
- "phpunit/phpunit": "^9",
- "psalm/phar": "^5.18"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/Context/functions.php",
- "src/Context/Internal/functions.php",
- "src/Ipc/functions.php",
- "src/Worker/functions.php"
- ],
- "psr-4": {
- "Amp\\Parallel\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- },
- {
- "name": "Stephen Coakley",
- "email": "me@stephencoakley.com"
- }
- ],
- "description": "Parallel processing component for Amp.",
- "homepage": "https://github.com/amphp/parallel",
- "keywords": [
- "async",
- "asynchronous",
- "concurrent",
- "multi-processing",
- "multi-threading"
- ],
- "support": {
- "issues": "https://github.com/amphp/parallel/issues",
- "source": "https://github.com/amphp/parallel/tree/v2.3.0"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-09-14T19:16:14+00:00"
- },
- {
- "name": "amphp/parser",
- "version": "v1.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/parser.git",
- "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7",
- "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7",
- "shasum": ""
- },
- "require": {
- "php": ">=7.4"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "phpunit/phpunit": "^9",
- "psalm/phar": "^5.4"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Amp\\Parser\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "A generator parser to make streaming parsers simple.",
- "homepage": "https://github.com/amphp/parser",
- "keywords": [
- "async",
- "non-blocking",
- "parser",
- "stream"
- ],
- "support": {
- "issues": "https://github.com/amphp/parser/issues",
- "source": "https://github.com/amphp/parser/tree/v1.1.1"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-03-21T19:16:53+00:00"
- },
- {
- "name": "amphp/pipeline",
- "version": "v1.2.1",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/pipeline.git",
- "reference": "66c095673aa5b6e689e63b52d19e577459129ab3"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/pipeline/zipball/66c095673aa5b6e689e63b52d19e577459129ab3",
- "reference": "66c095673aa5b6e689e63b52d19e577459129ab3",
- "shasum": ""
- },
- "require": {
- "amphp/amp": "^3",
- "php": ">=8.1",
- "revolt/event-loop": "^1"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "amphp/phpunit-util": "^3",
- "phpunit/phpunit": "^9",
- "psalm/phar": "^5.18"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Amp\\Pipeline\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "Asynchronous iterators and operators.",
- "homepage": "https://amphp.org/pipeline",
- "keywords": [
- "amp",
- "amphp",
- "async",
- "io",
- "iterator",
- "non-blocking"
- ],
- "support": {
- "issues": "https://github.com/amphp/pipeline/issues",
- "source": "https://github.com/amphp/pipeline/tree/v1.2.1"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-07-04T00:56:47+00:00"
- },
- {
- "name": "amphp/process",
- "version": "v2.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/process.git",
- "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d",
- "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d",
- "shasum": ""
- },
- "require": {
- "amphp/amp": "^3",
- "amphp/byte-stream": "^2",
- "amphp/sync": "^2",
- "php": ">=8.1",
- "revolt/event-loop": "^1 || ^0.2"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "amphp/phpunit-util": "^3",
- "phpunit/phpunit": "^9",
- "psalm/phar": "^5.4"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions.php"
- ],
- "psr-4": {
- "Amp\\Process\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Bob Weinand",
- "email": "bobwei9@hotmail.com"
- },
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "A fiber-aware process manager based on Amp and Revolt.",
- "homepage": "https://amphp.org/process",
- "support": {
- "issues": "https://github.com/amphp/process/issues",
- "source": "https://github.com/amphp/process/tree/v2.0.3"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-04-19T03:13:44+00:00"
- },
- {
- "name": "amphp/serialization",
- "version": "v1.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/serialization.git",
- "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1",
- "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "dev-master",
- "phpunit/phpunit": "^9 || ^8 || ^7"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions.php"
- ],
- "psr-4": {
- "Amp\\Serialization\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "Serialization tools for IPC and data storage in PHP.",
- "homepage": "https://github.com/amphp/serialization",
- "keywords": [
- "async",
- "asynchronous",
- "serialization",
- "serialize"
- ],
- "support": {
- "issues": "https://github.com/amphp/serialization/issues",
- "source": "https://github.com/amphp/serialization/tree/master"
- },
- "time": "2020-03-25T21:39:07+00:00"
- },
- {
- "name": "amphp/socket",
- "version": "v2.3.1",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/socket.git",
- "reference": "58e0422221825b79681b72c50c47a930be7bf1e1"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1",
- "reference": "58e0422221825b79681b72c50c47a930be7bf1e1",
- "shasum": ""
- },
- "require": {
- "amphp/amp": "^3",
- "amphp/byte-stream": "^2",
- "amphp/dns": "^2",
- "ext-openssl": "*",
- "kelunik/certificate": "^1.1",
- "league/uri": "^6.5 | ^7",
- "league/uri-interfaces": "^2.3 | ^7",
- "php": ">=8.1",
- "revolt/event-loop": "^1 || ^0.2"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "amphp/phpunit-util": "^3",
- "amphp/process": "^2",
- "phpunit/phpunit": "^9",
- "psalm/phar": "5.20"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions.php",
- "src/Internal/functions.php",
- "src/SocketAddress/functions.php"
- ],
- "psr-4": {
- "Amp\\Socket\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Daniel Lowrey",
- "email": "rdlowrey@gmail.com"
- },
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.",
- "homepage": "https://github.com/amphp/socket",
- "keywords": [
- "amp",
- "async",
- "encryption",
- "non-blocking",
- "sockets",
- "tcp",
- "tls"
- ],
- "support": {
- "issues": "https://github.com/amphp/socket/issues",
- "source": "https://github.com/amphp/socket/tree/v2.3.1"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-04-21T14:33:03+00:00"
- },
- {
- "name": "amphp/sync",
- "version": "v2.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/sync.git",
- "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1",
- "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1",
- "shasum": ""
- },
- "require": {
- "amphp/amp": "^3",
- "amphp/pipeline": "^1",
- "amphp/serialization": "^1",
- "php": ">=8.1",
- "revolt/event-loop": "^1 || ^0.2"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "amphp/phpunit-util": "^3",
- "phpunit/phpunit": "^9",
- "psalm/phar": "5.23"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions.php"
- ],
- "psr-4": {
- "Amp\\Sync\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- },
- {
- "name": "Stephen Coakley",
- "email": "me@stephencoakley.com"
- }
- ],
- "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.",
- "homepage": "https://github.com/amphp/sync",
- "keywords": [
- "async",
- "asynchronous",
- "mutex",
- "semaphore",
- "synchronization"
- ],
- "support": {
- "issues": "https://github.com/amphp/sync/issues",
- "source": "https://github.com/amphp/sync/tree/v2.3.0"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-08-03T19:31:26+00:00"
- },
- {
- "name": "amphp/windows-registry",
- "version": "v1.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/amphp/windows-registry.git",
- "reference": "0d569e8f256cca974e3842b6e78b4e434bf98306"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/amphp/windows-registry/zipball/0d569e8f256cca974e3842b6e78b4e434bf98306",
- "reference": "0d569e8f256cca974e3842b6e78b4e434bf98306",
- "shasum": ""
- },
- "require": {
- "amphp/byte-stream": "^2",
- "amphp/process": "^2",
- "php": ">=8.1"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "psalm/phar": "^5.4"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Amp\\WindowsRegistry\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "Windows Registry Reader.",
- "support": {
- "issues": "https://github.com/amphp/windows-registry/issues",
- "source": "https://github.com/amphp/windows-registry/tree/v1.0.1"
- },
- "funding": [
- {
- "url": "https://github.com/amphp",
- "type": "github"
- }
- ],
- "time": "2024-01-30T23:01:51+00:00"
- },
{
"name": "anourvalar/eloquent-serialize",
- "version": "1.3.3",
+ "version": "1.3.4",
"source": {
"type": "git",
"url": "https://github.com/AnourValar/eloquent-serialize.git",
- "reference": "2f05023f1e465a91dc4f08483e6710325641a444"
+ "reference": "0934a98866e02b73e38696961a9d7984b834c9d9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/2f05023f1e465a91dc4f08483e6710325641a444",
- "reference": "2f05023f1e465a91dc4f08483e6710325641a444",
+ "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/0934a98866e02b73e38696961a9d7984b834c9d9",
+ "reference": "0934a98866e02b73e38696961a9d7984b834c9d9",
"shasum": ""
},
"require": {
@@ -927,9 +68,9 @@
],
"support": {
"issues": "https://github.com/AnourValar/eloquent-serialize/issues",
- "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.3"
+ "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.4"
},
- "time": "2025-05-28T17:07:28+00:00"
+ "time": "2025-07-30T15:45:57+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
@@ -1212,16 +353,16 @@
},
{
"name": "composer/ca-bundle",
- "version": "1.5.3",
+ "version": "1.5.8",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
- "reference": "3b1fc3f0be055baa7c6258b1467849c3e8204eb2"
+ "reference": "719026bb30813accb68271fee7e39552a58e9f65"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3b1fc3f0be055baa7c6258b1467849c3e8204eb2",
- "reference": "3b1fc3f0be055baa7c6258b1467849c3e8204eb2",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/719026bb30813accb68271fee7e39552a58e9f65",
+ "reference": "719026bb30813accb68271fee7e39552a58e9f65",
"shasum": ""
},
"require": {
@@ -1268,7 +409,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues",
- "source": "https://github.com/composer/ca-bundle/tree/1.5.3"
+ "source": "https://github.com/composer/ca-bundle/tree/1.5.8"
},
"funding": [
{
@@ -1278,26 +419,22 @@
{
"url": "https://github.com/composer",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
}
],
- "time": "2024-11-04T10:15:26+00:00"
+ "time": "2025-08-20T18:49:47+00:00"
},
{
"name": "composer/class-map-generator",
- "version": "1.4.0",
+ "version": "1.6.2",
"source": {
"type": "git",
"url": "https://github.com/composer/class-map-generator.git",
- "reference": "98bbf6780e56e0fd2404fe4b82eb665a0f93b783"
+ "reference": "ba9f089655d4cdd64e762a6044f411ccdaec0076"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/class-map-generator/zipball/98bbf6780e56e0fd2404fe4b82eb665a0f93b783",
- "reference": "98bbf6780e56e0fd2404fe4b82eb665a0f93b783",
+ "url": "https://api.github.com/repos/composer/class-map-generator/zipball/ba9f089655d4cdd64e762a6044f411ccdaec0076",
+ "reference": "ba9f089655d4cdd64e762a6044f411ccdaec0076",
"shasum": ""
},
"require": {
@@ -1306,10 +443,10 @@
"symfony/finder": "^4.4 || ^5.3 || ^6 || ^7"
},
"require-dev": {
- "phpstan/phpstan": "^1.6",
- "phpstan/phpstan-deprecation-rules": "^1",
- "phpstan/phpstan-phpunit": "^1",
- "phpstan/phpstan-strict-rules": "^1.1",
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-deprecation-rules": "^1 || ^2",
+ "phpstan/phpstan-phpunit": "^1 || ^2",
+ "phpstan/phpstan-strict-rules": "^1.1 || ^2",
"phpunit/phpunit": "^8",
"symfony/filesystem": "^5.4 || ^6"
},
@@ -1341,7 +478,7 @@
],
"support": {
"issues": "https://github.com/composer/class-map-generator/issues",
- "source": "https://github.com/composer/class-map-generator/tree/1.4.0"
+ "source": "https://github.com/composer/class-map-generator/tree/1.6.2"
},
"funding": [
{
@@ -1351,26 +488,22 @@
{
"url": "https://github.com/composer",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
}
],
- "time": "2024-10-03T18:14:00+00:00"
+ "time": "2025-08-20T18:52:43+00:00"
},
{
"name": "composer/composer",
- "version": "2.8.2",
+ "version": "2.8.12",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
- "reference": "6e543d03187c882ea1c6ba43add2467754427803"
+ "reference": "3e38919bc9a2c3c026f2151b5e56d04084ce8f0b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/composer/zipball/6e543d03187c882ea1c6ba43add2467754427803",
- "reference": "6e543d03187c882ea1c6ba43add2467754427803",
+ "url": "https://api.github.com/repos/composer/composer/zipball/3e38919bc9a2c3c026f2151b5e56d04084ce8f0b",
+ "reference": "3e38919bc9a2c3c026f2151b5e56d04084ce8f0b",
"shasum": ""
},
"require": {
@@ -1381,20 +514,20 @@
"composer/semver": "^3.3",
"composer/spdx-licenses": "^1.5.7",
"composer/xdebug-handler": "^2.0.2 || ^3.0.3",
- "justinrainbow/json-schema": "^5.3",
+ "justinrainbow/json-schema": "^6.5.1",
"php": "^7.2.5 || ^8.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
- "react/promise": "^3.2",
+ "react/promise": "^3.3",
"seld/jsonlint": "^1.4",
"seld/phar-utils": "^1.2",
"seld/signal-handler": "^2.0",
- "symfony/console": "^5.4.35 || ^6.3.12 || ^7.0.3",
- "symfony/filesystem": "^5.4.35 || ^6.3.12 || ^7.0.3",
- "symfony/finder": "^5.4.35 || ^6.3.12 || ^7.0.3",
+ "symfony/console": "^5.4.47 || ^6.4.25 || ^7.1.10",
+ "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.1.10",
+ "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.1.10",
"symfony/polyfill-php73": "^1.24",
"symfony/polyfill-php80": "^1.24",
"symfony/polyfill-php81": "^1.24",
- "symfony/process": "^5.4.35 || ^6.3.12 || ^7.0.3"
+ "symfony/process": "^5.4.47 || ^6.4.25 || ^7.1.10"
},
"require-dev": {
"phpstan/phpstan": "^1.11.8",
@@ -1402,7 +535,7 @@
"phpstan/phpstan-phpunit": "^1.4.0",
"phpstan/phpstan-strict-rules": "^1.6.0",
"phpstan/phpstan-symfony": "^1.4.0",
- "symfony/phpunit-bridge": "^6.4.3 || ^7.0.1"
+ "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3"
},
"suggest": {
"ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
@@ -1414,13 +547,13 @@
],
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "2.8-dev"
- },
"phpstan": {
"includes": [
"phpstan/rules.neon"
]
+ },
+ "branch-alias": {
+ "dev-main": "2.8-dev"
}
},
"autoload": {
@@ -1455,7 +588,7 @@
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/composer/issues",
"security": "https://github.com/composer/composer/security/policy",
- "source": "https://github.com/composer/composer/tree/2.8.2"
+ "source": "https://github.com/composer/composer/tree/2.8.12"
},
"funding": [
{
@@ -1465,13 +598,9 @@
{
"url": "https://github.com/composer",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
}
],
- "time": "2024-10-29T15:12:11+00:00"
+ "time": "2025-09-19T11:41:59+00:00"
},
{
"name": "composer/metadata-minifier",
@@ -1544,16 +673,16 @@
},
{
"name": "composer/pcre",
- "version": "3.3.1",
+ "version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
- "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4"
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4",
- "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
@@ -1563,19 +692,19 @@
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
- "phpstan/phpstan": "^1.11.10",
- "phpstan/phpstan-strict-rules": "^1.1",
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "3.x-dev"
- },
"phpstan": {
"includes": [
"extension.neon"
]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
}
},
"autoload": {
@@ -1603,7 +732,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
- "source": "https://github.com/composer/pcre/tree/3.3.1"
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
@@ -1619,20 +748,20 @@
"type": "tidelift"
}
],
- "time": "2024-08-27T18:44:43+00:00"
+ "time": "2024-11-12T16:29:46+00:00"
},
{
"name": "composer/semver",
- "version": "3.4.3",
+ "version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
- "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
+ "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
@@ -1684,7 +813,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
- "source": "https://github.com/composer/semver/tree/3.4.3"
+ "source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
@@ -1694,34 +823,30 @@
{
"url": "https://github.com/composer",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
}
],
- "time": "2024-09-19T14:15:21+00:00"
+ "time": "2025-08-20T19:15:30+00:00"
},
{
"name": "composer/spdx-licenses",
- "version": "1.5.8",
+ "version": "1.5.9",
"source": {
"type": "git",
"url": "https://github.com/composer/spdx-licenses.git",
- "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a"
+ "reference": "edf364cefe8c43501e21e88110aac10b284c3c9f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a",
- "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/edf364cefe8c43501e21e88110aac10b284c3c9f",
+ "reference": "edf364cefe8c43501e21e88110aac10b284c3c9f",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
- "phpstan/phpstan": "^0.12.55",
- "symfony/phpunit-bridge": "^4.2 || ^5"
+ "phpstan/phpstan": "^1.11",
+ "symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
@@ -1764,7 +889,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/spdx-licenses/issues",
- "source": "https://github.com/composer/spdx-licenses/tree/1.5.8"
+ "source": "https://github.com/composer/spdx-licenses/tree/1.5.9"
},
"funding": [
{
@@ -1780,7 +905,7 @@
"type": "tidelift"
}
],
- "time": "2023-11-20T07:44:33+00:00"
+ "time": "2025-05-12T21:07:07+00:00"
},
{
"name": "composer/xdebug-handler",
@@ -1953,50 +1078,6 @@
],
"time": "2025-02-21T08:52:11+00:00"
},
- {
- "name": "daverandom/libdns",
- "version": "v2.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/DaveRandom/LibDNS.git",
- "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a",
- "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a",
- "shasum": ""
- },
- "require": {
- "ext-ctype": "*",
- "php": ">=7.1"
- },
- "suggest": {
- "ext-intl": "Required for IDN support"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions.php"
- ],
- "psr-4": {
- "LibDNS\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "DNS protocol implementation written in pure PHP",
- "keywords": [
- "dns"
- ],
- "support": {
- "issues": "https://github.com/DaveRandom/LibDNS/issues",
- "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0"
- },
- "time": "2024-04-12T12:12:48+00:00"
- },
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@@ -2074,16 +1155,16 @@
},
{
"name": "doctrine/dbal",
- "version": "3.10.0",
+ "version": "3.10.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
- "reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214"
+ "reference": "65edaca19a752730f290ec2fb89d593cb40afb43"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/1cf840d696373ea0d58ad0a8875c0fadcfc67214",
- "reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/65edaca19a752730f290ec2fb89d593cb40afb43",
+ "reference": "65edaca19a752730f290ec2fb89d593cb40afb43",
"shasum": ""
},
"require": {
@@ -2099,14 +1180,14 @@
},
"require-dev": {
"doctrine/cache": "^1.11|^2.0",
- "doctrine/coding-standard": "13.0.0",
+ "doctrine/coding-standard": "14.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
- "phpstan/phpstan": "2.1.17",
+ "phpstan/phpstan": "2.1.30",
"phpstan/phpstan-strict-rules": "^2",
- "phpunit/phpunit": "9.6.23",
- "slevomat/coding-standard": "8.16.2",
- "squizlabs/php_codesniffer": "3.13.1",
+ "phpunit/phpunit": "9.6.29",
+ "slevomat/coding-standard": "8.24.0",
+ "squizlabs/php_codesniffer": "4.0.0",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0"
},
@@ -2168,7 +1249,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
- "source": "https://github.com/doctrine/dbal/tree/3.10.0"
+ "source": "https://github.com/doctrine/dbal/tree/3.10.3"
},
"funding": [
{
@@ -2184,7 +1265,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T21:11:04+00:00"
+ "time": "2025-10-09T09:05:12+00:00"
},
{
"name": "doctrine/deprecations",
@@ -2327,33 +1408,32 @@
},
{
"name": "doctrine/inflector",
- "version": "2.0.10",
+ "version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
- "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc"
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc",
- "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^11.0",
- "phpstan/phpstan": "^1.8",
- "phpstan/phpstan-phpunit": "^1.1",
- "phpstan/phpstan-strict-rules": "^1.3",
- "phpunit/phpunit": "^8.5 || ^9.5",
- "vimeo/psalm": "^4.25 || ^5.4"
+ "doctrine/coding-standard": "^12.0 || ^13.0",
+ "phpstan/phpstan": "^1.12 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
+ "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
+ "phpunit/phpunit": "^8.5 || ^12.2"
},
"type": "library",
"autoload": {
"psr-4": {
- "Doctrine\\Inflector\\": "lib/Doctrine/Inflector"
+ "Doctrine\\Inflector\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2398,7 +1478,7 @@
],
"support": {
"issues": "https://github.com/doctrine/inflector/issues",
- "source": "https://github.com/doctrine/inflector/tree/2.0.10"
+ "source": "https://github.com/doctrine/inflector/tree/2.1.0"
},
"funding": [
{
@@ -2414,7 +1494,7 @@
"type": "tidelift"
}
],
- "time": "2024-02-18T20:23:39+00:00"
+ "time": "2025-08-10T19:31:58+00:00"
},
{
"name": "doctrine/lexer",
@@ -2740,16 +1820,16 @@
},
{
"name": "filament/actions",
- "version": "v3.3.33",
+ "version": "v3.3.43",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/actions.git",
- "reference": "9eaddc610d9adc00d738b8b116cea1be35a88f85"
+ "reference": "4582f2da9ed0660685b8e0849d32f106bc8a4b2d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filamentphp/actions/zipball/9eaddc610d9adc00d738b8b116cea1be35a88f85",
- "reference": "9eaddc610d9adc00d738b8b116cea1be35a88f85",
+ "url": "https://api.github.com/repos/filamentphp/actions/zipball/4582f2da9ed0660685b8e0849d32f106bc8a4b2d",
+ "reference": "4582f2da9ed0660685b8e0849d32f106bc8a4b2d",
"shasum": ""
},
"require": {
@@ -2789,20 +1869,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
- "time": "2025-07-16T08:51:11+00:00"
+ "time": "2025-09-28T22:06:00+00:00"
},
{
"name": "filament/filament",
- "version": "v3.3.33",
+ "version": "v3.3.43",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/panels.git",
- "reference": "8e6618036c9235d968740d43bb8afb58fe705e5b"
+ "reference": "f61544ea879633e42e2ae8a2eafe034aecdad2b2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filamentphp/panels/zipball/8e6618036c9235d968740d43bb8afb58fe705e5b",
- "reference": "8e6618036c9235d968740d43bb8afb58fe705e5b",
+ "url": "https://api.github.com/repos/filamentphp/panels/zipball/f61544ea879633e42e2ae8a2eafe034aecdad2b2",
+ "reference": "f61544ea879633e42e2ae8a2eafe034aecdad2b2",
"shasum": ""
},
"require": {
@@ -2854,20 +1934,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
- "time": "2025-07-21T10:08:08+00:00"
+ "time": "2025-09-28T22:06:09+00:00"
},
{
"name": "filament/forms",
- "version": "v3.3.33",
+ "version": "v3.3.43",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/forms.git",
- "reference": "72ec2ede65d8e9fa979a066bce78812458793dde"
+ "reference": "da5401bf3684b6abc6cf1d8e152f01b25d815319"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filamentphp/forms/zipball/72ec2ede65d8e9fa979a066bce78812458793dde",
- "reference": "72ec2ede65d8e9fa979a066bce78812458793dde",
+ "url": "https://api.github.com/repos/filamentphp/forms/zipball/da5401bf3684b6abc6cf1d8e152f01b25d815319",
+ "reference": "da5401bf3684b6abc6cf1d8e152f01b25d815319",
"shasum": ""
},
"require": {
@@ -2910,20 +1990,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
- "time": "2025-07-21T10:07:59+00:00"
+ "time": "2025-10-06T21:42:10+00:00"
},
{
"name": "filament/infolists",
- "version": "v3.3.33",
+ "version": "v3.3.43",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/infolists.git",
- "reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa"
+ "reference": "4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filamentphp/infolists/zipball/89a3f1f236863e2035be3d7b0c68987508dd06fa",
- "reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa",
+ "url": "https://api.github.com/repos/filamentphp/infolists/zipball/4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de",
+ "reference": "4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de",
"shasum": ""
},
"require": {
@@ -2961,11 +2041,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
- "time": "2025-06-23T10:46:53+00:00"
+ "time": "2025-08-12T13:15:27+00:00"
},
{
"name": "filament/notifications",
- "version": "v3.3.33",
+ "version": "v3.3.43",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/notifications.git",
@@ -3017,16 +2097,16 @@
},
{
"name": "filament/support",
- "version": "v3.3.33",
+ "version": "v3.3.43",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/support.git",
- "reference": "0bf4856840e160624ba79f43aaaa3ec396140f1d"
+ "reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filamentphp/support/zipball/0bf4856840e160624ba79f43aaaa3ec396140f1d",
- "reference": "0bf4856840e160624ba79f43aaaa3ec396140f1d",
+ "url": "https://api.github.com/repos/filamentphp/support/zipball/afafd5e7a2f8cf052f70f989b52d82d0a1df5c78",
+ "reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78",
"shasum": ""
},
"require": {
@@ -3072,20 +2152,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
- "time": "2025-07-16T08:51:22+00:00"
+ "time": "2025-08-12T13:15:44+00:00"
},
{
"name": "filament/tables",
- "version": "v3.3.33",
+ "version": "v3.3.43",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/tables.git",
- "reference": "3f0d827c960f1ee4a67ab71c416ad67e24747dc4"
+ "reference": "2e1e3aeeeccd6b74e5d038325af52635d1108e4c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filamentphp/tables/zipball/3f0d827c960f1ee4a67ab71c416ad67e24747dc4",
- "reference": "3f0d827c960f1ee4a67ab71c416ad67e24747dc4",
+ "url": "https://api.github.com/repos/filamentphp/tables/zipball/2e1e3aeeeccd6b74e5d038325af52635d1108e4c",
+ "reference": "2e1e3aeeeccd6b74e5d038325af52635d1108e4c",
"shasum": ""
},
"require": {
@@ -3124,11 +2204,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
- "time": "2025-07-08T20:42:18+00:00"
+ "time": "2025-09-17T10:47:13+00:00"
},
{
"name": "filament/widgets",
- "version": "v3.3.33",
+ "version": "v3.3.43",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/widgets.git",
@@ -3172,16 +2252,16 @@
},
{
"name": "filp/whoops",
- "version": "2.16.0",
+ "version": "2.18.4",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "befcdc0e5dce67252aa6322d82424be928214fa2"
+ "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2",
- "reference": "befcdc0e5dce67252aa6322d82424be928214fa2",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d",
+ "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d",
"shasum": ""
},
"require": {
@@ -3231,7 +2311,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
- "source": "https://github.com/filp/whoops/tree/2.16.0"
+ "source": "https://github.com/filp/whoops/tree/2.18.4"
},
"funding": [
{
@@ -3239,20 +2319,20 @@
"type": "github"
}
],
- "time": "2024-09-25T12:00:00+00:00"
+ "time": "2025-08-08T12:00:00+00:00"
},
{
"name": "firebase/php-jwt",
- "version": "v6.10.2",
+ "version": "v6.11.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
- "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b"
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/firebase/php-jwt/zipball/30c19ed0f3264cb660ea496895cfb6ef7ee3653b",
- "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"shasum": ""
},
"require": {
@@ -3300,9 +2380,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
- "source": "https://github.com/firebase/php-jwt/tree/v6.10.2"
+ "source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
},
- "time": "2024-11-24T11:22:49+00:00"
+ "time": "2025-04-09T20:32:01+00:00"
},
{
"name": "fruitcake/php-cors",
@@ -3377,32 +2457,32 @@
},
{
"name": "glhd/aire",
- "version": "2.13.0",
+ "version": "2.14.0",
"source": {
"type": "git",
"url": "https://github.com/glhd/aire.git",
- "reference": "49627f7ed8fde11d88fa4f4a81a8ef80bea97677"
+ "reference": "f2807b9c4acd66618ff5ed31021a2f81f954026b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/glhd/aire/zipball/49627f7ed8fde11d88fa4f4a81a8ef80bea97677",
- "reference": "49627f7ed8fde11d88fa4f4a81a8ef80bea97677",
+ "url": "https://api.github.com/repos/glhd/aire/zipball/f2807b9c4acd66618ff5ed31021a2f81f954026b",
+ "reference": "f2807b9c4acd66618ff5ed31021a2f81f954026b",
"shasum": ""
},
"require": {
"ext-json": "*",
- "illuminate/events": ">=5.8.28 <10.48.0 || >10.48.1 <12.0.0",
- "illuminate/support": ">=5.8.28 <10.48.0 || >10.48.1 <12.0.0",
- "illuminate/view": ">=5.8.28 <10.48.0 || >10.48.1 <12.0.0"
+ "illuminate/events": ">=5.8.28 <10.48.0 || >10.48.1 <13.0.0",
+ "illuminate/support": ">=5.8.28 <10.48.0 || >10.48.1 <13.0.0",
+ "illuminate/view": ">=5.8.28 <10.48.0 || >10.48.1 <13.0.0"
},
"require-dev": {
"barryvdh/reflection-docblock": "^2.0",
"friendsofphp/php-cs-fixer": "^3.5",
"guzzlehttp/guzzle": "~6.0|~7.0",
"mockery/mockery": "^1.4",
- "orchestra/testbench": "^6.24|^7.10|^8|^9|9.x-dev|10.x-dev|dev-master",
+ "orchestra/testbench": "^6.24|^7.10|^8|^9|^10|11.x-dev|dev-master|dev-main",
"php-coveralls/php-coveralls": "^2.1",
- "phpunit/phpunit": "^9|^10.5",
+ "phpunit/phpunit": "^9|^10.5|^11.5",
"symfony/css-selector": "^5.4",
"symfony/dom-crawler": "^5.4"
},
@@ -3412,12 +2492,12 @@
"type": "library",
"extra": {
"laravel": {
- "providers": [
- "Galahad\\Aire\\Support\\AireServiceProvider"
- ],
"aliases": {
"Aire": "Galahad\\Aire\\Support\\Facades\\Aire"
- }
+ },
+ "providers": [
+ "Galahad\\Aire\\Support\\AireServiceProvider"
+ ]
}
},
"autoload": {
@@ -3444,9 +2524,9 @@
],
"support": {
"issues": "https://github.com/glhd/aire/issues",
- "source": "https://github.com/glhd/aire/tree/2.13.0"
+ "source": "https://github.com/glhd/aire/tree/2.14.0"
},
- "time": "2024-09-18T15:17:14+00:00"
+ "time": "2025-03-04T14:40:38+00:00"
},
{
"name": "glhd/aire-bootstrap",
@@ -3513,26 +2593,26 @@
},
{
"name": "glhd/conveyor-belt",
- "version": "2.1.0",
+ "version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/glhd/conveyor-belt.git",
- "reference": "471c97b359fbff00259ba88ad4de98ae9ce364ee"
+ "reference": "76f959546b0e4ca18ee46fc171157cf955f20817"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/glhd/conveyor-belt/zipball/471c97b359fbff00259ba88ad4de98ae9ce364ee",
- "reference": "471c97b359fbff00259ba88ad4de98ae9ce364ee",
+ "url": "https://api.github.com/repos/glhd/conveyor-belt/zipball/76f959546b0e4ca18ee46fc171157cf955f20817",
+ "reference": "76f959546b0e4ca18ee46fc171157cf955f20817",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^7.0",
"halaxa/json-machine": "^1.0",
- "illuminate/collections": "^8|^9|^10|^11|12.x-dev|dev-master",
- "illuminate/console": "^8|^9|^10|^11|12.x-dev|dev-master",
- "illuminate/http": "^8|^9|^10|^11|12.x-dev|dev-master",
- "illuminate/support": "^8|^9|^10|^11|12.x-dev|dev-master",
+ "illuminate/collections": "^8|^9|^10|^11|^12|13.x-dev|dev-master|dev-main",
+ "illuminate/console": "^8|^9|^10|^11|^12|13.x-dev|dev-master|dev-main",
+ "illuminate/http": "^8|^9|^10|^11|^12|13.x-dev|dev-master|dev-main",
+ "illuminate/support": "^8|^9|^10|^11|^12|13.x-dev|dev-master|dev-main",
"jdorn/sql-formatter": "^1.2",
"openspout/openspout": "^4.0",
"php": ">= 8.0",
@@ -3540,10 +2620,9 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
- "glhd/laravel-dumper": "^1.0",
"mockery/mockery": "^1.3",
- "orchestra/testbench": "^6.24|^7.10|^8|^9|9.x-dev|10.x-dev|dev-master",
- "phpunit/phpunit": "^9.5|^10.5"
+ "orchestra/testbench": "^6.24|^7.10|^8.33|^9.11|^10.0|11.x-dev|dev-master|dev-main",
+ "phpunit/phpunit": "^10.5|^11.5"
},
"type": "library",
"extra": {
@@ -3573,9 +2652,9 @@
],
"support": {
"issues": "https://github.com/glhd/conveyor-belt/issues",
- "source": "https://github.com/glhd/conveyor-belt/tree/2.1.0"
+ "source": "https://github.com/glhd/conveyor-belt/tree/2.2.0"
},
- "time": "2024-03-12T20:00:23+00:00"
+ "time": "2025-03-04T19:00:43+00:00"
},
{
"name": "graham-campbell/result-type",
@@ -3641,22 +2720,22 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.9.3",
+ "version": "7.10.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77"
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
- "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
- "guzzlehttp/psr7": "^2.7.0",
+ "guzzlehttp/promises": "^2.3",
+ "guzzlehttp/psr7": "^2.8",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@@ -3747,7 +2826,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.9.3"
+ "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
},
"funding": [
{
@@ -3763,20 +2842,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T13:37:11+00:00"
+ "time": "2025-08-23T22:36:01+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "2.2.0",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
- "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957",
"shasum": ""
},
"require": {
@@ -3784,7 +2863,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"type": "library",
"extra": {
@@ -3830,7 +2909,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.2.0"
+ "source": "https://github.com/guzzle/promises/tree/2.3.0"
},
"funding": [
{
@@ -3846,20 +2925,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T13:27:01+00:00"
+ "time": "2025-08-22T14:34:08+00:00"
},
{
"name": "guzzlehttp/psr7",
- "version": "2.7.1",
+ "version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
+ "reference": "21dc724a0583619cd1652f673303492272778051"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
- "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
+ "reference": "21dc724a0583619cd1652f673303492272778051",
"shasum": ""
},
"require": {
@@ -3875,7 +2954,7 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@@ -3946,7 +3025,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.7.1"
+ "source": "https://github.com/guzzle/psr7/tree/2.8.0"
},
"funding": [
{
@@ -3962,20 +3041,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T12:30:47+00:00"
+ "time": "2025-08-23T21:21:41+00:00"
},
{
"name": "guzzlehttp/uri-template",
- "version": "v1.0.4",
+ "version": "v1.0.5",
"source": {
"type": "git",
"url": "https://github.com/guzzle/uri-template.git",
- "reference": "30e286560c137526eccd4ce21b2de477ab0676d2"
+ "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2",
- "reference": "30e286560c137526eccd4ce21b2de477ab0676d2",
+ "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1",
+ "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1",
"shasum": ""
},
"require": {
@@ -3984,7 +3063,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.36 || ^9.6.15",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25",
"uri-template/tests": "1.0.0"
},
"type": "library",
@@ -4032,7 +3111,7 @@
],
"support": {
"issues": "https://github.com/guzzle/uri-template/issues",
- "source": "https://github.com/guzzle/uri-template/tree/v1.0.4"
+ "source": "https://github.com/guzzle/uri-template/tree/v1.0.5"
},
"funding": [
{
@@ -4048,32 +3127,66 @@
"type": "tidelift"
}
],
- "time": "2025-02-03T10:55:03+00:00"
+ "time": "2025-08-22T14:27:06+00:00"
+ },
+ {
+ "name": "hack-greenville/api",
+ "version": "1.0",
+ "dist": {
+ "type": "path",
+ "url": "app-modules/api",
+ "reference": "e24852b45c718b7a944e29d395d2c012535be91b"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "HackGreenville\\Api\\Providers\\ApiServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "HackGreenville\\Api\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "HackGreenville\\Api\\Tests\\": "tests/"
+ }
+ },
+ "license": [
+ "proprietary"
+ ],
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
},
{
- "name": "hack-greenville/api",
+ "name": "hack-greenville/event-importer",
"version": "1.0",
"dist": {
"type": "path",
- "url": "app-modules/api",
- "reference": "e24852b45c718b7a944e29d395d2c012535be91b"
+ "url": "app-modules/event-importer",
+ "reference": "9fa84244a6c83c248d47faa46b23e39c153e99a6"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
- "HackGreenville\\Api\\Providers\\ApiServiceProvider"
+ "HackGreenville\\EventImporter\\Providers\\EventImporterServiceProvider"
]
}
},
"autoload": {
"psr-4": {
- "HackGreenville\\Api\\": "src/"
+ "HackGreenville\\EventImporter\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
- "HackGreenville\\Api\\Tests\\": "tests/"
+ "HackGreenville\\EventImporter\\Tests\\": "tests/"
}
},
"license": [
@@ -4085,34 +3198,36 @@
}
},
{
- "name": "hack-greenville/event-importer",
+ "name": "hack-greenville/slack-events-bot",
"version": "1.0",
"dist": {
"type": "path",
- "url": "app-modules/event-importer",
- "reference": "9fa84244a6c83c248d47faa46b23e39c153e99a6"
+ "url": "app-modules/slack-events-bot",
+ "reference": "91a773ff075dc9917f87512e7ca55d7929ca7800"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
- "HackGreenville\\EventImporter\\Providers\\EventImporterServiceProvider"
+ "HackGreenville\\SlackEventsBot\\Providers\\SlackEventsBotServiceProvider"
]
}
},
"autoload": {
"psr-4": {
- "HackGreenville\\EventImporter\\": "src/"
+ "HackGreenville\\SlackEventsBot\\": "src/",
+ "HackGreenville\\SlackEventsBot\\Database\\Factories\\": "./database/factories/"
}
},
"autoload-dev": {
"psr-4": {
- "HackGreenville\\EventImporter\\Tests\\": "tests/"
+ "HackGreenville\\SlackEventsBot\\Tests\\": "tests/"
}
},
"license": [
- "proprietary"
+ "MIT"
],
+ "description": "Slack bot that relays information from HackGreenville Labs' Events API to Slack channels",
"transport-options": {
"symlink": true,
"relative": true
@@ -4120,20 +3235,20 @@
},
{
"name": "halaxa/json-machine",
- "version": "1.1.4",
+ "version": "1.2.5",
"source": {
"type": "git",
"url": "https://github.com/halaxa/json-machine.git",
- "reference": "5147f38f74d7ab3e27733e3f3bdabbd2fd28e3fa"
+ "reference": "d0f84abf79ac98145d478b66d2bcf363d706477c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/halaxa/json-machine/zipball/5147f38f74d7ab3e27733e3f3bdabbd2fd28e3fa",
- "reference": "5147f38f74d7ab3e27733e3f3bdabbd2fd28e3fa",
+ "url": "https://api.github.com/repos/halaxa/json-machine/zipball/d0f84abf79ac98145d478b66d2bcf363d706477c",
+ "reference": "d0f84abf79ac98145d478b66d2bcf363d706477c",
"shasum": ""
},
"require": {
- "php": "7.0 - 8.3"
+ "php": "7.2 - 8.4"
},
"require-dev": {
"ext-json": "*",
@@ -4147,6 +3262,9 @@
},
"type": "library",
"autoload": {
+ "files": [
+ "src/functions.php"
+ ],
"psr-4": {
"JsonMachine\\": "src/"
},
@@ -4167,7 +3285,7 @@
"description": "Efficient, easy-to-use and fast JSON pull parser",
"support": {
"issues": "https://github.com/halaxa/json-machine/issues",
- "source": "https://github.com/halaxa/json-machine/tree/1.1.4"
+ "source": "https://github.com/halaxa/json-machine/tree/1.2.5"
},
"funding": [
{
@@ -4175,27 +3293,27 @@
"type": "other"
}
],
- "time": "2023-11-28T21:12:40+00:00"
+ "time": "2025-07-07T13:38:34+00:00"
},
{
"name": "internachi/modular",
- "version": "2.2.0",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/InterNACHI/modular.git",
- "reference": "d50168908d1500f995b595ea56da8f010c53c5e0"
+ "reference": "e7ff4074001d3df50d9ce877385c55d88e254484"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/InterNACHI/modular/zipball/d50168908d1500f995b595ea56da8f010c53c5e0",
- "reference": "d50168908d1500f995b595ea56da8f010c53c5e0",
+ "url": "https://api.github.com/repos/InterNACHI/modular/zipball/e7ff4074001d3df50d9ce877385c55d88e254484",
+ "reference": "e7ff4074001d3df50d9ce877385c55d88e254484",
"shasum": ""
},
"require": {
"composer/composer": "^2.1",
"ext-dom": "*",
"ext-simplexml": "*",
- "illuminate/support": "^9|^10|^11|12.x-dev|dev-master",
+ "illuminate/support": "^9|^10|^11|^12|13.x-dev|dev-master|dev-main",
"php": ">=8.0"
},
"require-dev": {
@@ -4203,20 +3321,20 @@
"friendsofphp/php-cs-fixer": "^3.14",
"livewire/livewire": "^2.5|^3.0",
"mockery/mockery": "^1.5",
- "orchestra/testbench": ">=7.10|dev-master",
- "phpunit/phpunit": "^9.5|^10.5"
+ "orchestra/testbench": "^7.52|^8.33|^9.11|^10.0|dev-master|dev-main",
+ "phpunit/phpunit": "^9.5|^10.5|^11.5"
},
"type": "library",
"extra": {
"laravel": {
+ "aliases": {
+ "Modules": "InterNACHI\\Modular\\Support\\Facades\\Modules"
+ },
"providers": [
"InterNACHI\\Modular\\Support\\ModularServiceProvider",
"InterNACHI\\Modular\\Support\\ModularizedCommandsServiceProvider",
"InterNACHI\\Modular\\Support\\ModularEventServiceProvider"
- ],
- "aliases": {
- "Modules": "InterNACHI\\Modular\\Support\\Facades\\Modules"
- }
+ ]
}
},
"autoload": {
@@ -4243,9 +3361,9 @@
],
"support": {
"issues": "https://github.com/InterNACHI/modular/issues",
- "source": "https://github.com/InterNACHI/modular/tree/2.2.0"
+ "source": "https://github.com/InterNACHI/modular/tree/2.3.0"
},
- "time": "2024-04-06T00:59:47+00:00"
+ "time": "2025-02-28T22:15:39+00:00"
},
{
"name": "jdorn/sql-formatter",
@@ -4303,30 +3421,40 @@
},
{
"name": "justinrainbow/json-schema",
- "version": "5.3.0",
+ "version": "6.6.0",
"source": {
"type": "git",
"url": "https://github.com/jsonrainbow/json-schema.git",
- "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8"
+ "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8",
- "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8",
+ "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/68ba7677532803cc0c5900dd5a4d730537f2b2f3",
+ "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "ext-json": "*",
+ "marc-mabe/php-enum": "^4.0",
+ "php": "^7.2 || ^8.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
- "json-schema/json-schema-test-suite": "1.2.0",
- "phpunit/phpunit": "^4.8.35"
+ "friendsofphp/php-cs-fixer": "3.3.0",
+ "json-schema/json-schema-test-suite": "^23.2",
+ "marc-mabe/php-enum-phpstan": "^2.0",
+ "phpspec/prophecy": "^1.19",
+ "phpstan/phpstan": "^1.12",
+ "phpunit/phpunit": "^8.5"
},
"bin": [
"bin/validate-json"
],
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "6.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
"JsonSchema\\": "src/JsonSchema/"
@@ -4355,74 +3483,16 @@
}
],
"description": "A library to validate a json schema.",
- "homepage": "https://github.com/justinrainbow/json-schema",
+ "homepage": "https://github.com/jsonrainbow/json-schema",
"keywords": [
"json",
"schema"
],
"support": {
"issues": "https://github.com/jsonrainbow/json-schema/issues",
- "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0"
- },
- "time": "2024-07-06T21:00:26+00:00"
- },
- {
- "name": "kelunik/certificate",
- "version": "v1.1.3",
- "source": {
- "type": "git",
- "url": "https://github.com/kelunik/certificate.git",
- "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e",
- "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e",
- "shasum": ""
- },
- "require": {
- "ext-openssl": "*",
- "php": ">=7.0"
- },
- "require-dev": {
- "amphp/php-cs-fixer-config": "^2",
- "phpunit/phpunit": "^6 | 7 | ^8 | ^9"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.x-dev"
- }
+ "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.0"
},
- "autoload": {
- "psr-4": {
- "Kelunik\\Certificate\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "Access certificate details and transform between different formats.",
- "keywords": [
- "DER",
- "certificate",
- "certificates",
- "openssl",
- "pem",
- "x509"
- ],
- "support": {
- "issues": "https://github.com/kelunik/certificate/issues",
- "source": "https://github.com/kelunik/certificate/tree/v1.1.3"
- },
- "time": "2023-02-03T21:26:53+00:00"
+ "time": "2025-10-10T11:34:09+00:00"
},
{
"name": "kirschbaum-development/eloquent-power-joins",
@@ -4489,16 +3559,16 @@
},
{
"name": "knuckleswtf/scribe",
- "version": "5.2.1",
+ "version": "5.3.0",
"source": {
"type": "git",
"url": "https://github.com/knuckleswtf/scribe.git",
- "reference": "7d1866bfccc96559b753466afdc1f70ed6c6125e"
+ "reference": "380f9cd6de6d65d2e92930a8613d8a682e83e8c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/knuckleswtf/scribe/zipball/7d1866bfccc96559b753466afdc1f70ed6c6125e",
- "reference": "7d1866bfccc96559b753466afdc1f70ed6c6125e",
+ "url": "https://api.github.com/repos/knuckleswtf/scribe/zipball/380f9cd6de6d65d2e92930a8613d8a682e83e8c7",
+ "reference": "380f9cd6de6d65d2e92930a8613d8a682e83e8c7",
"shasum": ""
},
"require": {
@@ -4571,7 +3641,7 @@
],
"support": {
"issues": "https://github.com/knuckleswtf/scribe/issues",
- "source": "https://github.com/knuckleswtf/scribe/tree/5.2.1"
+ "source": "https://github.com/knuckleswtf/scribe/tree/5.3.0"
},
"funding": [
{
@@ -4579,20 +3649,20 @@
"type": "patreon"
}
],
- "time": "2025-05-01T01:14:54+00:00"
+ "time": "2025-07-28T22:27:25+00:00"
},
{
"name": "laravel/framework",
- "version": "v10.48.29",
+ "version": "v10.49.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "8f7f9247cb8aad1a769d6b9815a6623d89b46b47"
+ "reference": "f857267b80789327cd3e6b077bcf6df5846cf71b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/8f7f9247cb8aad1a769d6b9815a6623d89b46b47",
- "reference": "8f7f9247cb8aad1a769d6b9815a6623d89b46b47",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/f857267b80789327cd3e6b077bcf6df5846cf71b",
+ "reference": "f857267b80789327cd3e6b077bcf6df5846cf71b",
"shasum": ""
},
"require": {
@@ -4751,6 +3821,7 @@
},
"autoload": {
"files": [
+ "src/Illuminate/Collections/functions.php",
"src/Illuminate/Collections/helpers.php",
"src/Illuminate/Events/functions.php",
"src/Illuminate/Filesystem/functions.php",
@@ -4786,24 +3857,24 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-03-12T14:42:01+00:00"
+ "time": "2025-09-30T14:56:54+00:00"
},
{
"name": "laravel/helpers",
- "version": "v1.7.0",
+ "version": "v1.8.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/helpers.git",
- "reference": "6caaa242a23bc39b4e3cf57304b5409260a7a346"
+ "reference": "d0094b4bc4364560c8ee3a9e956596d760d4afab"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/helpers/zipball/6caaa242a23bc39b4e3cf57304b5409260a7a346",
- "reference": "6caaa242a23bc39b4e3cf57304b5409260a7a346",
+ "url": "https://api.github.com/repos/laravel/helpers/zipball/d0094b4bc4364560c8ee3a9e956596d760d4afab",
+ "reference": "d0094b4bc4364560c8ee3a9e956596d760d4afab",
"shasum": ""
},
"require": {
- "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
+ "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.2.0|^8.0"
},
"require-dev": {
@@ -4841,9 +3912,9 @@
"laravel"
],
"support": {
- "source": "https://github.com/laravel/helpers/tree/v1.7.0"
+ "source": "https://github.com/laravel/helpers/tree/v1.8.1"
},
- "time": "2023-11-30T14:09:05+00:00"
+ "time": "2025-09-02T15:31:25+00:00"
},
{
"name": "laravel/prompts",
@@ -4989,13 +4060,13 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "2.x-dev"
- },
"laravel": {
"providers": [
"Illuminate\\Notifications\\SlackChannelServiceProvider"
]
+ },
+ "branch-alias": {
+ "dev-master": "2.x-dev"
}
},
"autoload": {
@@ -5027,22 +4098,22 @@
},
{
"name": "laravel/tinker",
- "version": "v2.10.0",
+ "version": "v2.10.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/tinker.git",
- "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5"
+ "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/tinker/zipball/ba4d51eb56de7711b3a37d63aa0643e99a339ae5",
- "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5",
+ "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3",
+ "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3",
"shasum": ""
},
"require": {
- "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
- "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
- "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
+ "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.2.5|^8.0",
"psy/psysh": "^0.11.1|^0.12.0",
"symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0"
@@ -5050,10 +4121,10 @@
"require-dev": {
"mockery/mockery": "~1.3.3|^1.4.2",
"phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^8.5.8|^9.3.3"
+ "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0"
},
"suggest": {
- "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0)."
+ "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)."
},
"type": "library",
"extra": {
@@ -5087,9 +4158,9 @@
],
"support": {
"issues": "https://github.com/laravel/tinker/issues",
- "source": "https://github.com/laravel/tinker/tree/v2.10.0"
+ "source": "https://github.com/laravel/tinker/tree/v2.10.1"
},
- "time": "2024-09-23T13:32:56+00:00"
+ "time": "2025-01-27T14:24:01+00:00"
},
{
"name": "league/commonmark",
@@ -5282,16 +4353,16 @@
},
{
"name": "league/csv",
- "version": "9.24.1",
+ "version": "9.26.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
- "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8"
+ "reference": "7fce732754d043f3938899e5183e2d0f3d31b571"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8",
- "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8",
+ "url": "https://api.github.com/repos/thephpleague/csv/zipball/7fce732754d043f3938899e5183e2d0f3d31b571",
+ "reference": "7fce732754d043f3938899e5183e2d0f3d31b571",
"shasum": ""
},
"require": {
@@ -5307,7 +4378,7 @@
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"phpstan/phpstan-strict-rules": "^1.6.2",
- "phpunit/phpunit": "^10.5.16 || ^11.5.22",
+ "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.3.6",
"symfony/var-dumper": "^6.4.8 || ^7.3.0"
},
"suggest": {
@@ -5369,7 +4440,7 @@
"type": "github"
}
],
- "time": "2025-06-25T14:53:51+00:00"
+ "time": "2025-10-01T11:24:54+00:00"
},
{
"name": "league/flysystem",
@@ -5832,12 +4903,12 @@
"type": "library",
"extra": {
"laravel": {
- "providers": [
- "Malzariey\\FilamentDaterangepickerFilter\\FilamentDaterangepickerFilterServiceProvider"
- ],
"aliases": {
"FilamentDaterangepickerFilter": "Malzariey\\FilamentDaterangepickerFilter\\Facades\\FilamentDaterangepickerFilter"
- }
+ },
+ "providers": [
+ "Malzariey\\FilamentDaterangepickerFilter\\FilamentDaterangepickerFilterServiceProvider"
+ ]
}
},
"autoload": {
@@ -5876,18 +4947,91 @@
],
"time": "2024-07-01T22:18:22+00:00"
},
+ {
+ "name": "marc-mabe/php-enum",
+ "version": "v4.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/marc-mabe/php-enum.git",
+ "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef",
+ "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef",
+ "shasum": ""
+ },
+ "require": {
+ "ext-reflection": "*",
+ "php": "^7.1 | ^8.0"
+ },
+ "require-dev": {
+ "phpbench/phpbench": "^0.16.10 || ^1.0.4",
+ "phpstan/phpstan": "^1.3.1",
+ "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11",
+ "vimeo/psalm": "^4.17.0 | ^5.26.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-3.x": "3.2-dev",
+ "dev-master": "4.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "MabeEnum\\": "src/"
+ },
+ "classmap": [
+ "stubs/Stringable.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Marc Bennewitz",
+ "email": "dev@mabe.berlin",
+ "homepage": "https://mabe.berlin/",
+ "role": "Lead"
+ }
+ ],
+ "description": "Simple and fast implementation of enumerations with native PHP",
+ "homepage": "https://github.com/marc-mabe/php-enum",
+ "keywords": [
+ "enum",
+ "enum-map",
+ "enum-set",
+ "enumeration",
+ "enumerator",
+ "enummap",
+ "enumset",
+ "map",
+ "set",
+ "type",
+ "type-hint",
+ "typehint"
+ ],
+ "support": {
+ "issues": "https://github.com/marc-mabe/php-enum/issues",
+ "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2"
+ },
+ "time": "2025-09-14T11:18:39+00:00"
+ },
{
"name": "masterminds/html5",
- "version": "2.9.0",
+ "version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
- "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6"
+ "reference": "fcf91eb64359852f00d921887b219479b4f21251"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
- "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
+ "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
+ "reference": "fcf91eb64359852f00d921887b219479b4f21251",
"shasum": ""
},
"require": {
@@ -5939,9 +5083,9 @@
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
- "source": "https://github.com/Masterminds/html5-php/tree/2.9.0"
+ "source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
},
- "time": "2024-03-31T07:05:07+00:00"
+ "time": "2025-07-25T09:04:22+00:00"
},
{
"name": "monolog/monolog",
@@ -6270,29 +5414,29 @@
},
{
"name": "nette/utils",
- "version": "v4.0.7",
+ "version": "v4.0.8",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
- "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2"
+ "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2",
- "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2",
+ "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
+ "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"shasum": ""
},
"require": {
- "php": "8.0 - 8.4"
+ "php": "8.0 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
- "jetbrains/phpstorm-attributes": "dev-master",
+ "jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
- "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@@ -6310,6 +5454,9 @@
}
},
"autoload": {
+ "psr-4": {
+ "Nette\\": "src"
+ },
"classmap": [
"src/"
]
@@ -6350,22 +5497,22 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
- "source": "https://github.com/nette/utils/tree/v4.0.7"
+ "source": "https://github.com/nette/utils/tree/v4.0.8"
},
- "time": "2025-06-03T04:55:08+00:00"
+ "time": "2025-08-06T21:43:34+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v5.3.1",
+ "version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
- "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
"shasum": ""
},
"require": {
@@ -6384,7 +5531,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-master": "5.x-dev"
}
},
"autoload": {
@@ -6408,46 +5555,46 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1"
},
- "time": "2024-10-08T18:51:32+00:00"
+ "time": "2025-08-13T20:13:15+00:00"
},
{
"name": "nunomaduro/collision",
- "version": "v7.11.0",
+ "version": "v7.12.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
- "reference": "994ea93df5d4132f69d3f1bd74730509df6e8a05"
+ "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/collision/zipball/994ea93df5d4132f69d3f1bd74730509df6e8a05",
- "reference": "994ea93df5d4132f69d3f1bd74730509df6e8a05",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/995245421d3d7593a6960822063bdba4f5d7cf1a",
+ "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a",
"shasum": ""
},
"require": {
- "filp/whoops": "^2.16.0",
- "nunomaduro/termwind": "^1.15.1",
+ "filp/whoops": "^2.17.0",
+ "nunomaduro/termwind": "^1.17.0",
"php": "^8.1.0",
- "symfony/console": "^6.4.12"
+ "symfony/console": "^6.4.17"
},
"conflict": {
"laravel/framework": ">=11.0.0"
},
"require-dev": {
- "brianium/paratest": "^7.3.1",
- "laravel/framework": "^10.48.22",
- "laravel/pint": "^1.18.1",
- "laravel/sail": "^1.36.0",
+ "brianium/paratest": "^7.4.8",
+ "laravel/framework": "^10.48.29",
+ "laravel/pint": "^1.21.2",
+ "laravel/sail": "^1.41.0",
"laravel/sanctum": "^3.3.3",
- "laravel/tinker": "^2.10.0",
- "nunomaduro/larastan": "^2.9.8",
- "orchestra/testbench-core": "^8.28.3",
- "pestphp/pest": "^2.35.1",
+ "laravel/tinker": "^2.10.1",
+ "nunomaduro/larastan": "^2.10.0",
+ "orchestra/testbench-core": "^8.35.0",
+ "pestphp/pest": "^2.36.0",
"phpunit/phpunit": "^10.5.36",
"sebastian/environment": "^6.1.0",
- "spatie/laravel-ignition": "^2.8.0"
+ "spatie/laravel-ignition": "^2.9.1"
},
"type": "library",
"extra": {
@@ -6506,7 +5653,7 @@
"type": "patreon"
}
],
- "time": "2024-10-15T15:12:40+00:00"
+ "time": "2025-03-14T22:35:49+00:00"
},
{
"name": "nunomaduro/termwind",
@@ -6595,16 +5742,16 @@
},
{
"name": "openspout/openspout",
- "version": "v4.25.0",
+ "version": "v4.32.0",
"source": {
"type": "git",
"url": "https://github.com/openspout/openspout.git",
- "reference": "519affe730d92e1598720a6467227fc28550f0e6"
+ "reference": "41f045c1f632e1474e15d4c7bc3abcb4a153563d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/openspout/openspout/zipball/519affe730d92e1598720a6467227fc28550f0e6",
- "reference": "519affe730d92e1598720a6467227fc28550f0e6",
+ "url": "https://api.github.com/repos/openspout/openspout/zipball/41f045c1f632e1474e15d4c7bc3abcb4a153563d",
+ "reference": "41f045c1f632e1474e15d4c7bc3abcb4a153563d",
"shasum": ""
},
"require": {
@@ -6614,17 +5761,17 @@
"ext-libxml": "*",
"ext-xmlreader": "*",
"ext-zip": "*",
- "php": "~8.1.0 || ~8.2.0 || ~8.3.0"
+ "php": "~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"ext-zlib": "*",
- "friendsofphp/php-cs-fixer": "^3.64.0",
- "infection/infection": "^0.29.6",
- "phpbench/phpbench": "^1.3.1",
- "phpstan/phpstan": "^1.12.4",
- "phpstan/phpstan-phpunit": "^1.4.0",
- "phpstan/phpstan-strict-rules": "^1.6.1",
- "phpunit/phpunit": "^10.5.20 || ^11.3.6"
+ "friendsofphp/php-cs-fixer": "^3.86.0",
+ "infection/infection": "^0.31.2",
+ "phpbench/phpbench": "^1.4.1",
+ "phpstan/phpstan": "^2.1.22",
+ "phpstan/phpstan-phpunit": "^2.0.7",
+ "phpstan/phpstan-strict-rules": "^2.0.6",
+ "phpunit/phpunit": "^12.3.7"
},
"suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
@@ -6672,7 +5819,7 @@
],
"support": {
"issues": "https://github.com/openspout/openspout/issues",
- "source": "https://github.com/openspout/openspout/tree/v4.25.0"
+ "source": "https://github.com/openspout/openspout/tree/v4.32.0"
},
"funding": [
{
@@ -6684,25 +5831,26 @@
"type": "github"
}
],
- "time": "2024-09-24T09:03:42+00:00"
+ "time": "2025-09-03T16:03:54+00:00"
},
{
"name": "phpdocumentor/reflection",
- "version": "6.0.0",
+ "version": "6.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/Reflection.git",
- "reference": "61e2f1fe7683e9647b9ed8d9e53d08699385267d"
+ "reference": "d91b3270832785602adcc24ae2d0974ba99a8ff8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/61e2f1fe7683e9647b9ed8d9e53d08699385267d",
- "reference": "61e2f1fe7683e9647b9ed8d9e53d08699385267d",
+ "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/d91b3270832785602adcc24ae2d0974ba99a8ff8",
+ "reference": "d91b3270832785602adcc24ae2d0974ba99a8ff8",
"shasum": ""
},
"require": {
+ "composer-runtime-api": "^2",
"nikic/php-parser": "~4.18 || ^5.0",
- "php": "8.1.*|8.2.*|8.3.*",
+ "php": "8.1.*|8.2.*|8.3.*|8.4.*",
"phpdocumentor/reflection-common": "^2.1",
"phpdocumentor/reflection-docblock": "^5",
"phpdocumentor/type-resolver": "^1.2",
@@ -6711,7 +5859,8 @@
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
- "doctrine/coding-standard": "^12.0",
+ "doctrine/coding-standard": "^13.0",
+ "eliashaeussler/phpunit-attributes": "^1.7",
"mikey179/vfsstream": "~1.2",
"mockery/mockery": "~1.6.0",
"phpspec/prophecy-phpunit": "^2.0",
@@ -6719,7 +5868,7 @@
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-webmozart-assert": "^1.2",
"phpunit/phpunit": "^10.0",
- "psalm/phar": "^5.24",
+ "psalm/phar": "^6.0",
"rector/rector": "^1.0.0",
"squizlabs/php_codesniffer": "^3.8"
},
@@ -6731,6 +5880,9 @@
}
},
"autoload": {
+ "files": [
+ "src/php-parser/Modifiers.php"
+ ],
"psr-4": {
"phpDocumentor\\": "src/phpDocumentor"
}
@@ -6749,9 +5901,9 @@
],
"support": {
"issues": "https://github.com/phpDocumentor/Reflection/issues",
- "source": "https://github.com/phpDocumentor/Reflection/tree/6.0.0"
+ "source": "https://github.com/phpDocumentor/Reflection/tree/6.3.0"
},
- "time": "2024-05-23T19:28:12+00:00"
+ "time": "2025-06-06T13:39:18+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@@ -6808,16 +5960,16 @@
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "5.5.1",
+ "version": "5.6.3",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "0c70d2c566e899666f367ab7b80986beb3581e6f"
+ "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/0c70d2c566e899666f367ab7b80986beb3581e6f",
- "reference": "0c70d2c566e899666f367ab7b80986beb3581e6f",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9",
+ "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9",
"shasum": ""
},
"require": {
@@ -6826,7 +5978,7 @@
"php": "^7.4 || ^8.0",
"phpdocumentor/reflection-common": "^2.2",
"phpdocumentor/type-resolver": "^1.7",
- "phpstan/phpdoc-parser": "^1.7",
+ "phpstan/phpdoc-parser": "^1.7|^2.0",
"webmozart/assert": "^1.9.1"
},
"require-dev": {
@@ -6866,29 +6018,29 @@
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
- "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.5.1"
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3"
},
- "time": "2024-11-06T11:58:54+00:00"
+ "time": "2025-08-01T19:43:32+00:00"
},
{
"name": "phpdocumentor/type-resolver",
- "version": "1.9.0",
+ "version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "1fb5ba8d045f5dd984ebded5b1cc66f29459422d"
+ "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/1fb5ba8d045f5dd984ebded5b1cc66f29459422d",
- "reference": "1fb5ba8d045f5dd984ebded5b1cc66f29459422d",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a",
+ "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^1.0",
"php": "^7.3 || ^8.0",
"phpdocumentor/reflection-common": "^2.0",
- "phpstan/phpdoc-parser": "^1.18"
+ "phpstan/phpdoc-parser": "^1.18|^2.0"
},
"require-dev": {
"ext-tokenizer": "*",
@@ -6924,22 +6076,22 @@
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
- "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.9.0"
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0"
},
- "time": "2024-11-03T20:11:34+00:00"
+ "time": "2024-11-09T15:12:26+00:00"
},
{
"name": "phpoption/phpoption",
- "version": "1.9.3",
+ "version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"shasum": ""
},
"require": {
@@ -6947,7 +6099,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
@@ -6989,7 +6141,7 @@
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
- "source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
},
"funding": [
{
@@ -7001,34 +6153,34 @@
"type": "tidelift"
}
],
- "time": "2024-07-20T21:41:07+00:00"
+ "time": "2025-08-21T11:53:16+00:00"
},
{
"name": "phpstan/phpdoc-parser",
- "version": "1.33.0",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140"
+ "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140",
- "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495",
+ "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495",
"shasum": ""
},
"require": {
- "php": "^7.2 || ^8.0"
+ "php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/annotations": "^2.0",
- "nikic/php-parser": "^4.15",
+ "nikic/php-parser": "^5.3.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "^1.5",
- "phpstan/phpstan-phpunit": "^1.1",
- "phpstan/phpstan-strict-rules": "^1.0",
- "phpunit/phpunit": "^9.5",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
"symfony/process": "^5.2"
},
"type": "library",
@@ -7046,9 +6198,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
- "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0"
},
- "time": "2024-10-13T11:25:22+00:00"
+ "time": "2025-08-30T15:50:23+00:00"
},
{
"name": "psr/cache",
@@ -7513,16 +6665,16 @@
},
{
"name": "psy/psysh",
- "version": "v0.12.4",
+ "version": "v0.12.12",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
- "reference": "2fd717afa05341b4f8152547f142cd2f130f6818"
+ "reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818",
- "reference": "2fd717afa05341b4f8152547f142cd2f130f6818",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/cd23863404a40ccfaf733e3af4db2b459837f7e7",
+ "reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7",
"shasum": ""
},
"require": {
@@ -7549,12 +6701,12 @@
],
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "0.12.x-dev"
- },
"bamarni-bin": {
"bin-links": false,
"forward-command": false
+ },
+ "branch-alias": {
+ "dev-main": "0.12.x-dev"
}
},
"autoload": {
@@ -7572,12 +6724,11 @@
"authors": [
{
"name": "Justin Hileman",
- "email": "justin@justinhileman.info",
- "homepage": "http://justinhileman.com"
+ "email": "justin@justinhileman.info"
}
],
"description": "An interactive shell for modern PHP.",
- "homepage": "http://psysh.org",
+ "homepage": "https://psysh.org",
"keywords": [
"REPL",
"console",
@@ -7586,9 +6737,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
- "source": "https://github.com/bobthecow/psysh/tree/v0.12.4"
+ "source": "https://github.com/bobthecow/psysh/tree/v0.12.12"
},
- "time": "2024-06-10T01:18:23+00:00"
+ "time": "2025-09-20T13:46:31+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -7712,20 +6863,20 @@
},
{
"name": "ramsey/uuid",
- "version": "4.9.0",
+ "version": "4.9.1",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
- "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0"
+ "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0",
- "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
+ "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"shasum": ""
},
"require": {
- "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13",
+ "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -7784,29 +6935,29 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
- "source": "https://github.com/ramsey/uuid/tree/4.9.0"
+ "source": "https://github.com/ramsey/uuid/tree/4.9.1"
},
- "time": "2025-06-25T14:20:11+00:00"
+ "time": "2025-09-04T20:59:21+00:00"
},
{
"name": "react/promise",
- "version": "v3.2.0",
+ "version": "v3.3.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
- "reference": "8a164643313c71354582dc850b42b33fa12a4b63"
+ "reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63",
- "reference": "8a164643313c71354582dc850b42b33fa12a4b63",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
+ "reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
"shasum": ""
},
"require": {
"php": ">=7.1.0"
},
"require-dev": {
- "phpstan/phpstan": "1.10.39 || 1.4.10",
+ "phpstan/phpstan": "1.12.28 || 1.4.10",
"phpunit/phpunit": "^9.6 || ^7.5"
},
"type": "library",
@@ -7851,7 +7002,7 @@
],
"support": {
"issues": "https://github.com/reactphp/promise/issues",
- "source": "https://github.com/reactphp/promise/tree/v3.2.0"
+ "source": "https://github.com/reactphp/promise/tree/v3.3.0"
},
"funding": [
{
@@ -7859,79 +7010,7 @@
"type": "open_collective"
}
],
- "time": "2024-05-24T10:39:05+00:00"
- },
- {
- "name": "revolt/event-loop",
- "version": "v1.0.6",
- "source": {
- "type": "git",
- "url": "https://github.com/revoltphp/event-loop.git",
- "reference": "25de49af7223ba039f64da4ae9a28ec2d10d0254"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/25de49af7223ba039f64da4ae9a28ec2d10d0254",
- "reference": "25de49af7223ba039f64da4ae9a28ec2d10d0254",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1"
- },
- "require-dev": {
- "ext-json": "*",
- "jetbrains/phpstorm-stubs": "^2019.3",
- "phpunit/phpunit": "^9",
- "psalm/phar": "^5.15"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Revolt\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Aaron Piotrowski",
- "email": "aaron@trowski.com"
- },
- {
- "name": "Cees-Jan Kiewiet",
- "email": "ceesjank@gmail.com"
- },
- {
- "name": "Christian Lück",
- "email": "christian@clue.engineering"
- },
- {
- "name": "Niklas Keller",
- "email": "me@kelunik.com"
- }
- ],
- "description": "Rock-solid event loop for concurrent PHP applications.",
- "keywords": [
- "async",
- "asynchronous",
- "concurrency",
- "event",
- "event-loop",
- "non-blocking",
- "scheduler"
- ],
- "support": {
- "issues": "https://github.com/revoltphp/event-loop/issues",
- "source": "https://github.com/revoltphp/event-loop/tree/v1.0.6"
- },
- "time": "2023-11-30T05:34:44+00:00"
+ "time": "2025-08-19T18:57:03+00:00"
},
{
"name": "ryangjchandler/blade-capture-directive",
@@ -8013,34 +7092,34 @@
},
{
"name": "scyllaly/hcaptcha",
- "version": "4.4.7",
+ "version": "4.4.9",
"source": {
"type": "git",
"url": "https://github.com/Scyllaly/hcaptcha.git",
- "reference": "f5d3d669a5a21e63457e868effbfb44099c3f8fd"
+ "reference": "9cf702906770a39191167d7eb9a2358b81d10b02"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Scyllaly/hcaptcha/zipball/f5d3d669a5a21e63457e868effbfb44099c3f8fd",
- "reference": "f5d3d669a5a21e63457e868effbfb44099c3f8fd",
+ "url": "https://api.github.com/repos/Scyllaly/hcaptcha/zipball/9cf702906770a39191167d7eb9a2358b81d10b02",
+ "reference": "9cf702906770a39191167d7eb9a2358b81d10b02",
"shasum": ""
},
"require": {
- "illuminate/support": "5.*|6.*|7.*|8.*|^9.0|10.*|^11.0",
+ "illuminate/support": "5.*|6.*|7.*|8.*|^9.0|10.*|^11.0|^12.0",
"php": ">=5.5.5"
},
"require-dev": {
- "phpunit/phpunit": "~4.8|^9.5.10|^10.0"
+ "phpunit/phpunit": "~4.8|^9.5.10|^10.0|^11.0"
},
"type": "library",
"extra": {
"laravel": {
- "providers": [
- "Scyllaly\\HCaptcha\\HCaptchaServiceProvider"
- ],
"aliases": {
"HCaptcha": "Scyllaly\\HCaptcha\\Facades\\HCaptcha"
- }
+ },
+ "providers": [
+ "Scyllaly\\HCaptcha\\HCaptchaServiceProvider"
+ ]
}
},
"autoload": {
@@ -8066,9 +7145,9 @@
],
"support": {
"issues": "https://github.com/Scyllaly/hcaptcha/issues",
- "source": "https://github.com/Scyllaly/hcaptcha/tree/4.4.7"
+ "source": "https://github.com/Scyllaly/hcaptcha/tree/4.4.9"
},
- "time": "2024-08-31T06:44:47+00:00"
+ "time": "2025-04-10T07:27:15+00:00"
},
{
"name": "seld/jsonlint",
@@ -8549,16 +7628,16 @@
},
{
"name": "spatie/icalendar-generator",
- "version": "2.9.1",
+ "version": "2.9.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/icalendar-generator.git",
- "reference": "21b8d259156368812a6d55096499f289b265be4e"
+ "reference": "6fa4d5b20490afeebe711f6cad1733853c667aa2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/icalendar-generator/zipball/21b8d259156368812a6d55096499f289b265be4e",
- "reference": "21b8d259156368812a6d55096499f289b265be4e",
+ "url": "https://api.github.com/repos/spatie/icalendar-generator/zipball/6fa4d5b20490afeebe711f6cad1733853c667aa2",
+ "reference": "6fa4d5b20490afeebe711f6cad1733853c667aa2",
"shasum": ""
},
"require": {
@@ -8602,9 +7681,9 @@
],
"support": {
"issues": "https://github.com/spatie/icalendar-generator/issues",
- "source": "https://github.com/spatie/icalendar-generator/tree/2.9.1"
+ "source": "https://github.com/spatie/icalendar-generator/tree/2.9.2"
},
- "time": "2025-01-31T13:50:13+00:00"
+ "time": "2025-03-21T09:01:17+00:00"
},
{
"name": "spatie/invade",
@@ -8667,20 +7746,20 @@
},
{
"name": "spatie/laravel-data",
- "version": "4.11.1",
+ "version": "4.17.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-data.git",
- "reference": "df5b58baebae34475ca35338b4e9a131c9e2a8e0"
+ "reference": "6ec15bb6798128f01aecb67dcd18a937251a27a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-data/zipball/df5b58baebae34475ca35338b4e9a131c9e2a8e0",
- "reference": "df5b58baebae34475ca35338b4e9a131c9e2a8e0",
+ "url": "https://api.github.com/repos/spatie/laravel-data/zipball/6ec15bb6798128f01aecb67dcd18a937251a27a5",
+ "reference": "6ec15bb6798128f01aecb67dcd18a937251a27a5",
"shasum": ""
},
"require": {
- "illuminate/contracts": "^10.0|^11.0",
+ "illuminate/contracts": "^10.0|^11.0|^12.0",
"php": "^8.1",
"phpdocumentor/reflection": "^6.0",
"spatie/laravel-package-tools": "^1.9.0",
@@ -8689,18 +7768,17 @@
"require-dev": {
"fakerphp/faker": "^1.14",
"friendsofphp/php-cs-fixer": "^3.0",
- "inertiajs/inertia-laravel": "^1.2",
+ "inertiajs/inertia-laravel": "^2.0",
"livewire/livewire": "^3.0",
"mockery/mockery": "^1.6",
- "nesbot/carbon": "^2.63",
- "nunomaduro/larastan": "^2.0",
- "orchestra/testbench": "^8.0|^9.0",
- "pestphp/pest": "^2.31",
- "pestphp/pest-plugin-laravel": "^2.0",
- "pestphp/pest-plugin-livewire": "^2.1",
+ "nesbot/carbon": "^2.63|^3.0",
+ "orchestra/testbench": "^8.0|^9.0|^10.0",
+ "pestphp/pest": "^2.31|^3.0",
+ "pestphp/pest-plugin-laravel": "^2.0|^3.0",
+ "pestphp/pest-plugin-livewire": "^2.1|^3.0",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
- "phpunit/phpunit": "^10.0",
+ "phpunit/phpunit": "^10.0|^11.0|^12.0",
"spatie/invade": "^1.0",
"spatie/laravel-typescript-transformer": "^2.5",
"spatie/pest-plugin-snapshots": "^2.1",
@@ -8739,7 +7817,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-data/issues",
- "source": "https://github.com/spatie/laravel-data/tree/4.11.1"
+ "source": "https://github.com/spatie/laravel-data/tree/4.17.1"
},
"funding": [
{
@@ -8747,7 +7825,7 @@
"type": "github"
}
],
- "time": "2024-10-23T07:14:53+00:00"
+ "time": "2025-09-04T08:30:23+00:00"
},
{
"name": "spatie/laravel-package-tools",
@@ -8812,40 +7890,41 @@
},
{
"name": "spatie/php-structure-discoverer",
- "version": "2.2.0",
+ "version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/php-structure-discoverer.git",
- "reference": "271542206169d95dd2ffe346ddf11f37672553a2"
+ "reference": "6c46e069349c7f2f6ebbe00429332c9e6b70fa92"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/271542206169d95dd2ffe346ddf11f37672553a2",
- "reference": "271542206169d95dd2ffe346ddf11f37672553a2",
+ "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/6c46e069349c7f2f6ebbe00429332c9e6b70fa92",
+ "reference": "6c46e069349c7f2f6ebbe00429332c9e6b70fa92",
"shasum": ""
},
"require": {
- "amphp/amp": "^v3.0",
- "amphp/parallel": "^2.2",
- "illuminate/collections": "^10.0|^11.0",
+ "illuminate/collections": "^10.0|^11.0|^12.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.4.3",
"symfony/finder": "^6.0|^7.0"
},
"require-dev": {
- "illuminate/console": "^10.0|^11.0",
+ "amphp/parallel": "^2.2",
+ "illuminate/console": "^10.0|^11.0|^12.0",
"laravel/pint": "^1.0",
"nunomaduro/collision": "^7.0|^8.0",
- "nunomaduro/larastan": "^2.0.1",
- "orchestra/testbench": "^7.0|^8.0|^9.0",
- "pestphp/pest": "^2.0",
- "pestphp/pest-plugin-laravel": "^2.0",
+ "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
+ "pestphp/pest": "^2.0|^3.0",
+ "pestphp/pest-plugin-laravel": "^2.0|^3.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
- "phpunit/phpunit": "^9.5|^10.0",
+ "phpunit/phpunit": "^9.5|^10.0|^11.5.3",
"spatie/laravel-ray": "^1.26"
},
+ "suggest": {
+ "amphp/parallel": "When you want to use the Parallel discover worker"
+ },
"type": "library",
"extra": {
"laravel": {
@@ -8880,7 +7959,7 @@
],
"support": {
"issues": "https://github.com/spatie/php-structure-discoverer/issues",
- "source": "https://github.com/spatie/php-structure-discoverer/tree/2.2.0"
+ "source": "https://github.com/spatie/php-structure-discoverer/tree/2.3.2"
},
"funding": [
{
@@ -8888,20 +7967,20 @@
"type": "github"
}
],
- "time": "2024-08-29T10:43:45+00:00"
+ "time": "2025-09-22T14:58:17+00:00"
},
{
"name": "symfony/console",
- "version": "v6.4.23",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93"
+ "reference": "492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/9056771b8eca08d026cd3280deeec3cfd99c4d93",
- "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93",
+ "url": "https://api.github.com/repos/symfony/console/zipball/492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f",
+ "reference": "492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f",
"shasum": ""
},
"require": {
@@ -8966,7 +8045,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v6.4.23"
+ "source": "https://github.com/symfony/console/tree/v6.4.26"
},
"funding": [
{
@@ -8977,29 +8056,33 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-06-27T19:37:22+00:00"
+ "time": "2025-09-26T12:13:46+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v6.4.13",
+ "version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e"
+ "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/cb23e97813c5837a041b73a6d63a9ddff0778f5e",
- "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2",
+ "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "php": ">=8.2"
},
"type": "library",
"autoload": {
@@ -9031,7 +8114,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v6.4.13"
+ "source": "https://github.com/symfony/css-selector/tree/v7.3.0"
},
"funding": [
{
@@ -9047,7 +8130,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:18:03+00:00"
+ "time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -9118,16 +8201,16 @@
},
{
"name": "symfony/error-handler",
- "version": "v6.4.23",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "b088e0b175c30b4e06d8085200fa465b586f44fa"
+ "reference": "41bedcaec5b72640b0ec2096547b75fda72ead6c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/b088e0b175c30b4e06d8085200fa465b586f44fa",
- "reference": "b088e0b175c30b4e06d8085200fa465b586f44fa",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/41bedcaec5b72640b0ec2096547b75fda72ead6c",
+ "reference": "41bedcaec5b72640b0ec2096547b75fda72ead6c",
"shasum": ""
},
"require": {
@@ -9173,7 +8256,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v6.4.23"
+ "source": "https://github.com/symfony/error-handler/tree/v6.4.26"
},
"funding": [
{
@@ -9184,33 +8267,37 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-06-13T07:39:48+00:00"
+ "time": "2025-09-11T09:57:09+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v6.4.13",
+ "version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e"
+ "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e",
- "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191",
+ "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191",
"shasum": ""
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"symfony/event-dispatcher-contracts": "^2.5|^3"
},
"conflict": {
- "symfony/dependency-injection": "<5.4",
+ "symfony/dependency-injection": "<6.4",
"symfony/service-contracts": "<2.5"
},
"provide": {
@@ -9219,13 +8306,13 @@
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/config": "^5.4|^6.0|^7.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0",
- "symfony/error-handler": "^5.4|^6.0|^7.0",
- "symfony/expression-language": "^5.4|^6.0|^7.0",
- "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/config": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/error-handler": "^6.4|^7.0",
+ "symfony/expression-language": "^6.4|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/stopwatch": "^5.4|^6.0|^7.0"
+ "symfony/stopwatch": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -9253,7 +8340,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3"
},
"funding": [
{
@@ -9264,12 +8351,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-25T14:18:03+00:00"
+ "time": "2025-08-13T11:49:31+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -9349,25 +8440,25 @@
},
{
"name": "symfony/filesystem",
- "version": "v6.4.13",
+ "version": "v7.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3"
+ "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3",
- "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd",
+ "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd",
"shasum": ""
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
- "symfony/process": "^5.4|^6.4|^7.0"
+ "symfony/process": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -9395,7 +8486,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v6.4.13"
+ "source": "https://github.com/symfony/filesystem/tree/v7.3.2"
},
"funding": [
{
@@ -9406,25 +8497,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-10-25T15:07:50+00:00"
+ "time": "2025-07-07T08:17:47+00:00"
},
{
"name": "symfony/finder",
- "version": "v6.4.17",
+ "version": "v6.4.24",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7"
+ "reference": "73089124388c8510efb8d2d1689285d285937b08"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7",
- "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08",
+ "reference": "73089124388c8510efb8d2d1689285d285937b08",
"shasum": ""
},
"require": {
@@ -9459,7 +8554,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v6.4.17"
+ "source": "https://github.com/symfony/finder/tree/v6.4.24"
},
"funding": [
{
@@ -9470,32 +8565,36 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-12-29T13:51:37+00:00"
+ "time": "2025-07-15T12:02:45+00:00"
},
{
"name": "symfony/html-sanitizer",
- "version": "v6.4.21",
+ "version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/html-sanitizer.git",
- "reference": "f66d6585c6ece946239317c339f8b2860dfdf2db"
+ "reference": "8740fc48979f649dee8b8fc51a2698e5c190bf12"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/f66d6585c6ece946239317c339f8b2860dfdf2db",
- "reference": "f66d6585c6ece946239317c339f8b2860dfdf2db",
+ "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/8740fc48979f649dee8b8fc51a2698e5c190bf12",
+ "reference": "8740fc48979f649dee8b8fc51a2698e5c190bf12",
"shasum": ""
},
"require": {
"ext-dom": "*",
"league/uri": "^6.5|^7.0",
"masterminds/html5": "^2.7.2",
- "php": ">=8.1"
+ "php": ">=8.2"
},
"type": "library",
"autoload": {
@@ -9528,7 +8627,7 @@
"sanitizer"
],
"support": {
- "source": "https://github.com/symfony/html-sanitizer/tree/v6.4.21"
+ "source": "https://github.com/symfony/html-sanitizer/tree/v7.3.3"
},
"funding": [
{
@@ -9539,25 +8638,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-31T07:29:45+00:00"
+ "time": "2025-08-12T10:34:03+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v6.4.23",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "452d19f945ee41345fd8a50c18b60783546b7bd3"
+ "reference": "369241591d92bb5dfb4c6ccd6ee94378a45b1521"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/452d19f945ee41345fd8a50c18b60783546b7bd3",
- "reference": "452d19f945ee41345fd8a50c18b60783546b7bd3",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/369241591d92bb5dfb4c6ccd6ee94378a45b1521",
+ "reference": "369241591d92bb5dfb4c6ccd6ee94378a45b1521",
"shasum": ""
},
"require": {
@@ -9605,7 +8708,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v6.4.23"
+ "source": "https://github.com/symfony/http-foundation/tree/v6.4.26"
},
"funding": [
{
@@ -9616,25 +8719,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-26T09:17:58+00:00"
+ "time": "2025-09-16T08:22:30+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v6.4.23",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "2bb2cba685aabd859f22cf6946554e8e7f3c329a"
+ "reference": "8b0f963293aede77593c9845c8c0af34752e893a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2bb2cba685aabd859f22cf6946554e8e7f3c329a",
- "reference": "2bb2cba685aabd859f22cf6946554e8e7f3c329a",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/8b0f963293aede77593c9845c8c0af34752e893a",
+ "reference": "8b0f963293aede77593c9845c8c0af34752e893a",
"shasum": ""
},
"require": {
@@ -9719,7 +8826,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v6.4.23"
+ "source": "https://github.com/symfony/http-kernel/tree/v6.4.26"
},
"funding": [
{
@@ -9730,25 +8837,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-06-28T08:14:51+00:00"
+ "time": "2025-09-27T12:20:56+00:00"
},
{
"name": "symfony/mailer",
- "version": "v6.4.23",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
- "reference": "a480322ddf8e54de262c9bca31fdcbe26b553de5"
+ "reference": "012185cd31689b799d39505bd706be6d3a57cd3f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mailer/zipball/a480322ddf8e54de262c9bca31fdcbe26b553de5",
- "reference": "a480322ddf8e54de262c9bca31fdcbe26b553de5",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/012185cd31689b799d39505bd706be6d3a57cd3f",
+ "reference": "012185cd31689b799d39505bd706be6d3a57cd3f",
"shasum": ""
},
"require": {
@@ -9799,7 +8910,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/mailer/tree/v6.4.23"
+ "source": "https://github.com/symfony/mailer/tree/v6.4.26"
},
"funding": [
{
@@ -9810,25 +8921,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-06-26T21:24:02+00:00"
+ "time": "2025-09-11T09:57:09+00:00"
},
{
"name": "symfony/mime",
- "version": "v6.4.21",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "fec8aa5231f3904754955fad33c2db50594d22d1"
+ "reference": "61ab9681cdfe315071eb4fa79b6ad6ab030a9235"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/fec8aa5231f3904754955fad33c2db50594d22d1",
- "reference": "fec8aa5231f3904754955fad33c2db50594d22d1",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/61ab9681cdfe315071eb4fa79b6ad6ab030a9235",
+ "reference": "61ab9681cdfe315071eb4fa79b6ad6ab030a9235",
"shasum": ""
},
"require": {
@@ -9884,7 +8999,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v6.4.21"
+ "source": "https://github.com/symfony/mime/tree/v6.4.26"
},
"funding": [
{
@@ -9895,16 +9010,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-27T13:27:38+00:00"
+ "time": "2025-09-16T08:22:30+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -9963,7 +9082,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@@ -9974,6 +9093,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -9983,16 +9106,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": ""
},
"require": {
@@ -10041,7 +9164,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
},
"funding": [
{
@@ -10052,16 +9175,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -10124,7 +9251,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
},
"funding": [
{
@@ -10135,6 +9262,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10144,7 +9275,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -10205,7 +9336,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@@ -10216,6 +9347,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10225,7 +9360,7 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -10286,7 +9421,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -10297,6 +9432,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10306,7 +9445,7 @@
},
{
"name": "symfony/polyfill-php73",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
@@ -10362,7 +9501,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0"
},
"funding": [
{
@@ -10373,6 +9512,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10382,7 +9525,7 @@
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
@@ -10442,7 +9585,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
@@ -10453,6 +9596,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10462,7 +9609,7 @@
},
{
"name": "symfony/polyfill-php81",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
@@ -10518,7 +9665,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
},
"funding": [
{
@@ -10529,6 +9676,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10538,16 +9689,16 @@
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
- "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"shasum": ""
},
"require": {
@@ -10594,7 +9745,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
},
"funding": [
{
@@ -10605,16 +9756,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-07-08T02:45:35+00:00"
},
{
"name": "symfony/polyfill-uuid",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
@@ -10673,7 +9828,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0"
},
"funding": [
{
@@ -10684,6 +9839,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10693,16 +9852,16 @@
},
{
"name": "symfony/process",
- "version": "v6.4.20",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20"
+ "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20",
- "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20",
+ "url": "https://api.github.com/repos/symfony/process/zipball/48bad913268c8cafabbf7034b39c8bb24fbc5ab8",
+ "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8",
"shasum": ""
},
"require": {
@@ -10734,7 +9893,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v6.4.20"
+ "source": "https://github.com/symfony/process/tree/v6.4.26"
},
"funding": [
{
@@ -10745,25 +9904,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-10T17:11:00+00:00"
+ "time": "2025-09-11T09:57:09+00:00"
},
{
"name": "symfony/routing",
- "version": "v6.4.22",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "1f5234e8457164a3a0038a4c0a4ba27876a9c670"
+ "reference": "6fc4c445f22857d4b8b40a02b73f423ddab295de"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/1f5234e8457164a3a0038a4c0a4ba27876a9c670",
- "reference": "1f5234e8457164a3a0038a4c0a4ba27876a9c670",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/6fc4c445f22857d4b8b40a02b73f423ddab295de",
+ "reference": "6fc4c445f22857d4b8b40a02b73f423ddab295de",
"shasum": ""
},
"require": {
@@ -10817,7 +9980,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v6.4.22"
+ "source": "https://github.com/symfony/routing/tree/v6.4.26"
},
"funding": [
{
@@ -10828,12 +9991,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-27T16:08:38+00:00"
+ "time": "2025-09-11T09:57:09+00:00"
},
{
"name": "symfony/service-contracts",
@@ -10920,20 +10087,20 @@
},
{
"name": "symfony/string",
- "version": "v6.4.21",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "73e2c6966a5aef1d4892873ed5322245295370c6"
+ "reference": "f96476035142921000338bad71e5247fbc138872"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6",
- "reference": "73e2c6966a5aef1d4892873ed5322245295370c6",
+ "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
+ "reference": "f96476035142921000338bad71e5247fbc138872",
"shasum": ""
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
@@ -10943,11 +10110,11 @@
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
- "symfony/error-handler": "^5.4|^6.0|^7.0",
- "symfony/http-client": "^5.4|^6.0|^7.0",
- "symfony/intl": "^6.2|^7.0",
+ "symfony/emoji": "^7.1",
+ "symfony/http-client": "^6.4|^7.0",
+ "symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
- "symfony/var-exporter": "^5.4|^6.0|^7.0"
+ "symfony/var-exporter": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -10986,7 +10153,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v6.4.21"
+ "source": "https://github.com/symfony/string/tree/v7.3.4"
},
"funding": [
{
@@ -10997,25 +10164,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-18T15:23:29+00:00"
+ "time": "2025-09-11T14:36:48+00:00"
},
{
"name": "symfony/translation",
- "version": "v6.4.23",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "de8afa521e04a5220e9e58a1dc99971ab7cac643"
+ "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/de8afa521e04a5220e9e58a1dc99971ab7cac643",
- "reference": "de8afa521e04a5220e9e58a1dc99971ab7cac643",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/c8559fe25c7ee7aa9d28f228903a46db008156a4",
+ "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4",
"shasum": ""
},
"require": {
@@ -11081,7 +10252,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v6.4.23"
+ "source": "https://github.com/symfony/translation/tree/v6.4.26"
},
"funding": [
{
@@ -11092,12 +10263,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-06-26T21:24:02+00:00"
+ "time": "2025-09-05T18:17:25+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -11179,16 +10354,16 @@
},
{
"name": "symfony/uid",
- "version": "v6.4.23",
+ "version": "v6.4.24",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
- "reference": "9c8592da78d7ee6af52011eef593350d87e814c0"
+ "reference": "17da16a750541a42cf2183935e0f6008316c23f7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/uid/zipball/9c8592da78d7ee6af52011eef593350d87e814c0",
- "reference": "9c8592da78d7ee6af52011eef593350d87e814c0",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/17da16a750541a42cf2183935e0f6008316c23f7",
+ "reference": "17da16a750541a42cf2183935e0f6008316c23f7",
"shasum": ""
},
"require": {
@@ -11233,7 +10408,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/uid/tree/v6.4.23"
+ "source": "https://github.com/symfony/uid/tree/v6.4.24"
},
"funding": [
{
@@ -11244,25 +10419,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-06-26T08:06:12+00:00"
+ "time": "2025-07-10T08:14:14+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v6.4.23",
+ "version": "v6.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600"
+ "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600",
- "reference": "d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfae1497a2f1eaad78dbc0590311c599c7178d4a",
+ "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a",
"shasum": ""
},
"require": {
@@ -11274,7 +10453,6 @@
"symfony/console": "<5.4"
},
"require-dev": {
- "ext-iconv": "*",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/error-handler": "^6.3|^7.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0",
@@ -11318,7 +10496,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v6.4.23"
+ "source": "https://github.com/symfony/var-dumper/tree/v6.4.26"
},
"funding": [
{
@@ -11329,35 +10507,39 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-06-27T15:05:27+00:00"
+ "time": "2025-09-25T15:37:27+00:00"
},
{
"name": "symfony/var-exporter",
- "version": "v6.4.22",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
- "reference": "f28cf841f5654955c9f88ceaf4b9dc29571988a9"
+ "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-exporter/zipball/f28cf841f5654955c9f88ceaf4b9dc29571988a9",
- "reference": "f28cf841f5654955c9f88ceaf4b9dc29571988a9",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4",
+ "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4",
"shasum": ""
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
"symfony/property-access": "^6.4|^7.0",
"symfony/serializer": "^6.4|^7.0",
- "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ "symfony/var-dumper": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -11395,7 +10577,7 @@
"serialize"
],
"support": {
- "source": "https://github.com/symfony/var-exporter/tree/v6.4.22"
+ "source": "https://github.com/symfony/var-exporter/tree/v7.3.4"
},
"funding": [
{
@@ -11406,37 +10588,41 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-14T13:00:13+00:00"
+ "time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/yaml",
- "version": "v6.4.13",
+ "version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9"
+ "reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/e99b4e94d124b29ee4cf3140e1b537d2dad8cec9",
- "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
+ "reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
"shasum": ""
},
"require": {
- "php": ">=8.1",
- "symfony/deprecation-contracts": "^2.5|^3",
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
- "symfony/console": "<5.4"
+ "symfony/console": "<6.4"
},
"require-dev": {
- "symfony/console": "^5.4|^6.0|^7.0"
+ "symfony/console": "^6.4|^7.0"
},
"bin": [
"Resources/bin/yaml-lint"
@@ -11467,7 +10653,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v6.4.13"
+ "source": "https://github.com/symfony/yaml/tree/v7.3.3"
},
"funding": [
{
@@ -11478,12 +10664,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-25T14:18:03+00:00"
+ "time": "2025-08-27T11:34:33+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@@ -11760,44 +10950,44 @@
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
- "version": "v3.14.6",
+ "version": "v3.16.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
- "reference": "14e4517bd49130d6119228107eb21ae47ae120ab"
+ "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/14e4517bd49130d6119228107eb21ae47ae120ab",
- "reference": "14e4517bd49130d6119228107eb21ae47ae120ab",
+ "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f265cf5e38577d42311f1a90d619bcd3740bea23",
+ "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23",
"shasum": ""
},
"require": {
- "illuminate/routing": "^9|^10|^11",
- "illuminate/session": "^9|^10|^11",
- "illuminate/support": "^9|^10|^11",
- "maximebf/debugbar": "~1.23.0",
- "php": "^8.0",
+ "illuminate/routing": "^9|^10|^11|^12",
+ "illuminate/session": "^9|^10|^11|^12",
+ "illuminate/support": "^9|^10|^11|^12",
+ "php": "^8.1",
+ "php-debugbar/php-debugbar": "~2.2.0",
"symfony/finder": "^6|^7"
},
"require-dev": {
"mockery/mockery": "^1.3.3",
- "orchestra/testbench-dusk": "^5|^6|^7|^8|^9",
- "phpunit/phpunit": "^9.6|^10.5",
+ "orchestra/testbench-dusk": "^7|^8|^9|^10",
+ "phpunit/phpunit": "^9.5.10|^10|^11",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "3.14-dev"
- },
"laravel": {
- "providers": [
- "Barryvdh\\Debugbar\\ServiceProvider"
- ],
"aliases": {
"Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar"
- }
+ },
+ "providers": [
+ "Barryvdh\\Debugbar\\ServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "3.16-dev"
}
},
"autoload": {
@@ -11822,13 +11012,14 @@
"keywords": [
"debug",
"debugbar",
+ "dev",
"laravel",
"profiler",
"webprofiler"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-debugbar/issues",
- "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.6"
+ "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.0"
},
"funding": [
{
@@ -11840,7 +11031,7 @@
"type": "github"
}
],
- "time": "2024-10-18T13:15:12+00:00"
+ "time": "2025-07-14T11:56:43+00:00"
},
{
"name": "barryvdh/laravel-ide-helper",
@@ -11884,13 +11075,13 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "3.1-dev"
- },
"laravel": {
"providers": [
"Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider"
]
+ },
+ "branch-alias": {
+ "dev-master": "3.1-dev"
}
},
"autoload": {
@@ -11938,20 +11129,20 @@
},
{
"name": "barryvdh/reflection-docblock",
- "version": "v2.1.3",
+ "version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/ReflectionDocBlock.git",
- "reference": "c6fad15f7c878be21650c51e1f841bca7e49752e"
+ "reference": "d103774cbe7e94ddee7e4870f97f727b43fe7201"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/c6fad15f7c878be21650c51e1f841bca7e49752e",
- "reference": "c6fad15f7c878be21650c51e1f841bca7e49752e",
+ "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/d103774cbe7e94ddee7e4870f97f727b43fe7201",
+ "reference": "d103774cbe7e94ddee7e4870f97f727b43fe7201",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^8.5.14|^9"
@@ -11963,7 +11154,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "2.3.x-dev"
}
},
"autoload": {
@@ -11984,9 +11175,9 @@
}
],
"support": {
- "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.1.3"
+ "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.4.0"
},
- "time": "2024-10-23T11:41:03+00:00"
+ "time": "2025-07-17T06:07:30+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@@ -12041,16 +11232,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.19.0",
+ "version": "v1.25.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0"
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/8169513746e1bac70c85d6ea1524d9225d4886f0",
- "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
"shasum": ""
},
"require": {
@@ -12058,15 +11249,15 @@
"ext-mbstring": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
- "php": "^8.1.0"
+ "php": "^8.2.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.66.0",
- "illuminate/view": "^10.48.25",
- "larastan/larastan": "^2.9.12",
- "laravel-zero/framework": "^10.48.25",
+ "friendsofphp/php-cs-fixer": "^3.87.2",
+ "illuminate/view": "^11.46.0",
+ "larastan/larastan": "^3.7.1",
+ "laravel-zero/framework": "^11.45.0",
"mockery/mockery": "^1.6.12",
- "nunomaduro/termwind": "^1.17.0",
+ "nunomaduro/termwind": "^2.3.1",
"pestphp/pest": "^2.36.0"
},
"bin": [
@@ -12103,32 +11294,32 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2024-12-30T16:20:10+00:00"
+ "time": "2025-09-19T02:57:12+00:00"
},
{
"name": "laravel/sail",
- "version": "v1.37.1",
+ "version": "v1.46.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
- "reference": "7efa151ea0d16f48233d6a6cd69f81270acc6e93"
+ "reference": "eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sail/zipball/7efa151ea0d16f48233d6a6cd69f81270acc6e93",
- "reference": "7efa151ea0d16f48233d6a6cd69f81270acc6e93",
+ "url": "https://api.github.com/repos/laravel/sail/zipball/eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e",
+ "reference": "eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e",
"shasum": ""
},
"require": {
- "illuminate/console": "^9.52.16|^10.0|^11.0",
- "illuminate/contracts": "^9.52.16|^10.0|^11.0",
- "illuminate/support": "^9.52.16|^10.0|^11.0",
+ "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0",
+ "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0",
"php": "^8.0",
"symfony/console": "^6.0|^7.0",
"symfony/yaml": "^6.0|^7.0"
},
"require-dev": {
- "orchestra/testbench": "^7.0|^8.0|^9.0",
+ "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"phpstan/phpstan": "^1.10"
},
"bin": [
@@ -12166,25 +11357,25 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
- "time": "2024-10-29T20:18:14+00:00"
+ "time": "2025-09-23T13:44:39+00:00"
},
{
"name": "laravel/telescope",
- "version": "v5.2.6",
+ "version": "v5.14.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/telescope.git",
- "reference": "7ee46fbea8e3b01108575c8edf7377abddfe8bb9"
+ "reference": "99e9487604e67424d07b83b5ab3401850d237685"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/telescope/zipball/7ee46fbea8e3b01108575c8edf7377abddfe8bb9",
- "reference": "7ee46fbea8e3b01108575c8edf7377abddfe8bb9",
+ "url": "https://api.github.com/repos/laravel/telescope/zipball/99e9487604e67424d07b83b5ab3401850d237685",
+ "reference": "99e9487604e67424d07b83b5ab3401850d237685",
"shasum": ""
},
"require": {
"ext-json": "*",
- "laravel/framework": "^8.37|^9.0|^10.0|^11.0",
+ "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0",
"php": "^8.0",
"symfony/console": "^5.3|^6.0|^7.0",
"symfony/var-dumper": "^5.0|^6.0|^7.0"
@@ -12193,9 +11384,9 @@
"ext-gd": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"laravel/octane": "^1.4|^2.0|dev-develop",
- "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0",
+ "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0|^10.0",
"phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^9.0|^10.5"
+ "phpunit/phpunit": "^9.0|^10.5|^11.5"
},
"type": "library",
"extra": {
@@ -12233,77 +11424,9 @@
],
"support": {
"issues": "https://github.com/laravel/telescope/issues",
- "source": "https://github.com/laravel/telescope/tree/v5.2.6"
- },
- "time": "2024-11-25T20:34:58+00:00"
- },
- {
- "name": "maximebf/debugbar",
- "version": "v1.23.3",
- "source": {
- "type": "git",
- "url": "https://github.com/maximebf/php-debugbar.git",
- "reference": "687400043d77943ef95e8417cb44e1673ee57844"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/687400043d77943ef95e8417cb44e1673ee57844",
- "reference": "687400043d77943ef95e8417cb44e1673ee57844",
- "shasum": ""
- },
- "require": {
- "php": "^7.2|^8",
- "psr/log": "^1|^2|^3",
- "symfony/var-dumper": "^4|^5|^6|^7"
- },
- "require-dev": {
- "dbrekelmans/bdi": "^1",
- "phpunit/phpunit": "^8|^9",
- "symfony/panther": "^1|^2.1",
- "twig/twig": "^1.38|^2.7|^3.0"
- },
- "suggest": {
- "kriswallsmith/assetic": "The best way to manage assets",
- "monolog/monolog": "Log using Monolog",
- "predis/predis": "Redis storage"
+ "source": "https://github.com/laravel/telescope/tree/v5.14.0"
},
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.23-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "DebugBar\\": "src/DebugBar/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Maxime Bouroumeau-Fuseau",
- "email": "maxime.bouroumeau@gmail.com",
- "homepage": "http://maximebf.com"
- },
- {
- "name": "Barry vd. Heuvel",
- "email": "barryvdh@gmail.com"
- }
- ],
- "description": "Debug bar in the browser for php application",
- "homepage": "https://github.com/maximebf/php-debugbar",
- "keywords": [
- "debug",
- "debugbar"
- ],
- "support": {
- "issues": "https://github.com/maximebf/php-debugbar/issues",
- "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.3"
- },
- "time": "2024-10-29T12:24:25+00:00"
+ "time": "2025-10-06T14:52:49+00:00"
},
{
"name": "mockery/mockery",
@@ -12390,16 +11513,16 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.12.0",
+ "version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
- "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
@@ -12438,7 +11561,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
@@ -12446,7 +11569,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-12T14:39:25+00:00"
+ "time": "2025-08-01T08:46:24+00:00"
},
{
"name": "phar-io/manifest",
@@ -12566,6 +11689,79 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
+ {
+ "name": "php-debugbar/php-debugbar",
+ "version": "v2.2.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-debugbar/php-debugbar.git",
+ "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/3146d04671f51f69ffec2a4207ac3bdcf13a9f35",
+ "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8",
+ "psr/log": "^1|^2|^3",
+ "symfony/var-dumper": "^4|^5|^6|^7"
+ },
+ "replace": {
+ "maximebf/debugbar": "self.version"
+ },
+ "require-dev": {
+ "dbrekelmans/bdi": "^1",
+ "phpunit/phpunit": "^8|^9",
+ "symfony/panther": "^1|^2.1",
+ "twig/twig": "^1.38|^2.7|^3.0"
+ },
+ "suggest": {
+ "kriswallsmith/assetic": "The best way to manage assets",
+ "monolog/monolog": "Log using Monolog",
+ "predis/predis": "Redis storage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "DebugBar\\": "src/DebugBar/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maxime Bouroumeau-Fuseau",
+ "email": "maxime.bouroumeau@gmail.com",
+ "homepage": "http://maximebf.com"
+ },
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "Debug bar in the browser for php application",
+ "homepage": "https://github.com/php-debugbar/php-debugbar",
+ "keywords": [
+ "debug",
+ "debug bar",
+ "debugbar",
+ "dev"
+ ],
+ "support": {
+ "issues": "https://github.com/php-debugbar/php-debugbar/issues",
+ "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.4"
+ },
+ "time": "2025-07-22T14:01:30+00:00"
+ },
{
"name": "phpunit/php-code-coverage",
"version": "10.1.16",
@@ -12889,16 +12085,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "10.5.38",
+ "version": "10.5.58",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132"
+ "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132",
- "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca",
+ "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca",
"shasum": ""
},
"require": {
@@ -12908,7 +12104,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.12.0",
+ "myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.1",
@@ -12919,13 +12115,13 @@
"phpunit/php-timer": "^6.0.0",
"sebastian/cli-parser": "^2.0.1",
"sebastian/code-unit": "^2.0.0",
- "sebastian/comparator": "^5.0.3",
+ "sebastian/comparator": "^5.0.4",
"sebastian/diff": "^5.1.1",
"sebastian/environment": "^6.1.0",
- "sebastian/exporter": "^5.1.2",
+ "sebastian/exporter": "^5.1.4",
"sebastian/global-state": "^6.0.2",
"sebastian/object-enumerator": "^5.0.0",
- "sebastian/recursion-context": "^5.0.0",
+ "sebastian/recursion-context": "^5.0.1",
"sebastian/type": "^4.0.0",
"sebastian/version": "^4.0.1"
},
@@ -12970,7 +12166,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.38"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58"
},
"funding": [
{
@@ -12981,12 +12177,20 @@
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
"type": "tidelift"
}
],
- "time": "2024-10-28T13:06:21+00:00"
+ "time": "2025-09-28T12:04:46+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -13158,16 +12362,16 @@
},
{
"name": "sebastian/comparator",
- "version": "5.0.3",
+ "version": "5.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e"
+ "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
- "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e",
+ "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e",
"shasum": ""
},
"require": {
@@ -13223,15 +12427,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
}
],
- "time": "2024-10-18T14:56:07+00:00"
+ "time": "2025-09-07T05:25:07+00:00"
},
{
"name": "sebastian/complexity",
@@ -13424,16 +12640,16 @@
},
{
"name": "sebastian/exporter",
- "version": "5.1.2",
+ "version": "5.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "955288482d97c19a372d3f31006ab3f37da47adf"
+ "reference": "0735b90f4da94969541dac1da743446e276defa6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf",
- "reference": "955288482d97c19a372d3f31006ab3f37da47adf",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6",
+ "reference": "0735b90f4da94969541dac1da743446e276defa6",
"shasum": ""
},
"require": {
@@ -13442,7 +12658,7 @@
"sebastian/recursion-context": "^5.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
@@ -13490,15 +12706,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
- "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
}
],
- "time": "2024-03-02T07:17:12+00:00"
+ "time": "2025-09-24T06:09:11+00:00"
},
{
"name": "sebastian/global-state",
@@ -13734,23 +12962,23 @@
},
{
"name": "sebastian/recursion-context",
- "version": "5.0.0",
+ "version": "5.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "05909fb5bc7df4c52992396d0116aed689f93712"
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712",
- "reference": "05909fb5bc7df4c52992396d0116aed689f93712",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a",
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
@@ -13785,15 +13013,28 @@
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0"
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
}
],
- "time": "2023-02-03T07:05:40+00:00"
+ "time": "2025-08-10T07:50:56+00:00"
},
{
"name": "sebastian/type",
@@ -13906,16 +13147,16 @@
},
{
"name": "spatie/backtrace",
- "version": "1.7.1",
+ "version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/backtrace.git",
- "reference": "0f2477c520e3729de58e061b8192f161c99f770b"
+ "reference": "8c0f16a59ae35ec8c62d85c3c17585158f430110"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/backtrace/zipball/0f2477c520e3729de58e061b8192f161c99f770b",
- "reference": "0f2477c520e3729de58e061b8192f161c99f770b",
+ "url": "https://api.github.com/repos/spatie/backtrace/zipball/8c0f16a59ae35ec8c62d85c3c17585158f430110",
+ "reference": "8c0f16a59ae35ec8c62d85c3c17585158f430110",
"shasum": ""
},
"require": {
@@ -13953,7 +13194,8 @@
"spatie"
],
"support": {
- "source": "https://github.com/spatie/backtrace/tree/1.7.1"
+ "issues": "https://github.com/spatie/backtrace/issues",
+ "source": "https://github.com/spatie/backtrace/tree/1.8.1"
},
"funding": [
{
@@ -13965,34 +13207,34 @@
"type": "other"
}
],
- "time": "2024-12-02T13:28:15+00:00"
+ "time": "2025-08-26T08:22:30+00:00"
},
{
"name": "spatie/error-solutions",
- "version": "1.1.1",
+ "version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/error-solutions.git",
- "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67"
+ "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/error-solutions/zipball/ae7393122eda72eed7cc4f176d1e96ea444f2d67",
- "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67",
+ "url": "https://api.github.com/repos/spatie/error-solutions/zipball/e495d7178ca524f2dd0fe6a1d99a1e608e1c9936",
+ "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
- "illuminate/broadcasting": "^10.0|^11.0",
- "illuminate/cache": "^10.0|^11.0",
- "illuminate/support": "^10.0|^11.0",
- "livewire/livewire": "^2.11|^3.3.5",
+ "illuminate/broadcasting": "^10.0|^11.0|^12.0",
+ "illuminate/cache": "^10.0|^11.0|^12.0",
+ "illuminate/support": "^10.0|^11.0|^12.0",
+ "livewire/livewire": "^2.11|^3.5.20",
"openai-php/client": "^0.10.1",
- "orchestra/testbench": "^7.0|8.22.3|^9.0",
- "pestphp/pest": "^2.20",
- "phpstan/phpstan": "^1.11",
+ "orchestra/testbench": "8.22.3|^9.0|^10.0",
+ "pestphp/pest": "^2.20|^3.0",
+ "phpstan/phpstan": "^2.1",
"psr/simple-cache": "^3.0",
"psr/simple-cache-implementation": "^3.0",
"spatie/ray": "^1.28",
@@ -14031,7 +13273,7 @@
],
"support": {
"issues": "https://github.com/spatie/error-solutions/issues",
- "source": "https://github.com/spatie/error-solutions/tree/1.1.1"
+ "source": "https://github.com/spatie/error-solutions/tree/1.1.3"
},
"funding": [
{
@@ -14039,24 +13281,24 @@
"type": "github"
}
],
- "time": "2024-07-25T11:06:04+00:00"
+ "time": "2025-02-14T12:29:50+00:00"
},
{
"name": "spatie/flare-client-php",
- "version": "1.10.0",
+ "version": "1.10.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/flare-client-php.git",
- "reference": "140a42b2c5d59ac4ecf8f5b493386a4f2eb28272"
+ "reference": "bf1716eb98bd689451b071548ae9e70738dce62f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/140a42b2c5d59ac4ecf8f5b493386a4f2eb28272",
- "reference": "140a42b2c5d59ac4ecf8f5b493386a4f2eb28272",
+ "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/bf1716eb98bd689451b071548ae9e70738dce62f",
+ "reference": "bf1716eb98bd689451b071548ae9e70738dce62f",
"shasum": ""
},
"require": {
- "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0",
+ "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^8.0",
"spatie/backtrace": "^1.6.1",
"symfony/http-foundation": "^5.2|^6.0|^7.0",
@@ -14100,7 +13342,7 @@
],
"support": {
"issues": "https://github.com/spatie/flare-client-php/issues",
- "source": "https://github.com/spatie/flare-client-php/tree/1.10.0"
+ "source": "https://github.com/spatie/flare-client-php/tree/1.10.1"
},
"funding": [
{
@@ -14108,20 +13350,20 @@
"type": "github"
}
],
- "time": "2024-12-02T14:30:06+00:00"
+ "time": "2025-02-14T13:42:06+00:00"
},
{
"name": "spatie/ignition",
- "version": "1.15.0",
+ "version": "1.15.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/ignition.git",
- "reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2"
+ "reference": "31f314153020aee5af3537e507fef892ffbf8c85"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/ignition/zipball/e3a68e137371e1eb9edc7f78ffa733f3b98991d2",
- "reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2",
+ "url": "https://api.github.com/repos/spatie/ignition/zipball/31f314153020aee5af3537e507fef892ffbf8c85",
+ "reference": "31f314153020aee5af3537e507fef892ffbf8c85",
"shasum": ""
},
"require": {
@@ -14134,7 +13376,7 @@
"symfony/var-dumper": "^5.4|^6.0|^7.0"
},
"require-dev": {
- "illuminate/cache": "^9.52|^10.0|^11.0",
+ "illuminate/cache": "^9.52|^10.0|^11.0|^12.0",
"mockery/mockery": "^1.4",
"pestphp/pest": "^1.20|^2.0",
"phpstan/extension-installer": "^1.1",
@@ -14191,27 +13433,27 @@
"type": "github"
}
],
- "time": "2024-06-12T14:55:22+00:00"
+ "time": "2025-02-21T14:31:39+00:00"
},
{
"name": "spatie/laravel-ignition",
- "version": "2.9.0",
+ "version": "2.9.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ignition.git",
- "reference": "62042df15314b829d0f26e02108f559018e2aad0"
+ "reference": "1baee07216d6748ebd3a65ba97381b051838707a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/62042df15314b829d0f26e02108f559018e2aad0",
- "reference": "62042df15314b829d0f26e02108f559018e2aad0",
+ "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/1baee07216d6748ebd3a65ba97381b051838707a",
+ "reference": "1baee07216d6748ebd3a65ba97381b051838707a",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
- "illuminate/support": "^10.0|^11.0",
+ "illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.1",
"spatie/ignition": "^1.15",
"symfony/console": "^6.2.3|^7.0",
@@ -14220,12 +13462,12 @@
"require-dev": {
"livewire/livewire": "^2.11|^3.3.5",
"mockery/mockery": "^1.5.1",
- "openai-php/client": "^0.8.1",
- "orchestra/testbench": "8.22.3|^9.0",
- "pestphp/pest": "^2.34",
+ "openai-php/client": "^0.8.1|^0.10",
+ "orchestra/testbench": "8.22.3|^9.0|^10.0",
+ "pestphp/pest": "^2.34|^3.7",
"phpstan/extension-installer": "^1.3.1",
- "phpstan/phpstan-deprecation-rules": "^1.1.1",
- "phpstan/phpstan-phpunit": "^1.3.16",
+ "phpstan/phpstan-deprecation-rules": "^1.1.1|^2.0",
+ "phpstan/phpstan-phpunit": "^1.3.16|^2.0",
"vlucas/phpdotenv": "^5.5"
},
"suggest": {
@@ -14282,7 +13524,7 @@
"type": "github"
}
],
- "time": "2024-12-02T08:43:31+00:00"
+ "time": "2025-02-20T13:13:55+00:00"
},
{
"name": "theseer/tokenizer",
@@ -14341,7 +13583,7 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
- "php": "^8.1",
+ "php": "^8.3",
"ext-json": "*"
},
"platform-dev": {
diff --git a/config/app.php b/config/app.php
index fac6cf91..6bb3c0d1 100644
--- a/config/app.php
+++ b/config/app.php
@@ -197,6 +197,12 @@
App\Providers\EventServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\RouteServiceProvider::class,
+
+
+ /*
+ * Slack Events Bot
+ */
+ HackGreenville\SlackEventsBot\Providers\SlackEventsBotServiceProvider::class,
],
/*
diff --git a/config/scribe.php b/config/scribe.php
index fc7fa5e1..f8df707a 100644
--- a/config/scribe.php
+++ b/config/scribe.php
@@ -125,7 +125,6 @@
'extra_info' => 'You can retrieve your token by visiting your dashboard and clicking Generate API token.',
],
-
// Example requests for each endpoint will be shown in each of these languages.
// Supported options are: bash, javascript, php, python
// To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests
diff --git a/database/migrations/2025_07_06_000000_create_slack_workspaces_table.php b/database/migrations/2025_07_06_000000_create_slack_workspaces_table.php
new file mode 100644
index 00000000..84f5760f
--- /dev/null
+++ b/database/migrations/2025_07_06_000000_create_slack_workspaces_table.php
@@ -0,0 +1,27 @@
+id();
+ $table->string('team_id')->unique();
+ $table->string('team_name');
+ $table->text('access_token');
+ $table->string('bot_user_id');
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('slack_workspaces');
+ }
+};
diff --git a/database/migrations/2025_07_06_112939_create_jobs_table.php b/database/migrations/2025_07_06_112939_create_jobs_table.php
new file mode 100644
index 00000000..7a5df54c
--- /dev/null
+++ b/database/migrations/2025_07_06_112939_create_jobs_table.php
@@ -0,0 +1,31 @@
+bigIncrements('id');
+ $table->string('queue')->index();
+ $table->longText('payload');
+ $table->unsignedTinyInteger('attempts');
+ $table->unsignedInteger('reserved_at')->nullable();
+ $table->unsignedInteger('available_at');
+ $table->unsignedInteger('created_at');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('jobs');
+ }
+};
diff --git a/database/migrations/2025_07_06_113105_create_failed_jobs_table.php b/database/migrations/2025_07_06_113105_create_failed_jobs_table.php
new file mode 100644
index 00000000..51aaa6f9
--- /dev/null
+++ b/database/migrations/2025_07_06_113105_create_failed_jobs_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('uuid')->unique();
+ $table->text('connection');
+ $table->text('queue');
+ $table->longText('payload');
+ $table->longText('exception');
+ $table->timestamp('failed_at')->useCurrent();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('failed_jobs');
+ }
+};
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
index cb639f7b..cb57108d 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -28,3 +28,7 @@ services:
extends:
file: docker-compose.yml
service: hackgreenville-redis
+ hackgreenville-queue:
+ extends:
+ file: docker-compose.yml
+ service: hackgreenville-queue
diff --git a/docker-compose.yml b/docker-compose.yml
index 969c0187..72e6c913 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,7 +4,7 @@ version: '3'
services:
hackgreenville:
build:
- context: './vendor/laravel/sail/runtimes/${PHP_RUNTIME:-8.1}'
+ context: './vendor/laravel/sail/runtimes/${PHP_RUNTIME:-8.3}'
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP:-www-data}'
@@ -47,3 +47,21 @@ services:
- './redis:/data'
healthcheck:
test: ["CMD", "redis-cli", "ping"]
+
+ hackgreenville-queue:
+ build:
+ context: './vendor/laravel/sail/runtimes/${PHP_RUNTIME:-8.3}'
+ dockerfile: Dockerfile
+ args:
+ WWWGROUP: '${WWWGROUP:-www-data}'
+ image: hackgreenville
+ container_name: hackgreenville-queue
+ env_file:
+ - .env
+ volumes:
+ - './:/var/www/html'
+ command: 'php artisan queue:work'
+ restart: always
+ depends_on:
+ - hackgreenville-db
+ - hackgreenville-redis
diff --git a/EVENTS_API.md b/docs/EVENTS_API.md
similarity index 100%
rename from EVENTS_API.md
rename to docs/EVENTS_API.md
diff --git a/ORGS_API.md b/docs/ORGS_API.md
similarity index 100%
rename from ORGS_API.md
rename to docs/ORGS_API.md
diff --git a/docs/SLACK_BOT_API.md b/docs/SLACK_BOT_API.md
new file mode 100644
index 00000000..91540c06
--- /dev/null
+++ b/docs/SLACK_BOT_API.md
@@ -0,0 +1,76 @@
+# Slack Events Bot Module
+A Laravel module that posts HackGreenville events from the database to configured Slack channels.
+
+## Installation
+This module is automatically loaded as it's in the `app-modules` directory.
+
+## Configuration
+More information regarding the configuration of the Slack bot API can be found in the readme located at `app-modules/slack-events-bot/README.md`
+
+## Running Migrations
+The migrations will run automatically with:
+```bash
+php artisan migrate
+```
+
+## Available Commands
+```bash
+# Manually check for events and update Slack messages
+php artisan slack:check-events
+# Delete old messages (default: 90 days)
+php artisan slack:delete-old-messages
+php artisan slack:delete-old-messages --days=60
+```
+
+## Scheduled Tasks
+The module automatically schedules:
+- Event check: Every hour
+- Old message cleanup: Daily
+Make sure your Laravel scheduler is running:
+```bash
+php artisan schedule:work
+```
+
+## Slack Commands
+The bot supports the following slash commands:
+- `/add_channel` - Add the current channel to receive event updates (admin only)
+- `/remove_channel` - Remove the current channel from receiving updates (admin only)
+- `/check_api` - Manually trigger an event check (rate limited to once per 15 minutes per workspace)
+
+## Routes
+- `GET /slack/install` - Display Slack installation button
+- `GET /slack/auth` - OAuth callback for Slack
+- `POST /slack/events` - Webhook endpoint for Slack events and commands
+
+## Features
+- Posts weekly event summaries to configured Slack channels
+- Automatically updates messages when events change
+- Handles message chunking for large event lists
+- Rate limiting for manual checks
+- Admin-only channel management
+- OAuth installation flow
+- Automatic cleanup of old messages
+- Direct database integration (no API calls needed)
+
+## How It Works
+1. The bot queries the Event model directly every hour for new/updated events
+2. Events are filtered to show published events from 1 day ago to 14 days ahead
+3. Events are grouped by week (Sunday to Saturday)
+4. Messages are posted/updated in configured Slack channels
+5. If a week has many events, they're split across multiple messages
+6. Messages for the current week and next week (5 days early) are maintained
+
+## Configuration Options
+The module can be configured in `config/slack-events-bot.php`:
+- `days_to_look_back` - How many days in the past to include events (default: 1)
+- `days_to_look_ahead` - How many days in the future to include events (default: 14)
+- `max_message_character_length` - Maximum characters per Slack message (default: 3000)
+- `check_api_cooldown_minutes` - Cooldown period for manual checks (default: 15)
+- `old_messages_retention_days` - Days to keep old messages (default: 90)
+
+## Migration from Python
+This module is a Laravel port of the original Python slack-events-bot, now refactored to use the Event model directly instead of making API calls. This provides:
+- Better performance (no HTTP overhead)
+- Real-time data (no API caching delays)
+- Tighter integration with the application
+- Easier maintenance and debugging