panel icon indicating copy to clipboard operation
panel copied to clipboard

Plugin system

Open Boy132 opened this issue 1 month ago • 7 comments

grafik

Boy132 avatar Nov 06 '25 09:11 Boy132

[!IMPORTANT]

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Adds a plugin system: model, service, facade, enums, contract, policies, console commands, Filament admin resource/pages, panel integration, helper, provider hooks, language/frontend build integrations, config/translations, and Docker/Vite updates; loads plugins during app bootstrap and supports panel-scoped plugin loading, installation, updates, and asset management.

Changes

Cohort / File(s) Summary
Core plugin runtime & service
app/Services/Helpers/PluginService.php, app/Facades/Plugins.php
New PluginService centralizes plugin lifecycle (load, loadPanelPlugins, install, update, uninstall, manage composer packages, migrations, buildAssets, download/extract, enable/disable, updateLoadOrder, queries). Plugins facade proxies to the service.
Model, contracts, enums
app/Models/Plugin.php, app/Contracts/Plugins/HasPluginSettings.php, app/Enums/PluginStatus.php, app/Enums/PluginCategory.php
New Plugin Eloquent model implementing HasPluginSettings; handles discovery, metadata, updates, compatibility, providers/commands, settings delegation, readme, and helpers. New enums PluginStatus and PluginCategory with UI metadata.
Filament admin UI & resource pages
app/Filament/Admin/Resources/Plugins/PluginResource.php, app/Filament/Admin/Resources/Plugins/Pages/ListPlugins.php
New Filament Resource and List page: tabbed category filters, reorder support, per-record actions (install/update/enable/disable/settings/uninstall/etc.), import from file/URL workflows, dynamic badges and counts, and index/listing UI.
Console commands & stubs
app/Console/Commands/Plugin/*.php, app/Console/Commands/Plugin/*.stub
Adds multiple Artisan commands: make/install/update/list/disable/uninstall/composer (p:plugin:*) and associated plugin file stubs (Plugin.stub, PluginProvider.stub, PluginConfig.stub).
Providers & bootstrap integration
app/Providers/AppServiceProvider.php, app/Providers/Filament/*PanelProvider.php
Loads plugins during bootstrap: Plugins::loadPlugins() in AppServiceProvider::register(); panel providers now call Plugins::loadPanelPlugins($panel) before returning panel.
Policies, roles & permissions
app/Policies/PluginPolicy.php, app/Models/Role.php
New PluginPolicy using DefaultPolicies with modelName 'plugin'; Role::SPECIAL_PERMISSIONS updated to include 'plugin' => ['viewList','create','update','delete'].
Admin pages/business logic
app/Filament/Admin/Resources/Plugins/... (actions/notifications)
Table and action logic (authorization, modals, slide-overs), reorder handling, import workflows, and plugin-related notifications implemented inside resource/page classes.
Language, helper & services
app/Services/Helpers/LanguageService.php, app/helpers.php
LanguageService merges core and plugin-provided languages via Plugins::getPluginLanguages(); adds plugin_path() helper for consistent plugin paths.
Role / policy integrations
app/Providers/... (imports)
Providers import App\Facades\Plugins and adjust control flow to allow plugin loading (assign panel to $panel, load plugins, then return).
Frontend & build config
vite.config.js
Adds plugin asset globs to Vite input to include plugins/*/resources/css/**/*.css and plugins/*/resources/js/**/*.js in builds.
Docker / storage symlinks
Dockerfile, Dockerfile.dev
Create /pelican-data/plugins and symlink it to /var/www/html/plugins; update comments and symlink setup accordingly.
Translations, config, composer & gitignore
lang/en/admin/plugin.php, config/panel.php, composer.json, plugins/.gitignore
Add English translations for plugin admin UI, add panel.plugin config (dev_mode, max_import_size), add php artisan p:plugin:composer to composer post-install-cmd, and add plugins/.gitignore to ignore plugin files except .gitignore.
Filament pages & list utilities
app/Filament/Admin/Resources/Plugins/Pages/ListPlugins.php
New ListPlugins page supports reorderTable, tabbed category filters via PluginCategory::cases(), and delegates load order updates to Plugins::updateLoadOrder.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant ASP as AppServiceProvider
    participant PluginsFacade as Plugins Facade
    participant PluginSvc as PluginService
    participant PluginModel as Plugin Model
    participant Autoloader as Autoloader/Provider Loader

    App->>ASP: register()
    ASP->>PluginsFacade: Plugins::loadPlugins()
    PluginsFacade->>PluginSvc: loadPlugins()
    PluginSvc->>PluginModel: getRows()
    PluginModel-->>PluginSvc: plugin metadata list
    loop per plugin (load_order)
        PluginSvc->>PluginModel: shouldLoad(panelId?)
        alt compatible & enabled
            PluginSvc->>Autoloader: register autoloads, providers, commands
            PluginSvc->>PluginSvc: load translations, configs, views, migrations
        else mark errored or skip
        end
    end
    PluginSvc-->>PluginsFacade: loaded
    PluginsFacade-->>ASP: done
sequenceDiagram
    participant AdminUI as Filament Admin
    participant PluginResource as PluginResource
    participant PluginsSvc as PluginService
    participant Filesystem as Filesystem
    participant DB as Plugin Model / Metadata

    AdminUI->>PluginResource: user imports plugin (file/URL)
    PluginResource->>PluginsSvc: downloadPluginFromFile/Url()
    PluginsSvc->>Filesystem: validate + save + unzip
    Filesystem-->>PluginsSvc: extracted plugin path
    PluginsSvc->>DB: setMetaData / update rows
    PluginsSvc->>PluginsSvc: manageComposerPackages() -> install packages
    PluginsSvc->>PluginsSvc: runPluginMigrations()
    PluginsSvc-->>PluginResource: import success / error
    PluginResource-->>AdminUI: show notification / update list

Pre-merge checks

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.74% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The description consists only of an embedded image with no textual explanation of the changes. This is insufficient to convey meaningful information about the PR's objectives and scope. Provide a detailed textual description explaining the plugin system features being introduced, key design decisions, and implementation details.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Plugin system' is directly related to the changeset, which introduces comprehensive plugin system functionality including commands, services, facades, models, and UI resources.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot] avatar Nov 06 '25 09:11 coderabbitai[bot]

@coderabbitai review

Boy132 avatar Nov 06 '25 09:11 Boy132

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot] avatar Nov 06 '25 09:11 coderabbitai[bot]

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot] avatar Nov 06 '25 09:11 coderabbitai[bot]

@coderabbitai review

Boy132 avatar Nov 06 '25 10:11 Boy132

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot] avatar Nov 06 '25 10:11 coderabbitai[bot]

Hey, the permissions system is not fine-grained enough, it won't allow other users that aren't the root admin to import plugins. I imagine this can't be intentional, since this means multiple administrator accounts cannot exist.

I also have this error when the plugins are being unzipped:


    #0 /var/www/html/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php(363): App\\Services\\Helpers\\PluginService->downloadPluginFromFile(Object(Livewire\\Features\\SupportFileUploads\\TemporaryUploadedFile))
    #1 /var/www/html/app/Filament/Admin/Resources/Plugins/PluginResource.php(212): Illuminate\\Support\\Facades\\Facade::__callStatic('downloadPluginF...', Array)
    #2 /var/www/html/vendor/filament/support/src/Concerns/EvaluatesClosures.php(36): App\\Filament\\Admin\\Resources\\Plugins\\PluginResource::{closure:App\\Filament\\Admin\\Resources\\Plugins\\PluginResource::table():209}(Array, Object(App\\Filament\\Admin\\Resources\\Plugins\\Pages\\ListPlugins))
    #3 /var/www/html/vendor/filament/actions/src/Action.php(556): Filament\\Support\\Components\\Component->evaluate(Object(Closure), Array)
    #4 /var/www/html/vendor/filament/actions/src/Concerns/InteractsWithActions.php(259): Filament\\Actions\\Action->call(Array)
    #5 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Filament\\Pages\\BasePage->callMountedAction(Array)
    #6 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::{closure:Illuminate\\Container\\BoundMethod::call():35}()
    #7 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
    #8 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
    #9 /var/www/html/vendor/livewire/livewire/src/Wrapped.php(23): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array)
    #10 /var/www/html/vendor/livewire/livewire/src/Mechanisms/HandleComponents/HandleComponents.php(492): Livewire\\Wrapped->__call('callMountedActi...', Array)
    #11 /var/www/html/vendor/livewire/livewire/src/Mechanisms/HandleComponents/HandleComponents.php(101): Livewire\\Mechanisms\\HandleComponents\\HandleComponents->callMethods(Object(App\\Filament\\Admin\\Resources\\Plugins\\Pages\\ListPlugins), Array, Object(Livewire\\Mechanisms\\HandleComponents\\ComponentContext))
    #12 /var/www/html/vendor/livewire/livewire/src/LivewireManager.php(102): Livewire\\Mechanisms\\HandleComponents\\HandleComponents->update(Array, Array, Array)
    #13 /var/www/html/vendor/livewire/livewire/src/Mechanisms/HandleRequests/HandleRequests.php(94): Livewire\\LivewireManager->update(Array, Array, Array)
    #14 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(46): Livewire\\Mechanisms\\HandleRequests\\HandleRequests->handleUpdate()
    #15 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Route.php(265): Illuminate\\Routing\\ControllerDispatcher->dispatch(Object(Illuminate\\Routing\\Route), Object(Livewire\\Mechanisms\\HandleRequests\\HandleRequests), 'handleUpdate')
    #16 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Route.php(211): Illuminate\\Routing\\Route->runController()
    #17 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(822): Illuminate\\Routing\\Route->run()
    #18 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Routing\\Router->{closure:Illuminate\\Routing\\Router::runRouteWithinStack():821}(Object(Illuminate\\Http\\Request))
    #19 /var/www/html/app/Http/Middleware/LanguageMiddleware.php(23): Illuminate\\Pipeline\\Pipeline->{closure:Illuminate\\Pipeline\\Pipeline::prepareDestination():178}(Object(Illuminate\\Http\\Request))
    #20 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): App\\Http\\Middleware\\LanguageMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #21 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(50): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #22 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Routing\\Middleware\\SubstituteBindings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #23 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php(87): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #24 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #25 /var/www/html/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php(48): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #26 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\View\\Middleware\\ShareErrorsFromSession->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #27 /var/www/html/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(120): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #28 /var/www/html/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(63): Illuminate\\Session\\Middleware\\StartSession->handleStatefulRequest(Object(Illuminate\\Http\\Request), Object(Illuminate\\Session\\Store), Object(Closure))
    #29 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Session\\Middleware\\StartSession->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #30 /var/www/html/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php(36): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #31 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #32 /var/www/html/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php(74): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #33 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Cookie\\Middleware\\EncryptCookies->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #34 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #35 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(821): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
    #36 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
    #37 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
    #38 /var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
    #39 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
    #40 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->{closure:Illuminate\\Foundation\\Http\\Kernel::dispatchToRouter():197}(Object(Illuminate\\Http\\Request))
    #41 /var/www/html/vendor/livewire/livewire/src/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php(19): Illuminate\\Pipeline\\Pipeline->{closure:Illuminate\\Pipeline\\Pipeline::prepareDestination():178}(Object(Illuminate\\Http\\Request))
    #42 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Livewire\\Features\\SupportDisablingBackButtonCache\\DisableBackButtonCacheMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #43 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php(27): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #44 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #45 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(47): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #46 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #47 /var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePostSize.php(27): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #48 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\ValidatePostSize->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #49 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #50 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #51 /var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(48): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #52 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #53 /var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php(58): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #54 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\TrustProxies->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #55 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #56 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #57 /var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePathEncoding.php(26): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #58 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\ValidatePathEncoding->handle(Object(Illuminate\\Http\\Request), Object(Closure))
    #59 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}(Object(Illuminate\\Http\\Request))
    #60 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
    #61 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
    #62 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1220): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
    #63 /var/www/html/public/index.php(20): Illuminate\\Foundation\\Application->handleRequest(Object(Illuminate\\Http\\Request))
    #64 {main}

felinusfish avatar Nov 06 '25 12:11 felinusfish

@coderabbitai review

Boy132 avatar Dec 01 '25 12:12 Boy132

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot] avatar Dec 01 '25 12:12 coderabbitai[bot]