├── .gitignore ├── .npmignore ├── Gruntfile.coffee ├── History.txt ├── README.md ├── circle.yml ├── hubot-scripts.json ├── index.coffee ├── libs ├── charset-convert-stream.coffee ├── entries.coffee └── rss-checker.coffee ├── package.json ├── scripts └── hubot-rss-reader.coffee └── tests ├── dummy_bot.coffee ├── test_helper.coffee └── test_rss_checker.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *#* 3 | .DS_Store 4 | node_modules 5 | tmp 6 | *.log 7 | *.rdb 8 | .hubot_history 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *#* 3 | .DS_Store 4 | test/ 5 | tmp/ 6 | circle.yml 7 | *.log 8 | *.rdb 9 | .hubot_history 10 | hubot-scripts.json 11 | Gruntfile.coffee 12 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (grunt) -> 4 | 5 | require 'coffee-errors' 6 | 7 | grunt.loadNpmTasks 'grunt-contrib-watch' 8 | grunt.loadNpmTasks 'grunt-coffeelint' 9 | grunt.loadNpmTasks 'grunt-simple-mocha' 10 | grunt.loadNpmTasks 'grunt-notify' 11 | 12 | grunt.registerTask 'test', [ 'coffeelint', 'simplemocha' ] 13 | grunt.registerTask 'default', [ 'test', 'watch' ] 14 | 15 | grunt.initConfig 16 | 17 | coffeelint: 18 | options: 19 | max_line_length: 20 | value: 0 21 | indentation: 22 | value: 2 23 | newlines_after_classes: 24 | level: 'error' 25 | no_empty_param_list: 26 | level: 'error' 27 | no_unnecessary_fat_arrows: 28 | level: 'ignore' 29 | dist: 30 | files: 31 | src: [ 32 | '**/*.coffee' 33 | '!node_modules/**' 34 | ] 35 | 36 | simplemocha: 37 | options: 38 | ui: 'bdd' 39 | reporter: 'spec' 40 | compilers: 'coffee:coffee-script' 41 | ignoreLeaks: no 42 | dist: 43 | src: [ 'tests/test_*.coffee' ] 44 | 45 | watch: 46 | options: 47 | interrupt: yes 48 | dist: 49 | files: [ 50 | '**/*.{coffee,js}' 51 | '!node_modules/**' 52 | ] 53 | tasks: [ 'test' ] 54 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | === 0.8.3 2016-01-19 2 | 3 | * support lodash v4.0 #43 4 | * thank you for contributing @pastelInc 5 | 6 | === 0.8.2 2016-01-10 7 | 8 | * load scripts via index.coffee to support hubot-help #42 9 | * thank you for contributing @yuya-oc 10 | 11 | === 0.8.1 2015-11-20 12 | 13 | * fixed README 14 | 15 | === 0.8.0 2015-11-20 16 | 17 | * store entry URLs on brain #31 18 | * use only first xml encoding attributes #41 19 | 20 | === 0.7.1 2015-11-18 21 | 22 | * added ENV var "HUBOT_RSS_LIMIT_ON_ADD" #34 23 | * limit printing entries when adding new feed 24 | * default is 5 25 | * disable limit if "false" 26 | 27 | === 0.7.0 2015-11-18 28 | 29 | * fix for coffee-script 1.6.x #40 30 | * thank you for contributing @sairoutine 31 | * convert encoding if not UTF-8 #38 32 | * detect charset from "encoding" attribute of XML, then convert 33 | 34 | === 0.6.9 2015-06-27 35 | 36 | * added ENV var "HUBOT_RSS_PRINTIMAGE" #33 37 | * switch printnig image in summary 38 | * default is "true" 39 | * added ENV var "HUBOT_RSS_PRINTERROR" #33 40 | * switch printing error message from crawler (e.g. 404 not found) 41 | * default is "true" 42 | 43 | === 0.6.8 2015-06-08 44 | 45 | * added ENV var "HUBOT_RSS_IRCCOLORS" #32 46 | * default is "false" 47 | * switch IRC color message 48 | * thank you for contributing @skddc 49 | * using irc-colors npm 50 | 51 | === 0.6.7 2015-04-27 52 | 53 | * added ENV var "HUBOT_RSS_PRINTSUMMARY" #30 54 | * default is "true" 55 | * switch printing entry's summary 56 | * thank you for contributing @skddc 57 | * updated npm dependencies 58 | * bluebird, mocha, coffee-script 59 | 60 | === 0.6.6 2015-04-18 61 | 62 | * works on HipChat (probably) #29 63 | * thank you for contributing @motchang 64 | 65 | === 0.6.5 2015-01-22 66 | 67 | * set User-Agent with ENV variable "HUBOT_RSS_USERAGENT" #25 68 | * decode XML Entities in feed #26 69 | * thank you for contributing @patrys 70 | 71 | === 0.6.4 2015-01-11 72 | 73 | * use robot.logger if DEBUG is not enabled 74 | 75 | === 0.6.3 2015-01-11 76 | 77 | * use "loaded" event of robot.brain #6 78 | 79 | === 0.6.2 2015-01-11 80 | 81 | * queue robot.send, omit too many entries on add #21 82 | 83 | === 0.6.1 2015-01-09 84 | 85 | * add command "hubot rss delete #room_name" #23 86 | * add command "hubot rss dump" #23 87 | 88 | === 0.6.0 2015-01-09 89 | 90 | * bugfix for adapter "hubot-slack" v3 #21 91 | * update README, installation of coffee-script v1.8.x for Promise #20 92 | * thank you for contributing @uk-ar 93 | 94 | === 0.5.4 2014-10-09 95 | 96 | * set HTTP Request timeout 10 sec 97 | * fix tests #13 98 | 99 | === 0.5.3 2014-10-02 100 | 101 | * bugfix for empty summary 102 | 103 | === 0.5.2 2014-09-30 104 | 105 | * bugfix "new entry" notification on "hubot rss add [URL]" #15 106 | 107 | === 0.5.1 2014-09-29 108 | 109 | * use strict 110 | * little fix 111 | * catch error in RSSChecker.check() 112 | * fix RSSChecker.check 113 | * fix command "hubot rss add (url)" 114 | 115 | === 0.5.0 2014-09-29 116 | 117 | * find RSS if page is URL is not a Feed #14 118 | * replace all callback with Promise #13 119 | * fix error message 120 | 121 | === 0.4.0 2014-09-28 122 | 123 | * use entry's description if summary not exists 124 | * cleanup summary if it is html #12 125 | * remove multiple linefeed in summary #12 126 | * set first "img" tag as topic image 127 | 128 | === 0.3.3 2014-09-14 129 | 130 | * reduce too many error notifications #10 131 | 132 | === 0.3.2 2014-09-03 133 | 134 | * bugfix feed parse error #9 135 | 136 | === 0.3.1 2014-07-27 137 | 138 | * show entry summary if it exists #8 139 | 140 | === 0.3.0 2014-07-27 141 | 142 | * show entry summary #8 143 | 144 | === 0.2.0 2014-07-27 145 | 146 | * show feed title #7 147 | * modify robot.respond regex 148 | 149 | === 0.1.7 2014-07-26 150 | 151 | * add tests for RSSChecker#check 152 | 153 | === 0.1.6 2014-07-26 154 | 155 | * update README 156 | 157 | === 0.1.5 2014-07-26 158 | 159 | * set Header Emoji by ENV var "HUBOT_RSS_HEADER", default is "sushi" 160 | 161 | === 0.1.4 2014-07-26 162 | 163 | * bugfix "new entry" detection 164 | 165 | === 0.1.3 2014-07-26 166 | 167 | * use robot.messageRoom(room, text) 168 | * put "#" head of room name 169 | 170 | === 0.1.2 2014-07-26 171 | 172 | * modify interval of each feeds 1sec -> 5sec 173 | 174 | === 0.1.1 2014-07-26 175 | 176 | * bugfix sending room 177 | * test on Travis CI 178 | 179 | === 0.1.0 2014-07-26 180 | 181 | * remove rss-watcher, use feedparser and request 182 | * add tests 183 | 184 | 185 | === 0.0.3 2014-07-23 186 | 187 | * update for rss-watcher v1.2.0 188 | 189 | === 0.0.2 2014-07-22 190 | 191 | * hubot rss (register|add) URL 192 | * add/delete multiple feeds #2 193 | 194 | 195 | === 0.0.1 2014-07-22 196 | 197 | * first release 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hubot RSS Reader 2 | ================ 3 | RSS Reader for each Chat Channels, works with Hubot. 4 | 5 | [![Circle CI](https://circleci.com/gh/shokai/hubot-rss-reader.svg?style=svg)](https://circleci.com/gh/shokai/hubot-rss-reader) 6 | 7 | - https://github.com/shokai/hubot-rss-reader 8 | - https://www.npmjs.org/package/hubot-rss-reader 9 | 10 | ![screen shot](http://gyazo.com/234dfb14d76bb3de9efd88bfe8dc6522.png) 11 | 12 | Requirements 13 | ------------ 14 | 15 | - coffee-script 1.10+ 16 | - hubot-brain 17 | - recommend [hubot-mongodb-brain](http://npmjs.com/package/hubot-mongodb-brain). 18 | 19 | Install 20 | ------- 21 | 22 | % npm install hubot-rss-reader -save 23 | % npm install coffee-script@">=1.10.0" -save 24 | 25 | ### edit `external-script.json` 26 | 27 | ```json 28 | ["hubot-rss-reader"] 29 | ``` 30 | 31 | ### Configure (ENV vars) 32 | 33 | export DEBUG=hubot-rss-reader* # debug print 34 | export HUBOT_RSS_INTERVAL=600 # 600 sec (default) 35 | export HUBOT_RSS_HEADER=:sushi: # RSS Header Emoji (default is "sushi") 36 | export HUBOT_RSS_USERAGENT=hubot # (default is "hubot-rss-reader/#{package_version}") 37 | export HUBOT_RSS_PRINTSUMMARY=true # print summary (default is "true") 38 | export HUBOT_RSS_PRINTIMAGE=false # print image in summary (default is "true") 39 | export HUBOT_RSS_PRINTERROR=false # print error message (default is "true") 40 | export HUBOT_RSS_IRCCOLORS=true # use IRC color message (default is "false") 41 | export HUBOT_RSS_LIMIT_ON_ADD=false # limit printing entries on add new feed. (default is 5) 42 | 43 | Usage 44 | ----- 45 | 46 | ### add 47 | 48 | hubot rss add https://github.com/shokai.atom 49 | # or 50 | hubot rss register https://github.com/shokai.atom 51 | 52 | 53 | ### delete 54 | 55 | hubot rss delete https://github.com/shokai.atom 56 | hubot rss delete #room_name 57 | 58 | ### list 59 | 60 | hubot rss list 61 | hubot rss dump 62 | 63 | 64 | Test 65 | ---- 66 | 67 | % npm install 68 | 69 | % grunt 70 | # or 71 | % npm test 72 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.12 4 | notify: 5 | webhooks: 6 | - url: https://masuilab-hubot2.herokuapp.com/circleci-webhook?room=shookai 7 | -------------------------------------------------------------------------------- /hubot-scripts.json: -------------------------------------------------------------------------------- 1 | ["redis-brain.coffee"] 2 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = (robot, scripts) -> 5 | scriptsPath = path.resolve(__dirname, 'scripts') 6 | fs.exists scriptsPath, (exists) -> 7 | if exists 8 | for script in fs.readdirSync(scriptsPath) 9 | if scripts? and '*' not in scripts 10 | robot.loadFile(scriptsPath, script) if script in scripts 11 | else 12 | robot.loadFile(scriptsPath, script) 13 | -------------------------------------------------------------------------------- /libs/charset-convert-stream.coffee: -------------------------------------------------------------------------------- 1 | # detect charset from "encoding" attribute of XML 2 | # convert using iconv 3 | 4 | 'use strict' 5 | 6 | stream = require 'stream' 7 | Iconv = require('iconv').Iconv 8 | debug = require('debug')('hubot-rss-reader:charset-convert-stream') 9 | 10 | module.exports = -> 11 | 12 | iconv = null 13 | charset = null 14 | 15 | charsetConvertStream = stream.Transform() 16 | 17 | charsetConvertStream._transform = (chunk, enc, next) -> 18 | if charset is null and 19 | m = chunk.toString().match /<\?xml[^>]* encoding=['"]([^'"]+)['"]/ 20 | charset = m[1] 21 | debug "charset: #{charset}" 22 | if charset.toUpperCase() isnt 'UTF-8' 23 | iconv = new Iconv charset, 'UTF-8//TRANSLIT//IGNORE' 24 | if iconv? 25 | @push iconv.convert(chunk) 26 | else 27 | @push chunk 28 | next() 29 | 30 | return charsetConvertStream 31 | 32 | -------------------------------------------------------------------------------- /libs/entries.coffee: -------------------------------------------------------------------------------- 1 | # Data Store for entries 2 | 3 | module.exports = class Entries 4 | 5 | constructor: (@robot) -> 6 | @prefix = 'hubot-rss-reader:entry:' 7 | 8 | key: (url) -> 9 | "#{@prefix}#{url}" 10 | 11 | add: (url) -> 12 | @robot.brain.set @key(url), true 13 | 14 | remove: (url) -> 15 | @robot.brain.set @key(url), false 16 | 17 | include: (url) -> 18 | @robot.brain.get @key(url) 19 | -------------------------------------------------------------------------------- /libs/rss-checker.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # RSS Checker Component for Hubot RSS Reader 3 | # 4 | # Author: 5 | # @shokai 6 | 7 | 'use strict' 8 | 9 | events = require 'events' 10 | _ = require 'lodash' 11 | request = require 'request' 12 | FeedParser = require 'feedparser' 13 | Entities = require('html-entities').XmlEntities 14 | entities = new Entities 15 | async = require 'async' 16 | debug = require('debug')('hubot-rss-reader:rss-checker') 17 | cheerio = require 'cheerio' 18 | Promise = require 'bluebird' 19 | IrcColor = require 'irc-colors' 20 | 21 | charsetConvertStream = require './charset-convert-stream' 22 | Entries = require './entries' 23 | 24 | module.exports = class RSSChecker extends events.EventEmitter 25 | constructor: (@robot) -> 26 | @entries = new Entries @robot 27 | 28 | cleanup_summary = (html = '') -> 29 | summary = do (html) -> 30 | try 31 | $ = cheerio.load html 32 | if process.env.HUBOT_RSS_PRINTIMAGE is 'true' 33 | if img = $('img').attr('src') 34 | return img + '\n' + $.root().text() 35 | return $.root().text() 36 | catch 37 | return html 38 | lines = summary.split /[\r\n]/ 39 | lines = lines.map (line) -> if /^\s+$/.test line then '' else line 40 | summary = lines.join '\n' 41 | return summary.replace(/\n\n\n+/g, '\n\n') 42 | 43 | fetch: (args) -> 44 | new Promise (resolve, reject) => 45 | default_args = 46 | url: null 47 | room: null 48 | 49 | if typeof args is 'string' 50 | args = {url: args} 51 | for k,v of default_args 52 | unless args.hasOwnProperty k 53 | args[k] = v 54 | debug "fetch #{args.url}" 55 | debug args 56 | feedparser = new FeedParser 57 | req = request 58 | uri: args.url 59 | timeout: 10000 60 | encoding: null 61 | headers: 62 | 'User-Agent': process.env.HUBOT_RSS_USERAGENT 63 | 64 | req.on 'error', (err) -> 65 | reject err 66 | 67 | req.on 'response', (res) -> 68 | if res.statusCode isnt 200 69 | return reject "statusCode: #{res.statusCode}" 70 | this 71 | .pipe charsetConvertStream() 72 | .pipe feedparser 73 | 74 | feedparser.on 'error', (err) -> 75 | reject err 76 | 77 | entries = [] 78 | feedparser.on 'data', (chunk) => 79 | entry = 80 | url: chunk.link 81 | title: entities.decode(chunk.title or '') 82 | summary: cleanup_summary entities.decode(chunk.summary or chunk.description or '') 83 | feed: 84 | url: args.url 85 | title: entities.decode(feedparser.meta.title or '') 86 | toString: -> 87 | if process.env.HUBOT_RSS_IRCCOLORS is "true" 88 | s = "#{IrcColor.pink(process.env.HUBOT_RSS_HEADER)} #{@title} #{IrcColor.purple('- ['+@feed.title+']')}\n#{IrcColor.lightgrey.underline(@url)}" 89 | else 90 | s = "#{process.env.HUBOT_RSS_HEADER} #{@title} - [#{@feed.title}]\n#{@url}" 91 | 92 | if process.env.HUBOT_RSS_PRINTSUMMARY is "true" and @summary?.length > 0 93 | s += "\n#{@summary}" 94 | return s 95 | args: args 96 | 97 | debug entry 98 | entries.push entry 99 | unless @entries.include entry.url 100 | @entries.add entry.url 101 | @emit 'new entry', entry 102 | 103 | feedparser.on 'end', -> 104 | resolve entries 105 | 106 | check: (opts = {}) -> 107 | new Promise (resolve) => 108 | debug "start checking all feeds" 109 | feeds = [] 110 | for room, _feeds of (opts.feeds or @robot.brain.get('feeds')) 111 | feeds = feeds.concat _feeds 112 | resolve _.uniq feeds 113 | .then (feeds) => 114 | interval = 1 115 | Promise.each feeds, (url) => 116 | new Promise (resolve) -> 117 | setTimeout => 118 | resolve url 119 | , interval 120 | interval = 5000 121 | .then (url) => 122 | do (opts) => 123 | opts.url = url 124 | @fetch opts 125 | .catch (err) => 126 | debug err 127 | @emit 'error', {error: err, feed: {url: url}} 128 | .then (feeds) -> 129 | new Promise (resolve) -> 130 | debug "check done (#{feeds?.length or 0} feeds)" 131 | resolve feeds 132 | 133 | getAllFeeds: -> 134 | @robot.brain.get 'feeds' 135 | 136 | getFeeds: (room) -> 137 | @getAllFeeds()?[room] or [] 138 | 139 | setFeeds: (room, urls) -> 140 | return unless urls instanceof Array 141 | feeds = @robot.brain.get('feeds') or {} 142 | feeds[room] = urls 143 | @robot.brain.set 'feeds', feeds 144 | 145 | addFeed: (room, url) -> 146 | new Promise (resolve, reject) => 147 | feeds = @getFeeds room 148 | if _.includes feeds, url 149 | return reject "#{url} is already registered" 150 | feeds.push url 151 | @setFeeds room, feeds.sort() 152 | resolve "registered #{url}" 153 | 154 | deleteFeed: (room, url) -> 155 | new Promise (resolve, reject) => 156 | feeds = @getFeeds room 157 | unless _.includes feeds, url 158 | return reject "#{url} is not registered" 159 | feeds.splice feeds.indexOf(url), 1 160 | @setFeeds room, feeds 161 | resolve "deleted #{url}" 162 | 163 | deleteRoom: (name) -> 164 | new Promise (resolve, reject) => 165 | rooms = @getAllFeeds() or {} 166 | unless rooms.hasOwnProperty name 167 | return reject "room ##{name} is not exists" 168 | delete rooms[name] 169 | @robot.brain.set 'feeds', rooms 170 | resolve "deleted room ##{name}" 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-rss-reader", 3 | "private": false, 4 | "version": "0.8.3", 5 | "description": "Hubot RSS Reader", 6 | "main": "index.coffee", 7 | "scripts": { 8 | "test": "grunt test" 9 | }, 10 | "keywords": [ 11 | "hubot", 12 | "rss", 13 | "feed" 14 | ], 15 | "author": "Sho Hashimoto ", 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/shokai/hubot-rss-reader.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/shokai/hubot-rss-reader/issues" 23 | }, 24 | "homepage": "https://github.com/shokai/hubot-rss-reader", 25 | "dependencies": { 26 | "async": "*", 27 | "bluebird": "*", 28 | "cheerio": "*", 29 | "debug": "*", 30 | "feedparser": "*", 31 | "find-rss": "*", 32 | "html-entities": "*", 33 | "iconv": "*", 34 | "irc-colors": "^1.1.1", 35 | "lodash": "*", 36 | "request": "*" 37 | }, 38 | "devDependencies": { 39 | "coffee-errors": "*", 40 | "coffee-script": "^1.10", 41 | "coffeelint": "^1.13", 42 | "grunt": "*", 43 | "grunt-cli": "*", 44 | "grunt-coffeelint": "*", 45 | "grunt-contrib-watch": "*", 46 | "grunt-notify": "*", 47 | "grunt-simple-mocha": "*", 48 | "hubot-scripts": "*", 49 | "mocha": "*" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scripts/hubot-rss-reader.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Hubot RSS Reader 3 | # 4 | # Commands: 5 | # hubot rss add https://github.com/shokai.atom 6 | # hubot rss delete http://shokai.org/blog/feed 7 | # hubot rss delete #room_name 8 | # hubot rss list 9 | # hubot rss dump 10 | # 11 | # Author: 12 | # @shokai 13 | 14 | 'use strict' 15 | 16 | path = require 'path' 17 | _ = require 'lodash' 18 | debug = require('debug')('hubot-rss-reader') 19 | Promise = require 'bluebird' 20 | RSSChecker = require path.join __dirname, '../libs/rss-checker' 21 | FindRSS = Promise.promisify require 'find-rss' 22 | 23 | ## config 24 | package_json = require path.join __dirname, '../package.json' 25 | process.env.HUBOT_RSS_INTERVAL ||= 60*10 # 10 minutes 26 | process.env.HUBOT_RSS_HEADER ||= ':sushi:' 27 | process.env.HUBOT_RSS_USERAGENT ||= "hubot-rss-reader/#{package_json.version}" 28 | process.env.HUBOT_RSS_PRINTSUMMARY ||= "true" 29 | process.env.HUBOT_RSS_PRINTIMAGE ||= "true" 30 | process.env.HUBOT_RSS_PRINTERROR ||= "true" 31 | process.env.HUBOT_RSS_IRCCOLORS ||= "false" 32 | process.env.HUBOT_RSS_LIMIT_ON_ADD ||= 5 33 | 34 | module.exports = (robot) -> 35 | 36 | logger = 37 | info: (msg) -> 38 | return debug msg if debug.enabled 39 | msg = JSON.stringify msg if typeof msg isnt 'string' 40 | robot.logger.info "#{debug.namespace}: #{msg}" 41 | error: (msg) -> 42 | return debug msg if debug.enabled 43 | msg = JSON.stringify msg if typeof msg isnt 'string' 44 | robot.logger.error "#{debug.namespace}: #{msg}" 45 | 46 | send_queue = [] 47 | send = (envelope, body) -> 48 | send_queue.push {envelope: envelope, body: body} 49 | 50 | getRoom = (msg) -> 51 | switch robot.adapterName 52 | when 'hipchat' 53 | msg.message.user.reply_to 54 | else 55 | msg.message.room 56 | 57 | setInterval -> 58 | return if typeof robot.send isnt 'function' 59 | return if send_queue.length < 1 60 | msg = send_queue.shift() 61 | try 62 | robot.send msg.envelope, msg.body 63 | catch err 64 | logger.error "Error on sending to room: \"#{room}\"" 65 | logger.error err 66 | , 2000 67 | 68 | checker = new RSSChecker robot 69 | 70 | ## wait until connect redis 71 | robot.brain.once 'loaded', -> 72 | run = (opts) -> 73 | logger.info "checker start" 74 | checker.check opts 75 | .then -> 76 | logger.info "wait #{process.env.HUBOT_RSS_INTERVAL} seconds" 77 | setTimeout run, 1000 * process.env.HUBOT_RSS_INTERVAL 78 | , (err) -> 79 | logger.error err 80 | logger.info "wait #{process.env.HUBOT_RSS_INTERVAL} seconds" 81 | setTimeout run, 1000 * process.env.HUBOT_RSS_INTERVAL 82 | 83 | run() 84 | 85 | 86 | last_state_is_error = {} 87 | 88 | checker.on 'new entry', (entry) -> 89 | last_state_is_error[entry.feed.url] = false 90 | for room, feeds of checker.getAllFeeds() 91 | if room isnt entry.args.room and 92 | _.includes feeds, entry.feed.url 93 | logger.info "#{entry.title} #{entry.url} => #{room}" 94 | send {room: room}, entry.toString() 95 | 96 | checker.on 'error', (err) -> 97 | logger.error err 98 | if process.env.HUBOT_RSS_PRINTERROR isnt "true" 99 | return 100 | if last_state_is_error[err.feed.url] # reduce error notify 101 | return 102 | last_state_is_error[err.feed.url] = true 103 | for room, feeds of checker.getAllFeeds() 104 | if _.includes feeds, err.feed.url 105 | send {room: room}, "[ERROR] #{err.feed.url} - #{err.error.message or err.error}" 106 | 107 | robot.respond /rss\s+(add|register)\s+(https?:\/\/[^\s]+)$/im, (msg) -> 108 | url = msg.match[2].trim() 109 | last_state_is_error[url] = false 110 | logger.info "add #{url}" 111 | room = getRoom msg 112 | checker.addFeed(room, url) 113 | .then (res) -> 114 | new Promise (resolve) -> 115 | msg.send res 116 | resolve url 117 | .then (url) -> 118 | checker.fetch {url: url, room: room} 119 | .then (entries) -> 120 | entry_limit = 121 | if process.env.HUBOT_RSS_LIMIT_ON_ADD is 'false' 122 | entries.length 123 | else 124 | process.env.HUBOT_RSS_LIMIT_ON_ADD - 0 125 | for entry in entries.splice 0, entry_limit 126 | send {room: room}, entry.toString() 127 | if entries.length > 0 128 | send {room: room}, 129 | "#{process.env.HUBOT_RSS_HEADER} #{entries.length} entries has been omitted" 130 | , (err) -> 131 | msg.send "[ERROR] #{err}" 132 | return if err.message isnt 'Not a feed' 133 | checker.deleteFeed(room, url) 134 | .then -> 135 | FindRSS url 136 | .then (feeds) -> 137 | return if feeds?.length < 1 138 | msg.send _.flatten([ 139 | "found some Feeds from #{url}" 140 | feeds.map (i) -> " * #{i.url}" 141 | ]).join '\n' 142 | .catch (err) -> 143 | msg.send "[ERROR] #{err}" 144 | logger.error err.stack 145 | 146 | 147 | robot.respond /rss\s+delete\s+(https?:\/\/[^\s]+)$/im, (msg) -> 148 | url = msg.match[1].trim() 149 | logger.info "delete #{url}" 150 | checker.deleteFeed(getRoom(msg), url) 151 | .then (res) -> 152 | msg.send res 153 | .catch (err) -> 154 | msg.send err 155 | logger.error err.stack 156 | 157 | robot.respond /rss\s+delete\s+#([^\s]+)$/im, (msg) -> 158 | room = msg.match[1].trim() 159 | logger.info "delete ##{room}" 160 | checker.deleteRoom room 161 | .then (res) -> 162 | msg.send res 163 | .catch (err) -> 164 | msg.send err 165 | logger.error err.stack 166 | 167 | robot.respond /rss\s+list$/i, (msg) -> 168 | feeds = checker.getFeeds getRoom(msg) 169 | if feeds.length < 1 170 | msg.send "nothing" 171 | else 172 | msg.send feeds.join "\n" 173 | 174 | robot.respond /rss dump$/i, (msg) -> 175 | feeds = checker.getAllFeeds() 176 | msg.send JSON.stringify feeds, null, 2 177 | -------------------------------------------------------------------------------- /tests/dummy_bot.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class DummyBot 2 | 3 | constructor: -> 4 | @_brain = {} 5 | 6 | @brain = 7 | get: (key) => 8 | @_brain[key] 9 | set: (key, value) => 10 | @_brain[key] = value 11 | -------------------------------------------------------------------------------- /tests/test_helper.coffee: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test' 2 | -------------------------------------------------------------------------------- /tests/test_rss_checker.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | require path.resolve 'tests', 'test_helper' 3 | 4 | assert = require 'assert' 5 | RSSChecker = require path.resolve 'libs', 'rss-checker' 6 | Promise = require 'bluebird' 7 | DummyBot = require './dummy_bot' 8 | 9 | checker = new RSSChecker new DummyBot 10 | 11 | describe 'RSSChecker', -> 12 | 13 | it 'sohuld have method "fetch"', -> 14 | assert.equal typeof checker['fetch'], 'function' 15 | 16 | describe 'method "fetch"', -> 17 | 18 | it 'should emit the event "new entry", and callback entries Array', -> 19 | 20 | @timeout 5000 21 | 22 | checker = new RSSChecker new DummyBot 23 | _entries = [] 24 | checker.on 'new entry', (entry) -> 25 | _entries.push entry 26 | 27 | checker.fetch 'http://shokai.org/blog/feed' 28 | .then (entries) -> 29 | assert.ok entries instanceof Array 30 | for entry in entries 31 | assert.equal typeof entry.url, 'string', '"url" property not exists' 32 | assert.equal typeof entry.title, 'string', '"title" property not exists' 33 | assert.equal typeof entry.summary, 'string', '"summary" property not exists' 34 | assert.equal typeof entry.feed?.url, 'string', '"feed.url" property not exists' 35 | assert.equal typeof entry.feed?.title, 'string', '"feed.title" property not exists' 36 | assert.equal JSON.stringify(entries.sort()), JSON.stringify(_entries.sort()) 37 | 38 | 39 | 40 | 41 | it 'should not emit the event "new entry" if already crawled', -> 42 | 43 | @timeout 5000 44 | 45 | checker.on 'new entry', (entry) -> 46 | assert.ok false 47 | 48 | checker.fetch 'http://shokai.org/blog/feed' 49 | .then (entries) -> 50 | new Promise (resolve, reject) -> 51 | setTimeout -> 52 | resolve entries 53 | , 500 54 | 55 | 56 | 57 | it 'should have method "check"', -> 58 | assert.equal typeof checker['check'], 'function' 59 | 60 | describe 'methods "check"', -> 61 | 62 | it 'should emit the event "new entry"', -> 63 | 64 | @timeout 15000 65 | 66 | checker = new RSSChecker new DummyBot 67 | checker.on 'new entry', (entry) -> 68 | assert.ok true, 'detect new entry' 69 | 70 | checker.check 71 | feeds: [ 72 | 'http://shokai.org/blog/feed' 73 | 'https://github.com/shokai.atom' 74 | ] 75 | --------------------------------------------------------------------------------