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