配合Github Actions 做一个自动推送的 Rss 订阅机器人

前言

一直都有订阅 Rss 的习惯,前段时间配合钉钉的自定义机器人,建了一个在钉钉定时推送消息的群,当时是服务是跑在自己的小水管上的,并且是在自己博客的基础上的,依赖了 Flask。后续想着开源,考虑到单独一个 Rss 推送机器人依赖一个 Web 框架就显得比较笨重了,就着手将这部分服务独立出来。

钉钉 RSS 推送机器人

钉钉 RSS 推送机器人,主要内容分为两部分,第一部分是钉钉机器人;第二部分是订阅推送。

钉钉机器人

因为是自定义内容,钉钉现有的机器人不支持,所以我们需要借助钉钉的自定义机器人。

步骤一

【电脑钉钉 】-【群聊】-【群设置】-【智能群助手】-【添加更多】-【添加机器人】-【自定义】-【添加】,编辑机器人名称和选择添加的群组。完成必要的安全设置(至少选择一种),勾选 我已阅读并同意《自定义机器人服务及免责条款》,点击“完成”即可。

步骤二

复制出机器人的Webhook地址(格式如下截图),可用于向这个群发送消息。并点击【完成】,自定义机器人就添加成功了。

为了安全考虑,这里我们在创建机器人的时候,勾选“加签”。这样子会获得 webhook 和 secret 两个参数

1
2
webhook = 'https://oapi.dingtalk.com/robot/send?access_token=这里填写自己钉钉群自定义机器人的token'
secret = 'SEC11b9...这里填写自己的加密设置密钥'

消息推送

完成机器人的创建,就需要完成通过 api 将消息推送到群里,使用 DingtalkChatbot

安装

1
pip install DingtalkChatbot

例子

1
2
3
4
5
6
7
8
9
10
from dingtalkchatbot.chatbot import DingtalkChatbot
# WebHook地址
webhook = 'https://oapi.dingtalk.com/robot/send?access_token=这里填写自己钉钉群自定义机器人的token'
secret = 'SEC11b9...这里填写自己的加密设置密钥' # 可选:创建机器人勾选“加签”选项时使用
# 初始化机器人小丁
xiaoding = DingtalkChatbot(webhook) # 方式一:通常初始化方式
xiaoding = DingtalkChatbot(webhook, secret=secret) # 方式二:勾选“加签”选项时使用(v1.5以上新功能)
xiaoding = DingtalkChatbot(webhook, pc_slide=True) # 方式三:设置消息链接在PC端侧边栏打开(v1.5以上新功能)
# Text消息@所有人
xiaoding.send_text(msg='我就是小丁,小丁就是我!', is_at_all=True)

RSS 解析

rss 解析,这里使用 feedparser 进行解析。

安装

1
pip install DingtalkChatbot

例子

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
>>> import feedparser
>>> leetao_feed = feedparser.parse('https://www.leetao94.cn/feeds') # parse() 方法,解析对应的 url
>>> leetao_feed.feed # feed 对象
{'title': "Peach Blog's Feeds", 'title_detail': {'type': 'text/plain', 'language': None,
'base': 'https://www.leetao94.cn/feeds', 'value': "Peach Blog's Feeds"},
'id': 'https://www.leetao94.cn/feeds', 'guidislink': True,
'link': 'https://www.leetao94.cn/', 'updated': '2020-09-25T19:53:35Z',
'updated_parsed': time.struct_time(tm_year=2020, tm_mon=9, tm_mday=25, tm_hour=19, tm_min=53, tm_sec=35, tm_wday=4, tm_yday=269, tm_isdst=0),
'links': [{'href': 'https://www.leetao94.cn/', 'rel': 'alternate', 'type': 'text/html'},
{'href': 'https://www.leetao94.cn/feeds', 'rel': 'self', 'type': 'application/atom+xml'}],
'generator_detail': {'name': 'Werkzeug'}, 'generator': 'Werkzeug'}
>>> type(leetao_feed.entries) # 解析出来的文章列表对象
<class 'list'>
>>> len(leetao_feed.entries)
10
>>> leetao_feed.entries[0] # 单个文章的内容
{'title': 'Mybatis中特殊符号#{}和${}',
'title_detail': {'type': 'text/plain',
'language': None, 'base': 'https://www.leetao94.cn/feeds',
'value': 'Mybatis中特殊符号#{}和${}'},
'id': 'https://www.leetao94.cn/post/Mybatis中特殊符号#{}和${}',
'guidislink': True, 'link': 'https://www.leetao94.cn/post/Mybatis中特殊符号#{}和${}',
'updated': '2020-09-25T19:53:35Z',
'updated_parsed': time.struct_time(tm_year=2020, tm_mon=9, tm_mday=25, tm_hour=19, tm_min=53, tm_sec=35, tm_wday=4, tm_yday=269, tm_isdst=0),
'published': '2020-09-25T19:53:35Z',
'published_parsed': time.struct_time(tm_year=2020, tm_mon=9, tm_mday=25, tm_hour=19, tm_min=53, tm_sec=35, tm_wday=4, tm_yday=269, tm_isdst=0),
'links': [{'href': 'https://www.leetao94.cn/post/Mybatis中特殊符号#{}和${}',
'rel': 'alternate', 'type': 'text/html'}], 'authors': [{'name': 'Leetao'}],
'author_detail': {'name': 'Leetao'}, 'author': 'Leetao',
'content': [{'type': 'text/html', 'language': None, 'base': 'https://www.leetao94.cn/feeds',
'value': '...'}

具体用法可以参考官方文档:https://pythonhosted.org/feedparser/

RSS 钉钉机器人

准备工作完成之后,就是将机器人和 Rss 解析内容整合到一起,为了避免重复推送内容,很显然需要借助数据库,考虑简便性,采用 python 原生支持的 sqlite 数据库免安装;为了操作数据库方便,这里就使用轻量级的 ORM – peewee

模型定义

首先完成数据模型的定义,两个模型,一个是 RSS 源模型,一个是 History 历史记录模型。

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
# models.py
# -*- encoding: utf-8 -*-
from peewee import *
import datetime

db = SqliteDatabase('rss.db')


class BaseModel(Model):
class Meta:
database = db


class Rss(BaseModel):
feed = CharField(unique=True) # rss 源地址
cover = CharField(max_length=255) # 封面
title = CharField(max_length=20) # rss 的标题
url = CharField(max_length=255) # 官网地址 可选


class History(BaseModel):
url = CharField(max_length=255)
publish_at = DateField(default=datetime.datetime.now)


def create_tables():
with db:
db.create_tables([Rss, History])

模型定义完成,接下来就是代码的整合了。

整合代码

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
# -*- encoding: utf-8 -*-

"""
rss.py
钉钉 Rss 机器人

"""
from datetime import datetime
import feedparser
from dingtalkchatbot.chatbot import DingtalkChatbot, CardItem, ActionCard
import dateparser
from models import db, Rss, History
import os

class RssRobot:
def __init__(self):
self.robot = DingtalkChatbot(
os.environ.get("DD_WEBHOOK"),
pc_slide=True, secret=os.environ.get("DD_SECRET")) # 两个环境变量

def parse_rss(self):
rss_list = Rss.select()
rss_card_dict = {}
post_url_list = [rss_history.url for rss_history in
History.select().where(History.publish_at == datetime.today().strftime("%Y-%m-%d"))] # 查询当天的历史记录
for rss in rss_list:
rss_history_list = []
card_list = [
CardItem(title=rss.title, url=rss.url, pic_url=rss.cover)]
feed = feedparser.parse(rss.feed)
for entry in feed.entries:
if entry.link not in post_url_list and self.is_today(entry): # 判断链接是否是当天的并且没有推送过的
card_list.append(CardItem(title=f'{entry.title}', url=entry.link,
pic_url='https://ftp.bmp.ovh/imgs/2020/07/6cdb9f606677c9e3.jpg'))
rss_history_list.append(History(url=entry.link))

if len(card_list) > 1:
rss_card_dict[rss.title] = card_list
with db.atomic():
History.bulk_create(rss_history_list, batch_size=10)

return rss_card_dict

def is_today(self, entry):
return dateparser.parse(entry['updated']).date() == datetime.today().date()

def send_rss(self):
rss_card_dict = self.parse_rss()
for key in rss_card_dict:
self.robot.send_feed_card(rss_card_dict[key])


def send_rss():
rss_bot = RssRobot()
rss_bot.send_rss()

if __name__ == '__main__':
send_rss()

到这里,算是完成了 Rss 钉钉机器人的大部分功能了,还差唯一的定时推送,原本考虑是用 Python 的常见的定时任务库去完成的,但是考虑的后续部署,还需要一台服务器,奈何自己的阿里云服务器最低配不一定能支持住了,就想着结合 Github Actions 把服务器也省了。

Github Actions

Github ActionsGithub推出的一个新的功能,可以为我们的项目自动化地构建工作流,例如代码检查,自动化打包,测试,发布版本等等。入口在项目pull request的旁边。


这里就不再详细介绍 Github Actions 了,贴一下我写的 workflows 文件

workflows

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
# 钉钉 rss 推送机器人
name: DingDingRssBot

on:
schedule: # 设置定时任务
- cron: '0 0/1 * * *'

jobs:
build:

runs-on: ubuntu-latest
steps:
- name: 'Checkout codes'
uses: actions/checkout@v2 # 拉取最新的代码
- name: Set up Python 3.7.6
uses: actions/setup-python@v2 # 设置 python 版本
with:
python-version: 3.7.6
- name: Install dependencies # 安装依赖
run: |
cd robot
pip install -r requirements.txt
- name: send rss
env:
DD_WEBHOOK: ${{ secrets.webhook }}
DD_SECRET: ${{ secrets.secret }}
run: |
cd robot
python rss.py
- name: Commit
run: |
git config --global user.email leetao94cn@gmail.com
git config --global user.name lt94
git add .
git commit -m "update" -a
- name: Push changes
uses: ad-m/github-push-action@master
with:
branch: main
github_token: ${{ secrets.TOKEN }}

其中三个环境变量需要我们自行配置,在项目的 Settings 中的 Secrets 中设置

其中 TOKEN 是 Person access tokens,需要在个人设置中申请,申请时需要勾选 admin:repo_hook,repo,workflow 三个选项。

Person access tokens 主要是用来将修改推送到仓库的,借助 Github Actions 运行完成之后会把代码删除,会导致产生的修改丢失,这样子的话历史记录就无法保存,因此我们需要在每次推送完成之后,将对数据库的修改推送到仓库中。

总结

到此所有配置完成,Github Actions 会定时执行 rss 推送任务。

有兴趣的可以用钉钉搜索群号:35731271,或者扫描二维码