daneel 0.0.1 → 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore CHANGED
@@ -1,18 +1,12 @@
1
- *.gem
2
- *.rbc
3
- .bundle/
4
- .config/
5
- .yardoc/
6
- _yardoc/
7
- Gemfile.lock
8
- InstalledFiles
9
- coverage/
10
- doc/
11
- lib/bundler/man/
12
- pkg/
13
- rdoc/
14
- spec/reports/
15
- test/tmp/
16
- test/version_tmp/
17
- tmp/
18
- vendor/bundle/
1
+ /*.gem
2
+ /.bundle/
3
+ /.env
4
+ /.yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /exec/
8
+ /Gemfile.lock
9
+ /pkg/
10
+ /rdoc/
11
+ /tmp/
12
+ /vendor/bundle/
data/Gemfile CHANGED
@@ -2,3 +2,11 @@ source 'http://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in daneel.gemspec
4
4
  gemspec
5
+
6
+ # Override gem dependencies that aren't released yet
7
+ gem 'sinatra', :github => 'sinatra'
8
+ gem 'sparks', :github => 'indirect/sparks', :branch => 'master'
9
+
10
+ group :development do
11
+ gem 'pry-debugger'
12
+ end
@@ -0,0 +1 @@
1
+ web: ruby -Ku -rbundler/setup -Ilib -S ./bin/daneel -v -s -a campfire -n "wes" -f "Wesabot"
@@ -0,0 +1,18 @@
1
+ require 'daneel'
2
+
3
+ trap(:INT) do
4
+ puts "Quitting...\n"
5
+ exit 0
6
+ end
7
+
8
+ options = Daneel::Options.parse(ARGV)
9
+ options[:logger] = Daneel::Logger.new(STDOUT, options[:verbose])
10
+
11
+ if options.has_key?(:server)
12
+ require 'daneel/server'
13
+ sopts = options.delete(:server)
14
+ sopts[:logger] = options[:logger]
15
+ Daneel::Server.new(sopts).run
16
+ end
17
+
18
+ Daneel::Bot.new(options).run
@@ -1,20 +1,27 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/daneel/version', __FILE__)
1
+ # encoding: UTF-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'daneel/version'
3
5
 
4
6
  Gem::Specification.new do |gem|
5
- gem.authors = ["Andre Arko"]
7
+ gem.name = "daneel"
8
+ gem.version = Daneel::VERSION
9
+ gem.summary = %q{A 19,230-year-old robot}
10
+ gem.description = %q{Daneel is a chatbot inspired by the late, lamented Wesabot. And also Hubot.}
11
+ gem.authors = ["André Arko"]
6
12
  gem.email = ["andre@arko.net"]
7
- gem.description = %q{A 19,230-year-old Campfire bot}
8
- gem.summary = %q{A library and rack app to bot up your Campfire rooms}
9
13
  gem.homepage = "http://github.com/indirect/daneel"
14
+ gem.license = "MIT"
10
15
 
11
- gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
- gem.files = `git ls-files`.split("\n")
13
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
- gem.name = "daneel"
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
19
  gem.require_paths = ["lib"]
16
- gem.version = Daneel::VERSION
17
20
 
18
- gem.add_dependency "firering", "~> 1.2.0"
19
- gem.add_dependency "activerecord", "~> 3.1.0"
21
+ gem.add_dependency "sparks", "~> 0.4"
22
+ gem.add_dependency "sinatra", "~> 1.4.0"
23
+ gem.add_dependency "puma", "~> 1.6.3"
24
+
25
+ gem.add_development_dependency "rake", "~> 10.0"
26
+ gem.add_development_dependency "bundler", "~> 1.2"
20
27
  end
@@ -1,5 +1,7 @@
1
- require "daneel/version"
1
+ require 'daneel/bot'
2
+ require 'daneel/logger'
3
+ require 'daneel/options'
4
+ require 'daneel/version'
2
5
 
3
6
  module Daneel
4
- # Your code goes here...
5
7
  end
@@ -0,0 +1,37 @@
1
+ require 'daneel/message'
2
+ require 'daneel/plugin'
3
+ require 'daneel/room'
4
+ require 'daneel/user'
5
+
6
+ module Daneel
7
+ class Adapter < Plugin
8
+
9
+ def run
10
+ # listen to rooms and dispatch messages to robot.receive
11
+ end
12
+
13
+ def say(room_id, message)
14
+ # get the message into the room!
15
+ end
16
+
17
+ def announce(message)
18
+ # say the message into every room the bot is in
19
+ end
20
+
21
+ def me
22
+ @me ||= User.new(0, "R. Daneel Olivaw").tap do |me|
23
+ me.short_name = "Daneel"
24
+ me.initials = "DO"
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def named(name)
30
+ require File.join('daneel/adapters', name.downcase)
31
+ adapter = Daneel::Adapters.const_get(name.capitalize)
32
+ adapter || raise("Couldn't find Daneel::Adapters::#{a.capitalize}")
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,93 @@
1
+ require 'sparks'
2
+ require 'daneel/adapter'
3
+
4
+ module Daneel
5
+ class Adapters
6
+ class Campfire < Daneel::Adapter
7
+ requires_env %w(CAMPFIRE_SUBDOMAIN CAMPFIRE_API_TOKEN CAMPFIRE_ROOM_IDS)
8
+
9
+ def initialize(robot)
10
+ super
11
+ domain = ENV['CAMPFIRE_SUBDOMAIN']
12
+ token = ENV['CAMPFIRE_API_TOKEN']
13
+ @fire = Sparks.new(domain, token, :logger => logger)
14
+
15
+ ENV['CAMPFIRE_ROOM_IDS'].split(",").map(&:to_i).map do |id|
16
+ # Get info about the room state
17
+ room = Room.new(id, self, @fire.room(id))
18
+ robot.data.rooms[id] = room
19
+ # Save the user info for all the users in the room
20
+ room.data["users"].each do |data|
21
+ user = User.new(data["id"], data["name"], data)
22
+ robot.data.users[user.id] = user
23
+ end
24
+ end
25
+ end
26
+
27
+ def run
28
+ @threads ||= []
29
+ robot.data.rooms.each do |id, room|
30
+ t = Thread.new { watch_room(room) } until t
31
+ t.abort_on_exception = true
32
+ @threads << t
33
+ end
34
+ @threads.each{|t| t.join }
35
+ end
36
+
37
+ def say(id, *texts)
38
+ texts.each do |text|
39
+ text =~ /\n/ ? @fire.paste(id, text) : @fire.speak(id, text)
40
+ end
41
+ end
42
+
43
+ def announce(*texts)
44
+ robot.data.rooms.each do |id, room|
45
+ say id, *texts
46
+ end
47
+ end
48
+
49
+ def leave
50
+ # stop the listening threads
51
+ @threads.each{|t| t.kill }
52
+ # leave each room
53
+ robot.data.rooms.each{|r| @fire.room(r.id).leave }
54
+ end
55
+
56
+ def me
57
+ @me ||= begin
58
+ data = @fire.me
59
+ me = User.new(data["id"], data["name"], data)
60
+ robot.data.users[me.id] = me
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def find_user(id)
67
+ robot.data.users[id] ||= begin
68
+ data = @fire.user(id)
69
+ User.new(data["id"], data["name"], data)
70
+ end
71
+ end
72
+
73
+ def watch_room(room)
74
+ @fire.watch(room.id) do |data|
75
+ next if data["type"] == "TimestampMessage"
76
+
77
+ # TODO pass through self-messages, once they are filtered by
78
+ # the accept? method on scripts
79
+ next if data["user_id"] == me.id
80
+
81
+ text = data["body"]
82
+ time = Time.parse(data["created_at"]) rescue Time.now
83
+ type = data["type"].gsub(/Message$/, '').downcase
84
+ mesg = Message.new(text, time, type)
85
+ room = robot.data.rooms[data["room_id"]]
86
+ user = find_user(data["user_id"])
87
+ robot.receive room, mesg, user
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,36 @@
1
+ require 'daneel/adapter'
2
+ require 'readline'
3
+
4
+ module Daneel
5
+ class Adapters
6
+ class Shell < Daneel::Adapter
7
+
8
+ def initialize(robot)
9
+ super
10
+ @room = Room.new("shell", self)
11
+ @user = User.new(1, ENV['USER'])
12
+ end
13
+
14
+ def run
15
+ # End the line we were on when we exit
16
+ trap(:EXIT){ print "\n" }
17
+
18
+ while text = Readline.readline("> ", true)
19
+ next if text.empty?
20
+ message = Message.new(text, Time.now, "text")
21
+ robot.receive @room, message, @user
22
+ end
23
+ end
24
+
25
+ def say(id, *strings)
26
+ puts *strings
27
+ end
28
+
29
+ def announce(*strings)
30
+ puts
31
+ say @room.id, *strings
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,89 @@
1
+ require 'daneel/adapter'
2
+ require 'daneel/script'
3
+ require 'daneel/data'
4
+
5
+ module Daneel
6
+ class Bot
7
+ attr_reader :adapter, :data, :full_name, :logger, :name, :scripts
8
+ attr_accessor :debug_mode
9
+
10
+ def initialize(options = {})
11
+ @logger = options[:logger] || Daneel::Logger.new
12
+ @name = options[:name] || "daneel"
13
+ @full_name = options[:full_name] || options[:name] || "R. Daneel Olivaw"
14
+ @debug_mode = options[:verbose] && options[:adapter] && options[:adapter] != "shell"
15
+
16
+ @data = Data.new
17
+ logger.debug "Data source #{data.class}"
18
+
19
+ Script.files.each{|file| try_require file }
20
+ # TODO add script priorities to replicate this
21
+ list = Script.list
22
+ list.push list.delete(Scripts::ImageSearch)
23
+ list.push list.delete(Scripts::Chatty)
24
+ @scripts = list.map{|s| s.new(self) }
25
+ logger.debug "Booted with scripts: #{@scripts.map(&:class).inspect}"
26
+
27
+ @adapter = Adapter.named(options[:adapter] || "shell").new(self)
28
+ logger.debug "Using the #{adapter.class} adapter"
29
+ end
30
+
31
+ def receive(room, message, user)
32
+ logger.debug "[room #{room.id}] #{user.name}: #{message.text}"
33
+ message.command = command_from(message.text)
34
+
35
+ scripts.each do |script|
36
+ next unless script.accepts?(room, message, user)
37
+ script.receive(room, message, user)
38
+ break if message.done
39
+ end
40
+ message
41
+ rescue => e
42
+ msg = %|#{e.class}: #{e.message}\n #{e.backtrace.join("\n ")}|
43
+ logger.error msg
44
+ adapter.announce "crap, something went wrong. :(", msg if @debug_mode
45
+ end
46
+
47
+ def run
48
+ # TODO add i18n so that people can customize their bot's attitude
49
+ # http://titusd.co.uk/2010/03/04/i18n-internationalization-without-rails/
50
+ # TODO add Confabulator processing so the bot can be chatty without being static
51
+
52
+ # Heroku cycles every process at least once per day by sending it a TERM
53
+ trap(:TERM) do
54
+ @adapter.announce "asked to stop, brb"
55
+ exit
56
+ end
57
+
58
+ @adapter.announce "hey guys"
59
+ @adapter.run
60
+ rescue Interrupt
61
+ adapter.leave
62
+ end
63
+
64
+ def user
65
+ @adapter.me
66
+ end
67
+
68
+ def inspect
69
+ %|#<#{self.class}:#{object_id} @name="#{name}" @adapter=#{adapter.class}>|
70
+ end
71
+
72
+ private
73
+
74
+ def command_from(text)
75
+ return if text.nil? || text.empty?
76
+ m = text.match(/^@#{name}\s+(.*)/i)
77
+ m ||= text.match(/^#{name}(?:[,:]\s*|\s+)(.*)/i)
78
+ m ||= text.match(/^\s*(.*?)(?:,\s*)?\b#{name}[.!?\s]*$/i)
79
+ m && m[1]
80
+ end
81
+
82
+ def try_require(name)
83
+ require name
84
+ rescue Script::DepError => e
85
+ logger.warn "Couldn't load #{File.basename(name)}: #{e.message}"
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,30 @@
1
+ module Daneel
2
+ # Non-persistent storage, extend if you want more.
3
+ class Data
4
+
5
+ def initialize
6
+ @store = {}
7
+ end
8
+
9
+ def save
10
+ # it's a hash, what do you want?
11
+ end
12
+
13
+ def [](key)
14
+ @store[key]
15
+ end
16
+
17
+ def []=(key, value)
18
+ @store[key] = value
19
+ end
20
+
21
+ def users
22
+ @store["users"] ||= {}
23
+ end
24
+
25
+ def rooms
26
+ @store["rooms"] ||= {}
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ require 'logger'
2
+
3
+ module Daneel
4
+ class Logger < ::Logger
5
+
6
+ def initialize(io = $stdout, verbose = false)
7
+ super
8
+ self.level = Logger::INFO unless verbose
9
+ self.formatter = proc do |severity, datetime, progname, msg|
10
+ "#{severity} #{msg}\n"
11
+ end
12
+ end
13
+
14
+ def inspect
15
+ %|#<#{self.class}:#{object_id} @level=#{level}>|
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ require 'shellwords'
2
+
3
+ module Daneel
4
+ class Message
5
+ attr_reader :command, :done, :room, :text, :time, :type
6
+ attr_accessor :args
7
+
8
+ def initialize(text, time = Time.now, type = :text)
9
+ @text, @time, @type = text, time, type
10
+ end
11
+
12
+ def command=(text)
13
+ @command = text
14
+ @args = text ? Shellwords.split(text) : nil
15
+ rescue ArgumentError # shellwords didn't like this
16
+ @args = nil
17
+ end
18
+
19
+ def done!
20
+ @done = true
21
+ end
22
+
23
+ def inspect
24
+ "#<#{self.class} #{text.inspect} #{time.inspect} #{type.inspect}>"
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ require 'optparse'
2
+
3
+ module Daneel
4
+ module Options
5
+
6
+ def self.parse(args)
7
+ options = {}
8
+
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: daneel [options]"
11
+
12
+ opts.on("-v", "--verbose", "Print debugging information") do |v|
13
+ options[:verbose] = v
14
+ end
15
+
16
+ opts.on("-s", "--server [PORT]", "Run the HTTP server on PORT (default 3333)") do |port|
17
+ options[:server] = {:port => port}
18
+ end
19
+
20
+ opts.on("-a", "--adapter ADAPTER", "Which interaction adapter to use") do |name|
21
+ options[:adapter] = name
22
+ end
23
+
24
+ opts.on("-n", "--name NAME", "The name your bot should respond to") do |name|
25
+ options[:name] = name
26
+ end
27
+
28
+ opts.on("-f", "--full-name FULLNAME", "The name your bot will use to refer to itself") do |name|
29
+ options[:full_name] = name
30
+ end
31
+ end.parse(args)
32
+
33
+ return options
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ module Daneel
2
+ class Plugin
3
+ attr_reader :robot
4
+
5
+ def initialize(robot)
6
+ @robot = robot
7
+ end
8
+
9
+ def logger
10
+ robot.logger
11
+ end
12
+
13
+ class DepError < LoadError; end
14
+
15
+ def self.requires_env(*keys)
16
+ missing = keys.flatten.select{|k| ENV[k].nil? || ENV[k].empty? }
17
+ raise DepError, "#{missing.join(',')} must be set" if missing.any?
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Daneel
2
+ class Room
3
+ attr_reader :id
4
+ attr_accessor :data
5
+
6
+ def initialize(id, adapter, data = nil)
7
+ @id, @adapter, @data = id, adapter, data
8
+ end
9
+
10
+ def say(*strings)
11
+ @adapter.say @id, *strings
12
+ end
13
+
14
+ def inspect
15
+ %|#<#{self.class} @id=#{@id.inspect} @adapter=#{@adapter.class}>|
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ require 'daneel/plugin'
2
+
3
+ module Daneel
4
+ class Script < Plugin
5
+
6
+ def accepts?(room, message, user)
7
+ true unless user.id == robot.user.id
8
+ end
9
+
10
+ def receive(room, message, user)
11
+ # do stuff here!
12
+ end
13
+
14
+ def help
15
+ # return a hash of commands and descriptions for help listings
16
+ {}
17
+ end
18
+
19
+ # Track scripts that have loaded so we can use them
20
+ class << self
21
+
22
+ def list
23
+ @list ||= []
24
+ end
25
+
26
+ def inherited(subclass)
27
+ list << subclass
28
+ end
29
+
30
+ def files
31
+ Dir[File.expand_path("../scripts/*.rb", __FILE__)]
32
+ end
33
+
34
+ # TODO accept method for script classes
35
+ # should handle options like :text, :enter, :leave, :topic, :all
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,106 @@
1
+ require 'daneel/script'
2
+
3
+ module Daneel
4
+ module Scripts
5
+ class Chatty < Daneel::Script
6
+ # TODO make this script the last-priority script
7
+ # priority 20
8
+
9
+ def receive(room, message, user)
10
+ # Said to the room in general
11
+ case message.text
12
+ when /^(night|good ?night)(,?\s(all|every(body|one)))$/i
13
+ room.say "goodnight, #{user}"
14
+ when /^(morning|good ?morning)(,?\s(all|every(body|one)))$/i
15
+ room.say "good morning, #{user}"
16
+ end
17
+
18
+ # Said directly to the bot
19
+ case message.command
20
+ when nil
21
+ # don't reply to things not addressed to the bot
22
+ when /^\s*$/
23
+ # question questioners, exclaim at exclaimers, dot dotters
24
+ message.text.match(/([?!.])$/)
25
+ room.say "#{user}#{$1}"
26
+ when /^(hey|hi|hello|sup|howdy)/i
27
+ room.say "#{$1} #{user}"
28
+ when /how are (you|things)|how\'s it (going|hanging)/i
29
+ room.say [
30
+ "Oh, you know, the usual.",
31
+ "can't complain",
32
+ "alright, how about you?",
33
+ "people say things, I say things back"
34
+ ].sample
35
+ when /(^later|(?:good\s*)?bye)/i
36
+ room.say("#{$1} #{user}")
37
+ when /you rock|awesome|cool|nice/i
38
+ room.say [
39
+ "Thanks, #{user}, you're pretty cool yourself.",
40
+ "I try.",
41
+ "Aw, shucks. Thanks, #{user}."
42
+ ].sample
43
+ when /(^|you|still)\s*there/i, /\byt\b/i
44
+ room.say %w{Yup y}.sample
45
+ when /wake up|you awake/i
46
+ room.say("yo")
47
+ when /thanks|thank you/i
48
+ room.say ["No problem.", "np", "any time", "that's what I'm here for", "You're welcome."].sample
49
+ when /^(good\s?night|(?:g')?night)$/i
50
+ room.say [
51
+ "see you later, #{user}",
52
+ "later, #{user}",
53
+ "night",
54
+ "goodnight",
55
+ "bye",
56
+ "have a good night"
57
+ ].sample
58
+ when /^(see you(?: later)?)$/i
59
+ room.say [
60
+ "see you later, #{user}",
61
+ "later, #{user}",
62
+ "bye",
63
+ "later",
64
+ "see ya",
65
+ ].sample
66
+ when /^(?:(?:get|grab|fetch|bring) (.*?)|i need|time for)(?: (?:a|some))? coffee$/i
67
+ person = $1
68
+ if person =~ /i|me|us/
69
+ person, do_they = "you", "do you"
70
+ else
71
+ do_they = "does #{person}"
72
+ end
73
+
74
+ room.say [
75
+ "would #{person} like cream or sugar?",
76
+ "how #{do_they} take it?",
77
+ "coming right up",
78
+ "It is by caffeine alone I set my mind in motion",
79
+ "It is by the beans of Java that thoughts acquire speed",
80
+ "The hands acquire shakes, the shakes become a warning",
81
+ "It is by caffeine alone I set my mind in motion"
82
+ ].sample
83
+ else
84
+ room.say [
85
+ "I have no idea what you're talking about, #{user}.",
86
+ "eh?",
87
+ "oh, interesting",
88
+ "say more, #{user}",
89
+ "#{user}, you do realize that you're talking to a bot with a very limited vocabulary, don't you?",
90
+ "Whatever, #{user}.",
91
+ # TODO implement Bot#other_person
92
+ # "#{bot.other_person(user)}, tell #{user} to leave me alone.",
93
+ "Not now, #{user}.",
94
+ "brb crying",
95
+ "what do you think, #{user}?",
96
+ "That's really something.",
97
+ "but what can I do? I'm just a lowly bot",
98
+ "I'll get some electrons on that right away",
99
+ "How do you feel when someone says '#{message.command}' to you, #{user}?"
100
+ ].sample
101
+ end
102
+ end
103
+
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,21 @@
1
+ require 'daneel/script'
2
+
3
+ module Daneel
4
+ module Scripts
5
+ class Echo < Daneel::Script
6
+
7
+ def receive(room, message, user)
8
+ case message.command
9
+ when /^(?:echo|say)\s(.+)/
10
+ room.say $1
11
+ message.done!
12
+ end
13
+ end
14
+
15
+ def help
16
+ {"echo TEXT" => "are you copying me? stop copying me!"}
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ require 'daneel/script'
2
+
3
+ module Daneel
4
+ module Scripts
5
+ class Help < Daneel::Script
6
+
7
+ def receive(room, message, user)
8
+ case message.command
9
+ when /help$/
10
+ col = helps.keys.map(&:length).max + 2
11
+ room.say helps.map{|k,v| "%-#{col}s %s" % [k,v] }.sort.join("\n")
12
+ message.done!
13
+ end
14
+ end
15
+
16
+ def help
17
+ {"help" => "show this help summary"}
18
+ end
19
+
20
+ private
21
+
22
+ def helps
23
+ @helps ||= begin
24
+ helps = {}
25
+ robot.scripts.each do |script|
26
+ helps.merge!(script.help)
27
+ end
28
+ logger.debug "Found helps: #{helps.inspect}"
29
+ helps
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,50 @@
1
+ require 'cgi'
2
+ require 'net/http/persistent'
3
+
4
+ module Daneel
5
+ # You need an Azure Marketplace Bing Search API key. You can sign up for one here:
6
+ # https://datamarket.azure.com/dataset/bing/search
7
+ # A free subscription allows 5,000 searches per month.
8
+ module Scripts
9
+ class ImageSearch < Daneel::Script
10
+ requires_env "AZURE_API_KEY"
11
+
12
+ def initialize(robot)
13
+ super
14
+ @token = ENV['AZURE_API_KEY']
15
+ @http = Net::HTTP::Persistent.new('daneel')
16
+ end
17
+
18
+ def receive(room, message, user)
19
+ case message.command
20
+ when /image me (.*?)/, /^(?:find) (?:me )?(?:a |another )?(?:picture of )?(.*)$/
21
+ room.say find_image_url_for($1)
22
+ message.done!
23
+ end
24
+ end
25
+
26
+ def help
27
+ {"find a THING" => "scours the internets for a picture of THING to show you"}
28
+ end
29
+
30
+ def find_image_url_for(search)
31
+ logger.debug "Searching for images of #{search}"
32
+ query = CGI.escape("'#{search.gsub(/'/, "\\\\'")}'")
33
+ uri = URI("https://api.datamarket.azure.com/Bing/Search/v1/Composite")
34
+ uri.query = "Sources=%27image%27&Adult=%27Moderate%27&$format=JSON&Query=#{query}"
35
+ request = Net::HTTP::Get.new(uri.request_uri)
36
+ request.basic_auth 'x', @token
37
+ response = @http.request uri, request
38
+ logger.debug "GET #{uri}"
39
+ results = JSON.parse(response.body)["d"]["results"].first["Image"]
40
+ logger.debug "got back #{results.size} images"
41
+ # Random image from the first 50 results
42
+ results.sample["MediaUrl"]
43
+ rescue => e
44
+ logger.error "#{e.class}: #{e.message}"
45
+ room.say "Sorry, something went wrong when I looked for '#{query}'"
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ require 'daneel/script'
2
+
3
+ module Daneel
4
+ module Scripts
5
+ class Meme < Daneel::Script
6
+
7
+ def receive(room, message, user)
8
+ case message.text
9
+ when /^[\:\*\(e]?facepalm[\:\*\)]?|EFACEPALM|:fp|m\($/i
10
+ room.say [
11
+ # picard facepalm
12
+ "https://img.skitch.com/20110224-875h7w1654tgdhgrxm9bikhkwq.jpg",
13
+ # polar bear facepalm
14
+ "https://img.skitch.com/20110224-bsd2ds251eit8d3t1y2mkjtfx8.jpg"
15
+ ].sample
16
+ when /^[\:\*\(e]?double ?facepalm[\:\*\)]?|:fpfp|m\( m\($/i
17
+ # picard + riker facepalm
18
+ room.say "https://img.skitch.com/20110224-ncacgpudhfr2s4te6nswenxaqt.jpg"
19
+ when /^i see your problem/i
20
+ # pony mechanic
21
+ room.say "https://img.skitch.com/20110224-8fmfwdmg6kkrcpijhamhqu7tm6.jpg"
22
+ when /^works on my machine|womm$/i
23
+ # works on my machine
24
+ room.say "https://img.skitch.com/20110224-jrcf6e4gc936a2mxc3mueah2in.png"
25
+ when /^stacktrace or gtfo|stacktrace or it didn't happen|stacktrace\!$/i
26
+ # stacktrace or gtfo
27
+ room.say "https://img.skitch.com/20110224-pqtmiici9wp9nygqi4nw8gs6hg.png"
28
+ when /^this is sparta\!*$/i
29
+ # this is sparta
30
+ room.say "https://img.skitch.com/20110225-k9xpadr2hk37pe5ed4crcqria1.png"
31
+ when /^i have no idea what i'm doing$/i
32
+ # I have no idea what I'm doing
33
+ room.say "https://img.skitch.com/20110304-1tcmatkhapiq6t51wqqq9igat5.jpg"
34
+ when /^party[?!.]*$/i
35
+ # party party party party party cat
36
+ room.say "https://img.skitch.com/20110309-qtd33sy8k5yrdaeqa9e119bwd1.jpg"
37
+ when /^bomb|system error$/i
38
+ # sorry, a system error has occurred
39
+ room.say "https://img.skitch.com/20110312-8g31a37spacdjgatr82g3g98j1.jpg"
40
+ when /^stop hitting yourself$/i
41
+ # and the angel said to him, Stop hitting yourself!
42
+ room.say "https://img.skitch.com/20110316-q7h49p69pjhhyy8756rha2a1jf.jpg"
43
+ when /^[:*(]php[:*)]$/
44
+ # PHP: training wheels without the bike
45
+ room.say "http://tnx.nl/php.jpg"
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ require 'daneel/script'
2
+ require 'pathname'
3
+
4
+ module Daneel
5
+ module Scripts
6
+ class Reload < Daneel::Script
7
+
8
+ def receive(room, message, user)
9
+ case message.command
10
+ when /^update$/
11
+ return unless in_git?
12
+ system("cd #{root} && git pull origin master && bundle install")
13
+ room.say "updated, brb"
14
+ restart
15
+ when /^reload$/
16
+ room.say "k, brb"
17
+ restart
18
+ end
19
+ end
20
+
21
+ def help
22
+ reload = {"reload" => "restarts and reloads #{robot.name}'s code"}
23
+ update = {"update" => "updates #{robot.name}'s code from git and restarts"}
24
+ in_git? ? reload.merge(update) : reload
25
+ end
26
+
27
+ private
28
+
29
+ def restart
30
+ bin = root.join("bin/daneel").realpath.to_s
31
+ cmd = [Gem.ruby, "-S", bin, *ARGV]
32
+
33
+ logger.debug "Reloading: #{cmd.join(' ')}"
34
+ exec *cmd
35
+ end
36
+
37
+ def root
38
+ @root ||= begin
39
+ root = Pathname.new(__FILE__).join("../../../..").expand_path
40
+ logger.debug "Found root directory #{root}"
41
+ root
42
+ end
43
+ end
44
+
45
+ def in_git?
46
+ @in_git ||= root.join(".git").directory?
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ require 'cgi'
2
+ require 'json'
3
+ require 'net/http/persistent'
4
+
5
+ module Daneel
6
+ module Scripts
7
+ class VineSearch < Daneel::Script
8
+
9
+ def initialize(robot)
10
+ super
11
+ @http = Net::HTTP::Persistent.new('daneel')
12
+ end
13
+
14
+ def receive(room, message, user)
15
+ case message.command
16
+ when /vine me (.+)$/, /^(?:find) (?:me )?(?:a |another )?(?:vine of )(.*)$/
17
+ url = find_vine_url_for($1)
18
+ if url
19
+ room.say url
20
+ else
21
+ room.say "Sorry, Twitter didn't have any Vines about that."
22
+ end
23
+ message.done!
24
+ end
25
+ end
26
+
27
+ def help
28
+ {"find a vine of THING" => "shows you a vine that was tweeted mentioning THING"}
29
+ end
30
+
31
+ def find_vine_url_for(search)
32
+ logger.debug "Searching for a vine of #{search}"
33
+ uri = URI("http://search.twitter.com/search.json")
34
+ query = CGI.escape("#{search} source:vine_for_ios")
35
+ uri.query = "rpp=5&include_entities=true&q=#{query}"
36
+ request = Net::HTTP::Get.new(uri.request_uri)
37
+ response = @http.request uri, request
38
+ logger.debug "GET #{uri}"
39
+ results = JSON.parse(response.body)["results"]
40
+ logger.debug "got back #{results.size} vines"
41
+ return nil if results.empty?
42
+ # Pull out the Vine display url of a random result
43
+ results.sample["entities"]["urls"].first["expanded_url"]
44
+ rescue => e
45
+ logger.error "#{e.class}: #{e.message}"
46
+ room.say "Sorry, something went wrong when I looked for '#{query}'"
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ require 'net/http/persistent'
2
+ require 'daneel'
3
+
4
+ module Daneel
5
+ class Server
6
+ attr_reader :logger
7
+
8
+ def initialize(options = {})
9
+ require 'daneel/web'
10
+ @logger = options[:logger] || Daneel::Logger.new
11
+ @options = {:app => Daneel::Web, :server => "puma"}.merge(options)
12
+ @options[:port] = ENV["PORT"] if ENV["PORT"]
13
+ # Rack expects the port key to be capitalized. Sad day.
14
+ @options[:Port] = @options.delete(:port) if @options[:port]
15
+ logger.debug "Server with options: #{@options}"
16
+ end
17
+
18
+ def run
19
+ Thread.new { run_server }
20
+ sleep 0.1 # boot server before allowing possible interaction
21
+ Thread.new { run_self_ping }
22
+ end
23
+
24
+ def run_server
25
+ Rack::Server.start(@options)
26
+ end
27
+
28
+ def run_self_ping
29
+ return unless ENV['HEROKU_URL']
30
+ uri = URI(ENV['HEROKU_URL'])
31
+ http = Net::HTTP::Persistent.new 'daneel'
32
+ loop do
33
+ http.request uri
34
+ sleep (60 * 20) # 20m
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+ module Daneel
3
+ # Represents a generic user. Doesn't have a room, because multiple rooms can
4
+ # contain the same user. The user's unique identifier is supplied by the
5
+ # adapter, and only needs to be unique within the context of that adapter.
6
+ class User
7
+ attr_reader :id, :name
8
+ attr_accessor :data, :initials, :short_name
9
+
10
+ def initialize(id, name, data = nil)
11
+ @id, @name, @data = id, name, data
12
+
13
+ # First, try to get initials from the upper-case letters
14
+ @initials = name.gsub(/\P{Upper}/,'')
15
+ # If that fails, just go with the first letter of each word
16
+ @initials = name.gsub(/(?<!^|\s)./,'') if @initials.empty?
17
+ # Short name is just the bit up to the first space
18
+ @short_name = name.match(/^(\S+)/)[0]
19
+ end
20
+
21
+ def to_s
22
+ [short_name, short_name.downcase, initials].sample
23
+ end
24
+
25
+ end
26
+ end
@@ -1,3 +1,3 @@
1
1
  module Daneel
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1"
3
3
  end
@@ -0,0 +1,12 @@
1
+ require 'sinatra/base'
2
+ module Daneel
3
+ class Web < Sinatra::Base
4
+ set :logging, true
5
+
6
+ get '/' do
7
+ content_type "text/plain"
8
+ "I hope to one day have a positronic brain, like my namesake R. Daneel Olivaw."
9
+ end
10
+
11
+ end
12
+ end
metadata CHANGED
@@ -1,104 +1,158 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: daneel
3
- version: !ruby/object:Gem::Version
4
- hash: 29
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 0
9
- - 1
10
- version: 0.0.1
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
11
6
  platform: ruby
12
- authors:
13
- - Andre Arko
7
+ authors:
8
+ - André Arko
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2011-11-09 00:00:00 -10:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
22
- name: firering
12
+ date: 2013-01-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sparks
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.4'
22
+ type: :runtime
23
23
  prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.4'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sinatra
32
+ requirement: !ruby/object:Gem::Requirement
25
33
  none: false
26
- requirements:
34
+ requirements:
27
35
  - - ~>
28
- - !ruby/object:Gem::Version
29
- hash: 31
30
- segments:
31
- - 1
32
- - 2
33
- - 0
34
- version: 1.2.0
36
+ - !ruby/object:Gem::Version
37
+ version: 1.4.0
35
38
  type: :runtime
36
- version_requirements: *id001
37
- - !ruby/object:Gem::Dependency
38
- name: activerecord
39
39
  prerelease: false
40
- requirement: &id002 !ruby/object:Gem::Requirement
40
+ version_requirements: !ruby/object:Gem::Requirement
41
41
  none: false
42
- requirements:
42
+ requirements:
43
43
  - - ~>
44
- - !ruby/object:Gem::Version
45
- hash: 3
46
- segments:
47
- - 3
48
- - 1
49
- - 0
50
- version: 3.1.0
44
+ - !ruby/object:Gem::Version
45
+ version: 1.4.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: puma
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 1.6.3
51
54
  type: :runtime
52
- version_requirements: *id002
53
- description: A 19,230-year-old Campfire bot
54
- email:
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.6.3
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '10.0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '10.0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: bundler
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.2'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.2'
94
+ description: Daneel is a chatbot inspired by the late, lamented Wesabot. And also
95
+ Hubot.
96
+ email:
55
97
  - andre@arko.net
56
- executables: []
57
-
98
+ executables:
99
+ - daneel
58
100
  extensions: []
59
-
60
101
  extra_rdoc_files: []
61
-
62
- files:
102
+ files:
63
103
  - .gitignore
64
104
  - Gemfile
105
+ - Gemfile.lock
106
+ - Procfile
65
107
  - Rakefile
108
+ - bin/daneel
66
109
  - daneel.gemspec
67
110
  - lib/daneel.rb
111
+ - lib/daneel/adapter.rb
112
+ - lib/daneel/adapters/campfire.rb
113
+ - lib/daneel/adapters/shell.rb
114
+ - lib/daneel/bot.rb
115
+ - lib/daneel/data.rb
116
+ - lib/daneel/logger.rb
117
+ - lib/daneel/message.rb
118
+ - lib/daneel/options.rb
119
+ - lib/daneel/plugin.rb
120
+ - lib/daneel/room.rb
121
+ - lib/daneel/script.rb
122
+ - lib/daneel/scripts/chatty.rb
123
+ - lib/daneel/scripts/echo.rb
124
+ - lib/daneel/scripts/help.rb
125
+ - lib/daneel/scripts/image_search.rb
126
+ - lib/daneel/scripts/meme.rb
127
+ - lib/daneel/scripts/reload.rb
128
+ - lib/daneel/scripts/vine_search.rb
129
+ - lib/daneel/server.rb
130
+ - lib/daneel/user.rb
68
131
  - lib/daneel/version.rb
69
- has_rdoc: true
132
+ - lib/daneel/web.rb
70
133
  homepage: http://github.com/indirect/daneel
71
- licenses: []
72
-
134
+ licenses:
135
+ - MIT
73
136
  post_install_message:
74
137
  rdoc_options: []
75
-
76
- require_paths:
138
+ require_paths:
77
139
  - lib
78
- required_ruby_version: !ruby/object:Gem::Requirement
140
+ required_ruby_version: !ruby/object:Gem::Requirement
79
141
  none: false
80
- requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- hash: 3
84
- segments:
85
- - 0
86
- version: "0"
87
- required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
147
  none: false
89
- requirements:
90
- - - ">="
91
- - !ruby/object:Gem::Version
92
- hash: 3
93
- segments:
94
- - 0
95
- version: "0"
148
+ requirements:
149
+ - - ! '>='
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
96
152
  requirements: []
97
-
98
153
  rubyforge_project:
99
- rubygems_version: 1.3.7
154
+ rubygems_version: 1.8.24
100
155
  signing_key:
101
156
  specification_version: 3
102
- summary: A library and rack app to bot up your Campfire rooms
157
+ summary: A 19,230-year-old robot
103
158
  test_files: []
104
-