butler 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -0
- data/GPL.txt +340 -0
- data/LICENSE.txt +52 -0
- data/README +37 -0
- data/Rakefile +334 -0
- data/bin/botcontrol +230 -0
- data/data/butler/config_template.yaml +4 -0
- data/data/butler/dialogs/backup.rb +19 -0
- data/data/butler/dialogs/botcontrol.rb +4 -0
- data/data/butler/dialogs/config.rb +1 -0
- data/data/butler/dialogs/create.rb +53 -0
- data/data/butler/dialogs/delete.rb +3 -0
- data/data/butler/dialogs/en/backup.yaml +6 -0
- data/data/butler/dialogs/en/botcontrol.yaml +5 -0
- data/data/butler/dialogs/en/create.yaml +11 -0
- data/data/butler/dialogs/en/delete.yaml +2 -0
- data/data/butler/dialogs/en/help.yaml +17 -0
- data/data/butler/dialogs/en/info.yaml +13 -0
- data/data/butler/dialogs/en/list.yaml +4 -0
- data/data/butler/dialogs/en/notyetimplemented.yaml +2 -0
- data/data/butler/dialogs/en/rename.yaml +3 -0
- data/data/butler/dialogs/en/start.yaml +3 -0
- data/data/butler/dialogs/en/sync_plugins.yaml +3 -0
- data/data/butler/dialogs/en/uninstall.yaml +5 -0
- data/data/butler/dialogs/en/unknown_command.yaml +2 -0
- data/data/butler/dialogs/help.rb +11 -0
- data/data/butler/dialogs/info.rb +27 -0
- data/data/butler/dialogs/interactive.rb +1 -0
- data/data/butler/dialogs/list.rb +10 -0
- data/data/butler/dialogs/notyetimplemented.rb +1 -0
- data/data/butler/dialogs/rename.rb +4 -0
- data/data/butler/dialogs/selectbot.rb +2 -0
- data/data/butler/dialogs/start.rb +5 -0
- data/data/butler/dialogs/sync_plugins.rb +30 -0
- data/data/butler/dialogs/uninstall.rb +17 -0
- data/data/butler/dialogs/unknown_command.rb +1 -0
- data/data/butler/plugins/core/logout.rb +41 -0
- data/data/butler/plugins/core/plugins.rb +134 -0
- data/data/butler/plugins/core/privilege.rb +103 -0
- data/data/butler/plugins/core/user.rb +166 -0
- data/data/butler/plugins/dev/eval.rb +64 -0
- data/data/butler/plugins/dev/nometa.rb +14 -0
- data/data/butler/plugins/dev/onhandlers.rb +93 -0
- data/data/butler/plugins/dev/raw.rb +36 -0
- data/data/butler/plugins/dev/rawlog.rb +77 -0
- data/data/butler/plugins/games/eightball.rb +54 -0
- data/data/butler/plugins/games/mastermind.rb +174 -0
- data/data/butler/plugins/irc/action.rb +36 -0
- data/data/butler/plugins/irc/join.rb +38 -0
- data/data/butler/plugins/irc/notice.rb +36 -0
- data/data/butler/plugins/irc/part.rb +38 -0
- data/data/butler/plugins/irc/privmsg.rb +36 -0
- data/data/butler/plugins/irc/quit.rb +36 -0
- data/data/butler/plugins/operator/deop.rb +41 -0
- data/data/butler/plugins/operator/devoice.rb +41 -0
- data/data/butler/plugins/operator/limit.rb +47 -0
- data/data/butler/plugins/operator/op.rb +41 -0
- data/data/butler/plugins/operator/voice.rb +41 -0
- data/data/butler/plugins/public/help.rb +69 -0
- data/data/butler/plugins/public/login.rb +72 -0
- data/data/butler/plugins/public/usage.rb +49 -0
- data/data/butler/plugins/service/clones.rb +56 -0
- data/data/butler/plugins/service/define.rb +47 -0
- data/data/butler/plugins/service/log.rb +183 -0
- data/data/butler/plugins/service/svn.rb +91 -0
- data/data/butler/plugins/util/cycle.rb +98 -0
- data/data/butler/plugins/util/load.rb +41 -0
- data/data/butler/plugins/util/pong.rb +29 -0
- data/data/butler/strings/random/acknowledge.en.yaml +5 -0
- data/data/butler/strings/random/gratitude.en.yaml +3 -0
- data/data/butler/strings/random/hello.en.yaml +4 -0
- data/data/butler/strings/random/ignorance.en.yaml +7 -0
- data/data/butler/strings/random/ignorance_about.en.yaml +3 -0
- data/data/butler/strings/random/insult.en.yaml +3 -0
- data/data/butler/strings/random/rejection.en.yaml +12 -0
- data/data/man/botcontrol.1 +17 -0
- data/lib/access.rb +187 -0
- data/lib/access/admin.rb +16 -0
- data/lib/access/privilege.rb +122 -0
- data/lib/access/role.rb +102 -0
- data/lib/access/savable.rb +18 -0
- data/lib/access/user.rb +180 -0
- data/lib/access/yamlbase.rb +126 -0
- data/lib/butler.rb +188 -0
- data/lib/butler/bot.rb +247 -0
- data/lib/butler/control.rb +93 -0
- data/lib/butler/dialog.rb +64 -0
- data/lib/butler/initialvalues.rb +40 -0
- data/lib/butler/irc/channel.rb +135 -0
- data/lib/butler/irc/channels.rb +96 -0
- data/lib/butler/irc/client.rb +351 -0
- data/lib/butler/irc/hostmask.rb +53 -0
- data/lib/butler/irc/message.rb +184 -0
- data/lib/butler/irc/parser.rb +125 -0
- data/lib/butler/irc/parser/commands.rb +83 -0
- data/lib/butler/irc/parser/generic.rb +343 -0
- data/lib/butler/irc/socket.rb +378 -0
- data/lib/butler/irc/string.rb +186 -0
- data/lib/butler/irc/topic.rb +15 -0
- data/lib/butler/irc/user.rb +265 -0
- data/lib/butler/irc/users.rb +112 -0
- data/lib/butler/plugin.rb +249 -0
- data/lib/butler/plugin/configproxy.rb +35 -0
- data/lib/butler/plugin/mapper.rb +85 -0
- data/lib/butler/plugin/matcher.rb +55 -0
- data/lib/butler/plugin/onhandlers.rb +70 -0
- data/lib/butler/plugin/trigger.rb +58 -0
- data/lib/butler/plugins.rb +147 -0
- data/lib/butler/version.rb +17 -0
- data/lib/cloptions.rb +217 -0
- data/lib/cloptions/adapters.rb +24 -0
- data/lib/cloptions/switch.rb +132 -0
- data/lib/configuration.rb +223 -0
- data/lib/dialogline.rb +296 -0
- data/lib/dialogline/localizations.rb +24 -0
- data/lib/durations.rb +57 -0
- data/lib/event.rb +295 -0
- data/lib/event/at.rb +64 -0
- data/lib/event/every.rb +56 -0
- data/lib/event/timed.rb +112 -0
- data/lib/installer.rb +75 -0
- data/lib/iterator.rb +34 -0
- data/lib/log.rb +68 -0
- data/lib/log/comfort.rb +85 -0
- data/lib/log/converter.rb +23 -0
- data/lib/log/entry.rb +152 -0
- data/lib/log/fakeio.rb +55 -0
- data/lib/log/file.rb +54 -0
- data/lib/log/filereader.rb +81 -0
- data/lib/log/forward.rb +49 -0
- data/lib/log/methods.rb +39 -0
- data/lib/log/nolog.rb +18 -0
- data/lib/log/splitter.rb +26 -0
- data/lib/ostructfixed.rb +26 -0
- data/lib/ruby/array/columnize.rb +38 -0
- data/lib/ruby/dir/mktree.rb +28 -0
- data/lib/ruby/enumerable/join.rb +13 -0
- data/lib/ruby/exception/detailed.rb +24 -0
- data/lib/ruby/file/append.rb +11 -0
- data/lib/ruby/file/write.rb +11 -0
- data/lib/ruby/hash/zip.rb +15 -0
- data/lib/ruby/kernel/bench.rb +15 -0
- data/lib/ruby/kernel/daemonize.rb +42 -0
- data/lib/ruby/kernel/non_verbose.rb +17 -0
- data/lib/ruby/kernel/safe_fork.rb +18 -0
- data/lib/ruby/range/stepped.rb +11 -0
- data/lib/ruby/string/arguments.rb +72 -0
- data/lib/ruby/string/chunks.rb +15 -0
- data/lib/ruby/string/post_arguments.rb +44 -0
- data/lib/ruby/string/unescaped.rb +17 -0
- data/lib/scheduler.rb +164 -0
- data/lib/scriptfile.rb +101 -0
- data/lib/templater.rb +86 -0
- data/test/cloptions.rb +134 -0
- data/test/cv.rb +28 -0
- data/test/irc/client.rb +85 -0
- data/test/irc/client_login.txt +53 -0
- data/test/irc/client_subscribe.txt +8 -0
- data/test/irc/message.rb +30 -0
- data/test/irc/messages.txt +64 -0
- data/test/irc/parser.rb +13 -0
- data/test/irc/profile_parser.rb +12 -0
- data/test/irc/users.rb +28 -0
- metadata +256 -0
data/lib/access/role.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
class Access
|
10
|
+
# Access::Role's are a set of privileges with (optionally)
|
11
|
+
# an additional restriction (which is applied globally).
|
12
|
+
class Role
|
13
|
+
module Base
|
14
|
+
# Create a new Role
|
15
|
+
def create(role, description=nil, privileges=[])
|
16
|
+
raise "Role #{role} already exists" if exists?(role)
|
17
|
+
role = Role.new(role, description)
|
18
|
+
role.access = access
|
19
|
+
role.base = self
|
20
|
+
add(role)
|
21
|
+
role
|
22
|
+
end
|
23
|
+
|
24
|
+
# Restore an Access::Privilege from it's storable data
|
25
|
+
def load(*args) # :nodoc:
|
26
|
+
return nil unless data = super
|
27
|
+
roles = access.role
|
28
|
+
data[:roles] = data[:roles].map { |role| roles[role] }
|
29
|
+
array = data.values_at(:id, :description)
|
30
|
+
array << data
|
31
|
+
role = new(*array)
|
32
|
+
role.access = access
|
33
|
+
role.base = self
|
34
|
+
role
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :id
|
39
|
+
attr_reader :description
|
40
|
+
|
41
|
+
def initialize(role, description=nil, other={})
|
42
|
+
@id = role
|
43
|
+
@privileges = Privileges.new(self, other[:privileges])
|
44
|
+
@roles = Roles.new(self, other[:roles])
|
45
|
+
@description = description || "No description"
|
46
|
+
end
|
47
|
+
|
48
|
+
def storable
|
49
|
+
{
|
50
|
+
:id => @id,
|
51
|
+
:description => @description,
|
52
|
+
:privileges => @privileges.storable,
|
53
|
+
:roles => @roles.storable,
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def allows?(privilege, condition=nil)
|
58
|
+
@privileges.allow?(privilege, condition) || @roles.allow?(privilege, condition)
|
59
|
+
end
|
60
|
+
|
61
|
+
def eql?(other)
|
62
|
+
self.class == other.class && @id.eql?(other.id)
|
63
|
+
end
|
64
|
+
|
65
|
+
def hash
|
66
|
+
@id.hash
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Roles
|
71
|
+
def initialize(owner, roles=nil)
|
72
|
+
@owner = owner
|
73
|
+
@roles = roles || []
|
74
|
+
end
|
75
|
+
|
76
|
+
def allow?(privilege, condition=nil)
|
77
|
+
@roles.any? { |role| role.allows?(privilege, condition) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def add(role)
|
81
|
+
@roles << role
|
82
|
+
@owner.save
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete(role)
|
86
|
+
@roles.delete(role)
|
87
|
+
@owner.save
|
88
|
+
end
|
89
|
+
|
90
|
+
def list
|
91
|
+
@roles
|
92
|
+
end
|
93
|
+
|
94
|
+
def each(&block)
|
95
|
+
@roles.each(&block)
|
96
|
+
end
|
97
|
+
|
98
|
+
def storable
|
99
|
+
@roles.map { |role| role.id }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
class Access
|
10
|
+
module Savable
|
11
|
+
attr_accessor :access
|
12
|
+
attr_accessor :base
|
13
|
+
|
14
|
+
def save
|
15
|
+
base.save(id())
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/access/user.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'access'
|
10
|
+
require 'access/savable'
|
11
|
+
require 'access/admin'
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
class Access
|
16
|
+
# Access::User
|
17
|
+
# Use nil-id if you don't want the object to be stored.
|
18
|
+
class User
|
19
|
+
include Savable
|
20
|
+
|
21
|
+
module Base
|
22
|
+
# Create a new - inactive(!) - user
|
23
|
+
def create(user_id, credentials, meta=nil, admin=false, opt={})
|
24
|
+
raise "User-id #{user_id} already exists" if exists?(user_id)
|
25
|
+
credentials = credentials ? access.hash_credentials(credentials, user_id) : "*"
|
26
|
+
user = User.new(
|
27
|
+
user_id,
|
28
|
+
credentials,
|
29
|
+
meta,
|
30
|
+
admin,
|
31
|
+
{:active => !!opt.delete(:active)}.merge(opt)
|
32
|
+
)
|
33
|
+
user.access = access
|
34
|
+
user.base = self
|
35
|
+
add(user)
|
36
|
+
user
|
37
|
+
end
|
38
|
+
|
39
|
+
# Restore an Access::User from it's storable data
|
40
|
+
def load(*args) # :nodoc:
|
41
|
+
return nil unless data = super
|
42
|
+
array = data.values_at(:id, :credentials, :meta, :admin)
|
43
|
+
array << data
|
44
|
+
user = User.new(*array)
|
45
|
+
user.access = access
|
46
|
+
user.base = self
|
47
|
+
user
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :id
|
52
|
+
attr_reader :credentials
|
53
|
+
attr_reader :privileges
|
54
|
+
attr_reader :roles
|
55
|
+
|
56
|
+
attr_accessor :meta
|
57
|
+
|
58
|
+
# The data needed to restore a user.
|
59
|
+
# Simplified to hashes and scalar values.
|
60
|
+
def storable
|
61
|
+
{
|
62
|
+
:id => @id,
|
63
|
+
:credentials => @credentials,
|
64
|
+
:meta => @meta,
|
65
|
+
:admin => @admin,
|
66
|
+
:active => @active,
|
67
|
+
:privileges => @privileges.storable,
|
68
|
+
:roles => @roles.storable,
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
# access: the access-instance the user is tied to (necessary for storing)
|
73
|
+
# id: a string/integer, identifying the user
|
74
|
+
# credentials: (if not subclassed) a string authenticating the user, will be hashed before storing.
|
75
|
+
# meta: meta data about the user
|
76
|
+
# admin: admin's have all privileges granted
|
77
|
+
def initialize(id, credentials, meta=nil, admin=false, other={})
|
78
|
+
@id = id
|
79
|
+
@credentials = credentials
|
80
|
+
@admin = admin
|
81
|
+
@active = other.has_key?(:active) ? other[:active] : false
|
82
|
+
@meta = meta
|
83
|
+
@roles = Roles.new(self, other[:role])
|
84
|
+
@privileges = Privileges.new(self, other[:privileges])
|
85
|
+
@logged = false
|
86
|
+
|
87
|
+
extend(Admin) if @admin
|
88
|
+
end
|
89
|
+
|
90
|
+
def credentials=(value)
|
91
|
+
@credentials = value ? access.hash_credentials(value, @id) : "*"
|
92
|
+
save
|
93
|
+
end
|
94
|
+
|
95
|
+
# Check if a user has sufficient privileges to be allowed for certain
|
96
|
+
# privilege with certain restriction parameters.
|
97
|
+
# WARNING! This method does not care about login- nor active-state.
|
98
|
+
# Use authorized? to do that
|
99
|
+
def privileged?(privilege, parameters=nil)
|
100
|
+
@privileges.allow?(privilege, parameters) || @roles.allow?(privilege, parameters)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Same as privileged? but also takes active? and logged? into consideration.
|
104
|
+
# I.e. if a user is inactive or not logged in, he is not authorized for anything.
|
105
|
+
def authorized?(*args)
|
106
|
+
@active && @logged && privileged?(*args)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Check if user is admin
|
110
|
+
def admin?
|
111
|
+
false
|
112
|
+
end
|
113
|
+
|
114
|
+
# Check if user is activated (deactivated users have no privileges)
|
115
|
+
def active?
|
116
|
+
@active
|
117
|
+
end
|
118
|
+
|
119
|
+
# Check if user is deactivated (deactivated users have no privileges)
|
120
|
+
def inactive?
|
121
|
+
!@active
|
122
|
+
end
|
123
|
+
|
124
|
+
# Activate a user
|
125
|
+
def activate
|
126
|
+
@active = true
|
127
|
+
save
|
128
|
+
end
|
129
|
+
|
130
|
+
# Deactivate a user (deactivated users have no privileges)
|
131
|
+
def deactivate
|
132
|
+
@active = false
|
133
|
+
save
|
134
|
+
end
|
135
|
+
|
136
|
+
# Set active state to value
|
137
|
+
def active=(value)
|
138
|
+
@active = value
|
139
|
+
save
|
140
|
+
end
|
141
|
+
|
142
|
+
def login
|
143
|
+
@logged = true
|
144
|
+
end
|
145
|
+
|
146
|
+
def logout
|
147
|
+
@logged = false
|
148
|
+
end
|
149
|
+
|
150
|
+
def logged=(value)
|
151
|
+
@logged = value
|
152
|
+
end
|
153
|
+
|
154
|
+
def logged?
|
155
|
+
@logged
|
156
|
+
end
|
157
|
+
|
158
|
+
def eql?(other)
|
159
|
+
self.class == other.class && @id.eql?(other.id)
|
160
|
+
end
|
161
|
+
alias == eql? # in ruby1.8 that's unecessary as == uses eql? per default
|
162
|
+
|
163
|
+
def hash
|
164
|
+
@id.hash
|
165
|
+
end
|
166
|
+
|
167
|
+
def inspect # :nodoc:
|
168
|
+
"#<%s:0x%08x base: %s id: %s credentials: %s %s%s%s>" % [
|
169
|
+
self.class,
|
170
|
+
object_id << 1,
|
171
|
+
"#{@base.class}(#{(class <<@base; self; end).ancestors.first})",
|
172
|
+
@id.inspect,
|
173
|
+
@credentials,
|
174
|
+
@active ? 'active' : 'inactive',
|
175
|
+
admin? ? ' admin' : '',
|
176
|
+
logged? ? ' logged' : ''
|
177
|
+
]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'access'
|
10
|
+
require 'yaml'
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
class Access
|
15
|
+
# An Access compatible storage backend
|
16
|
+
class YAMLBase
|
17
|
+
include Enumerable
|
18
|
+
|
19
|
+
attr_accessor :access
|
20
|
+
|
21
|
+
# extender: a module that will extend this instance and provide additional
|
22
|
+
# functionality
|
23
|
+
# framework: the Access instance this data-storage is tied to
|
24
|
+
# path: the path to the data
|
25
|
+
def initialize(extender, path=nil)
|
26
|
+
@path = path
|
27
|
+
@extender = extender
|
28
|
+
@data = {}
|
29
|
+
extend(extender)
|
30
|
+
|
31
|
+
@path ||= default_path
|
32
|
+
raise "Path must be a directory (#@path)" unless File.directory?(@path)
|
33
|
+
end
|
34
|
+
|
35
|
+
# The full path and filename to the data object belonging to id
|
36
|
+
def filename(id)
|
37
|
+
"#{@path}/#{escape(id)}.yaml"
|
38
|
+
end
|
39
|
+
|
40
|
+
def escape(id)
|
41
|
+
id.gsub(/[\x00-\x1f.%]/) { |m| "%%%02x"%m }.gsub("/", ".")
|
42
|
+
end
|
43
|
+
|
44
|
+
def path2id(path)
|
45
|
+
path[(@path.length+1)..-6].gsub(".", "/").gsub(/%([\dA-Fa-f]{2})/) { $1.to_i(16).chr }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Load an entry, will not write to cache, returns nil if entry doesn't exist
|
49
|
+
def load(id)
|
50
|
+
file = filename(id)
|
51
|
+
File.exist?(file) ? YAML.load_file(file) : nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Loads all entries into cache
|
55
|
+
def cache_all
|
56
|
+
slice = (@path.length+1)..-6
|
57
|
+
Dir.glob("#{@path}/*.yaml") { |path| # /**
|
58
|
+
id = path[slice]
|
59
|
+
@data[id] ||= load(id)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Retrieve the object with id, if force_load cache will be ignored.
|
65
|
+
def [](id, force_load=false)
|
66
|
+
if force_load then
|
67
|
+
@data[id] = load(id)
|
68
|
+
else
|
69
|
+
@data[id] ||= load(id)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add a record to the storage
|
74
|
+
def <<(record)
|
75
|
+
@data[record.id] = record
|
76
|
+
save(record.id)
|
77
|
+
end
|
78
|
+
alias add <<
|
79
|
+
|
80
|
+
# Delete a record from the storage
|
81
|
+
def delete(record)
|
82
|
+
id = record.kind_of?(@type) ? record.id : record
|
83
|
+
@data.delete(id)
|
84
|
+
File.delete(filename(id))
|
85
|
+
end
|
86
|
+
|
87
|
+
def keys
|
88
|
+
Dir.glob("#{@path}/*.yaml").map { |path|
|
89
|
+
path2id(path)
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Iterate over records, yielding id and object
|
94
|
+
def each
|
95
|
+
slice = (@path.length+1)..-6
|
96
|
+
Dir.glob("#{@path}/*.yaml") { |path|
|
97
|
+
id = path[slice]
|
98
|
+
yield(id, @data[id] || load(id))
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Iterate over records, yielding id
|
103
|
+
def each_key
|
104
|
+
Dir.glob("#{@path}/*.yaml") { |path|
|
105
|
+
yield(path2id(path))
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
# Check existency of an object.
|
110
|
+
def exists?(id)
|
111
|
+
@data.has_key?(id) || File.exists?(filename(id))
|
112
|
+
end
|
113
|
+
alias exist? exists?
|
114
|
+
|
115
|
+
# Synchronize data-cache with filesystem.
|
116
|
+
def save(id=nil)
|
117
|
+
if @data.has_key?(id) then
|
118
|
+
File.open(filename(id), 'w') { |fh| fh.write(@data[id].storable.to_yaml) }
|
119
|
+
elsif id.nil? then
|
120
|
+
@data.each_key { |key| save(key) }
|
121
|
+
else
|
122
|
+
raise "Could not save '#{id}' since it's not in @data"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/butler.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'access'
|
10
|
+
require 'butler/initialvalues'
|
11
|
+
require 'butler/version'
|
12
|
+
require 'configuration'
|
13
|
+
require 'fileutils'
|
14
|
+
require 'log/comfort'
|
15
|
+
require 'rbconfig'
|
16
|
+
require 'ruby/file/write'
|
17
|
+
require 'ruby/kernel/daemonize'
|
18
|
+
require 'yaml'
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
class Butler
|
23
|
+
@path = nil
|
24
|
+
@log_device = nil
|
25
|
+
extend Log::Comfort
|
26
|
+
|
27
|
+
class <<self
|
28
|
+
attr_accessor :path
|
29
|
+
|
30
|
+
# start a bot
|
31
|
+
# if you provide a block, it will yield the bot instance and you can control
|
32
|
+
# it. If not, it will enter the Bot#event_loop.
|
33
|
+
# options:
|
34
|
+
# -:in_dir => path to the directory the bot can be found [Butler.bots]
|
35
|
+
# -:daemonize => whether this script should become a deamon or not [true]
|
36
|
+
# -...
|
37
|
+
def start(path, botname, opts={}, &block)
|
38
|
+
Thread.abort_on_exception = true # if $DEBUG # FIXME
|
39
|
+
path ||= @path
|
40
|
+
butler = nil
|
41
|
+
|
42
|
+
if opts.delete(:daemonize) then
|
43
|
+
start_daemon(path, botname, opts, &block)
|
44
|
+
else
|
45
|
+
start_interactive(path, botname, opts, &block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def start_daemon(path, botname, opts={}, &block)
|
50
|
+
butler, pidfile = nil, nil
|
51
|
+
info "Daemonizing"
|
52
|
+
pidfile = test_pidfile(path.run, botname)
|
53
|
+
daemonize(path.base) { }
|
54
|
+
begin
|
55
|
+
File.write(pidfile, $$)
|
56
|
+
butler = Bot.new(path, botname)
|
57
|
+
path = butler.path
|
58
|
+
$stderr = File.open(path.log+"/error.log", "w")
|
59
|
+
$stdout = File.open(path.log+"/out.log", "w")
|
60
|
+
trap("SIGHUP") { butler.quit }
|
61
|
+
butler.output_to_logfiles
|
62
|
+
butler.plugins.load_all
|
63
|
+
butler.login
|
64
|
+
info("Running #{botname} with PID #{$$} as daemon")
|
65
|
+
if block then butler.event_loop(&block) else sleep end
|
66
|
+
rescue SystemExit => e
|
67
|
+
info("Exit, terminating #{botname} (in #{e.backtrace.first}")
|
68
|
+
rescue Exception => e
|
69
|
+
exception(e)
|
70
|
+
else
|
71
|
+
info("EventLoop ended, terminating #{botname}")
|
72
|
+
ensure
|
73
|
+
butler.quit if butler
|
74
|
+
File.delete(pidfile) if pidfile and File.exist?(pidfile)
|
75
|
+
info("Terminated")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def start_interactive(path, botname, opts, &block)
|
80
|
+
butler, pidfile = nil, nil
|
81
|
+
pidfile = test_pidfile(path.run, botname)
|
82
|
+
File.write(pidfile, $$)
|
83
|
+
info("Running #{botname} with PID #{$$} interactively")
|
84
|
+
butler = Bot.new(path, botname)
|
85
|
+
butler.plugins.load_all
|
86
|
+
butler.login
|
87
|
+
if block then butler.event_loop(&block) else sleep end
|
88
|
+
rescue SystemExit => e
|
89
|
+
info("Exit #{botname} (in #{e.backtrace.first}")
|
90
|
+
rescue Interrupt => e
|
91
|
+
info("Interrupt, terminating #{botname} (in #{e.backtrace.first}")
|
92
|
+
rescue Exception => e
|
93
|
+
exception(e)
|
94
|
+
ensure
|
95
|
+
File.delete(pidfile) if pidfile and File.exist?(pidfile)
|
96
|
+
butler.quit if butler #and butler.logged_in? # FIXME, make the commented code happen
|
97
|
+
info("Terminated")
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_pidfile(path, botname)
|
101
|
+
pidfile = "#{path}/#{botname}.pid"
|
102
|
+
FileUtils.mkdir_p(path, :mode => 0755)
|
103
|
+
raise "Can't write to pid-directory (#{path.run})" if !File.writable?(path)
|
104
|
+
if File.exist?(pidfile) && pid = File.read(pidfile) then
|
105
|
+
if pid.empty? then
|
106
|
+
raise "Already a Butler named '#{botname}' starting up"
|
107
|
+
else
|
108
|
+
raise "Already a Butler named '#{botname}' running with PID #{pid.chomp}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
pidfile
|
112
|
+
end
|
113
|
+
|
114
|
+
def stop(path, botname)
|
115
|
+
path ||= @path
|
116
|
+
pidfile = "#{path.run}/#{botname}.pid"
|
117
|
+
if File.exist?(pidfile) then
|
118
|
+
pid = File.read(pidfile).to_i
|
119
|
+
begin
|
120
|
+
Process.kill("HUP", pid)
|
121
|
+
rescue Errno::ESRCH; end
|
122
|
+
begin
|
123
|
+
File.delete(pidfile)
|
124
|
+
rescue Errno::ENOENT; end
|
125
|
+
end
|
126
|
+
rescue => e
|
127
|
+
exception(e)
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
|
131
|
+
# creates a backup of all important files of a bot
|
132
|
+
def backup(path, botname, backup)
|
133
|
+
path ||= @path
|
134
|
+
bot_path = "#{path.bots}/#{botname}"
|
135
|
+
FileUtils.mkdir_p(File.dirname(backup), :mode => 0755)
|
136
|
+
FileUtils.cp_r(bot_path, backup)
|
137
|
+
end
|
138
|
+
|
139
|
+
# tests if a bot named +botname+ exists
|
140
|
+
def exists?(path, botname)
|
141
|
+
path ||= @path
|
142
|
+
bot_path = "#{path.bots}/#{botname}"
|
143
|
+
File.directory?(bot_path)
|
144
|
+
end
|
145
|
+
|
146
|
+
# returns a list with bots
|
147
|
+
def list(path)
|
148
|
+
path ||= @path
|
149
|
+
Dir[path.bots+"/*/"].map { |dir| dir[%r{([^/]+)/$}, 1] }
|
150
|
+
end
|
151
|
+
|
152
|
+
# creates a new bot
|
153
|
+
def create(path, botname, opts={})
|
154
|
+
path ||= @path
|
155
|
+
bot_path = "#{path.bots}/#{botname}"
|
156
|
+
Structure.each { |dir, mode|
|
157
|
+
FileUtils.mkdir_p(dir.sub(/BOTPATH/, bot_path), :mode => mode)
|
158
|
+
}
|
159
|
+
conf = Configuration.new(
|
160
|
+
ConfigurationStructure.first.sub(/BOTPATH/, bot_path),
|
161
|
+
ConfigurationStructure[1..-1]
|
162
|
+
)
|
163
|
+
EmptyConfig.each { |k,v|
|
164
|
+
conf[k] = v
|
165
|
+
}
|
166
|
+
FileUtils.cp_r(path.plugin_repository, bot_path)
|
167
|
+
end
|
168
|
+
|
169
|
+
# deletes a bot
|
170
|
+
def delete(path, botname)
|
171
|
+
path ||= @path
|
172
|
+
bot_path = "#{path.bots}/#{botname}"
|
173
|
+
FileUtils.rm_r(bot_path)
|
174
|
+
end
|
175
|
+
|
176
|
+
# renames a bot
|
177
|
+
def rename(path, old_name, new_name)
|
178
|
+
path ||= @path
|
179
|
+
old_bot_path = "#{path.bots}/#{old_name}"
|
180
|
+
new_bot_path = "#{path.bots}/#{new_name}"
|
181
|
+
FileUtils.mv(old_bot_path, new_bot_path)
|
182
|
+
end
|
183
|
+
|
184
|
+
def new(*args, &block)
|
185
|
+
Bot.new(*args, &block)
|
186
|
+
end
|
187
|
+
end # <<Butler
|
188
|
+
end
|