├── 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 |
--------------------------------------------------------------------------------