├── log └── .gitkeep ├── .rspec ├── tasks └── .gitkeep ├── .gitignore ├── lib ├── creepy │ ├── hooks.rb │ ├── notifies.rb │ ├── tasks │ │ ├── console.rb │ │ ├── base.rb │ │ ├── cli.rb │ │ ├── new.rb │ │ ├── stream.rb │ │ └── find.rb │ ├── tasks.rb │ ├── logger.rb │ ├── notifies │ │ └── im_kayac_com.rb │ ├── configuration.rb │ ├── hooks │ │ ├── mongo.rb │ │ ├── user.rb │ │ ├── keyword.rb │ │ └── event.rb │ └── helper.rb └── creepy.rb ├── config ├── accounts.yml.sample └── config.rb ├── creepy.rb ├── spec ├── creepy │ ├── notifies │ │ └── im_kayac_com_spec.rb │ ├── configration_spec.rb │ └── hooks │ │ ├── user_spec.rb │ │ ├── event_spec.rb │ │ └── keyword_spec.rb └── spec_helper.rb ├── Gemfile └── README.md /log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | -cfs 2 | -------------------------------------------------------------------------------- /tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | tasks/*.rb 3 | config/*.yml 4 | log/*.log 5 | -------------------------------------------------------------------------------- /lib/creepy/hooks.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Dir[File.dirname(__FILE__) + '/hooks/*.rb'].each {|f| require f} 4 | -------------------------------------------------------------------------------- /lib/creepy/notifies.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | Dir[File.dirname(__FILE__) + '/notifies/*.rb'].each {|f| require f} 4 | -------------------------------------------------------------------------------- /lib/creepy.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | require 'creepy/configuration' 5 | require 'creepy/helper' 6 | require 'creepy/logger' 7 | require 'creepy/tasks' 8 | require 'creepy/hooks' 9 | require 'creepy/notifies' 10 | end 11 | -------------------------------------------------------------------------------- /lib/creepy/tasks/console.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Tasks 5 | class Console < Base 6 | Tasks.add_task :console, self 7 | 8 | desc 'Boots up the creepy pry console' 9 | 10 | def console 11 | Pry.start 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/accounts.yml.sample: -------------------------------------------------------------------------------- 1 | --- 2 | :twitter: 3 | :consumer_key: your_consumer_key 4 | :consumer_secret: your_consumer_secret 5 | :oauth_token: your_oauth_token 6 | :oauth_token_secret: your_oauth_token_secret 7 | :im_kayac_com: 8 | :username: your_username 9 | # :password: your_password 10 | # :sig_key: your_sig 11 | -------------------------------------------------------------------------------- /creepy.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | root = File.dirname(__FILE__) 5 | $LOAD_PATH.unshift File.join(root, 'lib') 6 | $LOAD_PATH.unshift File.join(root, 'config') 7 | ENV['BUNDLE_GEMFILE'] ||= File.join(root, 'Gemfile') 8 | 9 | require 'rubygems' 10 | require 'bundler' 11 | Bundler.require 12 | require 'creepy' 13 | require 'config' 14 | 15 | Creepy::Tasks::Cli.start ARGV 16 | -------------------------------------------------------------------------------- /spec/creepy/notifies/im_kayac_com_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') 4 | 5 | describe Creepy::Notifies::ImKayacCom do 6 | describe :call do 7 | it 'call メソッドが実装されていること' do 8 | im_kayac_com = Creepy::Notifies::ImKayacCom.new(username: 'user') 9 | ImKayac.should_receive(:post).with('user', 'poyo: poyopoyo', {}) 10 | im_kayac_com.call('poyo', 'poyopoyo') 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'twitter', '~> 2.2.4' 4 | gem 'userstream', '~> 1.2.2' 5 | gem 'mongo', '~> 1.6.2' 6 | gem 'bson_ext', '~> 1.6.2' 7 | gem 'natto', '~> 0.9.4', require: false 8 | gem 'im-kayac', '~> 0.0.5' 9 | gem 'configatron', '~> 2.9.1' 10 | gem 'thor', '0.14.6' 11 | gem 'activesupport', '~> 3.2.3', require: 'active_support/core_ext' 12 | gem 'i18n', '~> 0.6.0' 13 | gem 'pry', '~> 0.9.9.4' 14 | 15 | group :development do 16 | gem 'rspec', '~> 2.9.0' 17 | end 18 | -------------------------------------------------------------------------------- /lib/creepy/tasks.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Tasks 5 | class << self 6 | def mappings 7 | @mappings ||= ActiveSupport::OrderedHash.new 8 | end 9 | 10 | def add_task(name, klass) 11 | mappings[name] = klass 12 | end 13 | end 14 | end 15 | end 16 | 17 | require 'creepy/tasks/base' 18 | require 'creepy/tasks/cli' 19 | Dir[File.dirname(__FILE__) + '/tasks/*.rb'].each {|f| require f} 20 | Dir[Creepy.root + '/tasks/*.rb'].each {|f| require f} 21 | -------------------------------------------------------------------------------- /lib/creepy/tasks/base.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require 'thor/group' 4 | 5 | module Creepy 6 | module Tasks 7 | class Base < Thor::Group 8 | class << self 9 | def banner 10 | "#{basename} #{name.split('::').last.downcase}" 11 | end 12 | end 13 | 14 | attr_reader :logger 15 | 16 | private 17 | def tee(level, message) 18 | if [:error, :warn, :fatal].include?(level) 19 | shell.error message 20 | else 21 | shell.say message 22 | end 23 | logger.send level, message if logger 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/creepy/logger.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Logger 5 | extend self 6 | 7 | def new(name, *args) 8 | file = File.join(Creepy.log_dir, name) 9 | logger = ::Logger.new(file, *args) 10 | logger.progname = Creepy 11 | logger.formatter = ::Logger::Formatter.new 12 | 13 | logger 14 | end 15 | end 16 | 17 | module SimpleLogger 18 | extend self 19 | 20 | def new(name, *args) 21 | file = File.join(Creepy.log_dir, name) 22 | ::Logger.new(file, *args) 23 | end 24 | end 25 | 26 | register_default do |config| 27 | config.set_default :logger, Creepy::Logger.new('creepy.log') 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/creepy/tasks/cli.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Tasks 5 | class Cli < Base 6 | def setup 7 | task_name = ARGV.delete_at(0).to_s.downcase.to_sym if ARGV[0].present? 8 | task = Creepy::Tasks.mappings[task_name] 9 | 10 | if task 11 | task.start ARGV 12 | else 13 | self.class.help(shell) 14 | end 15 | end 16 | 17 | class << self 18 | def banner 19 | "#{basename} [task]" 20 | end 21 | 22 | def desc 23 | description = "Tasks:\n" 24 | Creepy::Tasks.mappings.map do |k, v| 25 | description << " #{basename} #{k.to_s.ljust(10)} # #{v.desc}\n" 26 | end 27 | 28 | description 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | root = File.join(File.dirname(__FILE__), '..') 4 | $LOAD_PATH.unshift File.join(root, 'lib') 5 | ENV['BUNDLE_GEMFILE'] ||= File.join(root, 'Gemfile') 6 | 7 | require 'rubygems' 8 | require 'bundler' 9 | Bundler.require 10 | require 'creepy' 11 | 12 | def create_credentials(screen_name) 13 | Hashie::Mash.new({ 14 | screen_name: screen_name 15 | }) 16 | end 17 | 18 | def create_status(screen_name, text, source = '') 19 | Hashie::Mash.new({ 20 | text: text, 21 | user: { 22 | screen_name: screen_name, 23 | }, 24 | source: source 25 | }) 26 | end 27 | 28 | def create_event_status(screen_name, event, text) 29 | Hashie::Mash.new({ 30 | event: event, 31 | target_object: { 32 | text: text 33 | }, 34 | source: { 35 | screen_name: screen_name, 36 | } 37 | }) 38 | end 39 | 40 | -------------------------------------------------------------------------------- /lib/creepy/notifies/im_kayac_com.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require 'digest/sha1' 4 | 5 | module Creepy 6 | module Notifies 7 | class ImKayacCom 8 | attr_accessor :username, :password, :sig_key 9 | 10 | extend Creepy::Configuration 11 | 12 | def initialize(options = {}) 13 | @username = options[:username] 14 | @password = options[:password] 15 | @sig_key = options[:sig_key] 16 | end 17 | 18 | def call(title, message, options = {}) 19 | message = "#{title}: #{message}" 20 | 21 | if @sig_key && !options[:sig] 22 | options[:sig] = Digest::SHA1.hexdigest(message + @sig_key) 23 | elsif @password && !options[:password] 24 | options[:password] = @password 25 | end 26 | 27 | ImKayac.post(username, message, options) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/creepy/configuration.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class Module 4 | def to_configatron(*args) 5 | self.name.to_configatron(*args) 6 | end 7 | end 8 | 9 | module Creepy 10 | class << self 11 | def defaults 12 | @defaults ||= [] 13 | end 14 | 15 | def reload_config! 16 | configatron.reset! 17 | defaults.each {|d| d.call} 18 | config_path = File.join(root, 'config', 'config.rb') 19 | load config_path 20 | end 21 | end 22 | 23 | module Configuration 24 | def config(*args) 25 | to_configatron(*args) 26 | end 27 | 28 | def configure 29 | yield config 30 | self 31 | end 32 | 33 | def register_default(&block) 34 | default = lambda do 35 | configure &block 36 | end 37 | Creepy.defaults << default 38 | default.call 39 | self 40 | end 41 | end 42 | 43 | extend Configuration 44 | end 45 | -------------------------------------------------------------------------------- /lib/creepy/hooks/mongo.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | register_default do |config| 5 | config.mongo do |mongo| 6 | mongo.set_default :host, 'localhost' 7 | mongo.set_default :port, 27017 8 | mongo.set_default :db_name, 'creepy' 9 | mongo.set_default :connection, nil 10 | mongo.set_default :db, nil 11 | end 12 | end 13 | 14 | module Hooks 15 | module Mongo 16 | extend self 17 | 18 | def new(db) 19 | lambda do |status| 20 | db.collection(key_from_status(status)).insert(status) 21 | end 22 | end 23 | 24 | def with_mecab(db) 25 | require 'natto' 26 | nm = Natto::MeCab.new('-O wakati') 27 | lambda do |status| 28 | if status.text 29 | status.keywords = nm.parse(status.text).split(' ') 30 | end 31 | db.collection(key_from_status(status)).insert(status) 32 | end 33 | end 34 | 35 | private 36 | def key_from_status(status) 37 | key = %w{friends event delete}.find {|key| status.key? key} || 'status' 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/creepy/helper.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Helper 5 | def root 6 | File.expand_path(File.join(File.dirname(__FILE__), '..', '..')) 7 | end 8 | 9 | def accounts 10 | YAML.load_file(File.join(root, 'config', 'accounts.yml')) 11 | end 12 | 13 | def log_dir 14 | File.join(Creepy.root, 'log') 15 | end 16 | 17 | def logger 18 | config.logger 19 | end 20 | 21 | def connection 22 | mongo = config.mongo 23 | mongo.connection ||= Mongo::Connection.new(mongo.host, mongo.port) 24 | end 25 | 26 | def db 27 | mongo = config.mongo 28 | mongo.db ||= connection.db(mongo.db_name) 29 | end 30 | 31 | def twitter(options = {}) 32 | Twitter.new(accounts[:twitter].merge(options)) 33 | end 34 | alias_method :client, :twitter 35 | 36 | def user_stream(options = {}) 37 | UserStream.client(accounts[:twitter].merge(options)) 38 | end 39 | alias_method :stream, :user_stream 40 | 41 | def im_kayac_com(options = {}) 42 | Creepy::Notifies::ImKayacCom.new(accounts[:im_kayac_com].merge(options)) 43 | end 44 | end 45 | 46 | extend Helper 47 | end 48 | -------------------------------------------------------------------------------- /lib/creepy/tasks/new.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Tasks 5 | class New < Base 6 | Tasks.add_task :new, self 7 | 8 | include Thor::Actions 9 | 10 | desc 'Create new task' 11 | 12 | def self.banner 13 | "#{super} [task_name]" 14 | end 15 | 16 | argument :task_name 17 | 18 | def create 19 | task_path = File.join(Creepy.root, 'tasks', "#{task_name}.rb") 20 | create_file(task_path) do 21 | template = open(__FILE__).readlines 22 | .grep(/^##/) 23 | .map {|l| l.gsub(/^##/, '')} 24 | .join('') 25 | template % { 26 | :task_class_name => task_name.camelize, 27 | :task_name => task_name 28 | } 29 | end 30 | end 31 | end 32 | end 33 | end 34 | 35 | ## # -*- coding: utf-8 -*- 36 | ## 37 | ## module Creepy 38 | ## module Tasks 39 | ## class %{task_class_name} < Base 40 | ## Tasks.add_task :%{task_name}, self 41 | ## 42 | ## def setup 43 | ## end 44 | ## 45 | ## def %{task_name} 46 | ## end 47 | ## 48 | ## def teardown 49 | ## end 50 | ## end 51 | ## end 52 | ## end 53 | -------------------------------------------------------------------------------- /lib/creepy/hooks/user.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Hooks 5 | class User 6 | attr_accessor :include, :hooks, :formatter, :notifies 7 | 8 | def initialize(options = {}, &block) 9 | @include = options.fetch :include, [] 10 | @hooks = options.fetch :hooks, [] 11 | @formatter = options.fetch :formatter, Formatter.default 12 | @notifies = options.fetch :notifies, [] 13 | yield self if block_given? 14 | end 15 | 16 | def call(status) 17 | return unless status.text 18 | return unless @include.flatten.any? {|u| status.user.screen_name == u} 19 | screen_name = status.user.screen_name 20 | @hooks.each {|h| h.call(screen_name, status)} 21 | title, message, options = @formatter.call(screen_name, status) 22 | @notifies.each {|n| n.call(title, message, options)} 23 | end 24 | 25 | module Formatter 26 | extend self 27 | 28 | def default 29 | lambda do |screen_name, status| 30 | ["@#{screen_name} Say", 31 | "#{status.text} from #{status.source.gsub(/<\/?[^>]*>/, '')}", 32 | {}] 33 | end 34 | end 35 | 36 | def simple 37 | lambda do |screen_name, status| 38 | ["@#{screen_name} Say", 39 | status.text.truncate(40), 40 | {}] 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/creepy/configration_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require File.join(File.dirname(__FILE__), '..', 'spec_helper') 4 | 5 | describe Creepy::Configuration do 6 | after(:each) do 7 | configatron.reset! 8 | end 9 | 10 | describe :config do 11 | it 'Configuration::Store のインスタンスであること' do 12 | Creepy.config.should be_a Configatron::Store 13 | end 14 | 15 | it '値が設定出来ること' do 16 | Creepy.config.hoge.should be_nil 17 | Creepy.config.hoge = :hoge 18 | Creepy.config.hoge.should == :hoge 19 | end 20 | end 21 | 22 | describe :configure do 23 | it 'ブロックに Creepy.config が渡されること' do 24 | Creepy.configure do |config| 25 | config.should == Creepy.config 26 | end 27 | end 28 | 29 | it 'ブロックで値が設定出来ること' do 30 | Creepy.config.hoge.should be_nil 31 | Creepy.configure do |config| 32 | config.hoge = :hoge 33 | end 34 | Creepy.config.hoge.should == :hoge 35 | end 36 | end 37 | 38 | describe :register_default do 39 | it 'ブロックに Creepy.config が渡されること' do 40 | Creepy.register_default do |config| 41 | config.should == Creepy.config 42 | end 43 | end 44 | 45 | it 'ブロックで値が設定出来ること' do 46 | Creepy.config.hoge.should be_nil 47 | Creepy.register_default do |config| 48 | config.hoge = :hoge 49 | end 50 | Creepy.config.hoge.should == :hoge 51 | end 52 | 53 | it 'restore_defaults 時に default が評価されること' do 54 | dummy = lambda {} 55 | dummy.should_receive(:call).twice 56 | Creepy.register_default do 57 | dummy.call 58 | end 59 | Creepy.reload_config! 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /config/config.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require 'yaml' 4 | 5 | Creepy.configure do |config| 6 | ## タスクの設定 7 | config.tasks do |tasks| 8 | ## Stream タスクの設定 9 | tasks.stream do |stream| 10 | ## パラメータの設定 11 | stream.params[:replies] = :all 12 | 13 | ## MongoDB に保存 14 | stream.hooks << Creepy::Hooks::Mongo.with_mecab(Creepy.db) 15 | 16 | ## Keyword 通知 17 | stream.hooks << Creepy::Hooks::Keyword.new do |keyword| 18 | keyword.include << 'twitter' 19 | keyword.exclude << /^.*(RT|QT):? @[\w]+.*$/i 20 | keyword.hooks << lambda do |keyword, status| 21 | status.keyword = keyword 22 | Creepy.db.collection('keyword').insert(status) 23 | end 24 | 25 | ## im.kayac.com に通知 26 | keyword.notifies << Creepy.im_kayac_com 27 | 28 | ## ログに保存 29 | logger = Creepy::SimpleLogger.new('creepy.keyword.log') 30 | keyword.notifies << lambda do |title, message, options = {}| 31 | logger.info "#{Time.now} #{title}: #{message.gsub(/\n/, ' ')}" 32 | end 33 | end 34 | 35 | ## Event 通知 36 | stream.hooks << Creepy::Hooks::Event.with_adapter do |adapter| 37 | ## 通知する event を設定 38 | adapter.notify :reply 39 | adapter.notify :retweet 40 | adapter.notify :direct_message 41 | adapter.notify :favorite 42 | adapter.notify :unfavorite 43 | adapter.notify :follow 44 | adapter.notify :list_member_added 45 | adapter.notify :list_member_removed 46 | adapter.notify :list_user_subscribed 47 | adapter.notify :list_user_unsubscribed 48 | 49 | ## im.kayac.com に通知 50 | adapter.notifies << Creepy.im_kayac_com 51 | 52 | ## ログに保存 53 | logger = Creepy::SimpleLogger.new('creepy.event.log') 54 | adapter.notifies << lambda do |title, message, options = {}| 55 | logger.info "#{Time.now} #{title}: #{message.gsub(/\n/, ' ')}" 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/creepy/tasks/stream.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Tasks 5 | class Stream < Base 6 | Tasks.add_task :stream, self 7 | 8 | desc 'Starts up creepy stream' 9 | 10 | extend Creepy::Configuration 11 | 12 | register_default do |stream| 13 | stream.set_default :params, {} 14 | stream.set_default :hooks, [] 15 | end 16 | 17 | def setup 18 | config = Creepy.config 19 | @client = Creepy.client 20 | @stream = Creepy.stream 21 | @hooks = self.class.config.hooks || [] 22 | @params = self.class.config.params || {} 23 | @logger = config.logger 24 | @credentials = @client.verify_credentials 25 | @hooks.each do |h| 26 | h.credentials = @credentials if h.respond_to? :credentials= 27 | end 28 | tee :info, "Stream#setup: read config" 29 | rescue 30 | tee :warn, "Stream#setup: #{$!.message} (#{$!.class})" 31 | raise SystemExit 32 | end 33 | 34 | def trap 35 | Signal.trap :HUP do 36 | begin 37 | Creepy.reload_config! 38 | setup 39 | rescue Error 40 | tee :error, "Stream#trap: #{$!.message} (#{$!.class})" 41 | end 42 | end 43 | Signal.trap :TERM do 44 | tee :info, 'Stream#trap: terminated' 45 | raise SystemExit 46 | end 47 | end 48 | 49 | def boot 50 | loop do 51 | tee :info, 'Stream#boot: start receive stream' 52 | request 53 | end 54 | end 55 | 56 | private 57 | def request 58 | @stream.user @params, &method(:read) 59 | rescue 60 | tee :error, "Stream#request: #{$!.message} (#{$!.class})" 61 | end 62 | 63 | def read(status) 64 | tee :debug, "Stream#read: receive body" 65 | @hooks.each {|h| h.call(status)} 66 | rescue 67 | tee :error, "Stream#read: #{$!.message} (#{$!.class})" 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/creepy/hooks/keyword.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Hooks 5 | class Keyword 6 | attr_accessor :include, :exclude, :hooks, :formatter, :notifies, :ignore_self, :credentials 7 | 8 | def initialize(options = {}, &block) 9 | @include = options.fetch :include, [] 10 | @exclude = options.fetch :exclude, [] 11 | @hooks = options.fetch :hooks, [] 12 | @formatter = options.fetch :formatter, Formatter.default 13 | @notifies = options.fetch :notifies, [] 14 | @ignore_self = options.fetch :ignore_self, false 15 | yield self if block_given? 16 | end 17 | 18 | def call(status) 19 | return unless status.text 20 | return unless @include.flatten.any? {|k| status.text.match(k)} 21 | keyword = $&.to_s 22 | return if @exclude.flatten.any? {|k| status.text.match(k)} 23 | return if ignore_self? && yourself?(status.user.screen_name) 24 | @hooks.each {|h| h.call(keyword, status)} 25 | title, message, options = @formatter.call(keyword, status) 26 | @notifies.each {|n| n.call(title, message, options)} 27 | end 28 | 29 | def ignore_self! 30 | @ignore_self = true 31 | end 32 | alias_method :ignore_self?, :ignore_self 33 | 34 | private 35 | def yourself?(screen_name) 36 | credentials.screen_name == screen_name 37 | end 38 | 39 | module Formatter 40 | extend self 41 | 42 | def default 43 | lambda do |keyword, status| 44 | ["@#{status.user.screen_name} \"#{keyword}\"", 45 | "#{status.text} from #{status.source.gsub(/<\/?[^>]*>/, '')}", 46 | {}] 47 | end 48 | end 49 | 50 | def simple 51 | lambda do |keyword, status| 52 | ["@#{status.user.screen_name} \"#{keyword}\"", 53 | status.text.truncate(40), 54 | {}] 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/creepy/hooks/user_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') 4 | 5 | describe Creepy::Hooks::User do 6 | context '@include=["9m"]' do 7 | before do 8 | @user = Creepy::Hooks::User.new 9 | @user.include << '9m' 10 | @hook = lambda {|screen_name, status|} 11 | @user.hooks << @hook 12 | @notify = lambda{|title, message, options|} 13 | @user.notifies << @notify 14 | end 15 | 16 | it '@9m からのツイートにマッチすること' do 17 | status = create_status('9m', 'あの夏') 18 | @hook.should_receive(:call) 19 | .with('9m', status) 20 | @user.formatter.should_receive(:call) 21 | .with('9m', status) 22 | .and_return(['@9m: Say', 'あの夏', {}]) 23 | @notify.should_receive(:call) 24 | .with('@9m: Say', 'あの夏', {}) 25 | @user.call(status) 26 | end 27 | 28 | it '@mitukiii からのツイートにマッチしないこと' do 29 | status = create_status('mitukiii', 'あの夏') 30 | @hook.should_not_receive(:call) 31 | @user.formatter.should_not_receive(:call) 32 | @notify.should_not_receive(:call) 33 | @user.call(status) 34 | end 35 | end 36 | 37 | describe :Formatter do 38 | before do 39 | @status = create_status('9m', 40 | 'あの夏' * 20, 41 | 'YoruFukurou') 42 | end 43 | 44 | describe :default do 45 | it 'default のフォーマット' do 46 | formatter = Creepy::Hooks::User::Formatter.default 47 | title, message = formatter.call('9m', @status) 48 | title.should == '@9m Say' 49 | message.should == "#{'あの夏' * 20} from YoruFukurou" 50 | end 51 | end 52 | 53 | describe :simple do 54 | it 'simple なフォーマット' do 55 | formatter = Creepy::Hooks::User::Formatter.simple 56 | title, message = formatter.call('9m', @status) 57 | title.should == '@9m Say' 58 | message.should == "あの夏あの夏あの夏あの夏あの夏あの夏あの夏あの夏あの夏あの夏あの夏あの夏あ..." 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/creepy/hooks/event_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') 4 | 5 | describe Creepy::Hooks::Event do 6 | context 'favorite イベント' do 7 | before do 8 | @credentials = create_credentials('mitukiii') 9 | @event = Creepy::Hooks::Event.new 10 | @event.stub!(:credentials).and_return(@credentials) 11 | @event.adapter = Creepy::Hooks::Event::Adapter.new 12 | @notify = Creepy::Notifies::ImKayacCom.new 13 | @event.adapter.notifies << @notify 14 | end 15 | 16 | context '自分からの favorite イベント' do 17 | before do 18 | @status = create_event_status('mitukiii', 'favorite', 'たーねこいんざおうちなうよー') 19 | end 20 | 21 | it 'favorite の handler notifies が呼ばれないこと' do 22 | on_handler = lambda {|status|} 23 | @event.adapter.on(:unfavorite, &on_handler) 24 | notify_handler = lambda {|status|} 25 | @event.adapter.notify(:unfavorite, ¬ify_handler) 26 | on_handler.should_not_receive(:call) 27 | notify_handler.should_not_receive(:call) 28 | @notify.should_not_receive(:call) 29 | @event.call(@status) 30 | end 31 | end 32 | 33 | context '他の人からの favorite イベント' do 34 | before do 35 | @status = create_event_status(rand.to_s, 'favorite', 'たーねこいんざおうちなうよー') 36 | end 37 | 38 | it 'favorite の handler と notifies が呼ばれること' do 39 | on_handler = lambda {|status|} 40 | @event.adapter.on(:favorite, &on_handler) 41 | notify_handler = lambda {|status|} 42 | @event.adapter.notify(:favorite, ¬ify_handler) 43 | on_handler.should_receive(:call) 44 | .with(@status) 45 | notify_handler.should_receive(:call) 46 | .with(@status) 47 | .and_return(['@mitukiii favorite', 'たーねこいんざおうちなうよー', {}]) 48 | @notify.should_receive(:call) 49 | .with('@mitukiii favorite', 'たーねこいんざおうちなうよー', {}) 50 | @event.call(@status) 51 | end 52 | 53 | it 'unfavorite の handler notifies が呼ばれないこと' do 54 | on_handler = lambda {|status|} 55 | @event.adapter.on(:unfavorite, &on_handler) 56 | notify_handler = lambda {|status|} 57 | @event.adapter.notify(:unfavorite, ¬ify_handler) 58 | on_handler.should_not_receive(:call) 59 | notify_handler.should_not_receive(:call) 60 | @notify.should_not_receive(:call) 61 | @event.call(@status) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/creepy/hooks/keyword_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') 4 | 5 | describe Creepy::Hooks::Keyword do 6 | context '@include=["たーねこ"] @exlucde=["はうすなう"]' do 7 | before do 8 | @credentials = create_credentials('mitukiii') 9 | @keyword = Creepy::Hooks::Keyword.new 10 | @keyword.stub!(:credentials).and_return(@credentials) 11 | @keyword.include << 'たーねこ' 12 | @keyword.exclude << 'はうすなう' 13 | @hook = lambda {|keyword, status|} 14 | @keyword.hooks << @hook 15 | @notify = lambda{|title, message, options|} 16 | @keyword.notifies << @notify 17 | end 18 | 19 | context '自分の "たーねこいんざおうちなうよー" というツイート' do 20 | it 'マッチすること' do 21 | status = create_status('mitukiii', 'たーねこいんざおうちなうよー') 22 | @hook.should_receive(:call) 23 | .with('たーねこ', status) 24 | @keyword.formatter.should_receive(:call) 25 | .with('たーねこ', status) 26 | .and_return(['@mitukiii: "たーねこ"', 'たーねこいんざおうちなうよー', {}]) 27 | @notify.should_receive(:call) 28 | .with('@mitukiii: "たーねこ"', 'たーねこいんざおうちなうよー', {}) 29 | @keyword.call(status) 30 | end 31 | 32 | context :ignore_self! do 33 | it 'マッチしないこと' do 34 | @keyword.ignore_self! 35 | status = create_status('mitukiii', 'たーねこいんざおうちなうよー') 36 | @hook.should_not_receive(:call) 37 | @keyword.formatter.should_not_receive(:call) 38 | @notify.should_not_receive(:call) 39 | @keyword.call(status) 40 | end 41 | 42 | after do 43 | @keyword.ignore_self = false 44 | end 45 | end 46 | end 47 | 48 | context '自分の "たーねこいんざはうすなうよー" というツイート' do 49 | it 'マッチしないこと' do 50 | status = create_status('mitukiii', 'たーねこいんざはうすなうよー') 51 | @hook.should_not_receive(:call) 52 | @keyword.formatter.should_not_receive(:call) 53 | @notify.should_not_receive(:call) 54 | @keyword.call(status) 55 | end 56 | end 57 | end 58 | 59 | describe :Formatter do 60 | before do 61 | @status = create_status('mitukiii', 62 | 'たーねこいんざおうちなうよー' * 4, 63 | 'YoruFukurou') 64 | end 65 | 66 | describe :default do 67 | it 'default のフォーマット' do 68 | formatter = Creepy::Hooks::Keyword::Formatter.default 69 | title, message = formatter.call('たーねこ', @status) 70 | title.should == '@mitukiii "たーねこ"' 71 | message.should == "#{'たーねこいんざおうちなうよー' * 4} from YoruFukurou" 72 | end 73 | end 74 | 75 | describe :simple do 76 | it 'simple なフォーマット' do 77 | formatter = Creepy::Hooks::Keyword::Formatter.simple 78 | title, message = formatter.call('たーねこ', @status) 79 | title.should == '@mitukiii "たーねこ"' 80 | message.should == "たーねこいんざおうちなうよーたーねこいんざおうちなうよーたーねこいんざおう..." 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/creepy/tasks/find.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Tasks 5 | class Find < Base 6 | Tasks.add_task :find, self 7 | 8 | desc 'Find tweets' 9 | 10 | DATE_FORMAT = '%Y-%m-%d %H:%M' 11 | TIME_ZONE = DateTime.now.zone.to_i.hours 12 | 13 | class_option :screen_name, :aliases => '-n', :type => :string, 14 | :desc => 'Filter screen_name separated by a comma.' 15 | class_option :keywords, :aliases => '-k', :type => :string, 16 | :desc => 'Filter keywords separated by a comma.' 17 | class_option :text, :aliases => '-t', :type => :string, 18 | :desc => 'A regular expression to filter the text.' 19 | 20 | class_option :deleted, :aliases => '-d', :type => :boolean, :default => false, 21 | :desc => 'Show deleted status only.' 22 | 23 | class_option :sort, :aliases => '-s', :type => :string, :default => 'id,desc', 24 | :desc => 'Sort key (Descending), or sort key & direction pair that are separated by a comma.' 25 | class_option :limit, :aliases => '-l', :type => :numeric, 26 | :desc => 'Number of tweets.' 27 | 28 | def setup 29 | @db = Creepy.db 30 | @col = @db['status'] 31 | @sel = {'text' => {'$exists' => true}} 32 | @opts = {} 33 | @highlight = nil 34 | end 35 | 36 | def build_selecter 37 | highlight_keywords = [] 38 | 39 | if options[:screen_name] 40 | users = options[:screen_name].split(',') 41 | @sel['user.screen_name'] = {'$in' => users} 42 | @col.ensure_index([['user.screen_name', Mongo::ASCENDING]]) 43 | end 44 | 45 | if options[:keywords] 46 | keywords = options[:keywords].split(',').map {|k| Regexp.escape(k)} 47 | highlight_keywords << keywords 48 | regexps = keywords.map {|k| Regexp.new(k, 'i')} 49 | @sel['keywords'] = {'$all' => regexps} 50 | @col.ensure_index([['keywords', Mongo::ASCENDING]]) 51 | end 52 | 53 | if options[:text] 54 | text = options[:text] 55 | highlight_keywords << text 56 | regexp = Regexp.new(text, 'i') 57 | @sel['text'] = {'$regex' => regexp} 58 | @col.ensure_index([['text', Mongo::ASCENDING]]) 59 | end 60 | 61 | if options[:deleted] 62 | sel = {'delete.status' => {'$exists' => true}} 63 | deleted_ids = @db['delete'].find(sel).to_a.map {|s| s['delete']['status']['id']} 64 | @sel['id'] = {'$in' => deleted_ids} 65 | @col.ensure_index([['id', Mongo::DESCENDING]]) 66 | end 67 | 68 | unless highlight_keywords.empty? 69 | @highlight = Regexp.new('(' + highlight_keywords.join('|') + ')', 'i') 70 | end 71 | end 72 | 73 | def build_options 74 | if options[:sort] 75 | key, direction = *options[:sort].split(',') 76 | direction = :desc unless direction 77 | @opts[:sort] = [key, direction] 78 | end 79 | 80 | if options[:limit] 81 | @opts[:limit] = options[:limit] 82 | end 83 | end 84 | 85 | def find 86 | @col.find(@sel, @opts).each do |status| 87 | created_at = (DateTime.parse(status['created_at']) + TIME_ZONE).strftime(DATE_FORMAT) 88 | screen_name = ('@' + status['user']['screen_name'] + ':').ljust(18) 89 | text = status['text'].gsub(/\n?$/, "\n") 90 | if @highlight 91 | text = text.gsub(@highlight, shell.set_color('\1', :red)) 92 | end 93 | 94 | shell.say "#{created_at}: #{screen_name} #{text}" 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/creepy/hooks/event.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Creepy 4 | module Hooks 5 | class Event 6 | attr_accessor :adapter, :credentials 7 | 8 | def self.with_adapter(&block) 9 | new(Adapter.new(&block)) 10 | end 11 | 12 | def initialize(adapter = nil) 13 | @adapter = adapter 14 | end 15 | 16 | def call(status) 17 | return unless @adapter 18 | if status.event 19 | return if yourself? status.source.screen_name 20 | event = status.event.to_sym 21 | elsif status.direct_message 22 | return if yourself? status.direct_message.sender_screen_name 23 | event = :direct_message 24 | elsif status.retweeted_status 25 | return unless yourself? status.retweeted_status.user.screen_name 26 | event = :retweet 27 | elsif yourself? status.in_reply_to_screen_name 28 | event = :reply 29 | else 30 | return 31 | end 32 | @adapter.call(event, status) 33 | end 34 | 35 | private 36 | def yourself?(screen_name) 37 | credentials.screen_name == screen_name 38 | end 39 | 40 | class Adapter 41 | attr_accessor :handlers, :notifies 42 | 43 | def initialize(options = {}, &block) 44 | @handlers = options.fetch :handlers, {} 45 | @notifies = options.fetch :notifies, [] 46 | yield self if block_given? 47 | end 48 | 49 | def handler(event) 50 | @handlers[event] ||= [] 51 | end 52 | 53 | def on(event, &block) 54 | event = event.to_sym 55 | handler(event) << block 56 | self 57 | end 58 | 59 | def notify(event, &block) 60 | if block.nil? && Formatter.respond_to?(event) 61 | block = Formatter.send(event) 62 | end 63 | on(event) do |status| 64 | title, message, options = block.call(status) 65 | @notifies.each {|n| n.call(title, message, options)} 66 | end 67 | end 68 | 69 | def call(event, status) 70 | handler(event).each {|h| h.call(status)} 71 | end 72 | end 73 | 74 | module Formatter 75 | extend self 76 | 77 | def reply 78 | lambda do |status| 79 | ["@#{status.user.screen_name} Mentioned", 80 | "#{status.text}", 81 | {}] 82 | end 83 | end 84 | 85 | def retweet 86 | lambda do |status| 87 | ["@#{status.user.screen_name} Retweeted", 88 | "#{status.retweeted_status.text}", 89 | {}] 90 | end 91 | end 92 | 93 | def direct_message 94 | lambda do |status| 95 | ["@#{status.direct_message.sender.screen_name} Sent message", 96 | "#{status.direct_message.text}", 97 | {}] 98 | end 99 | end 100 | 101 | def favorite 102 | lambda do |status| 103 | ["@#{status.source.screen_name} Favorited", 104 | status.target_object.text, 105 | {}] 106 | end 107 | end 108 | 109 | def unfavorite 110 | lambda do |status| 111 | ["@#{status.source.screen_name} Unfavorited", 112 | status.target_object.text, 113 | {}] 114 | end 115 | end 116 | 117 | def follow 118 | lambda do |status| 119 | ["@#{status.source.screen_name} Followed", 120 | "@#{status.target.screen_name}", 121 | {}] 122 | end 123 | end 124 | 125 | def list_member_added 126 | lambda do |status| 127 | ["@#{status.source.screen_name} Added to list", 128 | "#{status.target_object.full_name}", 129 | {}] 130 | end 131 | end 132 | 133 | def list_member_removed 134 | lambda do |status| 135 | ["@#{status.source.screen_name} Removed from list", 136 | "#{status.target_object.full_name}", 137 | {}] 138 | end 139 | end 140 | 141 | def list_user_subscribed 142 | lambda do |status| 143 | ["@#{status.source.screen_name} Subscribed list", 144 | "#{status.target_object.full_name}", 145 | {}] 146 | end 147 | end 148 | 149 | def list_user_unsubscribed 150 | lambda do |status| 151 | ["@#{status.source.screen_name} Unsubscribed list", 152 | "#{status.target_object.full_name}", 153 | {}] 154 | end 155 | end 156 | end 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # creepy 2 | 3 | Twitter のユーザーストリームを受け取ってアレコレするアプリ 4 | 5 | ## 主な機能 6 | 7 | * 全てのステータスの MongoDB への保存 8 | * 指定したキーワード文字列/正規表現にマッチしたツイートの通知 9 | * 指定したユーザーのツイートの通知 10 | * 指定したイベントの通知 11 | * MongoDB からツイートの検索 12 | 13 | ## 必要なもの 14 | 15 | Ruby 16 | MongoDB 17 | 18 | ### RubyGems 19 | 20 | twitter 21 | userstream 22 | mongo 23 | bson_ext 24 | natto 25 | im-kayac 26 | configatron 27 | thor 28 | activesupport 29 | i18n 30 | pry 31 | 32 | ## インストール 33 | 34 | git clone git://github.com/mitukiii/creepy.git 35 | cd creepy 36 | 37 | # 必要な依存 gem をインストール 38 | bundle install 39 | 40 | # アカウント設定ファイルのサンプルをコピー 41 | cp config/accounts.yml.sample config/accounts.yml 42 | 43 | # Twitter アカウントと im.kaya.com アカウントの設定 44 | emacs config/accounts.yml 45 | 46 | # その他アプリケーションの設定 47 | emacs config/config.rb 48 | 49 | ## 使い方 50 | 51 | # ユーザーストリームを受信開始する 52 | ./creepy.rb stream 53 | 54 | # ツイートを検索 55 | ./creepy.rb find 56 | 57 | ## 設定 58 | 59 | ### 基本事項 60 | 61 | 設定は config/config.rb ファイルを編集し Ruby で書く 62 | ハードルは高いがその分自由度も高い 63 | 64 | ### 受け取ったユーザーストリームのアレコレの設定 65 | 66 | Creepy.config.tasks.stream(以下 stream)に設定する 67 | 68 | ### ユーザーストリーム接続時のパラメータ設定 69 | 70 | stream.params がパラメータとしてそのまま送信される 71 | 72 | # 全てのリプライを受け取る 73 | stream.params[:replies] = :all 74 | 75 | # トラッキングするキーワードの設定 76 | stream.params[:track] = [:twitter, :tumblr] 77 | 78 | 詳しいことは Twitter の [UserStream の仕様書](https://dev.twitter.com/docs/streaming-api/user-streams) をどうぞ 79 | 80 | ### 受け取ったユーザーストリームについての設定 81 | 82 | stream.hooks に利用する hook を追加していく 83 | 84 | ### 全てのステータスの MongoDB への保存 85 | 86 | stream.hooks に Creepy::Hooks::Mongo クラスのインスタンスを追加する 87 | 88 | # 全てのステータスを種類ごとに collection に分け保存 89 | stream.hooks << Creepy::Hooks::Mongo.new(Creepy.config.mongo.db) 90 | 91 | # status.text の MeCab による分かち書きを status.keywords に配列として追加して保存 92 | stream.hooks << Creepy::Hooks::Mongo.with_mecab(Creepy.config.mongo.db) 93 | 94 | with_mecab で初期化すれば status.text が分かち書きされた上で保存されるので 95 | 後から検索したい時など役に立つ(かもしれない) 96 | 97 | MeCab が入ってない、入れるのが面倒くさい場合は new で初期化すれば良い 98 | 99 | ### 指定したキーワード文字列/正規表現にマッチしたツイートの通知 100 | 101 | stream.hooks に Creepy::Hooks::Keyword クラスのインスタンスを追加する 102 | 103 | keyword.include にマッチさせたいキーワード文字列/正規表現を設定 104 | keyword.exclude に除外したいキーワード文字列/正規表現を設定 105 | keyword.hooks に keyword と status を受け取る処理を設定 106 | keyword.notifies に title と message(, options)を受け取る処理を設定 107 | 108 | stream.hooks << Creepy::Hooks::Keyword.new do |keyword| 109 | # 自分のツイートを除外 110 | keyword.ignore_self! 111 | 112 | # キーワード設定 113 | keyword.include << 'twitter' 114 | 115 | # 除外キーワード設定 116 | keyword.exclude << /^.*(RT|QT):? @[\w]+.*$/i 117 | 118 | ## マッチした keyword と status を keyword collection に保存 119 | keyword.hooks << lambda do |keyword, status| 120 | status.keyword = keyword 121 | config.mongo.db.collection('keyword').insert(status) 122 | end 123 | 124 | ## im.kayac.com に通知 125 | keyword.notifies << Creepy::Notifies::ImKayacCom.new 126 | 127 | ## ログに保存 128 | log_file = File.join(log_dir, 'creepy.keyword.log') 129 | logger = Logger.new(log_file) 130 | keyword.notifies << lambda do |title, message, options = {}| 131 | logger.info "#{Time.now} #{title}: #{message.gsub(/\n/, ' ')}" 132 | end 133 | end 134 | 135 | hooks と notifies は 136 | hooks は生の keyword と status を受け取る 137 | notifies は keyword と status を元に加工された title と message(, options)を受け取る 138 | という点で違う 139 | 140 | notifies に渡される title と message(, options)は formatter を変更することでカスタマイズすつことも出来る 141 | デフォルトでは Creepy::Hooks::Keyword::Formatter.default が使用される 142 | formatter は keyword, status を受け取り [title, message, options] を返す必要がある 143 | 144 | formatter をカスタマイズすることにより im.kayac.com での通知時に Tweetbot を開く URL Scheme を追加することも出来る 145 | 146 | stream.hooks << Creepy::Hooks::Keyword.new do |keyword| 147 | ... 148 | keyword.formatter = lambda do |keyword, status| 149 | title, message, options = Creepy::Hooks::Keyword::Formatter.default.call(keyword, status) 150 | options[:handler] = "tweetbot://#{config.twitter.credentials.screen_name}/status/#{status.id}" 151 | 152 | [title, message, options] 153 | end 154 | ... 155 | end 156 | 157 | ### 指定したユーザーのツイートの通知 158 | 159 | stream.hooks に Creepy::Hooks::User クラスのインスタンスを追加する 160 | 161 | user.include に通知したいユーザーの screen_name を設定 162 | user.hooks に screen_name と status を受け取る処理を設定 163 | user.notifies に title と message(, options)を受け取る処理を設定 164 | 165 | stream.hooks << Creepy::Hooks::User.new do |user| 166 | ## ユーザー設定 167 | user.include << 'mitukiii' 168 | 169 | ## im.kayac.com に通知 170 | user.notifies << Creepy::Notifies::ImKayacCom.new 171 | 172 | ## ログに保存 173 | log_file = File.join(log_dir, 'creepy.user.log') 174 | logger = Logger.new(log_file) 175 | user.notifies << lambda do |title, message, options = {}| 176 | logger.info "#{Time.now} #{title}: #{message.gsub(/\n/, ' ')}" 177 | end 178 | end 179 | 180 | hooks と notifies、また formatter については Creepy::Hooks::Keyword と同様 181 | 182 | ### 指定したイベントの通知 183 | 184 | stream.hooks に Creepy::Hooks::Event クラスのインスタンスを追加する 185 | Creepy::Hooks::Event::Adapter を使って簡単に設定することが出来る 186 | 187 | stream.hooks << Creepy::Hooks::Event.with_adapter do |adapter| 188 | ## 通知する event を設定 189 | adapter.notify :reply 190 | adapter.notify :retweet 191 | adapter.notify :direct_message 192 | adapter.notify :favorite 193 | adapter.notify :unfavorite 194 | adapter.notify :follow 195 | adapter.notify :list_member_added 196 | adapter.notify :list_member_removed 197 | adapter.notify :list_user_subscribed 198 | adapter.notify :list_user_unsubscribed 199 | 200 | ## im.kayac.com に通知 201 | adapter.notifies << Creepy::Notifies::ImKayacCom.new 202 | 203 | ## ログに保存 204 | log_file = File.join(log_dir, 'creepy.event.log') 205 | logger = Logger.new(log_file) 206 | adapter.notifies << lambda do |title, message, options = {}| 207 | logger.info "#{Time.now} #{title}: #{message.gsub(/\n/, ' ')}" 208 | end 209 | end 210 | 211 | adapter.notify で通知させたいイベントをシンボルで指定する 212 | adapter.on でイベントごとの処理をカスタマイズすることも出来る 213 | 214 | stream.hooks << Creepy::Hooks::Event.with_adapter do |adapter| 215 | ... 216 | adapter.on(:replay) do |status| 217 | # リプライを受け取った時にしたい処理を書く 218 | end 219 | ... 220 | end 221 | 222 | あるいは全てのイベントを自分でハンドリングすることも出来る 223 | 224 | event_hook = Creepy::Hooks::Event.new 225 | event_hook.adapter = lambda do |event, status| 226 | # ここにイベントを受け取った時にしたい処理を書く 227 | end 228 | stream.hooks << event_hook 229 | 230 | ### 自分で hook を書く 231 | 232 | status を受け取る call メソッドか実装されたクラスか lambda / proc で処理が書ける 233 | 234 | # 標準出力に受け取ったステータスを表示 235 | stream.hooks << lambda do |status| 236 | puts status.inspect 237 | end 238 | 239 | ## ツイートの検索 240 | 241 | $ ./creepy.rb find --help 242 | Usage: 243 | creepy.rb find 244 | 245 | Options: 246 | -n, [--screen-name=SCREEN_NAME] # Filter screen_name separated by a comma. 247 | -k, [--keywords=KEYWORDS] # Filter keywords separated by a comma. 248 | -t, [--text=TEXT] # A regular expression to filter the text. 249 | -s, [--sort=SORT] # Sort key (Descending), or sort key & direction pair that are separated by a comma. 250 | # Default: id,desc 251 | -l, [--limit=N] # Number of tweets. 252 | 253 | Find tweets 254 | 255 | --screen-name でユーザー名をカンマ区切りで指定 256 | 複数指定した場合は OR 検索 257 | 258 | --keywords でキーワードをカンマ区切りで指定 259 | キーワードは MongoDB への 保存時に MeCab による分かち書きを保存している必要がある 260 | 複数指定した場合は AND 検索 261 | 262 | --text でテキストの正規表現を指定 263 | 264 | --sort でソートするキーと順番をカンマ区切り指定 265 | 例えば id の降順にしたい場合は id,desc と指定する 266 | 267 | --limit で検索するツイートの最大数を指定 268 | 269 | ## コピーライト 270 | 271 | http://sam.zoy.org/wtfpl 272 | --------------------------------------------------------------------------------