├── .gitignore ├── README.md ├── app.js ├── model.js ├── package.json ├── processes.json ├── public ├── images │ ├── body.gif │ ├── body.jpg │ ├── config.jpg │ ├── search.jpg │ └── trigger.jpg └── yue.css ├── templates └── index.mustache └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | 64 | # End of https://www.gitignore.io/api/node 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## IFTTJ = IFTTT + 即刻 2 | 3 | 4 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const Router = require('koa-router') 3 | const bodyParser = require('koa-bodyparser') 4 | const logger = require('koa-logger') 5 | const views = require('koa-views') 6 | const static = require('koa-static') 7 | const mount = require('koa-mount') 8 | const shortid = require('shortid') 9 | const RSS = require('rss') 10 | const htmlToText = require('html-to-text') 11 | const model = require('./model') 12 | const utils = require('./utils') 13 | 14 | const app = new Koa() 15 | const router = new Router() 16 | 17 | 18 | async function checkId(ctx, next) { 19 | const id = ctx.params.id 20 | ctx.assert(shortid.isValid(id), 404) 21 | await next() 22 | } 23 | 24 | router.get('/', async (ctx, next) => { 25 | const id = shortid.generate() 26 | const url = `${ctx.request.origin}/${id}` 27 | await ctx.render('index.mustache', { url }) 28 | }) 29 | 30 | router.get('/:id', checkId, async (ctx, next) => { 31 | const id = ctx.params.id 32 | const items = await model.getItems(id) 33 | const feed = new RSS({title: `IFTTJ: ${id}`}) 34 | for (let item of items) { 35 | let text = htmlToText.fromString(item.text, { 36 | ignoreHref: true, 37 | ignoreImage: true, 38 | }) 39 | feed.item({ 40 | guid: item.id, 41 | title: text, 42 | description: item.text, 43 | url: item.link, 44 | date: item.time, 45 | }) 46 | } 47 | ctx.type = 'text/xml' 48 | ctx.body = feed.xml() 49 | }) 50 | 51 | router.post('/:id', checkId, async (ctx, next) => { 52 | const id = ctx.params.id 53 | const body = ctx.request.body 54 | ctx.assert(body, 400) 55 | const item = utils.parse(body) 56 | await model.addItem(id, item) 57 | ctx.body = 'ok' 58 | }) 59 | 60 | 61 | app.proxy = true 62 | app.use(logger()) 63 | app.use(bodyParser({ 64 | enableTypes: ['text', 'json', 'form'] 65 | })) 66 | app.use(views(__dirname + '/templates')) 67 | app.use(mount('/static', static('public'))) 68 | 69 | app.use(router.routes()) 70 | app.use(router.allowedMethods()) 71 | 72 | 73 | const port = process.env.PORT || 3000 74 | app.listen(port, () => { 75 | console.log(`server started at ${port}`) 76 | }) 77 | -------------------------------------------------------------------------------- /model.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis') 2 | const Promise = require('bluebird') 3 | 4 | // promisify 5 | Promise.promisifyAll(redis.RedisClient.prototype) 6 | Promise.promisifyAll(redis.Multi.prototype) 7 | 8 | const client = redis.createClient(process.env.REDIS_URL) 9 | 10 | client.on('error', (err) => { 11 | console.log(`Redis error ${err}`) 12 | }) 13 | 14 | function getNextId() { 15 | return client.hincrbyAsync('unique_ids', 'item', 1) 16 | } 17 | 18 | async function getItems(id) { 19 | let itemIds = client.lrangeAsync(`items:${id}`, 0, -1) 20 | return Promise.map(itemIds, (itemId) => { 21 | return client.hgetallAsync(`item:${itemId}`) 22 | }) 23 | } 24 | 25 | async function addItem(id, item) { 26 | if (!item.time) { 27 | item.time = new Date() 28 | } 29 | const itemId = await getNextId() 30 | item.id = itemId 31 | await client.hmsetAsync(`item:${itemId}`, item) 32 | await client.lpushAsync(`items:${id}`, itemId) 33 | } 34 | 35 | module.exports = { getItems, addItem } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ifttj", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "engines": { 11 | "node": "8.4.x" 12 | }, 13 | "author": "wong2", 14 | "license": "MIT", 15 | "dependencies": { 16 | "bluebird": "^3.5.0", 17 | "html-to-text": "^3.2.0", 18 | "koa": "^2.2.0", 19 | "koa-bodyparser": "^4.2.0", 20 | "koa-logger": "^2.0.1", 21 | "koa-mount": "^3.0.0", 22 | "koa-router": "^7.1.1", 23 | "koa-static": "^3.0.0", 24 | "koa-views": "^6.0.1", 25 | "mustache": "^2.3.0", 26 | "redis": "^2.7.1", 27 | "rss": "^1.2.2", 28 | "shortid": "^2.2.8" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /processes.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps" : [ 3 | { 4 | "name": "ifttj", 5 | "script": "app.js", 6 | "exec_mode": "cluster", 7 | "instances": 0, 8 | "max_restarts": 3, 9 | "restart_delay": 2000, 10 | "env": { 11 | "PORT": 3004, 12 | "NODE_ENV": "production" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /public/images/body.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wong2/ifttj/fb77721d7eb0d8f7158b7ce17623a68f409138a4/public/images/body.gif -------------------------------------------------------------------------------- /public/images/body.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wong2/ifttj/fb77721d7eb0d8f7158b7ce17623a68f409138a4/public/images/body.jpg -------------------------------------------------------------------------------- /public/images/config.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wong2/ifttj/fb77721d7eb0d8f7158b7ce17623a68f409138a4/public/images/config.jpg -------------------------------------------------------------------------------- /public/images/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wong2/ifttj/fb77721d7eb0d8f7158b7ce17623a68f409138a4/public/images/search.jpg -------------------------------------------------------------------------------- /public/images/trigger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wong2/ifttj/fb77721d7eb0d8f7158b7ce17623a68f409138a4/public/images/trigger.jpg -------------------------------------------------------------------------------- /public/yue.css: -------------------------------------------------------------------------------- 1 | /** 2 | * yue.css 3 | * 4 | * yue.css is designed for readable content. 5 | * 6 | * Copyright (c) 2013 - 2017 by Hsiaoming Yang. 7 | */ 8 | 9 | .yue { 10 | font: 400 18px/1.62 -apple-system, BlinkMacSystemFont, "Segoe UI", "Droid Sans", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Droid Sans Fallback", "Microsoft YaHei", sans-serif; 11 | color: #444443; 12 | } 13 | 14 | .yue ::-moz-selection { 15 | background-color: rgba(0,0,0,0.2); 16 | } 17 | 18 | .yue ::selection { 19 | background-color: rgba(0,0,0,0.2); 20 | } 21 | .yue h1, 22 | .yue h2, 23 | .yue h3, 24 | .yue h4, 25 | .yue h5, 26 | .yue h6 { 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Droid Sans", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Droid Sans Fallback", "Microsoft YaHei", sans-serif; 28 | color: #222223; 29 | } 30 | .yue h1 { 31 | font-size: 1.8em; 32 | margin: 0.67em 0; 33 | } 34 | .yue > h1 { 35 | margin-top: 0; 36 | font-size: 2em; 37 | } 38 | .yue h2 { 39 | font-size: 1.5em; 40 | margin: 0.83em 0; 41 | } 42 | .yue h3 { 43 | font-size: 1.17em; 44 | margin: 1em 0; 45 | } 46 | .yue h4, 47 | .yue h5, 48 | .yue h6 { 49 | font-size: 1em; 50 | margin: 1.6em 0 1em 0; 51 | } 52 | .yue h6 { 53 | font-weight: 500; 54 | } 55 | .yue p { 56 | margin-top: 0; 57 | margin-bottom: 1.24em; 58 | } 59 | .yue a { 60 | color: #111; 61 | word-wrap: break-word; 62 | -webkit-text-decoration-color: rgba(0, 0, 0, 0.4); 63 | text-decoration-color: rgba(0, 0, 0, 0.4); 64 | } 65 | .yue a:hover { 66 | color: #555; 67 | -webkit-text-decoration-color: rgba(0, 0, 0, 0.6); 68 | text-decoration-color: rgba(0, 0, 0, 0.6); 69 | } 70 | .yue h1 a, .yue h2 a, .yue h3 a { 71 | text-decoration: none; 72 | } 73 | .yue strong, 74 | .yue b { 75 | font-weight: 700; 76 | color: #222223; 77 | } 78 | .yue em, 79 | .yue i { 80 | font-style: italic; 81 | color: #222223; 82 | } 83 | .yue img { 84 | max-width: 100%; 85 | height: auto; 86 | margin: 0.2em 0; 87 | } 88 | .yue a img { 89 | /* Remove border on IE */ 90 | border: none; 91 | } 92 | .yue figure { 93 | position: relative; 94 | clear: both; 95 | outline: 0; 96 | margin: 10px 0 30px; 97 | padding: 0; 98 | min-height: 100px; 99 | } 100 | .yue figure img { 101 | display: block; 102 | max-width: 100%; 103 | margin: auto auto 4px; 104 | box-sizing: border-box; 105 | } 106 | .yue figure figcaption { 107 | position: relative; 108 | width: 100%; 109 | text-align: center; 110 | left: 0; 111 | margin-top: 10px; 112 | font-weight: 400; 113 | font-size: 14px; 114 | color: #666665; 115 | } 116 | .yue figure figcaption a { 117 | text-decoration: none; 118 | color: #666665; 119 | } 120 | .yue hr { 121 | display: block; 122 | width: 14%; 123 | margin: 40px auto 34px; 124 | border: 0 none; 125 | border-top: 3px solid #dededc; 126 | } 127 | .yue blockquote { 128 | margin: 0 0 1.64em 0; 129 | border-left: 3px solid #dadada; 130 | padding-left: 12px; 131 | color: #666664; 132 | } 133 | .yue blockquote a { 134 | color: #666664; 135 | } 136 | .yue ul, 137 | .yue ol { 138 | margin: 0 0 24px 6px; 139 | padding-left: 16px; 140 | } 141 | .yue ul { 142 | list-style-type: square; 143 | } 144 | .yue ol { 145 | list-style-type: decimal; 146 | } 147 | .yue li { 148 | margin-bottom: 0.2em; 149 | } 150 | .yue li ul, 151 | .yue li ol { 152 | margin-top: 0; 153 | margin-bottom: 0; 154 | margin-left: 14px; 155 | } 156 | .yue li ul { 157 | list-style-type: disc; 158 | } 159 | .yue li ul ul { 160 | list-style-type: circle; 161 | } 162 | .yue li p { 163 | margin: 0.4em 0 0.6em; 164 | } 165 | .yue .unstyled { 166 | list-style-type: none; 167 | margin: 0; 168 | padding: 0; 169 | } 170 | .yue code, 171 | .yue tt { 172 | color: #808080; 173 | font-size: 0.96em; 174 | background-color: #f9f9f7; 175 | padding: 1px 2px; 176 | border: 1px solid #eee; 177 | border-radius: 3px; 178 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 179 | word-wrap: break-word; 180 | } 181 | .yue pre { 182 | margin: 1.64em 0; 183 | padding: 7px; 184 | border: none; 185 | border-left: 3px solid #dadada; 186 | padding-left: 10px; 187 | overflow: auto; 188 | line-height: 1.5; 189 | font-size: 0.96em; 190 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 191 | color: #4c4c4c; 192 | background-color: #f9f9f7; 193 | } 194 | .yue pre code, 195 | .yue pre tt { 196 | color: #4c4c4c; 197 | border: none; 198 | background: none; 199 | padding: 0; 200 | } 201 | .yue table { 202 | width: 100%; 203 | max-width: 100%; 204 | border-collapse: collapse; 205 | border-spacing: 0; 206 | margin-bottom: 1.5em; 207 | font-size: 0.96em; 208 | box-sizing: border-box; 209 | } 210 | .yue th, 211 | .yue td { 212 | text-align: left; 213 | padding: 4px 8px 4px 10px; 214 | border: 1px solid #dadada; 215 | } 216 | .yue td { 217 | vertical-align: top; 218 | } 219 | .yue tr:nth-child(even) { 220 | background-color: #efefee; 221 | } 222 | .yue iframe { 223 | display: block; 224 | max-width: 100%; 225 | margin-bottom: 30px; 226 | } 227 | .yue figure iframe { 228 | margin: auto; 229 | } 230 | .yue table pre { 231 | margin: 0; 232 | padding: 0; 233 | border: none; 234 | background: none; 235 | } 236 | @media (min-width: 1100px) { 237 | .yue blockquote { 238 | margin-left: -24px; 239 | padding-left: 20px; 240 | border-width: 4px; 241 | } 242 | .yue blockquote blockquote { 243 | margin-left: 0; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /templates/index.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | IFTTJ 24 | 41 | 42 | 43 |
44 |

IFTTJike

45 |
46 |

连接 IFTTT 即刻

47 |
48 |

原理

49 |

IFTTT 支持 Webhooks ,而即刻支持 RSS 订阅源作为追踪对象,那我们只要在它们之间做一个 Webhooks 到 RSS 的转换,就可以把它们连接起来了

50 |

开始

51 |

就在你打开这个页面时,你已经得到了一个专属的地址:

52 |
{{ url }}
53 |

我们将使用它来配置 IFTTT 和 即刻(以『某人发了新 tweet 为例』)

54 |

配置 IFTTT

55 |

1. 在 IFTTT 创建新 Applet: https://ifttt.com/create

56 |

2. this 处选择 Twitter

57 |

3. trigger 选择 New tweet by a specific user,填入要监控的 Twitter id

58 | 59 |

4. 进入 that 部分,搜索 webhook ,选择搜出来的 Maker Webhooks

60 | 61 |

5. 选择仅有的一个 action: Make a web request

62 |

6. 填入你的专属 URL

63 |
{{ url }}
64 |

Method 选择 POST,Content Type 选择 text/plain,结果如图:

65 |
66 | 67 |
URL 不一样是正常的
68 |
69 |

7: 配置 Body: Body 对应我们将在即刻看到的一条新消息内容,它分为两个部分:文本和链接。这个例子中,我们以推文作为文本部分,tweet 地址作为链接,在它们之间输入三个等号( === )作为分隔:

70 |
71 | 72 |
注意3个等号哦
73 |
74 |

8. 点击 Create action,最后点击 Finish 结束 IFTTT 的配置

75 |

即刻创建新提醒

76 |

接下来,只要在即刻创建新提醒时,选择『RSS订阅源有更新』这个机器人,并填入你的专属 URL 就可以了!

77 |
{{ url }}
78 |
79 | 80 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function parse(body) { 2 | let parts = body.split(/={3,}/) 3 | return { 4 | text: parts[0].trimRight(), 5 | link: (parts[1] || '').trim() 6 | } 7 | } 8 | 9 | module.exports = { parse } 10 | --------------------------------------------------------------------------------