# wechaty-puppet-discordbot

[Wechaty](https://wechaty.js.org) Puppet 实现，基于 [discord.js](https://discord.js.org) 接入 Discord 机器人。

## 功能

- 接收/发送私信（DM）和频道文字消息
- 接收图片、音频、视频、文件附件，自动保存到本地
- 发送图片、文件、语音（自动识别 mimeType，确保 Discord 内联播放）
- 群频道 @mention 支持
- 消息撤回
- 基于 LRU + FlashStore 的双层缓存（内存 + 磁盘持久化）

## 前置条件

1. 在 [Discord Developer Portal](https://discord.com/developers/applications) 创建一个应用
2. 在「Bot」页面创建机器人，记录 **Bot Token**
3. 在「Bot」→「Privileged Gateway Intents」中开启：
   - **Message Content Intent**
   - **Server Members Intent**
4. 将机器人邀请到你的服务器（OAuth2 → URL Generator，勾选 `bot` 权限，至少需要 `Send Messages`、`Read Message History`、`Manage Messages`）

## 安装

```bash
npm install wechaty-puppet-discordbot
```

## 快速开始

```ts
import { WechatyBuilder } from '@juzi/wechaty'
import { PuppetDiscord } from 'wechaty-puppet-discordbot'

const puppet = new PuppetDiscord({
  token: 'your_discord_bot_token',
})

const bot = WechatyBuilder.build({ puppet })

bot.on('message', async (message) => {
  if (message.self()) return
  if (message.text() === 'ping') {
    await message.say('pong')
  }
})

await bot.start()
```

## 配置项

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `token` | string | 是 | Discord Bot Token |
| `proxyUrl` | string | 否 | 代理地址，所有 Discord 流量（REST + WebSocket）均通过此代理转发 |
| `proxyToken` | string | 否 | 代理认证头，格式 `Basic <base64>` 或 `Bearer <token>`，凭证无法内嵌 URL 时使用 |

### 代理配置示例

```ts
// 本地代理（无认证）
const puppet = new PuppetDiscord({
  token: 'your_discord_bot_token',
  proxyUrl: 'http://127.0.0.1:7890',
})

// 远程代理（用户名密码内嵌 URL）
const puppet = new PuppetDiscord({
  token: 'your_discord_bot_token',
  proxyUrl: 'http://user:pass@proxy.example.com:8080',
})

// 远程代理（通过 proxyToken 单独传入认证）
const puppet = new PuppetDiscord({
  token: 'your_discord_bot_token',
  proxyUrl: 'http://proxy.example.com:8080',
  proxyToken: `Basic ${Buffer.from('user:pass').toString('base64')}`,
})
```

代理基于 `undici` 的 `ProxyAgent` 实现，支持 HTTP 和 HTTPS 代理协议。

## 运行示例

```bash
# 在 .env 文件中配置（或直接设置环境变量）
echo "BOT_TOKEN=your_discord_bot_token" > .env

npm run example
```

### 示例支持的测试指令

在 Discord 频道或私信中发送以下文字：

| 指令 | 效果 |
|------|------|
| `ping` | 回复 pong |
| `help` | 显示所有指令 |
| `info` | 显示频道名称和成员列表 |
| `send image` | 机器人发送一张示例图片 |
| `send audio` | 机器人发送一段示例语音 |
| `send file` | 机器人发送一个文本文件 |
| `recall` | 机器人发消息并在 3 秒后撤回 |
| `echo: <text>` | 机器人回显文字 |
| `announce <text>` | 设置频道公告（channel topic） |
| `get announce` | 读取当前频道公告 |
| `mention all` | 发送 @everyone |
| `quote` | 引用回复当前消息 |
| `read` | 标记当前会话已读（给最新消息加 ✅ 反应） |
| `@bot <text>` | 机器人回复被提及的消息 |
| 发送图片/音频/文件附件 | 机器人回复附件信息并保存到 `downloads/` |

## 发送消息

### 文字

```ts
// 私信或频道消息
await message.say('你好！')

// 群频道回复并 @mention 发送者
const room = message.room()
const sender = message.talker()
if (room) {
  await room.say('收到！', [sender])
}
```

### 图片 / 文件

```ts
import { FileBox } from '@juzi/file-box'

// 从 URL 发送图片
const image = FileBox.fromUrl('https://example.com/image.jpg', { name: 'image.jpg' })
await message.say(image)

// 从本地文件发送
const file = FileBox.fromFile('/path/to/document.pdf')
await message.say(file)
```

### 语音

Discord 无专属语音消息协议，音频文件作为附件上传后客户端会内联播放。

```ts
import { FileBox } from '@juzi/file-box'

// 从文件发送（支持 mp3 / ogg / wav / flac / aac / m4a / opus / amr）
const audio = FileBox.fromFile('/path/to/voice.mp3')
await message.say(audio)

// 从 Buffer 发送（需通过 mimeType 告知类型）
const buf = fs.readFileSync('voice.ogg')
const audio = FileBox.fromBuffer(buf, 'voice.ogg')
await message.say(audio)
```

> **自动扩展名补全**：若 FileBox 携带 `mimeType`（如 `audio/ogg`）但文件名无扩展名，puppet 会自动补全（如 `voice` → `voice.ogg`），确保 Discord 客户端识别并内联播放。

## 接收文件

```ts
import * as PUPPET from '@juzi/wechaty-puppet'

bot.on('message', async (message) => {
  const fileTypes = [
    PUPPET.types.Message.Image,
    PUPPET.types.Message.Audio,
    PUPPET.types.Message.Video,
    PUPPET.types.Message.Attachment,
  ]

  if (fileTypes.includes(message.type())) {
    const fileBox = await message.toFileBox()
    // 保存到本地
    await fileBox.toFile('./' + fileBox.name)
    console.log(`已保存: ${fileBox.name}`)
  }
})
```

## Discord → Wechaty 概念映射

| Discord | Wechaty |
|---------|---------|
| Guild 文字频道 | Room（`roomId = channel.id`）|
| User | Contact（`contactId = user.id`）|
| DM Channel | 私聊（`listenerId = bot.id`）|
| Bot | Self |

## 注意事项

- **Message Content Intent**：Discord 要求机器人在开发者后台手动开启此权限，否则收到的消息内容为空
- **私信（DM）**：需要开启 `Partials.Channel`（已内置），否则 Discord.js v14 会静默丢弃 DM 事件
- **消息撤回**：机器人只能撤回自己发出的消息，撤回他人消息需要 `Manage Messages` 权限
- **联系人列表**：通过消息事件动态收集，首次 `roomMemberList()` 会实时从 Discord API 拉取
- **缓存位置**：联系人/群数据持久化在 `~/.wechaty/puppet-discord-cache/`

## 开发

```bash
# 安装依赖
npm install

# 编译
npm run dist

# 监听模式编译
npm run dev

# 类型检查
npx tsc --noEmit
```

## License

MIT
