How the HoshinoBot Permission System Manages User Privileges in priv.py

The HoshinoBot permission system uses integer-based privilege levels defined in hoshino/priv.py to enforce access control, evaluating super-users, temporary block lists, static configuration lists, and dynamic group roles through the get_user_priv() and check_priv() functions.

HoshinoBot implements a lightweight yet robust permission model centered in hoshino/priv.py. This module assigns every user a numeric privilege level that determines which commands they can execute, balancing transient bans, permanent blacklists, and real-time group membership roles. The system is designed to be both efficient and flexible, allowing module developers to guard commands with simple integer comparisons.

Privilege Constants and Numeric Levels

The permission hierarchy is encoded as integer constants ranging from -999 (blocked) to 999 (super-user). These values are declared at the top of [hoshino/priv.py](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/priv.py#L12-L21):

Constant Value Meaning
BLACK -999 Blocked user (temporary or permanent ban)
DEFAULT 0 Unset or fallback level
NORMAL 1 Regular group member
PRIVATE 10 Private chat (non-group context)
ADMIN 21 Group administrator
OWNER 22 Group owner
WHITE 51 Whitelisted user (exempt from restrictions)
SUPERUSER / SU 999 Bot owner with full access

Higher integers indicate greater privileges. When enforcing permissions, the system checks if the user's level is greater than or equal to the required threshold.

Temporary and Static Block Lists

HoshinoBot employs a three-layer defense mechanism combining transient blocks, static configuration lists, and dynamic role detection.

Temporary Blocks

Two dictionaries in priv.py manage time-based bans:

_black_group = {}   # {group_id: expiry_datetime}

_black_user  = {}   # {user_id: expiry_datetime}

The functions set_block_group(group_id, time) and set_block_user(user_id, time) add entries with expiration timestamps (datetime.now() + time). The check functions check_block_group and check_block_user return True while the block is active and automatically purge expired entries. See the implementation in [hoshino/priv.py](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/priv.py#L23-L49).

Static Configuration Lists

Permanent access control is configured via:

These lists are evaluated inside get_user_priv (lines 44-62 of [priv.py](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/priv.py#L44-L62)).

Determining User Privileges with get_user_priv

The core authorization logic resides in get_user_priv(ev), which inspects a CQEvent and returns the user's integer privilege level. The function evaluates conditions in strict priority order:

def get_user_priv(ev: CQEvent):
    uid = ev.user_id
    if uid in hoshino.config.SUPERUSERS:
        return SUPERUSER
    if check_block_user(uid):
        return BLACK
    if uid in config.WHITE_LIST:
        return WHITE
    if ev['message_type'] == 'group':
        if not ev.anonymous:
            role = ev.sender.get('role')
            if role == 'member':      return NORMAL
            elif role == 'admin':    return ADMIN
            elif role == 'owner':    return OWNER
        return NORMAL
    if ev['message_type'] == 'private':
        return PRIVATE
    return NORMAL

This implementation is found in [hoshino/priv.py](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/priv.py#L55-L77). The hierarchy ensures that super-user status overrides all other checks, while temporary blocks take precedence over whitelist status. For group messages, the sender's role (member, admin, or owner) is mapped directly to the corresponding privilege constant.

Enforcing Permissions with check_priv

Once a user's privilege level is determined, command handlers enforce restrictions via check_priv(ev, require):

def check_priv(ev: CQEvent, require: int) -> bool:
    if ev['message_type'] == 'group':
        return bool(get_user_priv(ev) >= require)
    else:
        return False

Located in [hoshino/priv.py](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/priv.py#L80-L85), this function performs a simple integer comparison. It returns True only if the user's level is greater than or equal to the required threshold. Notably, it explicitly returns False for non-group messages, meaning privileged commands cannot be invoked via private chat by default.

Real-World Usage in Modules

Module developers integrate privilege checks by guarding command entry points with priv.check_priv. The pattern is consistent across the codebase: verify the requirement and return early if insufficient.

Super-User Restrictions

In the picfinder module, sensitive operations are restricted to super-users:

if not priv.check_priv(ev, priv.SUPERUSER):
    return

See the implementation in [hoshino/modules/picfinder/__init__.py](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/modules/picfinder/__init__.py#L98).

Admin-Only Commands

The gacha module restricts configuration commands to group administrators:

if not priv.check_priv(ev, priv.ADMIN):
    return

This appears in [hoshino/modules/priconne/gacha/__init__.py](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/modules/priconne/gacha/__init__.py#L68).

Service-Level Integration

The Service class in [hoshino/service.py](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/service.py#L163) combines privilege checks with feature enablement:

return self.check_enabled(gid) and not priv.check_block_group(gid) and priv.check_priv(ev, self.use_priv)

This ensures that a command is only executed if the service is enabled for the group, the group is not temporarily blocked, and the user meets the required privilege level.

Summary

  • Integer-based hierarchy: HoshinoBot assigns every user a numeric level from -999 (BLACK) to 999 (SUPERUSER), defined in hoshino/priv.py.
  • Three-layer defense: The system evaluates temporary block lists first, then static configuration lists (BLACK_LIST, WHITE_LIST), and finally dynamic group roles.
  • Priority evaluation: get_user_priv checks super-users, blocks, whitelists, and group roles in strict order, returning the highest applicable level.
  • Simple enforcement: check_priv compares the user's level against a required threshold using >=, returning False for private messages by default.
  • Modular integration: Command handlers across modules like picfinder and priconne/gacha guard execution with if not priv.check_priv(ev, priv.LEVEL): return.

Frequently Asked Questions

What is the highest privilege level in HoshinoBot?

The highest level is SUPERUSER (integer 999), defined as the constant SU in hoshino/priv.py. Users whose IDs are listed in config.SUPERUSERS receive this level, overriding all other permission checks including temporary blocks and group roles.

How does HoshinoBot handle temporary user bans?

Temporary bans are managed through the _black_user and _black_group dictionaries in priv.py, which map IDs to expiration timestamps. When set_block_user() or set_block_group() is called with a timedelta, the system stores datetime.now() + time as the expiry. get_user_priv() returns BLACK (-999) while the current time is less than the stored expiry, automatically excluding expired entries during checks.

Can private chat users execute privileged commands?

No. By design, check_priv() explicitly returns False when ev['message_type'] is not 'group'. This means commands requiring priv.NORMAL or higher cannot be triggered via private messages, ensuring that privilege checks are strictly enforced within group contexts where roles (member, admin, owner) are verifiable.

Where are static blacklists and whitelists configured?

Static lists are defined in the bot configuration, typically in hoshino/config/__bot__.py (based on the example in hoshino/config_example/__bot__.py). The BLACK_LIST contains user IDs permanently denied access, while WHITE_LIST contains users granted the WHITE level (51), exempting them from most restrictions regardless of their group role. These lists are evaluated inside get_user_priv() after super-user checks but before group role detection.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →