butler 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. data/CHANGELOG +4 -0
  2. data/GPL.txt +340 -0
  3. data/LICENSE.txt +52 -0
  4. data/README +37 -0
  5. data/Rakefile +334 -0
  6. data/bin/botcontrol +230 -0
  7. data/data/butler/config_template.yaml +4 -0
  8. data/data/butler/dialogs/backup.rb +19 -0
  9. data/data/butler/dialogs/botcontrol.rb +4 -0
  10. data/data/butler/dialogs/config.rb +1 -0
  11. data/data/butler/dialogs/create.rb +53 -0
  12. data/data/butler/dialogs/delete.rb +3 -0
  13. data/data/butler/dialogs/en/backup.yaml +6 -0
  14. data/data/butler/dialogs/en/botcontrol.yaml +5 -0
  15. data/data/butler/dialogs/en/create.yaml +11 -0
  16. data/data/butler/dialogs/en/delete.yaml +2 -0
  17. data/data/butler/dialogs/en/help.yaml +17 -0
  18. data/data/butler/dialogs/en/info.yaml +13 -0
  19. data/data/butler/dialogs/en/list.yaml +4 -0
  20. data/data/butler/dialogs/en/notyetimplemented.yaml +2 -0
  21. data/data/butler/dialogs/en/rename.yaml +3 -0
  22. data/data/butler/dialogs/en/start.yaml +3 -0
  23. data/data/butler/dialogs/en/sync_plugins.yaml +3 -0
  24. data/data/butler/dialogs/en/uninstall.yaml +5 -0
  25. data/data/butler/dialogs/en/unknown_command.yaml +2 -0
  26. data/data/butler/dialogs/help.rb +11 -0
  27. data/data/butler/dialogs/info.rb +27 -0
  28. data/data/butler/dialogs/interactive.rb +1 -0
  29. data/data/butler/dialogs/list.rb +10 -0
  30. data/data/butler/dialogs/notyetimplemented.rb +1 -0
  31. data/data/butler/dialogs/rename.rb +4 -0
  32. data/data/butler/dialogs/selectbot.rb +2 -0
  33. data/data/butler/dialogs/start.rb +5 -0
  34. data/data/butler/dialogs/sync_plugins.rb +30 -0
  35. data/data/butler/dialogs/uninstall.rb +17 -0
  36. data/data/butler/dialogs/unknown_command.rb +1 -0
  37. data/data/butler/plugins/core/logout.rb +41 -0
  38. data/data/butler/plugins/core/plugins.rb +134 -0
  39. data/data/butler/plugins/core/privilege.rb +103 -0
  40. data/data/butler/plugins/core/user.rb +166 -0
  41. data/data/butler/plugins/dev/eval.rb +64 -0
  42. data/data/butler/plugins/dev/nometa.rb +14 -0
  43. data/data/butler/plugins/dev/onhandlers.rb +93 -0
  44. data/data/butler/plugins/dev/raw.rb +36 -0
  45. data/data/butler/plugins/dev/rawlog.rb +77 -0
  46. data/data/butler/plugins/games/eightball.rb +54 -0
  47. data/data/butler/plugins/games/mastermind.rb +174 -0
  48. data/data/butler/plugins/irc/action.rb +36 -0
  49. data/data/butler/plugins/irc/join.rb +38 -0
  50. data/data/butler/plugins/irc/notice.rb +36 -0
  51. data/data/butler/plugins/irc/part.rb +38 -0
  52. data/data/butler/plugins/irc/privmsg.rb +36 -0
  53. data/data/butler/plugins/irc/quit.rb +36 -0
  54. data/data/butler/plugins/operator/deop.rb +41 -0
  55. data/data/butler/plugins/operator/devoice.rb +41 -0
  56. data/data/butler/plugins/operator/limit.rb +47 -0
  57. data/data/butler/plugins/operator/op.rb +41 -0
  58. data/data/butler/plugins/operator/voice.rb +41 -0
  59. data/data/butler/plugins/public/help.rb +69 -0
  60. data/data/butler/plugins/public/login.rb +72 -0
  61. data/data/butler/plugins/public/usage.rb +49 -0
  62. data/data/butler/plugins/service/clones.rb +56 -0
  63. data/data/butler/plugins/service/define.rb +47 -0
  64. data/data/butler/plugins/service/log.rb +183 -0
  65. data/data/butler/plugins/service/svn.rb +91 -0
  66. data/data/butler/plugins/util/cycle.rb +98 -0
  67. data/data/butler/plugins/util/load.rb +41 -0
  68. data/data/butler/plugins/util/pong.rb +29 -0
  69. data/data/butler/strings/random/acknowledge.en.yaml +5 -0
  70. data/data/butler/strings/random/gratitude.en.yaml +3 -0
  71. data/data/butler/strings/random/hello.en.yaml +4 -0
  72. data/data/butler/strings/random/ignorance.en.yaml +7 -0
  73. data/data/butler/strings/random/ignorance_about.en.yaml +3 -0
  74. data/data/butler/strings/random/insult.en.yaml +3 -0
  75. data/data/butler/strings/random/rejection.en.yaml +12 -0
  76. data/data/man/botcontrol.1 +17 -0
  77. data/lib/access.rb +187 -0
  78. data/lib/access/admin.rb +16 -0
  79. data/lib/access/privilege.rb +122 -0
  80. data/lib/access/role.rb +102 -0
  81. data/lib/access/savable.rb +18 -0
  82. data/lib/access/user.rb +180 -0
  83. data/lib/access/yamlbase.rb +126 -0
  84. data/lib/butler.rb +188 -0
  85. data/lib/butler/bot.rb +247 -0
  86. data/lib/butler/control.rb +93 -0
  87. data/lib/butler/dialog.rb +64 -0
  88. data/lib/butler/initialvalues.rb +40 -0
  89. data/lib/butler/irc/channel.rb +135 -0
  90. data/lib/butler/irc/channels.rb +96 -0
  91. data/lib/butler/irc/client.rb +351 -0
  92. data/lib/butler/irc/hostmask.rb +53 -0
  93. data/lib/butler/irc/message.rb +184 -0
  94. data/lib/butler/irc/parser.rb +125 -0
  95. data/lib/butler/irc/parser/commands.rb +83 -0
  96. data/lib/butler/irc/parser/generic.rb +343 -0
  97. data/lib/butler/irc/socket.rb +378 -0
  98. data/lib/butler/irc/string.rb +186 -0
  99. data/lib/butler/irc/topic.rb +15 -0
  100. data/lib/butler/irc/user.rb +265 -0
  101. data/lib/butler/irc/users.rb +112 -0
  102. data/lib/butler/plugin.rb +249 -0
  103. data/lib/butler/plugin/configproxy.rb +35 -0
  104. data/lib/butler/plugin/mapper.rb +85 -0
  105. data/lib/butler/plugin/matcher.rb +55 -0
  106. data/lib/butler/plugin/onhandlers.rb +70 -0
  107. data/lib/butler/plugin/trigger.rb +58 -0
  108. data/lib/butler/plugins.rb +147 -0
  109. data/lib/butler/version.rb +17 -0
  110. data/lib/cloptions.rb +217 -0
  111. data/lib/cloptions/adapters.rb +24 -0
  112. data/lib/cloptions/switch.rb +132 -0
  113. data/lib/configuration.rb +223 -0
  114. data/lib/dialogline.rb +296 -0
  115. data/lib/dialogline/localizations.rb +24 -0
  116. data/lib/durations.rb +57 -0
  117. data/lib/event.rb +295 -0
  118. data/lib/event/at.rb +64 -0
  119. data/lib/event/every.rb +56 -0
  120. data/lib/event/timed.rb +112 -0
  121. data/lib/installer.rb +75 -0
  122. data/lib/iterator.rb +34 -0
  123. data/lib/log.rb +68 -0
  124. data/lib/log/comfort.rb +85 -0
  125. data/lib/log/converter.rb +23 -0
  126. data/lib/log/entry.rb +152 -0
  127. data/lib/log/fakeio.rb +55 -0
  128. data/lib/log/file.rb +54 -0
  129. data/lib/log/filereader.rb +81 -0
  130. data/lib/log/forward.rb +49 -0
  131. data/lib/log/methods.rb +39 -0
  132. data/lib/log/nolog.rb +18 -0
  133. data/lib/log/splitter.rb +26 -0
  134. data/lib/ostructfixed.rb +26 -0
  135. data/lib/ruby/array/columnize.rb +38 -0
  136. data/lib/ruby/dir/mktree.rb +28 -0
  137. data/lib/ruby/enumerable/join.rb +13 -0
  138. data/lib/ruby/exception/detailed.rb +24 -0
  139. data/lib/ruby/file/append.rb +11 -0
  140. data/lib/ruby/file/write.rb +11 -0
  141. data/lib/ruby/hash/zip.rb +15 -0
  142. data/lib/ruby/kernel/bench.rb +15 -0
  143. data/lib/ruby/kernel/daemonize.rb +42 -0
  144. data/lib/ruby/kernel/non_verbose.rb +17 -0
  145. data/lib/ruby/kernel/safe_fork.rb +18 -0
  146. data/lib/ruby/range/stepped.rb +11 -0
  147. data/lib/ruby/string/arguments.rb +72 -0
  148. data/lib/ruby/string/chunks.rb +15 -0
  149. data/lib/ruby/string/post_arguments.rb +44 -0
  150. data/lib/ruby/string/unescaped.rb +17 -0
  151. data/lib/scheduler.rb +164 -0
  152. data/lib/scriptfile.rb +101 -0
  153. data/lib/templater.rb +86 -0
  154. data/test/cloptions.rb +134 -0
  155. data/test/cv.rb +28 -0
  156. data/test/irc/client.rb +85 -0
  157. data/test/irc/client_login.txt +53 -0
  158. data/test/irc/client_subscribe.txt +8 -0
  159. data/test/irc/message.rb +30 -0
  160. data/test/irc/messages.txt +64 -0
  161. data/test/irc/parser.rb +13 -0
  162. data/test/irc/profile_parser.rb +12 -0
  163. data/test/irc/users.rb +28 -0
  164. metadata +256 -0
@@ -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
@@ -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
@@ -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