commit 7b3ec6ae1eea38c8c484c4f0fda5bc091a455bf4 Author: dblock Date: Fri Jun 19 13:07:31 2015 -0400 Initial implementing, using Dentaku. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..8c18f1a --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--format documentation +--color diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..bdd4025 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,6 @@ +AllCops: + Exclude: + - vendor/**/* + - bin/**/* + +inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..30050ca --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,45 @@ +# This configuration was generated by `rubocop --auto-gen-config` +# on 2015-06-19 13:09:29 -0400 using RuboCop version 0.31.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +Lint/HandleExceptions: + Enabled: false + +# Offense count: 2 +Metrics/AbcSize: + Max: 21 + +# Offense count: 1 +Metrics/CyclomaticComplexity: + Max: 7 + +# Offense count: 19 +# Configuration parameters: AllowURI, URISchemes. +Metrics/LineLength: + Max: 130 + +# Offense count: 1 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 17 + +# Offense count: 16 +Style/Documentation: + Enabled: false + +# Offense count: 1 +# Configuration parameters: Exclude. +Style/FileName: + Enabled: false + +# Offense count: 1 +Style/ModuleFunction: + Enabled: false + +# Offense count: 1 +Style/RescueModifier: + Enabled: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e6ba548 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +rvm: + - 2.1.6 + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b018b31 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +### 0.1.0 (Next) + +* Initial public release - [@dblock](https://github.com/dblock). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f817934 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing to SlackMathbot + +This project is work of [many contributors](https://github.com/dblock/slack-mathbot/graphs/contributors). + +You're encouraged to submit [pull requests](https://github.com/dblock/slack-mathbot/pulls), [propose features and discuss issues](https://github.com/dblock/slack-mathbot/issues). + +In the examples below, substitute your Github username for `contributor` in URLs. + +## Fork the Project + +Fork the [project on Github](https://github.com/dblock/slack-mathbot) and check out your copy. + +``` +git clone https://github.com/contributor/slack-mathbot.git +cd slack-mathbot +git remote add upstream https://github.com/dblock/slack-mathbot.git +``` + +## Create a Topic Branch + +Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. + +``` +git checkout master +git pull upstream master +git checkout -b my-feature-branch +``` + +## Bundle Install and Test + +Ensure that you can build the project and run tests. + +``` +bundle install +bundle exec rake +``` + +## Write Tests + +Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. +Add to [spec](spec). + +We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. + +## Write Code + +Implement your feature or bug fix. + +Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop). +Run `bundle exec rubocop` and fix any style issues highlighted. + +Make sure that `bundle exec rake` completes without errors. + +## Write Documentation + +Document any external behavior in the [README](README.md). + +## Update Changelog + +Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. +Make it look like every other line, including your name and link to your Github account. + +## Commit Changes + +Make sure git knows your name and email address: + +``` +git config --global user.name "Your Name" +git config --global user.email "contributor@example.com" +``` + +Writing good commit logs is important. A commit log should describe what changed and why. + +``` +git add ... +git commit +``` + +## Push + +``` +git push origin my-feature-branch +``` + +## Make a Pull Request + +Go to https://github.com/contributor/slack-mathbot and select your feature branch. +Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. + +## Rebase + +If you've been working on a change for a while, rebase with upstream/master. + +``` +git fetch upstream +git rebase upstream/master +git push origin my-feature-branch -f +``` + +## Update CHANGELOG Again + +Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. + +``` +* [#123](https://github.com/dblock/slack-mathbot/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). +``` + +Amend your previous commit and force push the changes. + +``` +git commit --amend +git push origin my-feature-branch -f +``` + +## Check on Your Pull Request + +Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. + +## Be Patient + +It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! + +## Thank You + +Please do know that we really appreciate and value your time and work. We love you, really. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..02542db --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,25 @@ +## Installation + +Create a new Bot Integration under [services/new/bot](http://slack.com/services/new/bot). + +![](screenshots/register-bot.png) + +On the next screen, note the API token. + +## Deploy Slack-Gamebot + +[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) + +### Environment + +#### SLACK_API_TOKEN + +Set SLACK_API_TOKEN from the Bot integration settings on Slack. + +``` +heroku config:add SLACK_API_TOKEN=... +``` + +### Heroku Idling + +Heroku free tier applications will idle. Use [UptimeRobot](http://uptimerobot.com) or similar to prevent your instance from sleeping or pay for a production dyno. diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..c419b7b --- /dev/null +++ b/Gemfile @@ -0,0 +1,23 @@ +source 'http://rubygems.org' + +ruby '2.1.6' + +gem 'hashie' +gem 'slack-api', '~> 1.1.6', require: 'slack' +gem 'puma' +gem 'sinatra' +gem 'activesupport' +gem 'dentaku' + +group :development, :test do + gem 'rake', '~> 10.4' + gem 'rubocop', '0.31.0' + gem 'foreman' +end + +group :test do + gem 'rspec', '~> 3.2' + gem 'rack-test', '~> 0.6.2' + gem 'vcr' + gem 'webmock' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..425fe85 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,105 @@ +GEM + remote: http://rubygems.org/ + specs: + activesupport (4.2.1) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.3.8) + ast (2.0.0) + astrolabe (1.3.0) + parser (>= 2.2.0.pre.3, < 3.0) + crack (0.4.2) + safe_yaml (~> 1.0.0) + dentaku (1.2.6) + diff-lcs (1.2.5) + eventmachine (1.0.7) + faraday (0.9.1) + multipart-post (>= 1.2, < 3) + faraday_middleware (0.9.1) + faraday (>= 0.7.4, < 0.10) + faye-websocket (0.9.2) + eventmachine (>= 0.12.0) + websocket-driver (>= 0.5.1) + foreman (0.78.0) + thor (~> 0.19.1) + hashie (3.4.1) + i18n (0.7.0) + json (1.8.2) + minitest (5.6.1) + multi_json (1.11.0) + multipart-post (2.0.0) + parser (2.3.0.pre.2) + ast (>= 1.1, < 3.0) + powerpack (0.1.1) + puma (2.11.0) + rack (>= 1.1, < 2.0) + rack (1.6.0) + rack-protection (1.5.3) + rack + rack-test (0.6.3) + rack (>= 1.0) + rainbow (2.0.0) + rake (10.4.2) + rspec (3.2.0) + rspec-core (~> 3.2.0) + rspec-expectations (~> 3.2.0) + rspec-mocks (~> 3.2.0) + rspec-core (3.2.3) + rspec-support (~> 3.2.0) + rspec-expectations (3.2.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-mocks (3.2.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-support (3.2.2) + rubocop (0.31.0) + astrolabe (~> 1.3) + parser (>= 2.2.2.1, < 3.0) + powerpack (~> 0.1) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.4) + ruby-progressbar (1.7.5) + safe_yaml (1.0.4) + sinatra (1.4.5) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) + slack-api (1.1.6) + faraday (>= 0.7, < 0.10) + faraday_middleware (~> 0.8) + faye-websocket (~> 0.9.2) + multi_json (~> 1.0, >= 1.0.3) + thor (0.19.1) + thread_safe (0.3.5) + tilt (1.4.1) + tzinfo (1.2.2) + thread_safe (~> 0.1) + vcr (2.9.3) + webmock (1.21.0) + addressable (>= 2.3.6) + crack (>= 0.3.2) + websocket-driver (0.5.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + +PLATFORMS + ruby + +DEPENDENCIES + activesupport + dentaku + foreman + hashie + puma + rack-test (~> 0.6.2) + rake (~> 10.4) + rspec (~> 3.2) + rubocop (= 0.31.0) + sinatra + slack-api (~> 1.1.6) + vcr + webmock diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..81ae90d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2015 Daniel Doubrovkine, Artsy and Contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..4fd3163 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: bundle exec puma -p $PORT diff --git a/README.md b/README.md new file mode 100644 index 0000000..44773b7 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +Slack-Gamebot +============= + +[![Build Status](https://travis-ci.org/dblock/slack-mathbot.png)](https://travis-ci.org/dblock/slack-mathbot) + +A math bot for Slack. + +![](screenshots/two-plus-two.gif) + +## Installation + +Create a new Bot Integration under [services/new/bot](http://slack.com/services/new/bot). + +![](screenshots/register-bot.png) + +On the next screen, note the API token. + +Run `SLACK_API_TOKEN= foreman start` + +## Production Deployment + +See [DEPLOYMENT](DEPLOYMENT.md). + +## Usage + +### Commands + +#### mathbot calculate [expression] + +Calculates an expression, currently just basic math. See [Dentaku](https://github.com/rubysolo/dentaku) for what's supported. + +#### mathbot + +Shows MathBot version and links. + +#### mathbot hi + +Politely says 'hi' back. + +#### mathbot help + +Get help. + +## Contributing + +See [CONTRIBUTING](CONTRIBUTING.md). + +## Copyright and License + +Copyright (c) 2015, Daniel Doubrovkine, Artsy and [Contributors](CHANGELOG.md). + +This project is licensed under the [MIT License](LICENSE.md). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..32f34c9 --- /dev/null +++ b/Rakefile @@ -0,0 +1,18 @@ +require 'rubygems' +require 'bundler' + +Bundler.setup :default, :development + +unless ENV['RACK_ENV'] == 'production' + require 'rspec/core' + require 'rspec/core/rake_task' + + RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList['spec/**/*_spec.rb'] + end + + require 'rubocop/rake_task' + RuboCop::RakeTask.new + + task default: [:rubocop, :spec] +end diff --git a/app.json b/app.json new file mode 100644 index 0000000..26085b2 --- /dev/null +++ b/app.json @@ -0,0 +1,6 @@ +{ + "name": "Math bot for Slack", + "description": "Slack integration with math.", + "respository": "https://github.com/dblock/slack-mathbot", + "keywords": ["slack", "integration", "math"], +} diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..62238ee --- /dev/null +++ b/app.rb @@ -0,0 +1,11 @@ +require File.expand_path('../config/environment', __FILE__) + +require 'sinatra/base' + +module SlackMathbot + class Web < Sinatra::Base + get '/' do + 'Math is good for you.' + end + end +end diff --git a/app/slack-mathbot.rb b/app/slack-mathbot.rb new file mode 100644 index 0000000..0a6765b --- /dev/null +++ b/app/slack-mathbot.rb @@ -0,0 +1,18 @@ +require 'slack-mathbot/version' +require 'slack-mathbot/about' +require 'slack-mathbot/config' +require 'slack-mathbot/hooks' +require 'slack-mathbot/commands' +require 'slack-mathbot/app' + +module SlackMathbot + class << self + def configure + block_given? ? yield(Config) : Config + end + + def config + Config + end + end +end diff --git a/app/slack-mathbot/about.rb b/app/slack-mathbot/about.rb new file mode 100644 index 0000000..9401cdb --- /dev/null +++ b/app/slack-mathbot/about.rb @@ -0,0 +1,7 @@ +module SlackMathbot + ABOUT = <<-ABOUT + #{SlackMathbot::VERSION} + https://github.com/dblock/slack-mathbot + https://twitter.com/dblockdotorg + ABOUT +end diff --git a/app/slack-mathbot/app.rb b/app/slack-mathbot/app.rb new file mode 100644 index 0000000..87f9e71 --- /dev/null +++ b/app/slack-mathbot/app.rb @@ -0,0 +1,83 @@ +module SlackMathbot + class App + cattr_accessor :hooks + + include SlackMathbot::Hooks::Hello + include SlackMathbot::Hooks::Message + + def initialize + SlackMathbot.configure do |config| + config.token = ENV['SLACK_API_TOKEN'] || fail("Missing ENV['SLACK_API_TOKEN'].") + end + Slack.configure do |config| + config.token = SlackMathbot.config.token + end + end + + def config + SlackMathbot.config + end + + def self.instance + @instance ||= SlackMathbot::App.new + end + + def run + auth! + start! + end + + def stop! + client.stop + end + + private + + def logger + @logger ||= begin + $stdout.sync = true + Logger.new(STDOUT) + end + end + + def start! + loop do + client.start + @client = nil + end + end + + def client + @client ||= begin + client = Slack.realtime + hooks.each do |hook| + client.on hook do |data| + begin + send hook, data + rescue StandardError => e + logger.error e + begin + Slack.chat_postMessage(channel: data['channel'], text: e.message) if data.key?('channel') + rescue + # ignore + end + end + end + end + client + end + end + + def auth! + auth = Slack.auth_test + SlackMathbot.configure do |config| + config.url = auth['url'] + config.team = auth['team'] + config.user = auth['user'] + config.team_id = auth['team_id'] + config.user_id = auth['user_id'] + end + logger.info "Welcome '#{SlackMathbot.config.user}' to the '#{SlackMathbot.config.team}' team at #{SlackMathbot.config.url}." + end + end +end diff --git a/app/slack-mathbot/commands.rb b/app/slack-mathbot/commands.rb new file mode 100644 index 0000000..ab8311a --- /dev/null +++ b/app/slack-mathbot/commands.rb @@ -0,0 +1,6 @@ +require 'slack-mathbot/commands/base' +require 'slack-mathbot/commands/calculate' +require 'slack-mathbot/commands/about' +require 'slack-mathbot/commands/help' +require 'slack-mathbot/commands/hi' +require 'slack-mathbot/commands/unknown' diff --git a/app/slack-mathbot/commands/about.rb b/app/slack-mathbot/commands/about.rb new file mode 100644 index 0000000..8dd9328 --- /dev/null +++ b/app/slack-mathbot/commands/about.rb @@ -0,0 +1,9 @@ +module SlackMathbot + module Commands + class Default < Base + def self.call(data, _command, _arguments) + send_message data.channel, SlackMathbot::ABOUT + end + end + end +end diff --git a/app/slack-mathbot/commands/base.rb b/app/slack-mathbot/commands/base.rb new file mode 100644 index 0000000..f89caa1 --- /dev/null +++ b/app/slack-mathbot/commands/base.rb @@ -0,0 +1,16 @@ +module SlackMathbot + module Commands + class Base + def self.send_message(channel, text) + Slack.chat_postMessage(channel: channel, text: text) + end + + def self.logger + @logger ||= begin + $stdout.sync = true + Logger.new(STDOUT) + end + end + end + end +end diff --git a/app/slack-mathbot/commands/calculate.rb b/app/slack-mathbot/commands/calculate.rb new file mode 100644 index 0000000..a554d61 --- /dev/null +++ b/app/slack-mathbot/commands/calculate.rb @@ -0,0 +1,10 @@ +module SlackMathbot + module Commands + class Calculate < Base + def self.call(data, _command, arguments) + result = Dentaku::Calculator.new.evaluate(arguments.join) || 'Got nothing.' + send_message data.channel, result.to_s + end + end + end +end diff --git a/app/slack-mathbot/commands/help.rb b/app/slack-mathbot/commands/help.rb new file mode 100644 index 0000000..b9de666 --- /dev/null +++ b/app/slack-mathbot/commands/help.rb @@ -0,0 +1,9 @@ +module SlackMathbot + module Commands + class Help < Base + def self.call(data, _command, _arguments) + send_message data.channel, 'See https://github.com/dblock/slack-mathbot, please.' + end + end + end +end diff --git a/app/slack-mathbot/commands/hi.rb b/app/slack-mathbot/commands/hi.rb new file mode 100644 index 0000000..85711d3 --- /dev/null +++ b/app/slack-mathbot/commands/hi.rb @@ -0,0 +1,9 @@ +module SlackMathbot + module Commands + class Hi < Base + def self.call(data, _command, _arguments) + send_message data.channel, "Hi <@#{data.user}>!" + end + end + end +end diff --git a/app/slack-mathbot/commands/unknown.rb b/app/slack-mathbot/commands/unknown.rb new file mode 100644 index 0000000..ef43ec5 --- /dev/null +++ b/app/slack-mathbot/commands/unknown.rb @@ -0,0 +1,9 @@ +module SlackMathbot + module Commands + class Unknown < Base + def self.call(data, _command, _arguments) + send_message data.channel, "Sorry <@#{data.user}>, I don't understand that command!" + end + end + end +end diff --git a/app/slack-mathbot/config.rb b/app/slack-mathbot/config.rb new file mode 100644 index 0000000..8762192 --- /dev/null +++ b/app/slack-mathbot/config.rb @@ -0,0 +1,13 @@ +module SlackMathbot + module Config + extend self + + attr_accessor :token + attr_accessor :url + attr_accessor :user + attr_accessor :user_id + attr_accessor :team + attr_accessor :team_id + attr_accessor :secret + end +end diff --git a/app/slack-mathbot/hooks.rb b/app/slack-mathbot/hooks.rb new file mode 100644 index 0000000..7df4162 --- /dev/null +++ b/app/slack-mathbot/hooks.rb @@ -0,0 +1,3 @@ +require 'slack-mathbot/hooks/base' +require 'slack-mathbot/hooks/hello' +require 'slack-mathbot/hooks/message' diff --git a/app/slack-mathbot/hooks/base.rb b/app/slack-mathbot/hooks/base.rb new file mode 100644 index 0000000..55e1875 --- /dev/null +++ b/app/slack-mathbot/hooks/base.rb @@ -0,0 +1,10 @@ +module SlackMathbot + module Hooks + module Base + def included(caller) + caller.hooks ||= [] + caller.hooks << name.demodulize.underscore.to_sym + end + end + end +end diff --git a/app/slack-mathbot/hooks/hello.rb b/app/slack-mathbot/hooks/hello.rb new file mode 100644 index 0000000..8b0a2d1 --- /dev/null +++ b/app/slack-mathbot/hooks/hello.rb @@ -0,0 +1,11 @@ +module SlackMathbot + module Hooks + module Hello + extend Base + + def hello(_data) + logger.info "Successfully connected to #{SlackMathbot.config.url}." + end + end + end +end diff --git a/app/slack-mathbot/hooks/message.rb b/app/slack-mathbot/hooks/message.rb new file mode 100644 index 0000000..776e356 --- /dev/null +++ b/app/slack-mathbot/hooks/message.rb @@ -0,0 +1,33 @@ +module SlackMathbot + module Hooks + module Message + extend Base + + def message(data) + data = Hashie::Mash.new(data) + bot_name, command, arguments = parse_command(data.text) + return unless bot_name == SlackMathbot.config.user + klass = command_to_class(command || 'Default') + klass.call data, command, arguments + end + + private + + def parse_command(text) + return unless text + text = '= ' + text[1..text.length] if text[0] == '=' + parts = text.split.reject(&:blank?) + if parts && parts[0] == '=' + parts[0] = SlackMathbot.config.user + parts.insert 1, 'calculate' + end + [parts.first.downcase, parts[1].try(:downcase), parts[2..parts.length]] if parts && parts.any? + end + + def command_to_class(command) + klass = "SlackMathbot::Commands::#{command.titleize}".constantize rescue nil + klass || SlackMathbot::Commands::Unknown + end + end + end +end diff --git a/app/slack-mathbot/version.rb b/app/slack-mathbot/version.rb new file mode 100644 index 0000000..ec90e43 --- /dev/null +++ b/app/slack-mathbot/version.rb @@ -0,0 +1,3 @@ +module SlackMathbot + VERSION = '0.1.0' +end diff --git a/app/slack_mathbot.rb b/app/slack_mathbot.rb new file mode 100644 index 0000000..bb25115 --- /dev/null +++ b/app/slack_mathbot.rb @@ -0,0 +1 @@ +require 'slack-mathbot' diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..bf4320f --- /dev/null +++ b/config.ru @@ -0,0 +1,7 @@ +require File.expand_path('../app', __FILE__) + +Thread.new do + SlackMathbot::App.instance.run +end + +run SlackMathbot::Web diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..c2134eb --- /dev/null +++ b/config/application.rb @@ -0,0 +1,14 @@ +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'app')) +$LOAD_PATH.unshift(File.dirname(__FILE__)) + +require 'boot' + +Bundler.require :default, ENV['RACK_ENV'] + +Dir[File.expand_path('../initializers', __FILE__) + '/**/*.rb'].each do |file| + require file +end + +require File.expand_path('../application', __FILE__) + +require 'slack_mathbot' diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..ca944a8 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,5 @@ +require 'rubygems' +require 'bundler/setup' +require 'logger' +require 'active_support' +require 'active_support/core_ext' diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..f45d5f6 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,3 @@ +ENV['RACK_ENV'] ||= 'development' + +require File.expand_path('../application', __FILE__) diff --git a/config/initializers/slack/request.rb b/config/initializers/slack/request.rb new file mode 100644 index 0000000..66b1583 --- /dev/null +++ b/config/initializers/slack/request.rb @@ -0,0 +1,13 @@ +module Slack + module Request + private + + alias_method :_request, :request + + def request(method, path, options) + response = _request(method, path, options) + fail response['error'] unless response['ok'] if response.is_a?(Hash) + response + end + end +end diff --git a/screenshots/register-bot.png b/screenshots/register-bot.png new file mode 100644 index 0000000..391a201 Binary files /dev/null and b/screenshots/register-bot.png differ diff --git a/screenshots/two-plus-two.gif b/screenshots/two-plus-two.gif new file mode 100644 index 0000000..b95dbe0 Binary files /dev/null and b/screenshots/two-plus-two.gif differ diff --git a/spec/fixtures/slack/auth_test.yml b/spec/fixtures/slack/auth_test.yml new file mode 100644 index 0000000..50f08fe --- /dev/null +++ b/spec/fixtures/slack/auth_test.yml @@ -0,0 +1,103 @@ +--- +http_interactions: +- request: + method: post + uri: https://slack.com/api/auth.test + body: + encoding: UTF-8 + string: token=token + headers: + Accept: + - application/json; charset=utf-8 + User-Agent: + - Slack Ruby Gem 1.1.1 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - private, no-cache, no-store, must-revalidate + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 28 Apr 2015 12:55:23 GMT + Expires: + - Mon, 26 Jul 1997 05:00:00 GMT + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - identify + X-Content-Type-Options: + - nosniff + X-Oauth-Scopes: + - identify,read,post,client + X-Xss-Protection: + - '0' + Content-Length: + - '128' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"ok":true,"url":"https:\/\/math.slack.com\/","team":"calcteam","user":"calcuser","team_id":"TDEADBEEF","user_id":"UBAADFOOD"}' + http_version: + recorded_at: Tue, 28 Apr 2015 12:55:22 GMT +- request: + method: post + uri: https://slack.com/api/rtm.start + body: + encoding: UTF-8 + string: token=token + headers: + Accept: + - application/json; charset=utf-8 + User-Agent: + - Slack Ruby Gem 1.1.1 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Origin: + - '*' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 28 Apr 2015 21:41:33 GMT + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - '0' + Content-Length: + - '55' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"ok":false,"error":"invalid_auth"}' + http_version: + recorded_at: Tue, 28 Apr 2015 21:41:33 GMT +recorded_with: VCR 2.9.3 diff --git a/spec/fixtures/slack/user_info.yml b/spec/fixtures/slack/user_info.yml new file mode 100644 index 0000000..9751a21 --- /dev/null +++ b/spec/fixtures/slack/user_info.yml @@ -0,0 +1,59 @@ +--- +http_interactions: +- request: + method: post + uri: https://slack.com/api/users.info + body: + encoding: UTF-8 + string: token=token&user=user + headers: + Accept: + - application/json; charset=utf-8 + User-Agent: + - Slack Ruby Gem 1.1.1 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - private, no-cache, no-store, must-revalidate + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 29 Apr 2015 16:10:34 GMT + Expires: + - Mon, 26 Jul 1997 05:00:00 GMT + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - read + X-Content-Type-Options: + - nosniff + X-Oauth-Scopes: + - identify,read,post,client + X-Xss-Protection: + - '0' + Content-Length: + - '448' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"ok":true,"user":{"id":"U007","name":"username","deleted":false,"status":null,"color":"9f69e7","real_name":"","tz":"America\/Indiana\/Indianapolis","tz_label":"Eastern + Daylight Time","tz_offset":-14400,"profile":{"real_name":"","real_name_normalized":"","email":"dblock@dblock.org","image_24":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0015-24.png","image_32":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0015-32.png","image_48":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0015-48.png","image_72":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0015-72.png","image_192":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F272a%2Fimg%2Favatars%2Fava_0015.png"},"is_admin":true,"is_owner":true,"is_primary_owner":true,"is_restricted":false,"is_ultra_restricted":false,"is_bot":false,"has_files":false}}' + http_version: + recorded_at: Wed, 29 Apr 2015 16:10:34 GMT + diff --git a/spec/slack-mathbot/app_spec.rb b/spec/slack-mathbot/app_spec.rb new file mode 100644 index 0000000..93a9fe5 --- /dev/null +++ b/spec/slack-mathbot/app_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe SlackMathbot::App do + subject do + SlackMathbot::App.new + end + context 'not configured' do + before do + @slack_api_token = ENV.delete('SLACK_API_TOKEN') + end + after do + ENV['SLACK_API_TOKEN'] = @slack_api_token + end + it 'requires SLACK_API_TOKEN' do + expect { subject }.to raise_error RuntimeError, "Missing ENV['SLACK_API_TOKEN']." + end + end + context 'configured', vcr: { cassette_name: 'auth_test' } do + context 'run' do + before do + subject.send(:auth!) + end + it 'succeeds auth' do + expect(subject.config.url).to eq 'https://math.slack.com/' + expect(subject.config.team).to eq 'calcteam' + expect(subject.config.user).to eq 'calcuser' + expect(subject.config.team_id).to eq 'TDEADBEEF' + expect(subject.config.user_id).to eq 'UBAADFOOD' + end + end + end +end diff --git a/spec/slack-mathbot/commands/about_spec.rb b/spec/slack-mathbot/commands/about_spec.rb new file mode 100644 index 0000000..6ca4b20 --- /dev/null +++ b/spec/slack-mathbot/commands/about_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe SlackMathbot::Commands::Default do + it 'mathbot' do + expect(message: 'mathbot').to respond_with_slack_message(SlackMathbot::ABOUT) + end + it 'Mathbot' do + expect(message: 'Mathbot').to respond_with_slack_message(SlackMathbot::ABOUT) + end +end diff --git a/spec/slack-mathbot/commands/calculate_spec.rb b/spec/slack-mathbot/commands/calculate_spec.rb new file mode 100644 index 0000000..5e8c5d4 --- /dev/null +++ b/spec/slack-mathbot/commands/calculate_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe SlackMathbot::Commands::Calculate, vcr: { cassette_name: 'user_info' } do + it 'adds two numbers' do + expect(message: 'mathbot calculate 2+2', channel: 'channel').to respond_with_slack_message('4') + end + it 'adds two numbers via =' do + expect(message: '= 2+2', channel: 'channel').to respond_with_slack_message('4') + end + it 'adds two numbers via = without a space' do + expect(message: '=2+2', channel: 'channel').to respond_with_slack_message('4') + end + it 'sends something without an answer' do + expect(message: 'mathbot calculate pi', channel: 'channel').to respond_with_slack_message('Got nothing.') + end +end diff --git a/spec/slack-mathbot/commands/help_spec.rb b/spec/slack-mathbot/commands/help_spec.rb new file mode 100644 index 0000000..0db53b4 --- /dev/null +++ b/spec/slack-mathbot/commands/help_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe SlackMathbot::Commands::Help do + it 'help' do + expect(message: 'mathbot help').to respond_with_slack_message('See https://github.com/dblock/slack-mathbot, please.') + end +end diff --git a/spec/slack-mathbot/commands/hi_spec.rb b/spec/slack-mathbot/commands/hi_spec.rb new file mode 100644 index 0000000..b6fb021 --- /dev/null +++ b/spec/slack-mathbot/commands/hi_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe SlackMathbot::Commands::Hi do + it 'says hi' do + expect(message: 'mathbot hi').to respond_with_slack_message('Hi <@user>!') + end +end diff --git a/spec/slack-mathbot/commands/unknown_spec.rb b/spec/slack-mathbot/commands/unknown_spec.rb new file mode 100644 index 0000000..a6330c4 --- /dev/null +++ b/spec/slack-mathbot/commands/unknown_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe SlackMathbot::Commands::Unknown, vcr: { cassette_name: 'user_info' } do + it 'invalid command' do + expect(message: 'mathbot foobar').to respond_with_slack_message("Sorry <@user>, I don't understand that command!") + end + it 'does not respond to sad face' do + expect(SlackMathbot::Commands::Base).to_not receive(:send_message) + SlackMathbot::App.new.send(:message, text: ':((') + end +end diff --git a/spec/slack-mathbot/version_spec.rb b/spec/slack-mathbot/version_spec.rb new file mode 100644 index 0000000..754c246 --- /dev/null +++ b/spec/slack-mathbot/version_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe SlackMathbot do + it 'has a version' do + expect(SlackMathbot::VERSION).to_not be nil + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..ac07315 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,12 @@ +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..')) + +require 'rubygems' +require 'rspec' +require 'rack/test' + +require 'config/environment' +require 'slack-mathbot' + +Dir[File.join(File.dirname(__FILE__), 'support', '**/*.rb')].each do |file| + require file +end diff --git a/spec/support/slack-mathbot/respond_with_error.rb b/spec/support/slack-mathbot/respond_with_error.rb new file mode 100644 index 0000000..e35787c --- /dev/null +++ b/spec/support/slack-mathbot/respond_with_error.rb @@ -0,0 +1,30 @@ +require 'rspec/expectations' + +RSpec::Matchers.define :respond_with_error do |expected| + match do |actual| + channel, user, message = parse(actual) + app = SlackMathbot::App.new + SlackMathbot.config.user = 'mathbot' + begin + expect do + app.send(:message, text: message, channel: channel, user: user) + end.to raise_error ArgumentError, expected + rescue RSpec::Expectations::ExpectationNotMetError => e + @error_message = e.message + raise e + end + true + end + + failure_message do |actual| + _, _, message = parse(actual) + @error_message || "expected for '#{message}' to fail with '#{expected}'" + end + + private + + def parse(actual) + actual = { message: actual } unless actual.is_a?(Hash) + [actual[:channel] || 'channel', actual[:user] || 'user', actual[:message]] + end +end diff --git a/spec/support/slack-mathbot/respond_with_slack_message.rb b/spec/support/slack-mathbot/respond_with_slack_message.rb new file mode 100644 index 0000000..cbfb68b --- /dev/null +++ b/spec/support/slack-mathbot/respond_with_slack_message.rb @@ -0,0 +1,19 @@ +require 'rspec/expectations' + +RSpec::Matchers.define :respond_with_slack_message do |expected| + match do |actual| + channel, user, message = parse(actual) + app = SlackMathbot::App.new + SlackMathbot.config.user = 'mathbot' + expect(SlackMathbot::Commands::Base).to receive(:send_message).with(channel, expected) + app.send(:message, text: message, channel: channel, user: user) + true + end + + private + + def parse(actual) + actual = { message: actual } unless actual.is_a?(Hash) + [actual[:channel] || 'channel', actual[:user] || 'user', actual[:message]] + end +end diff --git a/spec/support/slack_api_key.rb b/spec/support/slack_api_key.rb new file mode 100644 index 0000000..fdaff36 --- /dev/null +++ b/spec/support/slack_api_key.rb @@ -0,0 +1,5 @@ +RSpec.configure do |config| + config.before :all do + ENV['SLACK_API_TOKEN'] ||= 'test' + end +end diff --git a/spec/support/slack_calculator.rb b/spec/support/slack_calculator.rb new file mode 100644 index 0000000..a80a44f --- /dev/null +++ b/spec/support/slack_calculator.rb @@ -0,0 +1,4 @@ +SlackMathbot.configure do |config| + config.token = 'testtoken' + config.user = 'mathbot' +end diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb new file mode 100644 index 0000000..768541a --- /dev/null +++ b/spec/support/vcr.rb @@ -0,0 +1,8 @@ +require 'vcr' + +VCR.configure do |config| + config.cassette_library_dir = 'spec/fixtures/slack' + config.hook_into :webmock + # config.default_cassette_options = { record: :new_episodes } + config.configure_rspec_metadata! +end