Hexo + TG bot 实现自动推送文章到telegram频道

Hexo + TG bot 实现自动推送文章到telegram频道

突发奇想,hexo能不能实现自动推送功能。选择tg主要是因为tg的bot很强大(对比tx的小肚鸡肠,已经非常不错了)。翻了一圈,发现基本都是基于github action实现的推送。对github action实在不熟悉,复现也失败了,可能是因为需要完全通过github action构建并更新(?。只能曲线救国,结合github action的例子和tg api doc,用shell脚本完成自动推送。

核心流程为:申请bot编写shell(linux)添加脚本启动

申请tg bot

首先添加一个叫 BotFather 的机器人,用来申请bot

image-20250218230347122

image-20250218230542295

接下来会依次要求填写 nameusername

nameusername 是不同的。name 指bot显示的名字,username 指bot实际的名字(用来搜索的名字)

image-20250218230847351

这里 namegubai's botusernamegubai_blog_bot

申请成功后的消息中, t.me/gubai_blog_bot 可以访问bot,下面提供的token是这个bot的唯一标识,非常重要,不能泄露

但是访问bot发现没有任何响应,因为还没有编写逻辑

image-20250218231329072

将bot拉入希望推送文章的频道/群组(别忘记给权限)

image-20250219191834013

tg中每个聊天窗口有一个独一无二的 ChatID

邀请bot @get_id_bot 进入频道,发送命令 /my_id@get_id_bot

image-20250219192723201

得到ChatID -1002492764196

为了规范频道,可以将所有和bot交互内容都删除。至于用来获得id的bot,可以踢掉,也可以禁言

编写shell

在任意目录看着顺眼的位置创建一个.sh文件,名字随意,这里是 TG_push.sh,给执行权限

1
chmod +x TG_push.sh
1
2
3
4
#!/bin/bash

TG_BOT_TOKEN=xxx # 填入自己的bot token
ChatID=xxx # 填入id

推送的思路有很多种,这里列出我的两种思路。

  • 获取最新编辑的文件,提取title和addrlink,组成message发送

  • 遍历所有 ./source/_posts下的文件,利用hexo Front-matter,自定义新的配置项 push (当然,也可以起其他key),当该项为 true (当然,可以改为其他value) 时,获取title和addrlink

第二种是后来想起Front-matter不被渲染后实践的,效果很好,比第一种方式稳定,而且支持多文章自选推送。

下面详细介绍第二种方式的实现,第一种方式的代码会放到最后。

设想格式为:

1
2
3
4
5
6
7
8
9
10
11
12
————————————
主页:xxxxx
博客:xxxxx
————————————
推送时间:xxxx
————update————
标题:xxx
链接:xxx
————————————
标题:xxx
链接:xxx
————————————

首先先完成 update 以上的部分,继续编写shell

1
2
3
4
5
6
7
8
9
10
11
currentTime=$(date +"%Y-%m-%d %H:%M:%S") #获取当前时间
commitMsg="
————————————————
main: https://www.gubaiovo.com
blog: https://blog.gubaiovo.com
————————————————

Time: ${currentTime}

——————UPDATE——————
"

这里要注意,换行不要直接写 \n 进行换行,会被各种神秘转义无法变成换行符。但是像上面在shell中编写时直接换行是可以的,而且最终呈现的效果也是换行。

这里可以先发送一次进行测试了。运行下方shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

TG_BOT_TOKEN=xxxxxx
ChatID=xxxxxx

currentTime=$(date +"%Y-%m-%d %H:%M:%S") #获取当前时间
commitMsg="
————————————————
main: xxx
blog: xxx
————————————————

Time: ${currentTime}

——————UPDATE——————
"
curl -s -X POST https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage -d chat_id=$ChatID -d text="$commitMsg"

image-20250219194830929

如果运行后像这样正常输出了,那么就完成一半了

如果运行后后台很久没反应,那么看看是不是代理出问题了

下面继续编写一个循环,用来遍历所有文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
path=/home/gubai/www/blog/source/_posts # 换成你的文章路径
# 这个函数用来追加需要push的文章信息
addUpdate() {
local commitMsg="$1"
local title="$2"
local addrlink="$3"
local link="https://blog.gubaiovo.com/posts/$addrlink.html"
commitMsg+=$(\nprintf "Title: %s\nURL: %s\n" "$title" "$link")
commitMsg+=$(printf "\n ————————————————")
echo "$commitMsg"
}

for file in "$path"/*; do
# 获取文件的前20行,也就是包含Front matter的范围。这里20可以根据你的Front matter的行数进行设定
fileContent=$(head -n 20 "$file")
# 获取push字段。如果你用了其他字段作为key,那么将 'push:' 改为你设定的字段
push=$(echo "$fileContent" | grep 'push:' | awk -F': ' '{print $2}')
# true用来决定推送,如果你用了其他的value来决定推送,改成你设定的value
if [ "$push" == "true" ]; then
# 提取 title 和 abbrlink
title=$(echo "$fileContent" | grep 'title:' | awk -F': ' '{print $2}')
addrlink=$(echo "$fileContent" | grep 'abbrlink: ' | awk -F': ' '{print $2}')
# 这里调用了上方addUpdate()函数,用来追加message
commitMsg=$(addUpdate "$commitMsg" "$title" "$addrlink")
# 将推送后的文章的push字段设为false,防止重复push(如果用了其他的key和value,记得更改)
sed -i 's/push: false/push: false/' "$file"
fi
done

现在shell脚本可以遍历所有文件,获得 push 字段,根据对应的value决定是否追加到message

目前为止的完整shell如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/bin/bash

TG_BOT_TOKEN=xxxxxx
ChatID=xxxxxx
currentTime=$(date +"%Y-%m-%d %H:%M:%S") #获取当前时间
path=/home/gubai/www/blog/source/_posts # 换成你的文章路径

commitMsg="
————————————————
main: xxx
blog: xxx
————————————————

Time: ${currentTime}

——————UPDATE——————
"

# 这个函数用来追加需要push的文章信息
addUpdate() {
local commitMsg="$1"
local title="$2"
local addrlink="$3"
local link="https://blog.gubaiovo.com/posts/$addrlink.html"
commitMsg+=$(\nprintf "Title: %s\nURL: %s\n" "$title" "$link")
commitMsg+=$(printf "\n ————————————————")
echo "$commitMsg"
}

for file in "$path"/*; do
# 获取文件的前20行,也就是包含Front matter的范围。这里20可以根据你的Front matter的行数进行设定
fileContent=$(head -n 20 "$file")
# 获取push字段。如果你用了其他字段作为key,那么将 'push:' 改为你设定的字段
push=$(echo "$fileContent" | grep 'push:' | awk -F': ' '{print $2}')
# true用来决定推送,如果你用了其他的value来决定推送,改成你设定的value
if [ "$push" == "true" ]; then
# 提取 title 和 abbrlink
title=$(echo "$fileContent" | grep 'title:' | awk -F': ' '{print $2}')
addrlink=$(echo "$fileContent" | grep 'abbrlink: ' | awk -F': ' '{print $2}')
# 这里调用了上方addUpdate()函数,用来追加message
commitMsg=$(addUpdate "$commitMsg" "$title" "$addrlink")
# 将推送后的文章的push字段设为false,防止重复push(如果用了其他的key和value,记得更改)
sed -i 's/push: false/push: false/' "$file"
fi
done


curl -s -X POST https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage -d chat_id=$ChatID -d text="$commitMsg"

虽然现在能实现获取需要push的文章并进行推送,但是,如果没有需要push的文章呢?

假设现在所有文章都没有push字段,或者push字段不是true,那么,最终的message是下面内容

1
2
3
4
5
6
7
8
————————————————
main: xxx
blog: xxx
————————————————

Time: ${currentTime}

——————UPDATE——————

bot会将这个板子推到频道,这显然不是我们想要的,我们想要的是没有需要push的就不push了。因此可以加一个变量 flag 用来标记是否需要推送。flag 初始化为 false ,如果在上方循环遍历中 if [ "$push" == "true" ] 成立了,那么就让 flagtrue 。在发送请求时先检测flag值,如果flag为true,那么就发送请求

按照这个思路完善脚本,在最开头添加

1
flag=false

if [ "$push" == "true" ] 判断中添加

1
2
3
4
5
6
7
8
if [ "$push" == "true" ]; then
# 设为true确定需要推送
flag=true
title=$(echo "$fileContent" | grep 'title:' | awk -F': ' '{print $2}')
addrlink=$(echo "$fileContent" | grep 'abbrlink: ' | awk -F': ' '{print $2}')
commitMsg=$(addUpdate "$commitMsg" "$title" "$addrlink")
sed -i 's/push: false/push: false/' "$file"
fi

在发送请求前添加

1
2
3
if [ "$flag" == false ]; then
exit 0
fi

如果flag为false,那么直接退出,也就不发送请求了。

修改后的shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/bin/bash

TG_BOT_TOKEN=xxxx
ChatID=xxxx
path=/home/gubai/www/blog/source/_posts
currentTime=$(date +"%Y-%m-%d %H:%M:%S")
flag=false

commitMsg="
————————————————
main: xxx
blog: xxx
————————————————

Time: ${currentTime}

——————UPDATE——————
"

addUpdate() {
local commitMsg="$1"
local title="$2"
local addrlink="$3"
local link="https://blog.gubaiovo.com/posts/$addrlink.html"
commitMsg+=$(\nprintf "Title: %s\nURL: %s\n" "$title" "$link")
commitMsg+=$(printf "\n ————————————————")
echo "$commitMsg"
}

for file in "$path"/*; do
fileContent=$(head -n 20 "$file")
push=$(echo "$fileContent" | grep 'push:' | awk -F': ' '{print $2}')
if [ "$push" == "true" ]; then
flag=true
title=$(echo "$fileContent" | grep 'title:' | awk -F': ' '{print $2}')
addrlink=$(echo "$fileContent" | grep 'abbrlink: ' | awk -F': ' '{print $2}')
commitMsg=$(addUpdate "$commitMsg" "$title" "$addrlink")
sed -i 's/push: false/push: false/' "$file"
fi
done

if [ "$flag" == false ]; then
exit 0
fi
curl -s -X POST https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage -d chat_id=$ChatID -d text="$commitMsg"

现在基本可以使用了。但是在使用过程中,会发现一个问题:title中特殊符号截断字符串

这是我在推送我的Ollama API调用时发现的,效果如下

image-20250219202050032

可以看到,lua文章是正常的,但是ollama的文章因为 & 被截断了。我们需要对title进行编码

添加编码函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# URL 编码函数
urlencode() {
local length="${#1}"
local i=0
local c
local encoded=""
while [ $i -lt $length ]; do
c="${1:$i:1}"
# 使用 iconv 检查字符是否为ascii,避免非ascii码文字(如中文)错误编码
if echo "$c" | iconv -f UTF-8 -t ISO-8859-1 >/dev/null 2>&1; then
case $c in
[a-zA-Z0-9.~_-]) encoded+="$c" ;;
*) encoded+=$(printf '%%%02X' "'$c") ;;
esac
else
encoded+="$c"
fi
((i++))
done
echo "$encoded"
}

调用函数,编码title

1
2
3
4
5
6
...
title=$(echo "$fileContent" | grep 'title:' | awk -F': ' '{print $2}')
addrlink=$(echo "$fileContent" | grep 'abbrlink: ' | awk -F': ' '{print $2}')
encodedTitle=$(urlencode "$title")
commitMsg=$(addUpdate "$commitMsg" "$encodedTitle" "$abbrlink")
...

image-20250219202843748

这样便正确实现了推送功能

自动调用推送

现在确实实现了推送,但是怎么才能在 hexo d 时自动调用呢

两种思路,一种为在shell cd xxx进入blog根目录,调用hexo d

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

# 这两行位置没有影响
cd /home/gubai/www/blog
hexo d

TG_BOT_TOKEN=xxx
ChatID=xxxx
path=/home/gubai/www/blog/source/_posts
currentTime=$(date +"%Y-%m-%d %H:%M:%S")
flag=false
...

还有一种思路,使用hexo插件 hexo-deployer-shell ,这个插件为deploy添加新的类型 shell ,能够执行shell命令

github: https://github.com/HakurouKen/hexo-deployer-shell

i
1
npm install hexo-deployer-shell --save

安装后进入 hexo 的 _config.yml,找到 deploy项,像这样填写

1
2
3
4
5
6
deploy:
- type: git
repository: xxx
branch: xxx
- type: shell
command: ~/www/blog/TG_push.sh

hexo在执行 hexo d 时,会按照 deploy 的顺序依次执行 git 和 shell,参考hexo文档

ALL CODE

第一种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/bin/bash

TG_BOT_TOKEN=xxxxx
ChatID=xxxxx
path=xxxx
currentTime=$(date +"%Y-%m-%d %H:%M:%S")

# 获取文件夹中最新的文件名
latestFile=$(ls -t $path | head -n 1)

# 获取最新文件的修改时间
latestFileTime=$(stat -c %Y "$path/$latestFile")
# 获取当前时间的时间戳
currentTimeStamp=$(date +%s)
# 计算时间差(秒)
timeDiff=$((currentTimeStamp - latestFileTime))
# 如果时间差超过1800秒(30分钟),则不进行推送
if [ $timeDiff -gt 1800 ]; then
exit 0
fi

# 获取文件的前20行
fileContent=$(head -n 20 "$path/$latestFile")

# URL 编码函数
urlencode() {
local length="${#1}"
local i=0
local c
local encoded=""
while [ $i -lt $length ]; do
c="${1:$i:1}"
# 使用 iconv 检查字符是否为中文
if echo "$c" | iconv -f UTF-8 -t ISO-8859-1 >/dev/null 2>&1; then
case $c in
[a-zA-Z0-9.~_-]) encoded+="$c" ;;
*) encoded+=$(printf '%%%02X' "'$c") ;;
esac
else
encoded+="$c"
fi
((i++))
done
echo "$encoded"
}

# 提取 title 和 abbrlink
title=$(echo "$fileContent" | grep 'title:' | awk -F': ' '{print $2}')
addrlink=$(echo "$fileContent" | grep 'abbrlink: ' | awk -F': ' '{print $2}')
encodedTitle=$(urlencode "$title")

commitMsg="
推送时间:$currentTime

更新:$encodedTitle

文章链接:https://blog.gubaiovo.com/posts/$addrlink.html
"
curl -s -X POST https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage -d chat_id=$ChatID -d text="$commitMsg"

第二种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/bin/bash

TG_BOT_TOKEN=xxxx
ChatID=xxxxx
path=xxxxx
currentTime=$(date +"%Y-%m-%d %H:%M:%S")
flag=false
# URL 编码函数
urlencode() {
local length="${#1}"
local i=0
local c
local encoded=""
while [ $i -lt $length ]; do
c="${1:$i:1}"
# 使用 iconv 检查字符是否为中文
if echo "$c" | iconv -f UTF-8 -t ISO-8859-1 >/dev/null 2>&1; then
case $c in
[a-zA-Z0-9.~_-]) encoded+="$c" ;;
*) encoded+=$(printf '%%%02X' "'$c") ;;
esac
else
encoded+="$c"
fi
((i++))
done
echo "$encoded"
}

addUpdate() {
local commitMsg="$1"
local title="$2"
local addrlink="$3"
local link="https://blog.gubaiovo.com/posts/$addrlink.html"
commitMsg+=$(printf "\nTitle: %s\nURL: %s\n" "$title" "$link")
commitMsg+=$(printf "\n————————————————")
echo "$commitMsg"
}


commitMsg="
————————————————
main: https://www.gubaiovo.com
blog: https://blog.gubaiovo.com
————————————————

Time: ${currentTime}

——————UPDATE——————
"

for file in "$path"/*; do
# 获取文件的前20行
fileContent=$(head -n 20 "$file")
push=$(echo "$fileContent" | grep 'push:' | awk -F': ' '{print $2}')
if [ "$push" == "true" ]; then
flag=true
# 提取 title 和 abbrlink
title=$(echo "$fileContent" | grep 'title:' | awk -F': ' '{print $2}')
addrlink=$(echo "$fileContent" | grep 'abbrlink: ' | awk -F': ' '{print $2}')
encodedTitle=$(urlencode "$title")
commitMsg=$(addUpdate "$commitMsg" "$encodedTitle" "$addrlink")
sed -i 's/push: false/push: false/' "$file"
fi
done

if [ "$flag" == false ]; then
exit 0
fi
curl -s -X POST https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage -d chat_id=$ChatID -d text="$commitMsg"

顾白的tg频道 https://t.me/gubaiblog