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_privfor command execution,manage_privfor 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 handlingon_prefix- Commands starting with specific stringson_fullmatch- Exact message matchingon_suffix- Commands ending with specific stringson_keyword- Substring matchingon_rex- Regular expression matchingon_command- Structured command handlingon_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:
- Service enablement check for the specific group (
check_enabled) - Group blocklist verification (
priv.check_block_group) - 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 groupdisable <service>- Deactivate a service for the current grouplsorservice 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
Serviceobjects defined inhoshino/service.py, each managing its own configuration, permissions, and trigger handlers. -
Trigger System: Decorators like
on_prefix,on_rex, andon_commandregister handlers with trigger managers inhoshino/trigger.py, using efficient matching algorithms for message routing. -
Permission Model: The
_check_allmethod 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
Serviceobject and attaching handlers via decorators, with optional scheduled jobs for periodic tasks. -
Runtime Management: Administrative commands in
hoshino/modules/botmanage/service_manage.pyallow 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →