How HoshinoBot's Service System Works: A Complete Guide to Creating Custom Services

HoshinoBot isolates functionality into modular Service objects that handle command registration, permission checks, and configuration management automatically.

The HoshinoBot service system provides a structured framework for organizing bot functionality into discrete, manageable units. In the ice9coffee/hoshinobot repository, each Service instance acts as a self-contained module that bundles related commands, triggers, and scheduled jobs while handling permissions and group-specific enable states internally.

Core Architecture of the HoshinoBot Service System

The Service Class (hoshino/service.py)

At the heart of the system lies the Service class defined in hoshino/service.py. When instantiated, a Service automatically registers itself in the global _loaded_services dictionary and optionally joins a service bundle for organizational grouping.

The constructor signature reveals the configuration options available:

class Service:
    def __init__(self, name, use_priv=None, manage_priv=None,
                 enable_on_default=None, visible=None, help_=None, bundle=None):
        # Registers service, loads JSON config from ~/.hoshino/service_config/

        # Sets permission levels and metadata...

Each Service maintains its own configuration JSON stored in ~/.hoshino/service_config/{service_name}.json, which persists visibility settings, default enable states, and group-specific allow/deny lists across bot restarts.

Configuration and State Management

The Service system handles complex state management automatically. When loading, each Service instance:

  • Retrieves its persisted configuration from the JSON store
  • Applies default permission levels (use_priv for command execution, manage_priv for administrative control)
  • Tracks which groups have explicitly enabled or disabled the service
  • Maintains visibility flags for service listing commands

This architecture allows group administrators to toggle services on or off per-group without modifying code, with all changes persisting to disk automatically.

How Triggers Work in HoshinoBot

Trigger Registration via Decorators

The HoshinoBot service system provides a comprehensive set of decorator methods for registering message handlers. These decorators abstract the complexity of trigger matching while providing clean, readable syntax for command definition.

Available decorators in hoshino/service.py include:

  • on_message - Raw message handling
  • on_prefix - Commands starting with specific strings
  • on_fullmatch - Exact message matching
  • on_suffix - Commands ending with specific strings
  • on_keyword - Substring matching
  • on_rex - Regular expression matching
  • on_command - Structured command handling
  • on_natural_language - Natural language processing

Each decorator creates a ServiceFunc wrapper that maintains references to both the service instance and the original handler function. For example, the on_prefix implementation:

def on_prefix(self, *prefix, only_to_me=False):
    def deco(func):
        sf = ServiceFunc(self, func, only_to_me)
        for p in prefix:
            trigger.prefix.add(p, sf)  # Registers with PrefixTrigger

        return func
    return deco

Trigger Matching and Execution (hoshino/trigger.py)

The actual message matching logic resides in hoshino/trigger.py, which defines concrete trigger classes including PrefixTrigger, SuffixTrigger, KeywordTrigger, and RexTrigger.

These triggers use efficient data structures for matching:

  • Prefix triggers utilize trie structures for longest-prefix matching
  • Keyword triggers scan for substring occurrences
  • Regex triggers compile patterns for efficient matching

When a message arrives, the PrefixTrigger.find_handler method extracts the relevant text slice, performs the lookup, and yields the associated ServiceFunc objects for execution.

Permission and Enable Checks

The _check_all Method

Before executing any handler, the HoshinoBot service system validates permissions through the _check_all method in hoshino/service.py. This centralized check ensures consistent security enforcement across all services.

The validation chain includes:

  1. Service enablement check for the specific group (check_enabled)
  2. Group blocklist verification (priv.check_block_group)
  3. User permission level validation against use_priv (priv.check_priv)
def _check_all(self, ev):
    gid = ev.group_id
    return self.check_enabled(gid) and not priv.check_block_group(gid) \
           and priv.check_priv(ev, self.use_priv)

This architecture allows fine-grained control where services can be restricted to specific groups or require elevated privileges without implementing custom permission logic in each handler.

Service Management Commands

Administrative control of services is handled through hoshino/modules/botmanage/service_manage.py, which provides commands for runtime service manipulation.

Group administrators can use commands like:

  • enable <service> - Activate a service for the current group
  • disable <service> - Deactivate a service for the current group
  • ls or service list - Display available services

These commands interact directly with the Service instances to modify their enable states and persist changes to the JSON configuration files.

Creating Custom Services in HoshinoBot

Minimal Service Example

Creating a custom HoshinoBot service requires only instantiating a Service object and attaching handlers using decorators. The minimal implementation demonstrates the framework's simplicity:


# my_service.py

from hoshino import Service

# Create service with unique name and help text

sv = Service('hello', help_='Simple greeting service', use_priv=0)

@sv.on_prefix('hi')
async def greet(bot, ev):
    await bot.send(ev, 'Hello! 👋')

Place this file in hoshino/modules/ or any importable path within the module directory structure. The bot automatically discovers and loads the service on startup, registering the prefix trigger without additional configuration.

Command-Style Service with Arguments

For services requiring structured arguments and complex logic, the on_command decorator provides session-based argument parsing:


# weather.py

import aiohttp
from hoshino import Service

sv = Service('weather', help_='Weather lookup service', visible=True)

@sv.on_command('weather', aliases=('w', '天气'))
async def weather_cmd(session):
    city = session.current_arg_text.strip()
    
    if not city:
        await session.send('Usage: !weather <city>', at_sender=True)
        return
    
    async with aiohttp.ClientSession() as s:
        async with s.get(f'https://api.example.com/weather?q={city}') as r:
            data = await r.json()
    
    await session.send(f'{city}: {data["temp"]}°C, {data["condition"]}')

This example demonstrates alias support, argument validation, and external API integration while maintaining the service's permission and enablement checks automatically.

Scheduled Jobs and Cron Tasks

Services can define periodic tasks using the scheduled_job decorator, which integrates with the underlying APScheduler:


# reminder.py

from hoshino import Service
import datetime

sv = Service('reminder', help_='Daily reminders', visible=False)

@sv.scheduled_job('cron', hour='9', minute='0')
async def daily_good_morning():
    # Get all groups where this service is enabled

    groups = await sv.get_enable_groups()
    
    for gid, bot_ids in groups.items():
        for bot_id in bot_ids:
            await sv.bot.send_group_msg(
                self_id=bot_id,
                group_id=gid,
                message='Good morning! 🌞'
            )

This pattern enables broadcast functionality while respecting per-group service enablement states, ensuring messages only reach groups that have explicitly enabled the reminder service.

Summary

  • Service Architecture: HoshinoBot organizes functionality into Service objects defined in hoshino/service.py, each managing its own configuration, permissions, and trigger handlers.

  • Trigger System: Decorators like on_prefix, on_rex, and on_command register handlers with trigger managers in hoshino/trigger.py, using efficient matching algorithms for message routing.

  • Permission Model: The _check_all method enforces group-specific enable states and privilege levels (use_priv, manage_priv) before executing any handler.

  • Custom Development: Creating new services requires only instantiating a Service object and attaching handlers via decorators, with optional scheduled jobs for periodic tasks.

  • Runtime Management: Administrative commands in hoshino/modules/botmanage/service_manage.py allow group admins to enable, disable, and list services without code changes.

Frequently Asked Questions

What is the minimum code required to create a working HoshinoBot service?

You need only import the Service class, instantiate it with a unique name, and apply a decorator to a handler function. Place the file in hoshino/modules/ and restart the bot. The service automatically loads, registers its triggers, and handles permission checks without additional boilerplate.

How does HoshinoBot determine which service handles an incoming message?

When a message arrives, the trigger managers in hoshino/trigger.py iterate through registered handlers. Each trigger type (prefix, suffix, keyword, regex) uses optimized lookup structures to find matching ServiceFunc wrappers. Before execution, the service's _check_all method verifies the service is enabled for that group and the user has sufficient privileges.

Can I restrict a custom service to specific user groups or permission levels?

Yes. When creating a Service instance, set the use_priv parameter to control who can invoke commands (0 for everyone, 1 for group admins, 2 for bot admins). The manage_priv parameter controls who can enable or disable the service. Additionally, group admins can toggle services per-group using the built-in enable and disable commands.

Where does HoshinoBot store service configuration and enable states?

Service configurations persist as JSON files in ~/.hoshino/service_config/{service_name}.json. These files store default enable states, visibility settings, and per-group allow/deny lists. The Service class automatically loads these configurations on instantiation and saves changes when administrators modify service states through chat commands.

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 →