october 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/Gemfile +35 -0
  2. data/README.md +14 -0
  3. data/Rakefile +38 -0
  4. data/bin/october +8 -0
  5. data/boot.rb +5 -0
  6. data/config/database.yml.example +3 -0
  7. data/config/irc.yml.example +9 -0
  8. data/config/plugins.yml.example +2 -0
  9. data/config/redis.yml.example +7 -0
  10. data/lib/october.rb +16 -0
  11. data/lib/october/base.rb +28 -0
  12. data/lib/october/config.rb +23 -0
  13. data/lib/october/debugger.rb +51 -0
  14. data/lib/october/environment.rb +24 -0
  15. data/lib/october/plugin.rb +26 -0
  16. data/lib/october/plugin/help.rb +20 -0
  17. data/lib/october/plugins.rb +94 -0
  18. data/lib/october/redis.rb +20 -0
  19. data/lib/october/version.rb +3 -0
  20. data/lib/october/watchdog.rb +36 -0
  21. data/october.gemspec +25 -0
  22. data/plugins/.gitkeep +0 -0
  23. data/plugins/autossh.rb +23 -0
  24. data/plugins/fortune.rb +13 -0
  25. data/plugins/help.rb +11 -0
  26. data/plugins/hudson.rb +73 -0
  27. data/plugins/hudson/config.rb +41 -0
  28. data/plugins/hudson/fetcher.rb +40 -0
  29. data/plugins/hudson/reporter.rb +104 -0
  30. data/plugins/hudson/test_run.rb +88 -0
  31. data/plugins/issues.rb +161 -0
  32. data/plugins/joke.rb +32 -0
  33. data/plugins/links.rb +42 -0
  34. data/plugins/links/link.rb +68 -0
  35. data/plugins/service.rb +11 -0
  36. data/plugins/update.rb +79 -0
  37. data/plugins/whisper.rb +70 -0
  38. data/spec/fixtures/hudson/console.log +37 -0
  39. data/spec/helpers/bot_context.rb +5 -0
  40. data/spec/october/environment_spec.rb +28 -0
  41. data/spec/plugins/hudson/reporter_spec.rb +27 -0
  42. data/spec/plugins/hudson/test_run_spec.rb +17 -0
  43. data/spec/plugins/hudson_spec.rb +44 -0
  44. data/spec/plugins/issues_spec.rb +26 -0
  45. data/spec/plugins/links_spec.rb +16 -0
  46. data/spec/plugins/whisper_spec.rb +6 -0
  47. data/spec/spec_helper.rb +13 -0
  48. metadata +185 -0
@@ -0,0 +1,161 @@
1
+ require 'github_api'
2
+
3
+ class Issues
4
+ include October::Plugin
5
+
6
+ self.prefix = /^!issues? /
7
+ register_help 'issue create title', 'create issue'
8
+ register_help 'issue create title | body', 'create issue with body'
9
+ register_help 'issue create title | milestone: 2', 'create issue for milestone'
10
+ register_help 'issue create title | assign: someone', 'create assigned issue'
11
+ register_help 'issue create title | assign: someone | body | milestone: 3', 'combined approach to create issue'
12
+ register_help 'issue convert number head => base', 'convert issue to pull request'
13
+ register_help 'pull head => base', 'creates a new pull request'
14
+
15
+ GIT = /[a-z0-9]{7}|[a-z0-9]{40}/
16
+ match /create (.+)$/, method: :create
17
+ match /convert (\d+) (.+?)\s*=>\s*(.+)$/, method: :convert
18
+ match /(?:issue\s+)?#(\d+)/, method: :issue, use_prefix: false
19
+ match /commit ([a-z0-9]{7}|[a-z0-9]{40})(?:[^a-z0-9]|$)/, method: :commit, use_prefix: false
20
+ match /pull (.+?)\s*=>\s*(.+)$/, method: :pull, use_prefix: false
21
+
22
+ def create(m, text)
23
+ issue = Retryable.do { api.issues.create(nil, nil, IssueParser.new(text).by(m.user.nick).to_hash) }
24
+ m.reply "created issue #{issue.number} - #{issue.html_url}"
25
+ rescue Github::Error::UnprocessableEntity => e
26
+ m.user.msg "Creation failed: " + e.message
27
+ end
28
+
29
+ def pull(m, head, base)
30
+ pull = Retryable.do { api.pull_requests.create(nil, nil, :head => head, :base => base, :title => head) }
31
+ m.reply "Simba, there is a new pull request! #{pull.html_url}"
32
+ rescue Github::Error::UnprocessableEntity => e
33
+ m.user.msg "Creation failed: " + e.message
34
+ end
35
+
36
+ def convert(m, number, head, base)
37
+ pull = Retryable.do { api.pull_requests.create nil, nil, :issue => number, :head => head, :base => base }
38
+ m.reply "Simba, there is a new pull request! #{pull.html_url}"
39
+ rescue Github::Error::UnprocessableEntity => e
40
+ m.user.msg "Converting failed: " + e.message
41
+ end
42
+
43
+ def issue(m, number)
44
+ if issue = Retryable.do { api.issues.find(api.user, api.repo, number) }
45
+ m.reply "#{issue.html_url} - #{issue.title}"
46
+ end
47
+ rescue Github::Error::UnprocessableEntity => e
48
+ m.user.msg "Issue failed: " + e.message
49
+ end
50
+
51
+ def commit(m, rev)
52
+ commit = Retryable.do { api.git_data.commit nil, nil, rev }
53
+ m.reply "https://github.com/#{api.user}/#{api.repo}/commit/#{commit.sha} by #{commit.author.name}"
54
+ rescue Github::Error::ResourceNotFound
55
+ m.user.msg "sorry, but commit #{rev} was not found"
56
+ end
57
+
58
+ def comment(m, number, message)
59
+ Retryable.do { api.issues.comments.create(api.user, api.repo, number, "body" => message)}
60
+ rescue Github::Error::ResourceNotFound
61
+ m.user.msg "sorry, but an error occurred while posting your comment"
62
+ end
63
+
64
+ # this method is used by the hudson plugin to figure out if a branch matches a pull request
65
+ def pull_request(m, branch_name)
66
+ pulls = Retryable.do { api.pull_requests.list(api.user, api.repo)}
67
+ pulls.detect do |pr|
68
+ full_pr = Retryable.do { api.pull_requests.find(api.user, api.repo, pr["number"]) }
69
+ return full_pr if full_pr["head"]["ref"] == branch_name
70
+ end
71
+ nil
72
+ rescue Github::Error::ResourceNotFound
73
+ m.user.msg "sorry, but an error occurred while fetching your pull request"
74
+ end
75
+
76
+ private
77
+
78
+ def config
79
+ self.class.config.symbolize_keys
80
+ end
81
+
82
+ def api
83
+ @api ||= Github.new config
84
+ end
85
+
86
+ class Retryable
87
+ attr_reader :attempts
88
+
89
+ def initialize(attempts = 2, &block)
90
+ @attempts = attempts
91
+ @block = block
92
+ end
93
+
94
+ def run!
95
+ attempts.times do |attempt|
96
+ value = @block.call(attempt + 1)
97
+ return value if value
98
+ end
99
+ end
100
+
101
+ def self.do &block
102
+ new(&block).run!
103
+ end
104
+ end
105
+
106
+ class IssueParser
107
+ def initialize(text)
108
+ @text = text
109
+ end
110
+
111
+ def tokens
112
+ @tokens ||= @text.split('|').map(&:strip)
113
+ end
114
+
115
+ def parse(name)
116
+ [tokens.find{ |t| t =~ /^#{name}:\s*(.+)$/ }, $1]
117
+ end
118
+
119
+ def attr(name)
120
+ parse(name).last
121
+ end
122
+
123
+ def token(name)
124
+ parse(name).first
125
+ end
126
+
127
+ def milestone
128
+ attr(:milestone)
129
+ end
130
+
131
+ def title
132
+ tokens.first
133
+ end
134
+
135
+ def by(user)
136
+ @user = user
137
+ self
138
+ end
139
+
140
+ def body
141
+ except = [token(:milestone), token(/assigne{2}?/), title]
142
+ tokens.find {|t| not except.include?(t) }
143
+ end
144
+
145
+ def assignee
146
+ attr(/assigne{2}?/)
147
+ end
148
+
149
+ def to_hash(user = @user)
150
+ text = body ? body.dup : ""
151
+ text += "\nrequested by: #{user}" if user
152
+ {
153
+ title: title,
154
+ body: text,
155
+ milestone: milestone,
156
+ assignee: assignee
157
+ }
158
+ end
159
+ end
160
+
161
+ end
@@ -0,0 +1,32 @@
1
+ gem 'typhoeus'
2
+ gem 'json'
3
+
4
+ class Joke
5
+ include October::Plugin
6
+ API_URL = "http://api.icndb.com/jokes/random?firstName=<first_name>&lastName=<last_name>"
7
+ HYDRA = Typhoeus::Hydra.new
8
+
9
+ match /joke(?: (\S+)(?: (\S+))?)?$/, method: :joke
10
+
11
+ register_help 'joke [first_name] [last_name]', 'Tells you a joke. Chuck Norris style!'
12
+ def joke(m, first_name = nil, last_name = nil)
13
+ first_name ||= m.user.nick
14
+
15
+ joke = request(first_name, last_name)
16
+ m.reply joke["value"]["joke"]
17
+ end
18
+
19
+
20
+ private
21
+
22
+ def request(first_name, last_name)
23
+ url = API_URL.dup
24
+ url.sub! "<first_name>", first_name.to_s
25
+ url.sub! "<last_name>", last_name.to_s
26
+
27
+ response = Typhoeus::Request.get(url)
28
+
29
+ JSON.parse(response.body)
30
+ end
31
+
32
+ end
@@ -0,0 +1,42 @@
1
+ gem 'redis-objects'
2
+
3
+ class Links
4
+ autoload :Link, 'links/link'
5
+
6
+ include October::Plugin
7
+
8
+ self.prefix = /^!links?(?:\s+)?/
9
+
10
+ match /add\s+(\S+)(?:\s+)?(.+)?$/, method: :add
11
+ match /rem(?:ove)?\s+(\d+)$/, method: :remove
12
+ match /list/, method: :list
13
+ match '', method: :list
14
+
15
+ register_help 'link[s] add uri [description]', 'add uri to database with optional description'
16
+ register_help 'link[s] [list]', 'list all saved links in this scope (channel of nick)'
17
+ register_help 'link[s] rem[ove] id', 'remove specific link'
18
+
19
+ def add m, url, desc = nil
20
+ link = Link.create url, scope(m), description: desc, user: m.user.nick
21
+ m.reply 'Link added'
22
+ end
23
+
24
+ def remove m, id
25
+ link = Link.find(id, scope(m))
26
+ if link and link.remove
27
+ m.reply "Link #{id} removed"
28
+ else
29
+ m.reply "Link #{id} not found"
30
+ end
31
+ end
32
+
33
+ def list m
34
+ links = Link.list scope(m)
35
+ m.reply links.map(&:to_list).join("\n")
36
+ end
37
+
38
+ private
39
+ def scope(message)
40
+ message.channel or message.user.nick or raise 'no scope'
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ class Links
2
+ class Link
3
+ include Redis::Objects
4
+ value 'url'
5
+ hash_key 'metadata'
6
+
7
+ attr_reader :id, :scope
8
+
9
+ def initialize id, scope = nil
10
+ @id = id
11
+ @scope = scope.dup.freeze if scope
12
+ end
13
+ alias :meta :metadata
14
+
15
+ def description
16
+ meta[:description]
17
+ end
18
+
19
+ def user
20
+ meta[:user]
21
+ end
22
+
23
+ def to_list
24
+ %{#{id}) #{url} #{description} by #{user}}
25
+ end
26
+
27
+ def remove
28
+ $redis.srem(scope, id)
29
+ end
30
+
31
+ class << self
32
+
33
+ def find id, scope = nil
34
+ if scope = links(scope)
35
+ Link.new(id, scope) if $redis.sismember(scope, id)
36
+ else
37
+ Link.new(id)
38
+ end
39
+ end
40
+
41
+ def list scope
42
+ ids = $redis.smembers links(scope)
43
+ ids.map { |id| Link.find(id) }
44
+ end
45
+
46
+ def links(scope)
47
+ "links:#{scope}" if scope
48
+ end
49
+
50
+ def create(url, scope, metadata = {})
51
+ metadata.reverse_merge! :timestamp => Time.now
52
+ metadata.symbolize_keys!
53
+ metadata.select!{ |k,v| v.present? }
54
+
55
+
56
+ id = $redis.incr 'links'
57
+ link = Link.new(id)
58
+
59
+ link.url = url
60
+ link.metadata.bulk_set metadata
61
+ $redis.sadd links(scope), id
62
+ link
63
+ end
64
+
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,11 @@
1
+ class Service
2
+ include October::Plugin
3
+ register_help 'guys [msg]', 'focuses all users in channel'
4
+
5
+ match /guys(\s+.+?)?$/, method: :guys
6
+
7
+ def guys(m, msg)
8
+ users = m.channel.users.keys.reject{|u| u == bot }
9
+ m.reply [users.join(', '), msg].compact.join(':')
10
+ end
11
+ end
@@ -0,0 +1,79 @@
1
+ class Update
2
+ include October::Plugin
3
+
4
+ match /selfupdate$/, method: :selfupdate
5
+ match /seppuku$/, method: :seppuku
6
+ match /spawn (.+)$/, method: :spawn
7
+ match /kill (-9 )?(.+)$/, method: :kill
8
+ match /running$/, method: :running
9
+ match /run (.+)$/, method: :run
10
+
11
+ $SPAWNS = []
12
+
13
+ def selfupdate(m)
14
+ m.user.msg "starting selfupdate..."
15
+ `git fetch origin`
16
+ `git reset --hard origin/master`
17
+ m.user.msg "installing gems..."
18
+ `bundle install`
19
+ m.user.msg "done!"
20
+
21
+ sleep(2)
22
+
23
+ seppuku(m)
24
+ end
25
+
26
+ def seppuku(m)
27
+ m.reply("bye... AGRG!! ahh...")
28
+ sleep(1)
29
+ exit(0)
30
+ end
31
+
32
+ def running(m)
33
+ m.reply "running processes: #{$SPAWNS.join(", ")}"
34
+ end
35
+
36
+ def kill(m, kill, pid)
37
+ pid = pid.to_i
38
+
39
+ signal = kill ? 'KILL' : 'TERM'
40
+ Process.kill(signal, pid)
41
+ m.reply "sent #{signal} to process #{pid}"
42
+ end
43
+
44
+ def run(m, command)
45
+ pid = nil
46
+ puts "current pid is #{Process.pid}"
47
+ IO.popen(command, err: [:child, :out]) do |io|
48
+ pid = io.pid
49
+ $SPAWNS.push pid
50
+ m.reply "process command #{command} with pid #{pid} started:"
51
+ t = Thread.new do
52
+ while row = io.gets
53
+ m.reply row.strip
54
+ end
55
+ end
56
+ unless t.join(10)
57
+ Process.kill('KILL', pid)
58
+ m.reply "process #{pid} killed beause of reading timeout (10s)"
59
+ else
60
+ m.reply "process #{pid} ended"
61
+ end
62
+ end
63
+ $SPAWNS.delete(pid)
64
+ end
65
+
66
+ def spawn(m, command)
67
+ pid = Process.spawn(command, :out => ['/dev/null'])
68
+
69
+ $SPAWNS.push pid
70
+ m.reply "spawned command '#{command}' with pid #{pid}"
71
+
72
+ Thread.new do
73
+ Process.wait(pid)
74
+ $SPAWNS.delete(pid)
75
+ m.reply "process #{pid} exited with status #{$?.exitstatus}"
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,70 @@
1
+ class Whisper
2
+ include October::Plugin
3
+
4
+ self.react_on = :private
5
+
6
+ self.prefix = ''
7
+
8
+ match /!whisper (.+)$/, method: :whisper
9
+ match /(.+)$/, method: :queue
10
+
11
+ listen_to :part, method: :close
12
+ listen_to :join, method: :flush
13
+
14
+ register_help 'whisper name', 'starts "whisper" mode, all messages will be sent to user when joins'
15
+
16
+ ACTIVE = Hash.new{ |hash, key| hash[key] = {} }
17
+ OPENED = {}
18
+
19
+ def whisper(m, nick)
20
+ synchronize(:whisper) do
21
+ ACTIVE[nick][m.user.nick] = [Time.now.to_s]
22
+ OPENED[m.user.nick] = nick
23
+ m.reply "Whisper mode started"
24
+ end
25
+ end
26
+
27
+ def queue(m, text)
28
+ return unless opened?(m.user)
29
+
30
+ synchronize(:whisper) do
31
+ nick = m.user.nick
32
+ opened = OPENED[nick]
33
+ return if text == "!whisper #{opened}"
34
+
35
+ ACTIVE[opened][nick] << text
36
+ m.reply "Message enqueued"
37
+ end
38
+ end
39
+
40
+ def close(m)
41
+ return unless opened?(m.user)
42
+
43
+ synchronize(:whisper) do
44
+ OPENED.delete(m.user.nick)
45
+ end
46
+ end
47
+
48
+ def flush(m)
49
+ user = m.user
50
+ return unless ACTIVE.has_key?(user.nick)
51
+
52
+ synchronize(:whisper) do
53
+ queue = ACTIVE[m.user.nick]
54
+ queue.keys.each do |sender|
55
+ messages = queue.delete(sender)
56
+ user.msg "You have #{messages.size - 1} messages from #{sender}:"
57
+
58
+ while line = messages.shift
59
+ user.msg line
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ private
66
+ def opened?(user)
67
+ return unless user
68
+ OPENED.has_key?(user.nick)
69
+ end
70
+ end