butler 1.8.0
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/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
|