october 0.1.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.
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