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