diff --git a/.env.example b/.env.example
index 3235504ce4..3a487249be 100644
--- a/.env.example
+++ b/.env.example
@@ -19,7 +19,7 @@ DATABASE_ADAPTER=mysql2
DATABASE_ENCODING=utf8
DATABASE_RECONNECT=true
DATABASE_NAME=huginn_development
-DATABASE_POOL=5
+DATABASE_POOL=10
DATABASE_USERNAME=root
DATABASE_PASSWORD=""
#DATABASE_HOST=your-domain-here.com
@@ -101,6 +101,9 @@ TUMBLR_OAUTH_SECRET=
DROPBOX_OAUTH_KEY=
DROPBOX_OAUTH_SECRET=
+WUNDERLIST_OAUTH_KEY=
+WUNDERLIST_OAUTH_SECRET=
+
#############################
# AWS and Mechanical Turk #
#############################
diff --git a/Gemfile b/Gemfile
index b6cbc7d1d5..b958f7b8d1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,19 +8,21 @@ gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent
gem 'wunderground', '~> 1.2.0' # WeatherAgent
gem 'forecast_io', '~> 2.0.0' # WeatherAgent
gem 'rturk', '~> 2.12.1' # HumanTaskAgent
-gem 'weibo_2', '~> 0.1' # Weibo Agents
gem 'hipchat', '~> 1.2.0' # HipchatAgent
gem 'xmpp4r', '~> 0.5.6' # JabberAgent
gem 'mqtt' # MQTTAgent
gem 'slack-notifier', '~> 1.0.0' # SlackAgent
gem 'hypdf', '~> 1.0.7' # PDFInfoAgent
+# Weibo Agents
+gem 'weibo_2', github: 'cantino/weibo_2', branch: 'master'
+
# GoogleCalendarPublishAgent
gem "google-api-client", require: 'google/api_client'
# Twitter Agents
-gem 'twitter', '~> 5.8.0' # Must to be loaded before cantino-twitter-stream.
-gem 'twitter-stream', github: 'dsander/twitter-stream', branch: 'huginn'
+gem 'twitter', '~> 5.14.0' # Must to be loaded before cantino-twitter-stream.
+gem 'twitter-stream', github: 'cantino/twitter-stream', branch: 'huginn'
gem 'omniauth-twitter'
# Tumblr Agents
@@ -37,6 +39,7 @@ gem 'haversine'
# Optional Services.
gem 'omniauth-37signals' # BasecampAgent
# gem 'omniauth-github'
+gem 'omniauth-wunderlist', github: 'wunderlist/omniauth-wunderlist', ref: 'd0910d0396107b9302aa1bc50e74bb140990ccb8'
# Bundler <1.5 does not recognize :x64_mingw as a valid platform name.
# Unfortunately, it can't self-update because it errors when encountering :x64_mingw.
diff --git a/Gemfile.lock b/Gemfile.lock
index e0ad71c70a..2e989c72be 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,12 +1,32 @@
GIT
- remote: git://github.com/dsander/twitter-stream.git
- revision: 1713b4fe5b387580364b39716bb5c26d6601c50f
+ remote: git://github.com/cantino/twitter-stream.git
+ revision: f7e7edb0bae013bffabf3598e7147773d9fd370f
branch: huginn
specs:
twitter-stream (0.1.15)
eventmachine (~> 1.0.7)
http_parser.rb (~> 0.6.0)
- simple_oauth (~> 0.2.0)
+ simple_oauth (~> 0.3.0)
+
+GIT
+ remote: git://github.com/cantino/weibo_2.git
+ revision: 00e57d29d8252126014b038cd738b02e05e4cfc5
+ branch: master
+ specs:
+ weibo_2 (0.1.7)
+ hashie (~> 2.0.4)
+ multi_json (~> 1)
+ oauth2 (~> 0.9.1)
+ rest-client (~> 1.8)
+
+GIT
+ remote: git://github.com/wunderlist/omniauth-wunderlist.git
+ revision: d0910d0396107b9302aa1bc50e74bb140990ccb8
+ ref: d0910d0396107b9302aa1bc50e74bb140990ccb8
+ specs:
+ omniauth-wunderlist (0.0.1)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.1)
GEM
remote: https://rubygems.org/
@@ -47,7 +67,7 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
- addressable (2.3.7)
+ addressable (2.3.8)
arel (6.0.0)
autoparse (0.3.3)
addressable (>= 2.3.1)
@@ -102,6 +122,8 @@ GEM
warden (~> 1.2.3)
diff-lcs (1.2.5)
docile (1.1.5)
+ domain_name (0.5.24)
+ unf (>= 0.0.5, < 1.0.0)
dotenv (0.11.1)
dotenv-deployment (~> 0.0.2)
dotenv-deployment (0.0.2)
@@ -122,7 +144,7 @@ GEM
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
- equalizer (0.0.9)
+ equalizer (0.0.11)
erector (0.10.0)
treetop (>= 1.2.3)
erubis (2.7.0)
@@ -190,8 +212,10 @@ GEM
httmultiparty (0.3.10)
httparty (>= 0.7.3)
multipart-post
- http (0.5.1)
- http_parser.rb
+ http (0.6.4)
+ http_parser.rb (~> 0.6.0)
+ http-cookie (1.0.2)
+ domain_name (~> 0.5)
http_parser.rb (0.6.0)
httparty (0.13.1)
json (~> 1.8)
@@ -205,7 +229,7 @@ GEM
json (1.8.2)
jsonpath (0.5.6)
multi_json
- jwt (1.3.0)
+ jwt (1.4.1)
kaminari (0.16.1)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
@@ -229,7 +253,7 @@ GEM
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
- mime-types (2.4.3)
+ mime-types (2.5)
mini_portile (0.6.2)
minitest (5.5.1)
mqtt (0.3.1)
@@ -281,7 +305,7 @@ GEM
slop (~> 3.4)
quiet_assets (1.1.0)
railties (>= 3.1, < 5.0)
- rack (1.6.0)
+ rack (1.6.1)
rack-test (0.6.3)
rack (>= 1.0)
rails (4.2.1)
@@ -321,7 +345,8 @@ GEM
ref (1.0.5)
responders (2.1.0)
railties (>= 4.2.0, < 5)
- rest-client (1.7.3)
+ rest-client (1.8.0)
+ http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
retriable (2.0.2)
@@ -378,7 +403,7 @@ GEM
jwt (>= 0.1.5)
multi_json (>= 1.0.0)
simple-rss (1.3.1)
- simple_oauth (0.2.0)
+ simple_oauth (0.3.1)
simplecov (0.9.2)
docile (~> 1.1.0)
multi_json (~> 1.0)
@@ -426,17 +451,17 @@ GEM
builder (>= 2.1.2)
jwt (>= 0.1.2)
multi_json (>= 1.3.0)
- twitter (5.8.0)
+ twitter (5.14.0)
addressable (~> 2.3)
buftok (~> 0.2.0)
equalizer (~> 0.0.9)
faraday (~> 0.9.0)
- http (~> 0.5.0)
+ http (~> 0.6.0)
http_parser.rb (~> 0.6.0)
json (~> 1.8)
memoizable (~> 0.4.0)
naught (~> 1.0)
- simple_oauth (~> 0.2.0)
+ simple_oauth (~> 0.3.0)
typhoeus (0.6.9)
ethon (>= 0.7.1)
tzinfo (1.2.2)
@@ -444,6 +469,9 @@ GEM
uglifier (2.7.0)
execjs (>= 0.3.0)
json (>= 1.8.0)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.7.1)
unicorn (4.8.3)
kgio (~> 2.6)
rack
@@ -457,11 +485,6 @@ GEM
webmock (1.17.4)
addressable (>= 2.2.7)
crack (>= 0.3.2)
- weibo_2 (0.1.7)
- hashie (~> 2.0.4)
- multi_json (~> 1)
- oauth2 (~> 0.9.1)
- rest-client (~> 1.7.3)
wunderground (1.2.0)
addressable
httparty (> 0.6.0)
@@ -521,6 +544,7 @@ DEPENDENCIES
omniauth-dropbox
omniauth-tumblr
omniauth-twitter
+ omniauth-wunderlist!
pg
protected_attributes (~> 1.0.8)
pry
@@ -547,7 +571,7 @@ DEPENDENCIES
therubyracer (~> 0.12.2)
tumblr_client
twilio-ruby (~> 3.11.5)
- twitter (~> 5.8.0)
+ twitter (~> 5.14.0)
twitter-stream!
typhoeus (~> 0.6.3)
tzinfo (>= 1.2.0)
@@ -556,6 +580,6 @@ DEPENDENCIES
unicorn
vcr
webmock (~> 1.17.4)
- weibo_2 (~> 0.1)
+ weibo_2!
wunderground (~> 1.2.0)
xmpp4r (~> 0.5.6)
diff --git a/README.md b/README.md
index dc24ee7667..a6d4404f0c 100644
--- a/README.md
+++ b/README.md
@@ -8,32 +8,32 @@ Huginn is a system for building agents that perform automated tasks for you onli
![the origin of the name](doc/imgs/the-name.png)
-#### Here are some of the things that you can do with Huginn right now:
+#### Here are some of the things that you can do with Huginn:
* Track the weather and get an email when it's going to rain (or snow) tomorrow ("Don't forget your umbrella!")
-* List terms that you care about and receive emails when their occurrence on Twitter changes. (For example, want to know when something interesting has happened in the world of Machine Learning? Huginn will watch the term "machine learning" on Twitter and tell you when there is a large spike.)
+* List terms that you care about and receive emails when their occurrence on Twitter changes. (For example, want to know when something interesting has happened in the world of Machine Learning? Huginn will watch the term "machine learning" on Twitter and tell you when there is a spike in discussion.)
* Watch for air travel or shopping deals
* Follow your project names on Twitter and get updates when people mention them
* Scrape websites and receive emails when they change
* Connect to Adioso, HipChat, Basecamp, Growl, FTP, IMAP, Jabber, JIRA, MQTT, nextbus, Pushbullet, Pushover, RSS, Bash, Slack, StubHub, translation APIs, Twilio, Twitter, Wunderground, and Weibo, to name a few.
-* Compose digest emails about things you care about to be sent at specific times of the day
+* Send digest emails with things that you care about at specific times during the day
* Track counts of high frequency events and send an SMS within moments when they spike, such as the term "san francisco emergency"
* Send and receive WebHooks
-* Run arbitrary JavaScript Agents on the server
+* Run custom JavaScript or CoffeeScript functions
* Track your location over time
* Create Amazon Mechanical Turk workflows as the inputs, or outputs, of agents (the Amazon Turk Agent is called the "HumanTaskAgent"). For example: "Once a day, ask 5 people for a funny cat photo; send the results to 5 more people to be rated; send the top-rated photo to 5 people for a funny caption; send to 5 final people to rate for funniest caption; finally, post the best captioned photo on my blog."
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cantino/huginn?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves, and join us in our [Gitter room](https://gitter.im/cantino/huginn) to discuss the project.
+Join us in our [Gitter room](https://gitter.im/cantino/huginn) to discuss the project and follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves.
-### We need your help!
+### Join us!
-Want to help with Huginn? All contributions are encouraged! You could make UI improvements, [add new Agents](https://github.com/cantino/huginn/wiki/Creating-a-new-agent), write documentation and tutorials, or try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open).
+Want to help with Huginn? All contributions are encouraged! You could make UI improvements, [add new Agents](https://github.com/cantino/huginn/wiki/Creating-a-new-agent), write [documentation and tutorials](https://github.com/cantino/huginn/wiki), or try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open). Please fork, add specs, and send pull requests!
-Really want an issue fixed/feature implemented? Or maybe you just want to solve some community issues and earn some extra coffee money? Then you should take a look at the [current bounties on Bountysource](https://www.bountysource.com/trackers/282580-huginn).
+Really want a fix or feature? Want to solve some community issues and earn some extra coffee money? Take a look at the [current bounties on Bountysource](https://www.bountysource.com/trackers/282580-huginn).
-Have an awesome an idea but not feeling quite up to contributing yet? Head over to our [Official 'suggest an agent' thread ](https://github.com/cantino/huginn/issues/353) and tell us about your cool idea!
+Have an awesome idea but not feeling quite up to contributing yet? Head over to our [Official 'suggest an agent' thread ](https://github.com/cantino/huginn/issues/353) and tell us!
## Examples
@@ -102,15 +102,5 @@ We assume your deployment will run over SSL. This is a very good idea! However,
Huginn is provided under the MIT License.
-## Community
-Huginn has its own IRC channel on freenode: #huginn.
-Some of us are hanging out there, come and say hello.
-
-## Contribution
-
-Huginn is a work in progress and is just getting started. Please get involved! You can [add new Agents](https://github.com/cantino/huginn/wiki/Creating-a-new-agent), expand the [Wiki](https://github.com/cantino/huginn/wiki), or help us simplify and strengthen the Agent API or core application.
-
-Please fork, add specs, and send pull requests!
-
[![Build Status](https://travis-ci.org/cantino/huginn.png)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.png)](https://coveralls.io/r/cantino/huginn) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Dependency Status](https://gemnasium.com/cantino/huginn.svg)](https://gemnasium.com/cantino/huginn) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282580)](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE)
diff --git a/app.json b/app.json
index 790bed82bc..6525572f99 100644
--- a/app.json
+++ b/app.json
@@ -4,7 +4,7 @@
"website": "https://github.com/cantino/huginn",
"repository": "https://github.com/cantino/huginn",
"env": {
- "BUILDPACK_URL": "https://github.com/ddollar/heroku-buildpack-multi.git",
+ "BUILDPACK_URL": "https://github.com/heroku/heroku-buildpack-multi.git",
"APP_SECRET_TOKEN": {
"generator": "secret"
},
@@ -19,5 +19,6 @@
"scripts": {
"postdeploy": "bundle exec rake db:migrate"
},
+ "addons": ["heroku-postgresql"],
"success_url": "/users/sign_up"
}
diff --git a/app/assets/javascripts/pages/agent-edit-page.js.coffee b/app/assets/javascripts/pages/agent-edit-page.js.coffee
index 62dcb77ebc..62642de3cd 100644
--- a/app/assets/javascripts/pages/agent-edit-page.js.coffee
+++ b/app/assets/javascripts/pages/agent-edit-page.js.coffee
@@ -4,6 +4,14 @@ class @AgentEditPage
@showCorrectRegionsOnStartup()
$("form.agent-form").on "submit", => @updateFromEditors()
+ $("#agent_name").each ->
+ # Select the number suffix if this is a cloned agent.
+ if matches = this.value.match(/ \(\d+\)$/)
+ this.focus()
+ if this.selectionStart?
+ this.selectionStart = matches.index
+ this.selectionEnd = this.value.length
+
# The type selector is only available on the new agent form.
if $("#agent_type").length
$("#agent_type").on "change", => @handleTypeChange(false)
diff --git a/app/assets/javascripts/pages/agent-show-page.js.coffee b/app/assets/javascripts/pages/agent-show-page.js.coffee
index 3b9ef02ff9..bfa6c2be17 100644
--- a/app/assets/javascripts/pages/agent-show-page.js.coffee
+++ b/app/assets/javascripts/pages/agent-show-page.js.coffee
@@ -2,6 +2,7 @@ class @AgentShowPage
constructor: ->
$(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", @fetchLogs
$(".agent-show #logs .clear").on "click", @clearLogs
+ $(".agent-show #memory .clear").on "click", @clearMemory
# Trigger tabs when navigated to.
if tab = window.location.href.match(/tab=(\w+)\b/i)?[1]
@@ -39,6 +40,20 @@ class @AgentShowPage
$("#logs .spinner").stop(true, true).fadeOut ->
$("#logs .refresh, #logs .clear").show()
+ clearMemory: (e) ->
+ if confirm("Are you sure you want to clear memory of this Agent?")
+ agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
+ e.preventDefault()
+ $("#memory .spinner").css(display: 'inline-block')
+ $("#memory .clear").hide()
+ $.post "/agents/#{agentId}/memory", { "_method": "DELETE" }
+ .done ->
+ $("#memory .spinner").fadeOut ->
+ $("#memory + .memory").text "{\n}\n"
+ .fail ->
+ $("#memory .spinner").fadeOut ->
+ $("#memory .clear").css(display: 'inline-block')
+
$ ->
Utils.registerPage(AgentShowPage, forPathsMatching: /^agents\/\d+/)
diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb
index 9119d9235b..d00936a6cd 100644
--- a/app/assets/stylesheets/application.css.scss.erb
+++ b/app/assets/stylesheets/application.css.scss.erb
@@ -61,7 +61,7 @@ img.odin {
display: none;
}
-img.spinner {
+.spinner {
display: none;
vertical-align: bottom;
}
@@ -172,6 +172,13 @@ span.not-applicable:after {
font-weight: bold;
}
+// Memory
+
+#memory .action-icon {
+ display: inline-block;
+ cursor: pointer;
+}
+
// Credentials and Ace Editor
#ace-credential-value {
@@ -251,8 +258,8 @@ h2 .scenario, a span.label.scenario {
width: 200px;
}
-$services: twitter 37signals github tumblr dropbox;
-$service-colors: #55acee #8fc857 #444444 #2c4762 #007EE5;
+$services: twitter 37signals github tumblr dropbox wunderlist;
+$service-colors: #55acee #8fc857 #444444 #2c4762 #007EE5 #ED5F27;
@mixin services {
@each $service in $services {
diff --git a/app/concerns/liquid_droppable.rb b/app/concerns/liquid_droppable.rb
index f6d210ede3..bf8adb5fb1 100644
--- a/app/concerns/liquid_droppable.rb
+++ b/app/concerns/liquid_droppable.rb
@@ -21,7 +21,12 @@ def each
end
included do
- const_set :Drop, Kernel.const_set("#{name}Drop", Class.new(Drop)) unless const_defined?("#{name}Drop")
+ const_set :Drop,
+ if Kernel.const_defined?(drop_name = "#{name}Drop")
+ Kernel.const_get(drop_name)
+ else
+ Kernel.const_set(drop_name, Class.new(Drop))
+ end
end
def to_liquid
diff --git a/app/controllers/agents_controller.rb b/app/controllers/agents_controller.rb
index a1cf386b91..d656b371bc 100644
--- a/app/controllers/agents_controller.rb
+++ b/app/controllers/agents_controller.rb
@@ -112,6 +112,16 @@ def propagate
end
end
+ def destroy_memory
+ @agent = current_user.agents.find(params[:id])
+ @agent.update!(memory: {})
+
+ respond_to do |format|
+ format.html { redirect_back "Memory erased for '#{@agent.name}'" }
+ format.json { head :ok }
+ end
+ end
+
def show
@agent = current_user.agents.find(params[:id])
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index d400d049f8..77fcbc071a 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -57,6 +57,8 @@ def omniauth_provider_icon(provider)
case provider.to_sym
when :twitter, :tumblr, :github, :dropbox
icon_tag("fa-#{provider}")
+ when :wunderlist
+ icon_tag("fa-list")
else
icon_tag("fa-lock")
end
diff --git a/app/jobs/agent_check_job.rb b/app/jobs/agent_check_job.rb
new file mode 100644
index 0000000000..0603bdd84e
--- /dev/null
+++ b/app/jobs/agent_check_job.rb
@@ -0,0 +1,15 @@
+class AgentCheckJob < ActiveJob::Base
+ # Given an Agent id, load the Agent, call #check on it, and then save it with an updated `last_check_at` timestamp.
+ def perform(agent_id)
+ agent = Agent.find(agent_id)
+ begin
+ return if agent.unavailable?
+ agent.check
+ agent.last_check_at = Time.now
+ agent.save!
+ rescue => e
+ agent.error "Exception during check. #{e.message}: #{e.backtrace.join("\n")}"
+ raise
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/jobs/agent_receive_job.rb b/app/jobs/agent_receive_job.rb
new file mode 100644
index 0000000000..385c3b8c76
--- /dev/null
+++ b/app/jobs/agent_receive_job.rb
@@ -0,0 +1,16 @@
+class AgentReceiveJob < ActiveJob::Base
+ # Given an Agent id and an array of Event ids, load the Agent, call #receive on it with the Event objects, and then
+ # save it with an updated `last_receive_at` timestamp.
+ def perform(agent_id, event_ids)
+ agent = Agent.find(agent_id)
+ begin
+ return if agent.unavailable?
+ agent.receive(Event.where(:id => event_ids).order(:id))
+ agent.last_receive_at = Time.now
+ agent.save!
+ rescue => e
+ agent.error "Exception during receive. #{e.message}: #{e.backtrace.join("\n")}"
+ raise
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/models/agent.rb b/app/models/agent.rb
index 2d1314506f..c0aa3d8ab9 100644
--- a/app/models/agent.rb
+++ b/app/models/agent.rb
@@ -387,24 +387,11 @@ def receive!(options={})
end
end
- # Given an Agent id and an array of Event ids, load the Agent, call #receive on it with the Event objects, and then
- # save it with an updated `last_receive_at` timestamp.
- #
- # This method is tagged with `handle_asynchronously` and will be delayed and run with delayed_job. It accepts Agent
- # and Event ids instead of a literal ActiveRecord models because it is preferable to serialize delayed_jobs with ids.
+ # This method will enqueue an AgentReceiveJob job. It accepts Agent and Event ids instead of a literal ActiveRecord
+ # models because it is preferable to serialize jobs with ids.
def async_receive(agent_id, event_ids)
- agent = Agent.find(agent_id)
- begin
- return if agent.unavailable?
- agent.receive(Event.where(:id => event_ids))
- agent.last_receive_at = Time.now
- agent.save!
- rescue => e
- agent.error "Exception during receive. #{e.message}: #{e.backtrace.join("\n")}"
- raise
- end
+ AgentReceiveJob.perform_later(agent_id, event_ids)
end
- handle_asynchronously :async_receive
# Given a schedule name, run `check` via `bulk_check` on all Agents with that schedule.
# This is called by bin/schedule.rb for each schedule in `SCHEDULES`.
@@ -425,24 +412,11 @@ def bulk_check(schedule)
end
end
- # Given an Agent id, load the Agent, call #check on it, and then save it with an updated `last_check_at` timestamp.
- #
- # This method is tagged with `handle_asynchronously` and will be delayed and run with delayed_job. It accepts an Agent
- # id instead of a literal Agent because it is preferable to serialize delayed_jobs with ids, instead of with the full
- # Agents.
+ # This method will enqueue an AgentCheckJob job. It accepts an Agent id instead of a literal Agent because it is
+ # preferable to serialize job with ids, instead of with the full Agents.
def async_check(agent_id)
- agent = Agent.find(agent_id)
- begin
- return if agent.unavailable?
- agent.check
- agent.last_check_at = Time.now
- agent.save!
- rescue => e
- agent.error "Exception during check. #{e.message}: #{e.backtrace.join("\n")}"
- raise
- end
+ AgentCheckJob.perform_later(agent_id)
end
- handle_asynchronously :async_check
end
end
diff --git a/app/models/agents/email_agent.rb b/app/models/agents/email_agent.rb
index d3397dd739..be1068189f 100644
--- a/app/models/agents/email_agent.rb
+++ b/app/models/agents/email_agent.rb
@@ -35,7 +35,7 @@ def receive(incoming_events)
incoming_events.each do |event|
log "Sending digest mail to #{user.email} with event #{event.id}"
recipients(event.payload).each do |recipient|
- SystemMailer.delay.send_message(:to => recipient, :subject => interpolated(event)['subject'], :headline => interpolated(event)['headline'], :body => interpolated(event)['body'], :groups => [present(event.payload)])
+ SystemMailer.send_message(:to => recipient, :subject => interpolated(event)['subject'], :headline => interpolated(event)['headline'], :body => interpolated(event)['body'], :groups => [present(event.payload)]).deliver_later
end
end
end
diff --git a/app/models/agents/email_digest_agent.rb b/app/models/agents/email_digest_agent.rb
index 3ee5e76b3d..1a89b5f1f8 100644
--- a/app/models/agents/email_digest_agent.rb
+++ b/app/models/agents/email_digest_agent.rb
@@ -42,7 +42,7 @@ def check
groups = self.memory['queue'].map { |payload| present(payload) }
log "Sending digest mail to #{user.email} with events [#{ids}]"
recipients.each do |recipient|
- SystemMailer.delay.send_message(:to => recipient, :subject => interpolated['subject'], :headline => interpolated['headline'], :groups => groups)
+ SystemMailer.send_message(:to => recipient, :subject => interpolated['subject'], :headline => interpolated['headline'], :groups => groups).deliver_later
end
self.memory['queue'] = []
self.memory['events'] = []
diff --git a/app/models/agents/google_calendar_publish_agent.rb b/app/models/agents/google_calendar_publish_agent.rb
index 18a35d5ee3..5ad4e3333a 100644
--- a/app/models/agents/google_calendar_publish_agent.rb
+++ b/app/models/agents/google_calendar_publish_agent.rb
@@ -18,7 +18,7 @@ class GoogleCalendarPublishAgent < Agent
2. New project -> Huginn
3. APIs & Auth -> Enable google calendar
4. Credentials -> Create new Client ID -> Service Account
- 5. Persist the generated private key to a path, ie: `/home/hugin/a822ccdefac89fac6330f95039c492dfa3ce6843.p12`
+ 5. Persist the generated private key to a path, ie: `/home/huginn/a822ccdefac89fac6330f95039c492dfa3ce6843.p12`
6. Grant access via google calendar UI to the service account email address for each calendar you wish to manage. For a whole google apps domain, you can [delegate authority](https://developers.google.com/+/domains/authentication/delegation)
diff --git a/app/models/agents/java_script_agent.rb b/app/models/agents/java_script_agent.rb
index c330e7aa0b..70dc082a07 100644
--- a/app/models/agents/java_script_agent.rb
+++ b/app/models/agents/java_script_agent.rb
@@ -25,6 +25,8 @@ class JavaScriptAgent < Agent
* `this.options(key)`
* `this.log(message)`
* `this.error(message)`
+ * `this.escapeHtml(htmlToEscape)`
+ * `this.unescapeHtml(htmlToUnescape)`
MD
form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
@@ -116,6 +118,8 @@ def execute_js(js_function, incoming_events = [])
memory.to_json
end
end
+ context["escapeHtml"] = lambda { |a, x| CGI.escapeHTML(x) }
+ context["unescapeHtml"] = lambda { |a, x| CGI.unescapeHTML(x) }
if (options['language'] || '').downcase == 'coffeescript'
context.eval(CoffeeScript.compile code)
@@ -176,6 +180,14 @@ def setup_javascript
doError(message);
}
+ Agent.escapeHtml = function(html) {
+ return escapeHtml(html);
+ }
+
+ Agent.unescapeHtml = function(html) {
+ return unescapeHtml(html);
+ }
+
Agent.check = function(){};
Agent.receive = function(){};
JS
diff --git a/app/models/agents/webhook_agent.rb b/app/models/agents/webhook_agent.rb
index fc227ef9a6..92f38ebad7 100644
--- a/app/models/agents/webhook_agent.rb
+++ b/app/models/agents/webhook_agent.rb
@@ -12,21 +12,20 @@ class WebhookAgent < Agent
https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || '<%= Utils.pretty_jsonify @agent.options || {} %>
+
Memory: -
<%= Utils.pretty_jsonify @agent.memory || {} %>+ <% if @agent.memory.present? %> + + + <% end %> +
<%= Utils.pretty_jsonify @agent.memory || {} %>diff --git a/bin/schedule.rb b/bin/schedule.rb index 1b077ad276..9546c3ec53 100755 --- a/bin/schedule.rb +++ b/bin/schedule.rb @@ -11,5 +11,5 @@ exit 1 end -scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY']) +scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY'].presence || 0.3) scheduler.run! diff --git a/bin/setup_heroku b/bin/setup_heroku index 2d16b89a59..afd7bfb1ec 100755 --- a/bin/setup_heroku +++ b/bin/setup_heroku @@ -81,10 +81,16 @@ unless $config['DOMAIN'] first_time = true end -set_value 'BUILDPACK_URL', "https://github.com/ddollar/heroku-buildpack-multi.git" +set_value 'BUILDPACK_URL', "https://github.com/heroku/heroku-buildpack-multi.git" set_value 'PROCFILE_PATH', "deployment/heroku/Procfile.heroku", force: false set_value 'ON_HEROKU', "true" +unless $config['DATABASE_URL'] + puts "Setting up the postgres addon" + puts capture("heroku addons:add heroku-postgresql") + puts +end + unless $config['SMTP_DOMAIN'] && $config['SMTP_USER_NAME'] && $config['SMTP_PASSWORD'] && $config['SMTP_SERVER'] && $config['EMAIL_FROM_ADDRESS'] puts "Okay, let's setup outgoing email settings. The simplest solution is to use the free sendgrid Heroku addon." puts "If you'd like to use your own server, or your Gmail account, please see .env.example and set" diff --git a/bin/threaded.rb b/bin/threaded.rb index 7cd503b9a3..56c99c46f7 100644 --- a/bin/threaded.rb +++ b/bin/threaded.rb @@ -1,5 +1,6 @@ require 'thread' require 'huginn_scheduler' +require 'twitter_stream' STDOUT.sync = true STDERR.sync = true @@ -33,7 +34,7 @@ def safely(&block) threads << Thread.new do safely do - @scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY']) + @scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY'].presence || 0.3) @scheduler.run! puts "Scheduler stopped ..." end diff --git a/config/application.rb b/config/application.rb index 0fed9f3374..f4156e403a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,7 +13,7 @@ class Application < Rails::Application # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/presenters) + config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/presenters #{config.root}/app/jobs) # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer @@ -52,5 +52,7 @@ class Application < Rails::Application # Do not swallow errors in after_commit/after_rollback callbacks. config.active_record.raise_in_transactional_callbacks = true + + config.active_job.queue_adapter = :delayed_job end end diff --git a/config/database.yml b/config/database.yml index 41e0072e65..996366dc2e 100644 --- a/config/database.yml +++ b/config/database.yml @@ -3,7 +3,7 @@ development: encoding: <%= ENV['DATABASE_ENCODING'].presence || "utf8" %> reconnect: <%= ENV['DATABASE_RECONNECT'].presence || "true" %> database: <%= ENV['DATABASE_NAME'].presence || "huginn_development" %> - pool: <%= ENV['DATABASE_POOL'].presence || "5" %> + pool: <%= ENV['DATABASE_POOL'].presence || "10" %> username: <%= ENV['DATABASE_USERNAME'].presence || "root" %> password: <%= ENV['DATABASE_PASSWORD'] || "" %> host: <%= ENV['DATABASE_HOST'] || "" %> @@ -29,7 +29,7 @@ production: encoding: <%= ENV['DATABASE_ENCODING'].presence || "utf8" %> reconnect: <%= ENV['DATABASE_RECONNECT'].presence || "true" %> database: <%= ENV['DATABASE_NAME'].presence || "huginn_production" %> - pool: <%= ENV['DATABASE_POOL'].presence || "5" %> + pool: <%= ENV['DATABASE_POOL'].presence || "10" %> username: <%= ENV['DATABASE_USERNAME'].presence || "root" %> password: <%= ENV['DATABASE_PASSWORD'].presence || "password" %> host: <%= ENV['DATABASE_HOST'] || "" %> diff --git a/config/initializers/ar_mysql_column_charset.rb b/config/initializers/ar_mysql_column_charset.rb index cade648ebf..aea6a68823 100644 --- a/config/initializers/ar_mysql_column_charset.rb +++ b/config/initializers/ar_mysql_column_charset.rb @@ -1,3 +1 @@ -ActiveSupport.on_load :active_record do - require 'ar_mysql_column_charset' -end +require 'ar_mysql_column_charset' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f806ef2385..6ae99abd33 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -263,6 +263,12 @@ config.omniauth :dropbox, key, secret end + if defined?(OmniAuth::Strategies::Wunderlist) && + (key = ENV["WUNDERLIST_OAUTH_KEY"]).present? && + (secret = ENV["WUNDERLIST_OAUTH_SECRET"]).present? + config.omniauth :wunderlist, key, secret + end + # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index dd10d119e0..8af9503fc9 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -32,6 +32,7 @@ en: github: "GitHub" 37signals: "37Signals (Basecamp)" dropbox: "Dropbox" + wunderlist: 'Wunderlist' passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." diff --git a/config/routes.rb b/config/routes.rb index 768a208523..c579b2b1f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,7 @@ post :handle_details_post put :leave_scenario delete :remove_events + delete :memory, action: :destroy_memory end collection do diff --git a/docker/scripts/init b/docker/scripts/init index 934f6ffd24..934e9a3de7 100755 --- a/docker/scripts/init +++ b/docker/scripts/init @@ -150,7 +150,7 @@ source /app/.env # Fixup the Procfile and prepare the PORT [ -n "\${DO_NOT_RUN_JOBS}" ] && perl -pi -e 's/^jobs:/#jobs:/' /app/Procfile -perl -pi -e 's/rails server\$/rails server -p \\\$PORT/' /app/Procfile +perl -pi -e 's/rails server\$/rails server -b 0.0.0.0 -p \\\$PORT/' /app/Procfile export PORT # Start huginn diff --git a/lib/ar_mysql_column_charset.rb b/lib/ar_mysql_column_charset.rb index b93710461d..64c80648cd 100644 --- a/lib/ar_mysql_column_charset.rb +++ b/lib/ar_mysql_column_charset.rb @@ -1,121 +1,16 @@ -require 'active_record' - # Module#prepend support for Ruby 1.9 require 'prepend' unless Module.method_defined?(:prepend) -module ActiveRecord::ConnectionAdapters - class ColumnDefinition - module CharsetSupport - attr_accessor :charset, :collation - end - - prepend CharsetSupport - end - - class TableDefinition - module CharsetSupport - def new_column_definition(name, type, options) - column = super - column.charset = options[:charset] - column.collation = options[:collation] - column - end - end - - prepend CharsetSupport - end - - class AbstractMysqlAdapter - module CharsetSupport - def prepare_column_options(column, types) - spec = super - spec[:charset] = column.charset.inspect if column.charset && column.charset != charset - spec[:collation] = column.collation.inspect if column.collation && column.collation != collation - spec - end +require 'active_support' - def migration_keys - super + [:charset, :collation] - end - - def utf8mb4_supported? - if @utf8mb4_supported.nil? - @utf8mb4_supported = !select("show character set like 'utf8mb4'").empty? - else - @utf8mb4_supported +ActiveSupport.on_load :active_record do + class << ActiveRecord::Base + def establish_connection(spec = nil) + super.tap { |ret| + if /mysql/i === connection.adapter_name + require 'ar_mysql_column_charset/main' end - end - - def charset_collation(charset, collation) - [charset, collation].map { |name| - case name - when nil - nil - when /\A(utf8mb4(_\w*)?)\z/ - if utf8mb4_supported? - $1 - else - "utf8#{$2}" - end - else - name.to_s - end - } - end - - def create_database(name, options = {}) - # utf8mb4 is used in column definitions; use utf8 for - # databases. - [:charset, :collation].each { |key| - case options[key] - when /\A(utf8mb4(_\w*)?)\z/ - options = options.merge(key => "utf8#{$2}") - end - } - super(name, options) - end - end - - prepend CharsetSupport - - class SchemaCreation - module CharsetSupport - def column_options(o) - column_options = super - column_options[:charset] = o.charset unless o.charset.nil? - column_options[:collation] = o.collation unless o.collation.nil? - column_options - end - - def add_column_options!(sql, options) - charset, collation = @conn.charset_collation(options[:charset], options[:collation]) - - if charset - sql << " CHARACTER SET #{charset}" - end - - if collation - sql << " COLLATE #{collation}" - end - - super - end - end - - prepend CharsetSupport - end - - class Column - module CharsetSupport - attr_reader :charset - - def initialize(*args) - super - @charset = @collation[/\A[^_]+/] unless @collation.nil? - end - end - - prepend CharsetSupport + } end end end diff --git a/lib/ar_mysql_column_charset/main.rb b/lib/ar_mysql_column_charset/main.rb new file mode 100644 index 0000000000..4b3041f326 --- /dev/null +++ b/lib/ar_mysql_column_charset/main.rb @@ -0,0 +1,118 @@ +raise "Do not directly load this library." unless defined?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter) + +module ActiveRecord::ConnectionAdapters + class ColumnDefinition + module CharsetSupport + attr_accessor :charset, :collation + end + + prepend CharsetSupport + end + + class TableDefinition + module CharsetSupport + def new_column_definition(name, type, options) + column = super + column.charset = options[:charset] + column.collation = options[:collation] + column + end + end + + prepend CharsetSupport + end + + class AbstractMysqlAdapter + module CharsetSupport + def prepare_column_options(column, types) + spec = super + spec[:charset] = column.charset.inspect if column.charset && column.charset != charset + spec[:collation] = column.collation.inspect if column.collation && column.collation != collation + spec + end + + def migration_keys + super + [:charset, :collation] + end + + def utf8mb4_supported? + if @utf8mb4_supported.nil? + @utf8mb4_supported = !select("show character set like 'utf8mb4'").empty? + else + @utf8mb4_supported + end + end + + def charset_collation(charset, collation) + [charset, collation].map { |name| + case name + when nil + nil + when /\A(utf8mb4(_\w*)?)\z/ + if utf8mb4_supported? + $1 + else + "utf8#{$2}" + end + else + name.to_s + end + } + end + + def create_database(name, options = {}) + # utf8mb4 is used in column definitions; use utf8 for + # databases. + [:charset, :collation].each { |key| + case options[key] + when /\A(utf8mb4(_\w*)?)\z/ + options = options.merge(key => "utf8#{$2}") + end + } + super(name, options) + end + end + + prepend CharsetSupport + + class SchemaCreation + module CharsetSupport + def column_options(o) + column_options = super + column_options[:charset] = o.charset unless o.charset.nil? + column_options[:collation] = o.collation unless o.collation.nil? + column_options + end + + def add_column_options!(sql, options) + charset, collation = @conn.charset_collation(options[:charset], options[:collation]) + + if charset + sql << " CHARACTER SET #{charset}" + end + + if collation + sql << " COLLATE #{collation}" + end + + super + end + end + + prepend CharsetSupport + end + + class Column + module CharsetSupport + attr_reader :charset + + def initialize(*args) + super + @charset = @collation[/\A[^_]+/] unless @collation.nil? + end + end + + prepend CharsetSupport + end + end +end diff --git a/lib/tasks/ar_mysql_column_charset.rake b/lib/tasks/ar_mysql_column_charset.rake new file mode 100644 index 0000000000..aea6a68823 --- /dev/null +++ b/lib/tasks/ar_mysql_column_charset.rake @@ -0,0 +1 @@ +require 'ar_mysql_column_charset' diff --git a/spec/controllers/agents_controller_spec.rb b/spec/controllers/agents_controller_spec.rb index fa7106286e..b7335b15dc 100644 --- a/spec/controllers/agents_controller_spec.rb +++ b/spec/controllers/agents_controller_spec.rb @@ -374,4 +374,24 @@ def valid_attributes(options = {}) } end end + + describe "DELETE memory" do + it "clears memory of the agent" do + agent = agents(:bob_website_agent) + agent.update!(memory: { "test" => 42 }) + sign_in users(:bob) + delete :destroy_memory, id: agent.to_param + expect(agent.reload.memory).to eq({}) + end + + it "does not clear memory of an agent not owned by the current user" do + agent = agents(:jane_website_agent) + agent.update!(memory: { "test" => 42 }) + sign_in users(:bob) + expect { + delete :destroy_memory, id: agent.to_param + }.to raise_error(ActiveRecord::RecordNotFound) + expect(agent.reload.memory).to eq({ "test" => 42}) + end + end end diff --git a/spec/env.test b/spec/env.test index dfc9fcc819..4f567ded96 100644 --- a/spec/env.test +++ b/spec/env.test @@ -7,4 +7,5 @@ THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET DROPBOX_OAUTH_KEY=dropboxoauthkey DROPBOX_OAUTH_SECRET=dropboxoauthsecret +WUNDERLIST_OAUTH_KEY=wunderoauthkey FAILED_JOBS_TO_KEEP=2 \ No newline at end of file diff --git a/spec/models/agents/java_script_agent_spec.rb b/spec/models/agents/java_script_agent_spec.rb index f9476997c4..d6b9380892 100644 --- a/spec/models/agents/java_script_agent_spec.rb +++ b/spec/models/agents/java_script_agent_spec.rb @@ -176,6 +176,20 @@ end end + describe "escaping and unescaping HTML" do + it "can escape and unescape html with this.escapeHtml and this.unescapeHtml in the javascript environment" do + @agent.options['code'] = 'Agent.check = function() { this.createEvent({ escaped: this.escapeHtml(\'test \"escaping\"