Merge pull request #549 from deanputney/master

TumblrPublishAgent

Andrew Cantino 10 年之前
父节点
当前提交
8d6090faf9

+ 3 - 0
.env.example

@@ -89,6 +89,9 @@ THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=
89 89
 GITHUB_OAUTH_KEY=
90 90
 GITHUB_OAUTH_SECRET=
91 91
 
92
+TUMBLR_OAUTH_KEY=
93
+TUMBLR_OAUTH_SECRET=
94
+
92 95
 #############################
93 96
 #  AWS and Mechanical Turk  #
94 97
 #############################

+ 4 - 0
Gemfile

@@ -20,6 +20,10 @@ gem 'twitter', '~> 5.8.0' # Must to be loaded before cantino-twitter-stream.
20 20
 gem 'cantino-twitter-stream', github: 'cantino/twitter-stream', branch: 'master'
21 21
 gem 'omniauth-twitter'
22 22
 
23
+# Tumblr Agents
24
+gem 'tumblr_client'
25
+gem 'omniauth-tumblr'
26
+
23 27
 # Optional Services.
24 28
 gem 'omniauth-37signals'          # BasecampAgent
25 29
 # gem 'omniauth-github'

+ 11 - 0
Gemfile.lock

@@ -214,6 +214,8 @@ GEM
214 214
       multi_json (~> 1.3)
215 215
       oauth2 (~> 0.9.3)
216 216
       omniauth (~> 1.2)
217
+    omniauth-tumblr (1.1)
218
+      omniauth-oauth (~> 1.0)
217 219
     omniauth-twitter (1.0.1)
218 220
       multi_json (~> 1.3)
219 221
       omniauth-oauth (~> 1.0)
@@ -340,6 +342,13 @@ GEM
340 342
     treetop (1.4.15)
341 343
       polyglot
342 344
       polyglot (>= 0.3.1)
345
+    tumblr_client (0.8.4)
346
+      faraday (~> 0.9.0)
347
+      faraday_middleware (~> 0.9.0)
348
+      json
349
+      mime-types
350
+      oauth
351
+      simple_oauth
343 352
     twilio-ruby (3.11.6)
344 353
       builder (>= 2.1.2)
345 354
       jwt (>= 0.1.2)
@@ -431,6 +440,7 @@ DEPENDENCIES
431 440
   nokogiri (~> 1.6.1)
432 441
   omniauth
433 442
   omniauth-37signals
443
+  omniauth-tumblr
434 444
   omniauth-twitter
435 445
   pg
436 446
   protected_attributes (~> 1.0.8)
@@ -454,6 +464,7 @@ DEPENDENCIES
454 464
   spring
455 465
   spring-commands-rspec
456 466
   therubyracer (~> 0.12.1)
467
+  tumblr_client
457 468
   twilio-ruby (~> 3.11.5)
458 469
   twitter (~> 5.8.0)
459 470
   typhoeus (~> 0.6.3)

+ 36 - 0
app/concerns/tumblr_concern.rb

@@ -0,0 +1,36 @@
1
+module TumblrConcern
2
+  extend ActiveSupport::Concern
3
+
4
+  included do
5
+    include Oauthable
6
+
7
+    valid_oauth_providers :tumblr
8
+  end
9
+
10
+  def tumblr_consumer_key
11
+    ENV['TUMBLR_OAUTH_KEY']
12
+  end
13
+
14
+  def tumblr_consumer_secret
15
+    ENV['TUMBLR_OAUTH_SECRET']
16
+  end
17
+
18
+  def tumblr_oauth_token
19
+    service.token
20
+  end
21
+
22
+  def tumblr_oauth_token_secret
23
+    service.secret
24
+  end
25
+
26
+  def tumblr
27
+    Tumblr.configure do |config|
28
+      config.consumer_key = tumblr_consumer_key
29
+      config.consumer_secret = tumblr_consumer_secret
30
+      config.oauth_token = tumblr_oauth_token
31
+      config.oauth_token_secret = tumblr_oauth_token_secret
32
+    end
33
+    
34
+    Tumblr::Client.new
35
+  end
36
+end

+ 1 - 1
app/helpers/application_helper.rb

@@ -43,7 +43,7 @@ module ApplicationHelper
43 43
 
44 44
   def icon_for_service(service)
45 45
     case service.to_sym
46
-    when :twitter, :github
46
+    when :twitter, :tumblr, :github
47 47
       "<i class='fa fa-#{service}'></i>".html_safe
48 48
     else
49 49
       "<i class='fa fa-lock'></i>".html_safe

+ 163 - 0
app/models/agents/tumblr_publish_agent.rb

@@ -0,0 +1,163 @@
1
+require "tumblr_client"
2
+
3
+module Agents
4
+  class TumblrPublishAgent < Agent
5
+    include TumblrConcern
6
+
7
+    cannot_be_scheduled!
8
+
9
+    description <<-MD
10
+      #{'## Include `tumblr_client` and `omniauth-tumblr` in your Gemfile to use this Agent!' if dependencies_missing?}
11
+
12
+      The TumblrPublishAgent publishes Tumblr posts from the events it receives.
13
+
14
+      To be able to use this Agent you need to authenticate with Tumblr in the [Services](/services) section first.
15
+
16
+
17
+
18
+      **Required fields:**
19
+
20
+      `blog_name` Your Tumblr URL (e.g. "mustardhamsters.tumblr.com") 
21
+
22
+      `post_type` One of [text, photo, quote, link, chat, audio, video] 
23
+
24
+
25
+      -------------
26
+
27
+      You may leave any of the following optional fields blank. Including a field not allowed for the specified `post_type` will cause a failure.
28
+
29
+      **Any post type**
30
+
31
+      * `state` published, draft, queue, private
32
+      * `tags` Comma-separated tags for this post
33
+      * `tweet` off, text for tweet
34
+      * `date` GMT date and time of the post as a string
35
+      * `format` html, markdown
36
+      * `slug` short text summary at end of the post URL
37
+
38
+      **Text** `title` `body` 
39
+
40
+      **Photo** `caption` `link`  `source`
41
+
42
+      **Quote** `quote` `source`
43
+
44
+      **Link** `title` `url` `description` 
45
+
46
+      **Chat** `title` `conversation`
47
+
48
+      **Audio** `caption` `external_url`
49
+
50
+      **Video** `caption` `embed`
51
+
52
+
53
+      -------------
54
+
55
+      [Full information on field options](https://www.tumblr.com/docs/en/api/v2#posting)
56
+
57
+      Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
58
+    MD
59
+
60
+    gem_dependency_check { defined?(Tumblr) }
61
+
62
+    def validate_options
63
+      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
64
+    end
65
+
66
+    def working?
67
+      event_created_within?(interpolated['expected_update_period_in_days']) && most_recent_event && most_recent_event.payload['success'] == true && !recent_error_logs?
68
+    end
69
+
70
+    def default_options
71
+      {
72
+        'expected_update_period_in_days' => "10",
73
+        'blog_name' => "{{blog_name}}",
74
+        'post_type' => "{{post_type}}",
75
+        'options' => {
76
+          'state' => "{{state}}",
77
+          'tags' => "{{tags}}",
78
+          'tweet' => "{{tweet}}",
79
+          'date' => "{{date}}",
80
+          'format' => "{{format}}",
81
+          'slug' => "{{slug}}",
82
+          'title' => "{{title}}",
83
+          'body' => "{{body}}",
84
+          'caption' => "{{caption}}",
85
+          'link' => "{{link}}",
86
+          'source' => "{{source}}",
87
+          'quote' => "{{quote}}",
88
+          'url' => "{{url}}",
89
+          'description' => "{{description}}",
90
+          'conversation' => "{{conversation}}",
91
+          'external_url' => "{{external_url}}",
92
+          'embed' => "{{embed}}",
93
+        },
94
+      }
95
+    end
96
+
97
+    def receive(incoming_events)
98
+      # if there are too many, dump a bunch to avoid getting rate limited
99
+      if incoming_events.count > 20
100
+        incoming_events = incoming_events.first(20)
101
+      end
102
+      incoming_events.each do |event|
103
+        blog_name = interpolated(event)['blog_name']
104
+        post_type = interpolated(event)['post_type']
105
+        options = interpolated(event)['options']
106
+        begin
107
+          post = publish_post(blog_name, post_type, options)
108
+          create_event :payload => {
109
+            'success' => true,
110
+            'published_post' => "["+blog_name+"] "+post_type,
111
+            'post_id' => post["id"],
112
+            'agent_id' => event.agent_id,
113
+            'event_id' => event.id
114
+          }
115
+        end
116
+      end
117
+    end
118
+
119
+    def publish_post(blog_name, post_type, options)      
120
+      options_obj = { 
121
+          :state => options['state'],
122
+          :tags => options['tags'],
123
+          :tweet => options['tweet'],
124
+          :date => options['date'],
125
+          :format => options['format'],
126
+          :slug => options['slug'],
127
+        }
128
+
129
+      case post_type
130
+      when "text"
131
+        options_obj[:title] = options['title']
132
+        options_obj[:body] = options['body']
133
+        tumblr.text(blog_name, options_obj)
134
+      when "photo"
135
+        options_obj[:caption] = options['caption']
136
+        options_obj[:link] = options['link']
137
+        options_obj[:source] = options['source']
138
+        tumblr.photo(blog_name, options_obj)
139
+      when "quote"
140
+        options_obj[:quote] = options['quote']
141
+        options_obj[:source] = options['source']
142
+        tumblr.quote(blog_name, options_obj)
143
+      when "link"
144
+        options_obj[:title] = options['title']
145
+        options_obj[:url] = options['url']
146
+        options_obj[:description] = options['description']
147
+        tumblr.link(blog_name, options_obj)
148
+      when "chat"
149
+        options_obj[:title] = options['title']
150
+        options_obj[:conversation] = options['conversation']
151
+        tumblr.chat(blog_name, options_obj)
152
+      when "audio"
153
+        options_obj[:caption] = options['caption']
154
+        options_obj[:external_url] = options['external_url']
155
+        tumblr.audio(blog_name, options_obj)
156
+      when "video"
157
+        options_obj[:caption] = options['caption']
158
+        options_obj[:embed] = options['embed']
159
+        tumblr.video(blog_name, options_obj)
160
+      end
161
+    end
162
+  end
163
+end

+ 6 - 0
config/initializers/devise.rb

@@ -219,6 +219,12 @@ Devise.setup do |config|
219 219
     config.omniauth :twitter, key, secret, authorize_params: {force_login: 'true', use_authorize: 'true'}
220 220
   end
221 221
 
222
+  if defined?(OmniAuth::Strategies::Tumblr) &&
223
+     (key = ENV["TUMBLR_OAUTH_KEY"]).present? &&
224
+     (secret = ENV["TUMBLR_OAUTH_SECRET"]).present?
225
+    config.omniauth :'tumblr', key, secret
226
+  end
227
+
222 228
   if defined?(OmniAuth::Strategies::ThirtySevenSignals) &&
223 229
      (key = ENV["THIRTY_SEVEN_SIGNALS_OAUTH_KEY"]).present? &&
224 230
      (secret = ENV["THIRTY_SEVEN_SIGNALS_OAUTH_SECRET"]).present?

+ 1 - 0
config/locales/devise.en.yml

@@ -51,6 +51,7 @@ en:
51 51
       failure: 'Could not authenticate you from %{kind} because "%{reason}".'
52 52
     omniauth_providers:
53 53
       twitter: 'Twitter'
54
+      tumblr: 'Tumblr'
54 55
       github: 'GitHub'
55 56
       37signals: '37Signals (Basecamp)'
56 57
     mailer:

+ 24 - 27
db/schema.rb

@@ -11,14 +11,11 @@
11 11
 #
12 12
 # It's strongly recommended that you check this file into your version control system.
13 13
 
14
-ActiveRecord::Schema.define(version: 20140901143732) do
15
-
16
-  # These are extensions that must be enabled in order to support this database
17
-  enable_extension "plpgsql"
14
+ActiveRecord::Schema.define(version: 20140906030139) do
18 15
 
19 16
   create_table "agent_logs", force: true do |t|
20 17
     t.integer  "agent_id",                      null: false
21
-    t.text     "message",           limit: 16777215,             null: false, charset: "utf8mb4", collation: "utf8mb4_bin"
18
+    t.text     "message",                       null: false, charset: "utf8mb4", collation: "utf8mb4_bin"
22 19
     t.integer  "level",             default: 3, null: false
23 20
     t.integer  "inbound_event_id"
24 21
     t.integer  "outbound_event_id"
@@ -28,7 +25,7 @@ ActiveRecord::Schema.define(version: 20140901143732) do
28 25
 
29 26
   create_table "agents", force: true do |t|
30 27
     t.integer  "user_id"
31
-    t.text     "options",               limit: 16777215,                                charset: "utf8mb4", collation: "utf8mb4_bin"
28
+    t.text     "options",                                                               charset: "utf8mb4", collation: "utf8mb4_bin"
32 29
     t.string   "type",                                                                                      collation: "utf8_bin"
33 30
     t.string   "name",                                                                  charset: "utf8mb4", collation: "utf8mb4_bin"
34 31
     t.string   "schedule",                                                                                  collation: "utf8_bin"
@@ -36,17 +33,17 @@ ActiveRecord::Schema.define(version: 20140901143732) do
36 33
     t.datetime "last_check_at"
37 34
     t.datetime "last_receive_at"
38 35
     t.integer  "last_checked_event_id"
39
-    t.datetime "created_at",                                               null: false
40
-    t.datetime "updated_at",                                               null: false
36
+    t.datetime "created_at"
37
+    t.datetime "updated_at"
41 38
     t.text     "memory",                limit: 2147483647,                              charset: "utf8mb4", collation: "utf8mb4_bin"
42 39
     t.datetime "last_web_request_at"
43 40
     t.integer  "keep_events_for",                          default: 0,     null: false
44 41
     t.datetime "last_event_at"
45 42
     t.datetime "last_error_log_at"
46
-    t.boolean  "propagate_immediately", default: false, null: false
47
-    t.boolean  "disabled",              default: false, null: false
48
-    t.string   "guid",                                                     null: false, charset: "ascii",   collation: "ascii_bin"
43
+    t.boolean  "propagate_immediately",                    default: false, null: false
44
+    t.boolean  "disabled",                                 default: false, null: false
49 45
     t.integer  "service_id"
46
+    t.string   "guid",                                                     null: false
50 47
   end
51 48
 
52 49
   add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree
@@ -67,8 +64,8 @@ ActiveRecord::Schema.define(version: 20140901143732) do
67 64
   create_table "delayed_jobs", force: true do |t|
68 65
     t.integer  "priority",                    default: 0
69 66
     t.integer  "attempts",                    default: 0
70
-    t.text     "handler",    limit: 16777215,                          charset: "utf8mb4", collation: "utf8mb4_bin"
71
-    t.text     "last_error", limit: 16777215,                          charset: "utf8mb4", collation: "utf8mb4_bin"
67
+    t.text     "handler",    limit: 16777215,             charset: "utf8mb4", collation: "utf8mb4_bin"
68
+    t.text     "last_error",                              charset: "utf8mb4", collation: "utf8mb4_bin"
72 69
     t.datetime "run_at"
73 70
     t.datetime "locked_at"
74 71
     t.datetime "failed_at"
@@ -83,11 +80,11 @@ ActiveRecord::Schema.define(version: 20140901143732) do
83 80
   create_table "events", force: true do |t|
84 81
     t.integer  "user_id"
85 82
     t.integer  "agent_id"
86
-    t.decimal  "lat",                           precision: 15, scale: 10
87
-    t.decimal  "lng",                           precision: 15, scale: 10
88
-    t.text     "payload",    limit: 2147483647,                                        charset: "utf8mb4", collation: "utf8mb4_bin"
89
-    t.datetime "created_at",                                              null: false
90
-    t.datetime "updated_at",                                              null: false
83
+    t.decimal  "lat",                         precision: 15, scale: 10
84
+    t.decimal  "lng",                         precision: 15, scale: 10
85
+    t.text     "payload",    limit: 16777215,                           charset: "utf8mb4", collation: "utf8mb4_bin"
86
+    t.datetime "created_at"
87
+    t.datetime "updated_at"
91 88
     t.datetime "expires_at"
92 89
   end
93 90
 
@@ -117,13 +114,13 @@ ActiveRecord::Schema.define(version: 20140901143732) do
117 114
   add_index "scenario_memberships", ["scenario_id"], name: "index_scenario_memberships_on_scenario_id", using: :btree
118 115
 
119 116
   create_table "scenarios", force: true do |t|
120
-    t.string   "name",                        null: false, charset: "utf8mb4", collation: "utf8mb4_bin"
121
-    t.integer  "user_id",                     null: false
117
+    t.string   "name",                         null: false, charset: "utf8mb4", collation: "utf8mb4_bin"
118
+    t.integer  "user_id",                      null: false
122 119
     t.datetime "created_at"
123 120
     t.datetime "updated_at"
124
-    t.text     "description",                              charset: "utf8mb4", collation: "utf8mb4_bin"
125
-    t.boolean  "public",      default: false, null: false
126
-    t.string   "guid",                        null: false, charset: "ascii",   collation: "ascii_bin"
121
+    t.text     "description",                               charset: "utf8mb4", collation: "utf8mb4_bin"
122
+    t.boolean  "public",       default: false, null: false
123
+    t.string   "guid",                         null: false, charset: "ascii",   collation: "ascii_bin"
127 124
     t.string   "source_url"
128 125
     t.string   "tag_bg_color"
129 126
     t.string   "tag_fg_color"
@@ -154,8 +151,8 @@ ActiveRecord::Schema.define(version: 20140901143732) do
154 151
     t.integer  "user_id",                           null: false
155 152
     t.string   "credential_name",                   null: false
156 153
     t.text     "credential_value",                  null: false
157
-    t.datetime "created_at",                        null: false
158
-    t.datetime "updated_at",                        null: false
154
+    t.datetime "created_at"
155
+    t.datetime "updated_at"
159 156
     t.string   "mode",             default: "text", null: false, collation: "utf8_bin"
160 157
   end
161 158
 
@@ -172,8 +169,8 @@ ActiveRecord::Schema.define(version: 20140901143732) do
172 169
     t.datetime "last_sign_in_at"
173 170
     t.string   "current_sign_in_ip"
174 171
     t.string   "last_sign_in_ip"
175
-    t.datetime "created_at",                                         null: false
176
-    t.datetime "updated_at",                                         null: false
172
+    t.datetime "created_at"
173
+    t.datetime "updated_at"
177 174
     t.boolean  "admin",                              default: false, null: false
178 175
     t.integer  "failed_attempts",                    default: 0
179 176
     t.string   "unlock_token"

+ 2 - 0
spec/env.test

@@ -1,6 +1,8 @@
1 1
 APP_SECRET_TOKEN=notarealappsecrettoken
2 2
 TWITTER_OAUTH_KEY=twitteroauthkey
3 3
 TWITTER_OAUTH_SECRET=twitteroauthsecret
4
+TUMBLR_OAUTH_KEY=tumblroauthsecret
5
+TUMBLR_OAUTH_SECRET=tumblroauthsecret
4 6
 THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY
5 7
 THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET
6 8
 FAILED_JOBS_TO_KEEP=2

+ 38 - 0
spec/models/agents/tumblr_publish_agent_spec.rb

@@ -0,0 +1,38 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::TumblrPublishAgent do
4
+  before do
5
+    @opts = {
6
+      :blog_name => "huginnbot.tumblr.com",
7
+      :post_type => "text",
8
+      :expected_update_period_in_days => "2",
9
+      :options => {
10
+        :title => "{{title}}",
11
+        :body => "{{body}}",
12
+      },
13
+    }
14
+
15
+    @checker = Agents::TumblrPublishAgent.new(:name => "HuginnBot", :options => @opts)
16
+    @checker.service = services(:generic)
17
+    @checker.user = users(:bob)
18
+    @checker.save!
19
+
20
+    @event = Event.new
21
+    @event.agent = agents(:bob_weather_agent)
22
+    @event.payload = { :title => "Gonna rain...", :body => 'San Francisco is gonna get wet' }
23
+    @event.save!
24
+
25
+    stub.any_instance_of(Agents::TumblrPublishAgent).tumblr {
26
+      stub!.text(anything, anything) { { "id" => "5" } }
27
+    }
28
+  end
29
+
30
+  describe '#receive' do
31
+    it 'should publish any payload it receives' do
32
+      Agents::TumblrPublishAgent.async_receive(@checker.id, [@event.id])
33
+      @checker.events.count.should eq(1)
34
+      @checker.events.first.payload['post_id'].should eq('5')
35
+      @checker.events.first.payload['published_post'].should eq('[huginnbot.tumblr.com] text')
36
+    end
37
+  end
38
+end