第 9 章:机器人开发
第 9 章:机器人开发
IRC Bot 是自动化运维和社区管理的利器。本章从零开始开发一个功能完整的 IRC 机器人。
9.1 IRC Bot 开发概述
9.1.1 Bot 的应用场景
| 场景 | 功能 | 示例 |
|---|
| 社区管理 | 自动封禁、欢迎消息、规则查询 | !ban, !rules |
| DevOps | CI/CD 通知、部署控制、监控告警 | !deploy, !status |
| 信息聚合 | RSS 订阅、新闻推送、天气查询 | !weather, !news |
| 开发辅助 | 代码搜索、文档查询、Issue 通知 | !docs, !issue |
| 娱乐 | 游戏、投票、随机选择 | !poll, !choose |
| 统计 | 消息统计、活跃度分析 | !stats |
9.1.2 语言选择
| 语言 | 库 | 特点 | 推荐度 |
|---|
| Python | irc, slate, python-irclib2 | 生态丰富,易上手 | ⭐⭐⭐⭐⭐ |
| Go | girc, goirc | 高性能,单二进制 | ⭐⭐⭐⭐ |
| Node.js | irc-framework, node-irc | 事件驱动,npm 生态 | ⭐⭐⭐⭐ |
| Rust | irc-rs | 安全高效 | ⭐⭐⭐ |
| Perl | Bot::BasicBot | IRC 传统语言 | ⭐⭐⭐ |
9.2 Python Bot 开发
9.2.1 基础 Bot 框架
#!/usr/bin/env python3
"""
基础 IRC Bot 框架
使用 irc 库实现
"""
import irc.client
import irc.connection
import ssl
import logging
import re
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger('IRCBot')
class IRCBot:
def __init__(self, server, port, nickname, channels, password=None):
self.server = server
self.port = port
self.nickname = nickname
self.channels = channels
self.password = password
self.reactor = irc.client.Reactor()
self.connection = None
# 注册事件处理器
self.reactor.add_global_handler('welcome', self.on_connect)
self.reactor.add_global_handler('join', self.on_join)
self.reactor.add_global_handler('pubmsg', self.on_public_message)
self.reactor.add_global_handler('privmsg', self.on_private_message)
self.reactor.add_global_handler('kick', self.on_kick)
self.reactor.add_global_handler('disconnect', self.on_disconnect)
# 命令注册表
self.commands = {}
self.register_command('help', self.cmd_help, '显示帮助信息')
self.register_command('ping', self.cmd_ping, '测试 Bot 响应')
self.register_command('info', self.cmd_info, '显示服务器信息')
def register_command(self, name, handler, description=''):
"""注册命令"""
self.commands[name] = {
'handler': handler,
'description': description
}
logger.info(f'注册命令: {name}')
def connect(self):
"""连接到 IRC 服务器"""
ssl_factory = irc.connection.Factory(wrapper=ssl.wrap_socket)
logger.info(f'连接到 {self.server}:{self.port}')
self.connection = self.reactor.server().connect(
self.server,
self.port,
self.nickname,
connect_factory=ssl_factory
)
def start(self):
"""启动 Bot"""
self.connect()
self.reactor.process_forever()
def on_connect(self, connection, event):
"""连接成功回调"""
logger.info('已连接到 IRC 服务器')
# 通过 SASL 或 NickServ 认证
if self.password:
connection.privmsg('NickServ', f'IDENTIFY {self.password}')
logger.info('已发送认证请求')
# 加入频道
for channel in self.channels:
connection.join(channel)
logger.info(f'已加入频道: {channel}')
def on_join(self, connection, event):
"""加入频道回调"""
nick = event.source.nick
channel = event.target
if nick == self.nickname:
logger.info(f'已加入 {channel}')
def on_public_message(self, connection, event):
"""公共消息回调"""
nick = event.source.nick
channel = event.target
message = event.arguments[0]
# 检查命令前缀
if message.startswith('!'):
parts = message[1:].split()
if parts and parts[0] in self.commands:
cmd = parts[0]
args = parts[1:]
self.commands[cmd]['handler'](connection, channel, nick, args)
def on_private_message(self, connection, event):
"""私信回调"""
nick = event.source.nick
message = event.arguments[0]
logger.info(f'收到私信: {nick}: {message}')
# 回复私信
connection.privmsg(nick, f'你好!我是 {self.nickname}。使用 !help 查看可用命令。')
def on_kick(self, connection, event):
"""被踢出回调"""
channel = event.target
kicker = event.source.nick
reason = event.arguments[1] if len(event.arguments) > 1 else ''
logger.warning(f'被 {kicker} 从 {channel} 踢出: {reason}')
# 自动重新加入
connection.join(channel)
def on_disconnect(self, connection, event):
"""断开连接回调"""
logger.warning('已断开连接,尝试重连...')
try:
self.connect()
except irc.client.ServerConnectionError:
logger.error('重连失败')
# ===== 命令处理器 =====
def cmd_help(self, connection, channel, nick, args):
"""帮助命令"""
help_text = '可用命令: ' + ', '.join(
f'!{cmd} ({info["description"]})'
for cmd, info in self.commands.items()
)
connection.privmsg(channel, help_text)
def cmd_ping(self, connection, channel, nick, args):
"""Ping 命令"""
connection.privmsg(channel, f'{nick}: Pong!')
def cmd_info(self, connection, channel, nick, args):
"""信息命令"""
connection.privmsg(channel, f'{self.nickname} v1.0 - Python IRC Bot')
if __name__ == '__main__':
bot = IRCBot(
server='irc.example.com',
port=6697,
nickname='MyBot',
channels=['#general', '#dev'],
password='bot_password'
)
bot.start()
9.2.2 使用 irc-framework(推荐)
#!/usr/bin/env python3
"""
使用 python-irclib2 的高级 Bot
"""
import irc.client
import irc.connection
import ssl
import asyncio
import aiohttp
class AdvancedBot:
def __init__(self):
self.client = irc.client.Reactor()
self.conn = None
async def fetch_url(self, url):
"""异步 HTTP 请求"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()
def on_welcome(self, conn, event):
conn.join('#general')
# 请求 IRCv3 能力
conn.send_raw('CAP REQ :sasl message-tags server-time')
def on_pubmsg(self, conn, event):
nick = event.source.nick
msg = event.arguments[0]
channel = event.target
# URL 自动预览
urls = re.findall(r'https?://\S+', msg)
if urls:
# 异步处理 URL 预览
asyncio.create_task(self.preview_url(conn, channel, urls[0]))
# 天气查询
if msg.startswith('!weather '):
city = msg[9:].strip()
asyncio.create_task(self.weather(conn, channel, city))
async def preview_url(self, conn, channel, url):
"""URL 预览"""
try:
async with aiohttp.ClientSession() as session:
async with session.head(url, allow_redirects=True) as resp:
content_type = resp.headers.get('content-type', '')
title = ''
if 'text/html' in content_type:
html = await resp.text()
import re as regex
match = regex.search(r'<title[^>]*>([^<]+)</title>', html, re.IGNORECASE)
if match:
title = match.group(1)
conn.privmsg(channel, f'[链接] {url} - {title}')
except Exception as e:
logger.error(f'URL 预览失败: {e}')
async def weather(self, conn, channel, city):
"""天气查询"""
try:
url = f'https://wttr.in/{city}?format=3'
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
weather = (await resp.text()).strip()
conn.privmsg(channel, f'天气: {weather}')
except Exception as e:
conn.privmsg(channel, f'天气查询失败: {e}')
9.3 Go Bot 开发
9.3.1 基础 Go Bot
package main
import (
"crypto/tls"
"fmt"
"log"
"strings"
"time"
"github.com/lrstanley/girc"
)
func main() {
// 创建 IRC 客户端
client := girc.New(girc.Config{
Server: "irc.example.com",
Port: 6697,
Nick: "GoBot",
User: "gobot",
Name: "Go IRC Bot",
SSL: true,
TLSConfig: &tls.Config{InsecureSkipVerify: false},
})
// 连接事件
client.Handlers.Add(girc.CONNECTED, func(client *girc.Client, event girc.Event) {
log.Println("已连接到 IRC 服务器")
client.Cmd.Join("#general")
client.Cmd.Join("#dev")
})
// 消息事件
client.Handlers.Add(girc.PRIVMSG, func(client *girc.Client, event girc.Event) {
if len(event.Params) < 2 {
return
}
channel := event.Params[0]
message := event.Params[1]
nick := event.Source.Name
// 处理命令
if strings.HasPrefix(message, "!") {
parts := strings.Fields(message[1:])
if len(parts) == 0 {
return
}
switch parts[0] {
case "help":
client.Cmd.Message(channel, fmt.Sprintf("%s: 可用命令: !help, !ping, !time, !uptime", nick))
case "ping":
client.Cmd.Message(channel, fmt.Sprintf("%s: Pong!", nick))
case "time":
now := time.Now().Format("2006-01-02 15:04:05")
client.Cmd.Message(channel, fmt.Sprintf("当前时间: %s", now))
case "uptime":
uptime := time.Since(startTime)
client.Cmd.Message(channel, fmt.Sprintf("运行时间: %s", uptime.Round(time.Second)))
}
}
})
// 断线重连
for {
if err := client.Connect(); err != nil {
log.Printf("连接错误: %v,5 秒后重试...", err)
time.Sleep(5 * time.Second)
}
}
}
var startTime = time.Now()
9.4 Weechat 脚本
9.4.1 Weechat 脚本生态
Weechat 支持多种脚本语言:Python、Perl、Ruby、Lua、Tcl、Guile (Scheme)。
# 安装 Weechat 脚本
/script install autojoin.py
/script install go.py
/script install grep.py
# 查看已安装脚本
/script list
9.4.2 Python 脚本示例
# -*- coding: utf-8 -*-
"""
Weechat 脚本:自动回复 Bot
"""
import weechat
import re
SCRIPT_NAME = "autobot"
SCRIPT_AUTHOR = "Admin"
SCRIPT_VERSION = "1.0"
SCRIPT_LICENSE = "MIT"
SCRIPT_DESC = "IRC 自动回复机器人"
# 配置
config = {
"trigger_pattern": r"\bhello\b",
"reply": "你好!有什么可以帮助你的吗?",
"channels": "#general,#dev",
"enabled": "on"
}
def on_message(data, signal, signal_data):
"""处理消息"""
if config["enabled"] != "on":
return weechat.WEECHAT_RC_OK
# 解析消息
server = signal.split(",")[0]
msg = weechat.info_get_hashtable("irc_message_parse", {"message": signal_data})
if not msg or "channel" not in msg:
return weechat.WEECHAT_RC_OK
channel = msg["channel"]
nick = msg["nick"]
# 检查频道
allowed_channels = config["channels"].split(",")
if channel not in allowed_channels:
return weechat.WEECHAT_RC_OK
# 检查触发条件
message = msg.get("text", "")
if re.search(config["trigger_pattern"], message, re.IGNORECASE):
weechat.command("", f"/msg {channel} {nick}: {config['reply']}")
return weechat.WEECHAT_RC_OK
def on_command(data, buffer, args):
"""命令处理"""
if args == "on":
config["enabled"] = "on"
weechat.prnt("", "AutoBot 已启用")
elif args == "off":
config["enabled"] = "off"
weechat.prnt("", "AutoBot 已禁用")
else:
weechat.prnt("", "用法: /autobot on|off")
return weechat.WEECHAT_RC_OK
# 初始化
if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
weechat.hook_signal("*,irc_in2_privmsg", "on_message", "")
weechat.hook_command("autobot", "控制自动回复机器人", "on|off", "", "", "on_command", "")
weechat.prnt("", f"{SCRIPT_NAME} v{SCRIPT_VERSION} 已加载")
9.5 常用开源 Bot
9.5.1 现成 Bot 框架
9.5.2 Limnoria 安装与使用
# 安装
pip install limnoria
# 初始化
supybot-wizard
# 按照向导完成配置
# 启动
supybot /opt/limnoria/config.conf
# 常用插件
# Admin, Channel, Config, Google, Misc, Owner, RSS, Stats, Web
9.5.3 Sopel 安装与使用
# 安装
pip install sopel
# 创建配置
sopel configure
# 启动
sopel start
# 自定义模块 (放在 ~/.sopel/modules/ 下)
# ~/.sopel/modules/hello.py
import sopel.module
@sopel.module.commands('hello')
@sopel.module.example('.hello')
def hello(bot, trigger):
"""打招呼"""
bot.say(f'你好,{trigger.nick}!')
@sopel.module.commands('weather')
def weather(bot, trigger):
"""天气查询"""
if not trigger.group(3):
bot.say('用法: .weather 城市名')
return
city = trigger.group(3)
# 调用天气 API
bot.say(f'{city} 的天气: 晴天 25°C')
9.6 Bot 开发最佳实践
9.6.1 代码结构
ircbot/
├── main.py # 入口
├── config.py # 配置管理
├── bot.py # Bot 核心
├── commands/ # 命令模块
│ ├── __init__.py
│ ├── info.py # 信息查询
│ ├── admin.py # 管理命令
│ ├── fun.py # 娱乐命令
│ └── devops.py # DevOps 集成
├── plugins/ # 插件
│ ├── rss.py
│ ├── weather.py
│ └── stats.py
├── utils/ # 工具函数
│ ├── irc.py
│ └── storage.py
├── requirements.txt
├── Dockerfile
└── docker-compose.yml
9.6.2 错误处理
import functools
import logging
logger = logging.getLogger('ircbot')
def handle_errors(func):
"""命令错误处理装饰器"""
@functools.wraps(func)
def wrapper(bot, channel, nick, args):
try:
return func(bot, channel, nick, args)
except Exception as e:
logger.error(f'命令 {func.__name__} 执行失败: {e}')
bot.connection.privmsg(channel, f'错误: {str(e)[:200]}')
return wrapper
@handle_errors
def cmd_search(bot, channel, nick, args):
"""搜索命令"""
if not args:
raise ValueError('请提供搜索关键词')
# ... 搜索逻辑 ...
9.6.3 持久化存储
import sqlite3
import json
class BotStorage:
def __init__(self, db_path='bot.db'):
self.db = sqlite3.connect(db_path)
self.create_tables()
def create_tables(self):
self.db.execute('''
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
)
''')
self.db.execute('''
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel TEXT,
nick TEXT,
quote TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
self.db.commit()
def get(self, key, default=None):
row = self.db.execute('SELECT value FROM settings WHERE key = ?', (key,)).fetchone()
return json.loads(row[0]) if row else default
def set(self, key, value):
self.db.execute('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
(key, json.dumps(value)))
self.db.commit()
def add_quote(self, channel, nick, quote):
self.db.execute('INSERT INTO quotes (channel, nick, quote) VALUES (?, ?, ?)',
(channel, nick, quote))
self.db.commit()
def get_random_quote(self, channel):
row = self.db.execute(
'SELECT nick, quote FROM quotes WHERE channel = ? ORDER BY RANDOM() LIMIT 1',
(channel,)
).fetchone()
return f'{row[0]}: {row[1]}' if row else None
9.7 ⚠️ 注意事项
| 事项 | 说明 |
|---|
| 速率限制 | Bot 发送消息需要节流,避免被服务器封禁 |
| 异常处理 | 网络断开、超时等异常需要妥善处理 |
| 安全存储 | API Token、密码等敏感信息不要硬编码 |
| 权限控制 | 区分普通用户和管理员命令 |
| 日志记录 | 记录所有重要操作,便于审计 |
| 测试环境 | 先在测试频道测试,再部署到生产 |
| 资源占用 | 避免 Bot 占用过多 CPU/内存 |
扩展阅读
下一章: 第 10 章:Docker 部署 — 使用 Docker 和 Docker Compose 容器化部署 IRC 服务器。