🐦使用Pipedream同步RSS至Twitter(X.com)

2024-12-30|2025-4-23
Yawatasensei
Yawatasensei
type
status
date
slug
summary
tags
category
icon
password
😀
之前使用过IFTTTDlvr.it进行RSS到Twitter的同步,现在这两个平台都已经收费,无法继续白嫖。在尝试同步RSS到BlueSky的时候发现了PipeDream这个网站,允许用户使用Python或者NodeJS搭建简单的工作流,所以理论上也是可以将RSS同步到Twitter,例如个人博客、豆瓣在看在读等等支持通过RSS Feed输出的信息流。在完成实践并测试成功之后,有了这篇指南,希望可以帮助到需要的朋友。尽管Twitter已经改名叫X,但是我还是更习惯称呼它为Twitter,那是一个黄金时代。

📝 RSS同步Twitter搭建前提

  • 一个Twitter开发者平台账号,申请入口为:Developer Platform,个人使用免费Plan即可,每个月允许500条POSTS。对于同步豆瓣或者个人博客来说,足够使用。Twitter开发者平台注册时会要求填写申请理由,例如:
notion image
我这里提供一份开发者平台申请理由范本,可根据自己需要修改:
🛠
Synchronize RSS to my personal X account for recording movies I have watched, music I have listened to, or content I have posted on my blog, etc. This functionality allows me to maintain a comprehensive and organized record of my personal experiences, preferences, and creative outputs. By integrating RSS feeds with my X account, I can easily keep track of the various forms of media I engage with, whether it's a captivating film that I watched over the weekend, a new album that I discovered and fell in love with, or an insightful blog post that I crafted and shared with my audience.
This synchronization process not only enhances my personal organization but also serves as a digital diary of sorts, capturing my evolving tastes and interests over time. It provides a convenient way to look back at my past activities and reflect on how my preferences have changed or remained consistent. Moreover, this feature can be a valuable tool for personal branding, allowing me to showcase my diverse interests and creative endeavors to my network.
Totally personal applications, this system is designed to cater to my individual needs and privacy preferences. It ensures that all the data being synchronized is managed within the confines of my personal X account, offering a secure and private space where I can store and access my records without any external interference. This level of personalization and control is crucial for maintaining the integrity of my digital identity and ensuring that my personal data remains protected and accessible only to me.
In summary, this RSS synchronization feature transforms my X account into a multifunctional hub where I can document, organize, and reflect upon my personal and creative experiences. It's a tool that not only simplifies my daily interactions with various forms of media but also enriches my digital presence by offering a detailed and dynamic representation of my personal journey.
  • 一个PipeDream账号,申请入口为:pipedream,可以使用Google或者Github账号登录。同样免费计划即可,可以创建3个工作流,足够使用。

📝 RSS同步Twitter配置流程

获取Twitter Key And Tokens

打开https://developer.x.com/en/portal/projects-and-apps,在左侧菜单点击Projects & AppsOverview ,然后系统已经自动生成了一个App,我们需要的是获取这个App的Api以及认证所需的Token。
点击User authentication settings 下的Set Up 按钮,进入配置页面:
notion image
  • App Permissions选择Read and write ,因为我们要发布推文;
  • Type of App选择Web App, Automated App or Bot
  • App InfoCallback URLWebsite URL 填写自己的域名,如果没有就填写pipedream的。
  • 点击Save 会输出一个Client ID和Token,但是这个不是我们需要的,没有什么用。
返回Overview页面,点击PROJECT APP后面的小钥匙图标:
notion image
点击Access Token and Secret 后面的Generate 按钮,会生成Access Token和Access Token Secret,一定要复制保存下来,等下会用到。
点击API Key and Secret 后面的Regenerate ,会生成API KeyAPI Key Secret,同样保存下来,等下在PipeDream中会用到。

配置Pipedream环境变量

现在假设你已经完成了Pipedream的注册,进入后管理后台,点击左侧菜单的Settings ,然后选择Environment Variables
notion image
点击右上的+ New Variable 添加新的变量,总计需要添加4条,对应关系如下:
  • TWITTER_ACCESS_SECRET : 获取的Access Token Secret
  • TWITTER_ACCESS_TOKEN : 获取的Access Token
  • TWITTER_API_SECRET : 获取的API Key Secret
  • TWITTER_API_KEY : 获取的API Key Secret
对应关系不要搞乱,变量名称也不要有错误。

配置Pipedream Project和Workflow

点击左侧菜单的Projects ,然后点击右上角的+ New Project ,填写项目名称,建议填写英文,字数不要太多,因为如果想要删除项目,要重新输入一遍项目名称,很麻烦。例如我就填写Twitter,然后点击Create Project ,完成项目的创建。
在列表中点击刚刚创建的项目名称,然后点击页面中硕大的一个+New 按钮,选择Workflow ,修改一下Workflow的名称,同样名称写的简单点,不然删除很麻烦,其他内容不用动,直接Create Workflow

添加RSS Trigger触发器

notion image
点击Add Trigger ,先添加一个触发器。因为我们是想在RSS有新的条目时,自动发送一条推文,所以这里的触发器就是RSS Feed。
notion image
My Sources中选择New Item in RSS Feed
notion image
Timer :建议选择8小时查询一次,降低请求。UTC懒得选择的话,维持默认也可以。
Feed URL :填写你要读取的RSS Feed地址。
Published After:维持默认不用管。如果想避免发送重复内容或者一次发送太多,可以选择当前时间较前一点点的时间。
点击Save and continue 完成Trigger触发器的配置。Pipedream会尝试读取你填写的RSS Feed地址,并返回读取结果。

添加RSS触发动作

完成上面RSS Trigger的创建之后,我们就需要配置当RSS有新的内容时,所要触发的动作,也就是在Twitter上发送一条推文。
点击画板上,Trigger下面的+号来添加Action。
notion image
选择NodeRun Node Code
notion image
在右侧的代码编辑器中,复制下面的代码内容:
import { TwitterApi } from 'twitter-api-v2'; // 假设已安装 twitter-api-v2 库 import * as cheerio from 'cheerio'; export default defineComponent({ async run({ steps, $:context }) { // 初始化 Twitter API 客户端 const client = new TwitterApi({ appKey: process.env.TWITTER_API_KEY, // 使用环境变量 appSecret: process.env.TWITTER_API_SECRET, // 使用环境变量 accessToken: process.env.TWITTER_ACCESS_TOKEN, // 使用环境变量 accessSecret: process.env.TWITTER_ACCESS_SECRET, // 使用环境变量 }); // 获取触发事件中的链接和标题 const { link, title } = steps.trigger.event; // 抓取网页内容(可选,用于验证 Open Graph 元数据) let html = ''; try { const response = await fetch(link); if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`); html = await response.text(); } catch (error) { console.error("Failed to fetch webpage:", error); return; } // 使用 cheerio 解析 HTML(可选,用于验证 Open Graph 元数据) const $ = cheerio.load(html); const description = $('meta[property="og:description"]').attr('content') || "No description available"; const image = $('meta[property="og:image"]').attr('content'); // 截断 title,确保推文字数不超过 280 个字符 const maxTweetLength = 280; const linkLength = link.length; const maxTitleLength = maxTweetLength - linkLength - 20; // 保留一些空间给固定文本 const truncatedTitle = title.length > maxTitleLength ? title.slice(0, maxTitleLength) + "..." : title; // 生成推文内容 const tweetText = `🔥 New content found: \n"${truncatedTitle}"\n${link}`; // 发布推文(无需手动上传图片) try { const response = await client.v2.tweet({ text: tweetText }); console.log("Tweet posted successfully:", response.data); } catch (error) { console.error("Failed to post tweet:", error); } }, });
然后点击Test 进行代码测试,如果没有问题的话,这时候你的Twitter中就会有一条自动发送消息。然后点击Deploy完成这个通过Pipedream推送RSS内容到Twitter的工作流部署。

Twitter推文(Tweet)效果

notion image
效果大概如图所示(有点丑,但可以用),可根据自己需要对以下代码部分进行修改:
// 生成推文内容 const tweetText = `🔥 New content found: \n"${truncatedTitle}"\n${link}`;
之后如果我有新的代码调整,也会同步更新到这篇文章内。

Twitter同步代码优化版本

  • 增加了针对影视、音乐、读书不同的emoji;
  • 增加了封面图上传功能;
  • 增加了错误判断。
import { TwitterApi } from 'twitter-api-v2'; import * as cheerio from 'cheerio'; import fetch from 'node-fetch'; // ==================== // 环境验证模块 // ==================== const REQUIRED_ENV = [ 'TWITTER_API_KEY', 'TWITTER_API_SECRET', 'TWITTER_ACCESS_TOKEN', 'TWITTER_ACCESS_SECRET' ]; function validateEnvironment() { const missingVars = REQUIRED_ENV.filter(v => !process.env[v]); if (missingVars.length > 0) { throw new Error(`缺失必需环境变量: ${missingVars.join(', ')}`); } } // ==================== // 媒体类型配置 // ==================== const ALLOWED_MIME_TYPES = new Set([ 'image/jpeg', 'image/png', 'image/webp', 'image/gif' ]); const EXTENSION_MIME_MAP = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', gif: 'image/gif' }; // ==================== // 核心功能模块 // ==================== export default defineComponent({ async run({ steps, $ }) { // 环境验证 validateEnvironment(); // 初始化客户端(混合使用v1和v2 API) const twitterClient = new TwitterApi({ appKey: process.env.TWITTER_API_KEY, appSecret: process.env.TWITTER_API_SECRET, accessToken: process.env.TWITTER_ACCESS_TOKEN, accessSecret: process.env.TWITTER_ACCESS_SECRET }); // 提取文章数据 const { link, title, description } = steps.trigger.event; const $doc = cheerio.load(description || ''); // 封面图解析(增强选择器) const findCoverImage = () => { const candidates = [ $doc('meta[property="og:image"]').attr('content'), $doc('meta[name="twitter:image"]').attr('content'), $doc('img').first().attr('src') ]; return candidates.find(url => url && url.startsWith('http')); }; // 媒体上传器(带MIME类型推断) const uploadMediaWithValidation = async (imageUrl) => { try { // 请求头配置 const fetchController = new AbortController(); const fetchTimeout = setTimeout(() => fetchController.abort(), 15000); // 获取图像数据 const response = await fetch(imageUrl, { signal: fetchController.signal, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': new URL(imageUrl).origin } }); clearTimeout(fetchTimeout); // 响应验证 if (!response.ok) throw new Error(`HTTP ${response.status}`); // 推断MIME类型 const determineMimeType = () => { // 从Content-Type头获取 const headerType = response.headers.get('content-type')?.split(';')[0]?.trim(); if (headerType && ALLOWED_MIME_TYPES.has(headerType)) return headerType; // 从URL扩展名获取 const extension = new URL(imageUrl).pathname.split('.').pop()?.toLowerCase(); return EXTENSION_MIME_MAP[extension] || 'application/octet-stream'; }; const mimeType = determineMimeType(); if (!ALLOWED_MIME_TYPES.has(mimeType)) { throw new Error(`不支持的媒体类型: ${mimeType}`); } // 上传到Twitter(使用v1 API) const buffer = await response.buffer(); const uploadResult = await twitterClient.v1.uploadMedia(buffer, { mimeType: mimeType, // ✔️ 正确的参数名 additional_owners: [] }); console.log('媒体上传成功', { mediaId: uploadResult, mimeType, size: buffer.length }); return uploadResult; } catch (error) { console.error('媒体上传失败', { url: imageUrl, error: error.message, stack: error.stack }); return null; } }; // 推文内容构造器 const buildTweetContent = () => { const MAX_TWEET_LENGTH = 280; const hostMapping = { 'movie.douban.com': '🎬', 'book.douban.com': '📖', 'music.douban.com': '🎵' }; // 清理标题 const cleanTitle = title .replace(/^(看过|听过|读过)[《「]|[」》\s]+$/g, '') .trim(); // 基础内容 const hostname = new URL(link).hostname; const baseContent = `${hostMapping[hostname] || '🌟'} ${cleanTitle}\n🔗 ${link}`; // 长度验证(按字节计算) const encoder = new TextEncoder(); const baseBytes = encoder.encode(baseContent).length; if (baseBytes <= MAX_TWEET_LENGTH) return baseContent; // 动态截断算法 const linkBytes = encoder.encode(link).length + 3; // URL + 换行符和符号 const titleMaxBytes = MAX_TWEET_LENGTH - linkBytes - 4; // 安全余量 let byteCount = 0; const validChars = []; for (const char of cleanTitle) { const charBytes = encoder.encode(char).length; if (byteCount + charBytes > titleMaxBytes) break; byteCount += charBytes; validChars.push(char); } return `${hostMapping[hostname] || '🌟'} ${validChars.join('')}…\n🔗 ${link}`; }; // ==================== // 主工作流程 // ==================== try { const coverUrl = findCoverImage(); let mediaId = null; // 处理封面图 if (coverUrl) { console.log('开始处理封面图:', coverUrl); mediaId = await uploadMediaWithValidation(coverUrl); } // 发布推文(使用v2 API) const tweetContent = buildTweetContent(); const tweetData = await twitterClient.v2.tweet({ text: tweetContent, ...(mediaId && { media: { media_ids: [mediaId] } }) }); // 日志输出 console.log('推文发布成功', { tweetId: tweetData.data.id, contentPreview: tweetContent.slice(0, 100) + '...', mediaAttached: !!mediaId, createdAt: new Date().toISOString() }); return tweetData; } catch (error) { // 增强错误处理 console.error('核心流程错误', { errorType: error.constructor.name, message: error.message, stackTrace: error.stack, currentStep: mediaId ? 'tweeting' : 'uploading' }); // 重抛经过修饰的错误 throw new Error(`发布失败: ${error.message}`); } } });

🤗 总结归纳

整个教程整体来说没有太多的技术难度,主要就是一些复制与粘贴的工作。不过因为之前已经在Pipedream中使用了2个Workflow用于BlueSky的RSS同步,所以一共3个的Workflow限制目前刚刚好,如果再多的话,免费版的账号就不支持了。后续会尝试将BlueSky和Twitter的同步尝试写在一个Workflow里。
如无意外,这应该是2024年的最后一篇正式文章。提前祝大家新年快乐,2025年一切顺利。

2025/01/02 更新

代码目前存在一个问题,当URL含有中文字符时,在Twitter会产生截断,我想想办法怎么修一下。

📎 参考文章

 
💡
有关Workflow安装或者使用上的问题,欢迎您在底部评论区留言,一起交流~
使用Caddy反向代理加速NotionNext博客图片访问OpenWRT配置Bird使用OSPF国内外动态路由分流
Loading...