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

IRC 服务器搭建完全指南 / 第 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 框架

Bot 语言 特点 适用场景
Limnoria Python Supybot 分支,功能丰富 通用
Sopel Python 模块化,易扩展 通用
Hubot Node.js GitHub 出品,脚本丰富 DevOps
Eggdrop Tcl 最古老的 IRC Bot 频道管理
IRCCloud Bot Go 轻量高效 轻量应用

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