mirror of
https://github.com/awfufu/go-hurobot.git
synced 2026-03-01 05:29:43 +08:00
feat: add support for external script commands
This commit is contained in:
41
example-command-cn.sh
Executable file
41
example-command-cn.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# example-command-cn.sh
|
||||
#
|
||||
# -> https://github.com/awfufu/go-hurobot/blob/main/example-command-cn.sh
|
||||
#
|
||||
# 这是一个关于如何为机器人创建外部命令的示例。
|
||||
#
|
||||
# 1. 将此脚本(或其软链接)放置在机器人工作目录下的 `cmds/` 目录中。
|
||||
# 脚本或软链接的文件名将成为命令名。
|
||||
# 例如,如果你将其命名为 `hello`,则命令将为 `/hello`。
|
||||
# 确保脚本具有可执行权限:`chmod +x cmds/hello`。
|
||||
#
|
||||
# 2. 获取参数:
|
||||
# 传递给命令的参数可以通过标准的 bash 参数($1, $2, ...)获取。
|
||||
#
|
||||
# 3. 获取消息上下文:
|
||||
# 机器人会将原始消息的上下文注入为环境变量:
|
||||
# - QBOT_USER_ID : 发送者的 QQ 号
|
||||
# - QBOT_GROUP_ID : 群组 ID(如果是群消息)
|
||||
# - QBOT_MSG_ID : 消息 ID
|
||||
# - QBOT_CHAT_TYPE : 聊天类型(数值)
|
||||
# - QBOT_NAME : 发送者昵称
|
||||
# - QBOT_GROUP_CARD : 发送者群名片
|
||||
#
|
||||
# 4. 返回响应:
|
||||
# 任何打印到标准输出(echo)的内容都会作为回复发送回聊天。
|
||||
|
||||
USER_ID="${QBOT_USER_ID}"
|
||||
NAME="${QBOT_NAME}"
|
||||
ARG_1="$1"
|
||||
|
||||
if [ -z "$ARG_1" ]; then
|
||||
echo "用法: /example-command-cn.sh <内容>"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "你好, ${NAME} (${USER_ID})!"
|
||||
echo "你说了: ${ARG_1}"
|
||||
echo "这条消息是由外部脚本处理的。"
|
||||
echo "了解更多: https://github.com/awfufu/go-hurobot/blob/main/example-command-cn.sh"
|
||||
41
example-command.sh
Executable file
41
example-command.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# example-command.sh
|
||||
#
|
||||
# -> https://github.com/awfufu/go-hurobot/blob/main/example-command.sh
|
||||
#
|
||||
# This is an example of how to create an external command for the bot.
|
||||
#
|
||||
# 1. Place this script (or a symlink to it) in the `cmds/` directory inside the bot's working directory.
|
||||
# The filename of the script/symlink will be the command name.
|
||||
# For example, if you name it `hello`, the command will be `/hello`.
|
||||
# Ensure the script is executable: `chmod +x cmds/hello`.
|
||||
#
|
||||
# 2. Accessing Arguments:
|
||||
# Arguments passed to the command are available as standard bash arguments ($1, $2, ...).
|
||||
#
|
||||
# 3. Accessing Message Context:
|
||||
# The bot injects the original message context as environment variables:
|
||||
# - QBOT_USER_ID : Sender's QQ number
|
||||
# - QBOT_GROUP_ID : Group ID (if in group)
|
||||
# - QBOT_MSG_ID : Message ID
|
||||
# - QBOT_CHAT_TYPE : Chat Type (numeric)
|
||||
# - QBOT_NAME : Sender's Nickname
|
||||
# - QBOT_GROUP_CARD : Sender's Group Card name
|
||||
#
|
||||
# 4. Returning Responses:
|
||||
# Whatever you print to stdout (echo) will be sent back to the chat as a reply.
|
||||
|
||||
USER_ID="${QBOT_USER_ID}"
|
||||
NAME="${QBOT_NAME}"
|
||||
ARG_1="$1"
|
||||
|
||||
if [ -z "$ARG_1" ]; then
|
||||
echo "Usage: /example-command.sh <something>"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Hello, ${NAME} (${USER_ID})!"
|
||||
echo "You said: ${ARG_1}"
|
||||
echo "This message was processed by a script."
|
||||
echo "Learn more: https://github.com/awfufu/go-hurobot/blob/main/example-command.sh"
|
||||
3
go.mod
3
go.mod
@@ -14,6 +14,9 @@ require (
|
||||
require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
)
|
||||
|
||||
10
go.sum
10
go.sum
@@ -2,18 +2,26 @@ github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f
|
||||
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/awfufu/qbot v0.2.2 h1:rl7+j6JNjGzjjO5AHNqRXdM09Gwc+mQGsgBerJYVfVg=
|
||||
github.com/awfufu/qbot v0.2.2/go.mod h1:t6pYm54N7/YrxoMYFerZe2qx0t505iPeQQpTJvTNY+8=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package cmds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/awfufu/go-hurobot/internal/config"
|
||||
"github.com/awfufu/go-hurobot/internal/db"
|
||||
"github.com/awfufu/qbot"
|
||||
@@ -98,6 +103,7 @@ func HandleCommand(b *qbot.Sender, msg *qbot.Message) {
|
||||
|
||||
cmd, exists := cmdMap[cmdName]
|
||||
if !exists {
|
||||
tryFallbackCommand(b, msg, cmdName, raw)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -143,6 +149,9 @@ func HandleCommand(b *qbot.Sender, msg *qbot.Message) {
|
||||
if args == nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.SendGroupPoke(msg.GroupID, msg.UserID)
|
||||
|
||||
// argCount logic simplified as there is no skip
|
||||
argCount := len(args)
|
||||
if (cmdBase.MinArgs > 0 && argCount < cmdBase.MinArgs) || (cmdBase.MaxArgs > 0 && argCount > cmdBase.MaxArgs) {
|
||||
@@ -376,3 +385,85 @@ func str2int64(s string) int64 {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func tryFallbackCommand(b *qbot.Sender, msg *qbot.Message, cmdName string, rawArgs string) {
|
||||
// Security check: cmdName should be a simple filename, no path traversal
|
||||
if strings.Contains(cmdName, "/") || strings.Contains(cmdName, "\\") || strings.Contains(cmdName, "..") {
|
||||
return
|
||||
}
|
||||
|
||||
path := filepath.Join("cmds", cmdName)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return // Not found or error
|
||||
}
|
||||
if info.IsDir() {
|
||||
return // Is directory
|
||||
}
|
||||
// Check if executable? os.Stat doesn't easily tell us, but we can try to exec.
|
||||
// On Linux checking Mode() & 0111 is a hint, but attempting to run is robust.
|
||||
|
||||
// Parse args
|
||||
var args []string
|
||||
if rawArgs != "" {
|
||||
// Use shlex to split arguments like a shell
|
||||
parts, err := shlex.Split(rawArgs)
|
||||
if err != nil {
|
||||
b.SendGroupMsg(msg.GroupID, "Error parsing arguments: "+err.Error())
|
||||
return
|
||||
}
|
||||
args = parts
|
||||
}
|
||||
|
||||
// Prepare command
|
||||
cmd := exec.Command(path, args...)
|
||||
|
||||
// Inject Environment Variables
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("QBOT_CHAT_TYPE=%d", msg.ChatType),
|
||||
fmt.Sprintf("QBOT_MSG_ID=%d", msg.MsgID),
|
||||
fmt.Sprintf("QBOT_REPLY_ID=%d", msg.ReplyID),
|
||||
fmt.Sprintf("QBOT_USER_ID=%d", msg.UserID),
|
||||
"QBOT_NAME="+msg.Name,
|
||||
fmt.Sprintf("QBOT_TIME=%d", msg.Time),
|
||||
fmt.Sprintf("QBOT_GROUP_ID=%d", msg.GroupID),
|
||||
"QBOT_GROUP_CARD="+msg.GroupCard,
|
||||
fmt.Sprintf("QBOT_GROUP_ROLE=%d", msg.GroupRole),
|
||||
)
|
||||
|
||||
// No special env or dir? "find work dir's ./cmds/" implies CWD is the bot's CWD.
|
||||
// That is default for exec.Command.
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Fallback cmd %s failed: %v", cmdName, err)
|
||||
// If output is not empty, show it, otherwise show error
|
||||
msgContent := fmt.Sprintf("Error: %v", err)
|
||||
if len(outputStr) > 0 {
|
||||
msgContent += "\nOutput:\n" + truncateFallbackOutput(outputStr)
|
||||
}
|
||||
b.SendGroupReplyMsg(msg.GroupID, msg.MsgID, msgContent)
|
||||
return
|
||||
}
|
||||
|
||||
if len(outputStr) > 0 {
|
||||
b.SendGroupReplyMsg(msg.GroupID, msg.MsgID, truncateFallbackOutput(outputStr))
|
||||
} else {
|
||||
// No output, just verify success? maybe silent if success?
|
||||
// User requirement "if not exist... ignore", but here it existed and ran.
|
||||
// Usually a confused user wants feedback.
|
||||
// "ok" or nothing? sh command returns "ok" only if empty output.
|
||||
b.SendGroupReplyMsg(msg.GroupID, msg.MsgID, "ok")
|
||||
}
|
||||
}
|
||||
|
||||
func truncateFallbackOutput(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) > 4000 {
|
||||
return s[:4000] + "... (truncated)"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user