Appearance
User Management
Current version: v0.0.7
telegram-bot-essentials/user-management (namespace TelegramBotEssentials\UserManagement) gives admins a browsable, sortable list of bot users, a per-user detail screen built from pluggable sections, and automatic interaction logging. Other modules (notably User Wallet) extend this list by registering their own sort or section rather than forking the admin UI.
Installation
bash
composer require telegram-bot-essentials/user-management
php artisan migratePublish config if you want to change the default sort:
bash
php artisan vendor:publish --tag=tbe-user-management-configphp
// config/tbe-user-management.php
return [
'default_sort' => 'last_interaction',
'default_sort_direction' => 'desc',
];Sorting the user list — BotUserSorts
Helper: botUserSorts(). A keyed registry of TelegramBotEssentials\UserManagement\DTOs\BotUserSort. Three sorts ship by default (registered in the service provider's boot()): last_interaction, created_at, username.
php
class BotUserSort
{
public function __construct(
public string $key,
public string $label,
public Closure $apply, // (Builder $query, string $direction) => Builder
public ?Closure $display = null, // (BotUser $user) => string, shown in the list
public bool|Closure $active = true,
) {}
}Registering a new sort from your own package or app:
php
botUserSorts()->addSort(new BotUserSort(
key: 'wallet_balance',
label: __('my-package::bot_users.sorts.wallet_balance'),
apply: fn ($query, $direction) => $query
->leftJoin('bot_user_wallets', /* ... */)
->orderBy('bot_user_wallets.balance', $direction)
->select('bot_users.*'),
display: fn (BotUser $user) => $user->wallet_balance ?? '0',
active: fn () => settings()->get('billing.user_wallet.status'),
));active must be a Closure for conditional sorts
getSorts()/getSort() filter out any sort whose isActive() returns false, and isActive() evaluates active at call time — inside a request, after wHook()->bot() is resolved. If a sort's availability depends on a per-bot Settings value, always pass a Closure (not a plain bool) so the check happens per-request, not once at application boot. This is also why sort-building queries that JOIN another table (like username and wallet_balance above) should select('bot_users.*') explicitly — otherwise joined columns leak into the hydrated BotUser model.
API
php
botUserSorts()->getSorts(); // Collection<string, BotUserSort> — active only
botUserSorts()->getSort('username'); // ?BotUserSort — null if missing or inactive
botUserSorts()->getDefaultKey(); // config('tbe-user-management.default_sort') if active, else first active key
botUserSorts()->resolve($key); // $key if valid+active, else getDefaultKey()
botUserSorts()->next($currentKey); // cycles to the next active sort key, for a "change sort" button
botUserSorts()->resolveDirection($dir); // $dir if 'asc'/'desc', else config default
botUserSorts()->toggleDirection($dir); // flips asc/desc
botUserSorts()->directionIndicator($dir); // '⬆️' / '⬇️'
botUserSorts()->apply($key, $query, $direction); // resolves + applies in one callBotUsersFeature uses exactly this API to render the list screen and the "change sort" / "change direction" inline buttons, so a newly registered sort appears in the cycle automatically with zero UI code.
Per-user detail screen — UserManagementSections
Helper: userManagementSections(). Lets other packages contribute a block to the per-user admin detail view (e.g. "show this user's wallet balance and a top-up button") without touching this package's Feature classes.
php
class UserSection
{
public function __construct(
public string $key,
public int $order, // display order, ascending
public SectionMode $mode, // BUTTON | INLINE
public Closure $label, // (BotUser $user) => string
public ?Closure $target = null, // required for BUTTON mode: (BotUser $user) => string (callback_data)
public ?Closure $content = null, // required for INLINE mode: (BotUser $user) => array (extra message content)
public bool|Closure $active = true,
) {}
}SectionMode::BUTTON adds a labeled inline button (its target closure supplies the callback_data, typically encodeCallback(...) into your own package's CallbackQuery); SectionMode::INLINE instead injects content directly into the detail message body. The constructor enforces this at registration time — a BUTTON section without target, or an INLINE section without content, throws immediately.
php
userManagementSections()->addSection(new UserSection(
key: 'wallet',
order: 10,
mode: SectionMode::INLINE,
label: fn (BotUser $user) => __('tbe-user-wallet::bot_users.sections.wallet.label'),
content: fn (BotUser $user) => ['balance' => $user->wallet?->balance ?? 0],
active: fn (BotUser $user) => settings()->get('billing.user_wallet.status'),
));Registering the same key twice throws TbeException — section keys must be globally unique across all installed packages. getSectionsFor($user) returns only the sections whose active($user) (or plain bool) resolves true, sorted by order.
Automatic interaction logging — BotUserAction
The package listens to Essence's BotUpdateReceived event (Listeners\LogBotInteractions, wired in Providers\EventServiceProvider) and records every message/callback a user sends into the bot_user_actions table — no opt-in required once the package is installed:
| Column | Description |
|---|---|
bot_user_id | Who did it |
update_type | message or callback_query |
state | The resolved handler, as TYPE->method (from the active state or the callback data) |
action | The message text, or the pressed inline button's label |
History-navigation actions (paging back and forth through the action-history UI itself: userActionsHistory, allActionsHistory, actionsPage, allActionsPage, actionsSetPage, allActionsSetPage) are filtered out via BotUserAction::isHistoryNavigation() so browsing history doesn't pollute the history.
Admin UI
BotUsersQuery / BotUsersAnswer / BotUsersFeature / BotUsersKey provide the list screen (search, sort, paginate) and the per-user detail screen (sections + action history) — install the package, and admins get a working "Users" menu item immediately.
How User Wallet extends this module
User Wallet is the canonical example consumer: it registers a wallet_balance BotUserSort (guarded by active: fn () => settings()->get('billing.user_wallet.status')) and — depending on version — a wallet UserSection, wrapped in a class_exists(BotUserSorts::class) guard so User Wallet works whether or not User Management happens to be installed. See User Wallet for the concrete registration.