october 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +35 -0
- data/README.md +14 -0
- data/Rakefile +38 -0
- data/bin/october +8 -0
- data/boot.rb +5 -0
- data/config/database.yml.example +3 -0
- data/config/irc.yml.example +9 -0
- data/config/plugins.yml.example +2 -0
- data/config/redis.yml.example +7 -0
- data/lib/october.rb +16 -0
- data/lib/october/base.rb +28 -0
- data/lib/october/config.rb +23 -0
- data/lib/october/debugger.rb +51 -0
- data/lib/october/environment.rb +24 -0
- data/lib/october/plugin.rb +26 -0
- data/lib/october/plugin/help.rb +20 -0
- data/lib/october/plugins.rb +94 -0
- data/lib/october/redis.rb +20 -0
- data/lib/october/version.rb +3 -0
- data/lib/october/watchdog.rb +36 -0
- data/october.gemspec +25 -0
- data/plugins/.gitkeep +0 -0
- data/plugins/autossh.rb +23 -0
- data/plugins/fortune.rb +13 -0
- data/plugins/help.rb +11 -0
- data/plugins/hudson.rb +73 -0
- data/plugins/hudson/config.rb +41 -0
- data/plugins/hudson/fetcher.rb +40 -0
- data/plugins/hudson/reporter.rb +104 -0
- data/plugins/hudson/test_run.rb +88 -0
- data/plugins/issues.rb +161 -0
- data/plugins/joke.rb +32 -0
- data/plugins/links.rb +42 -0
- data/plugins/links/link.rb +68 -0
- data/plugins/service.rb +11 -0
- data/plugins/update.rb +79 -0
- data/plugins/whisper.rb +70 -0
- data/spec/fixtures/hudson/console.log +37 -0
- data/spec/helpers/bot_context.rb +5 -0
- data/spec/october/environment_spec.rb +28 -0
- data/spec/plugins/hudson/reporter_spec.rb +27 -0
- data/spec/plugins/hudson/test_run_spec.rb +17 -0
- data/spec/plugins/hudson_spec.rb +44 -0
- data/spec/plugins/issues_spec.rb +26 -0
- data/spec/plugins/links_spec.rb +16 -0
- data/spec/plugins/whisper_spec.rb +6 -0
- data/spec/spec_helper.rb +13 -0
- metadata +185 -0
data/plugins/issues.rb
ADDED
@@ -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
|
data/plugins/joke.rb
ADDED
@@ -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
|
data/plugins/links.rb
ADDED
@@ -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
|
data/plugins/service.rb
ADDED
@@ -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
|
data/plugins/update.rb
ADDED
@@ -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
|
data/plugins/whisper.rb
ADDED
@@ -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
|