强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 9 章:机器人开发

第 9 章:机器人开发

IRC Bot 是自动化运维和社区管理的利器。本章从零开始开发一个功能完整的 IRC 机器人。


9.1 IRC Bot 开发概述

9.1.1 Bot 的应用场景

场景功能示例
社区管理自动封禁、欢迎消息、规则查询!ban, !rules
DevOpsCI/CD 通知、部署控制、监控告警!deploy, !status
信息聚合RSS 订阅、新闻推送、天气查询!weather, !news
开发辅助代码搜索、文档查询、Issue 通知!docs, !issue
娱乐游戏、投票、随机选择!poll, !choose
统计消息统计、活跃度分析!stats

9.1.2 语言选择

语言特点推荐度
Pythonirc, slate, python-irclib2生态丰富,易上手⭐⭐⭐⭐⭐
Gogirc, goirc高性能,单二进制⭐⭐⭐⭐
Node.jsirc-framework, node-irc事件驱动,npm 生态⭐⭐⭐⭐
Rustirc-rs安全高效⭐⭐⭐
PerlBot::BasicBotIRC 传统语言⭐⭐⭐

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 框架

Bot语言特点适用场景
LimnoriaPythonSupybot 分支,功能丰富通用
SopelPython模块化,易扩展通用
HubotNode.jsGitHub 出品,脚本丰富DevOps
EggdropTcl最古老的 IRC Bot频道管理
IRCCloud BotGo轻量高效轻量应用

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 服务器。