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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Butler
|
|
2
|
+
module IRC
|
|
3
|
+
Topic = Struct.new(:text, :set_by, :set_at) unless defined? Topic
|
|
4
|
+
class Topic
|
|
5
|
+
def inspect
|
|
6
|
+
"#<%s %s --%s on %s>" % [
|
|
7
|
+
self.class,
|
|
8
|
+
text,
|
|
9
|
+
set_by,
|
|
10
|
+
set_at.strftime("%Y-%m-%d %H:%M")
|
|
11
|
+
]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
# See LICENSE.txt for permissions.
|
|
5
|
+
#++
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Butler
|
|
10
|
+
module IRC
|
|
11
|
+
# Enumerable: channels (IRC::Channel instances) the user is known to be in
|
|
12
|
+
class User
|
|
13
|
+
module Flags
|
|
14
|
+
ADMIN = 1
|
|
15
|
+
IRCOP = 2
|
|
16
|
+
AOP = 4
|
|
17
|
+
OP = 8
|
|
18
|
+
VOICE = 16
|
|
19
|
+
UOP = 32
|
|
20
|
+
end
|
|
21
|
+
include Flags
|
|
22
|
+
|
|
23
|
+
USER_STATUS = [:unknown, :myself, :online, :out_of_sight, :offline, :mutated]
|
|
24
|
+
|
|
25
|
+
PREFIXES = Hash.new(0)
|
|
26
|
+
PREFIXES["@"] = OP
|
|
27
|
+
PREFIXES["+"] = VOICE
|
|
28
|
+
PREFIXES["-"] = UOP
|
|
29
|
+
|
|
30
|
+
include Comparable
|
|
31
|
+
include Enumerable
|
|
32
|
+
|
|
33
|
+
#nickname of a user
|
|
34
|
+
attr_reader :nick
|
|
35
|
+
|
|
36
|
+
#username of a user
|
|
37
|
+
attr_reader :user
|
|
38
|
+
|
|
39
|
+
#hostpart of a user
|
|
40
|
+
attr_reader :host
|
|
41
|
+
|
|
42
|
+
#realname of a user
|
|
43
|
+
attr_reader :real
|
|
44
|
+
|
|
45
|
+
#users last known status (:unknown, :online, :out_of_sight, :offline)
|
|
46
|
+
attr_reader :status
|
|
47
|
+
|
|
48
|
+
#lowercase nickname
|
|
49
|
+
attr_reader :compare
|
|
50
|
+
|
|
51
|
+
def initialize(users, channels, nick=nil, user=nil, host=nil, real=nil)
|
|
52
|
+
@nick, @user, @host, @real = nil
|
|
53
|
+
@users = users
|
|
54
|
+
@nick = nick.to_str.strip_user_prefixes.freeze if nick
|
|
55
|
+
@user = user.freeze if user
|
|
56
|
+
@host = host.freeze if host
|
|
57
|
+
@real = real.freeze if real
|
|
58
|
+
|
|
59
|
+
@compare = @nick ? @nick.downcase : "\xff"
|
|
60
|
+
@all_channels = channels
|
|
61
|
+
@channels = {} # downcased channelname => flags
|
|
62
|
+
@status = :unknown
|
|
63
|
+
@is_away = false
|
|
64
|
+
@away_message = ""
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Iterate over all users in this channel
|
|
68
|
+
def each
|
|
69
|
+
@channels.each_value { |channel| yield @all_channels[channel] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def channels
|
|
73
|
+
@all_channels.map_names(*@channels.keys)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def channel_names
|
|
77
|
+
@channels.keys
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# update userdata (set user, host, real if they are nil)
|
|
81
|
+
def update(user=nil, host=nil, real=nil) #:nodoc:
|
|
82
|
+
raise ArgumentError, "Non assignable new value for user" if @user and user and @user != user
|
|
83
|
+
raise ArgumentError, "Non assignable new value for host" if @host and host and @host != host
|
|
84
|
+
raise ArgumentError, "Non assignable new value for real" if @real and real and @real != real
|
|
85
|
+
@user = user.freeze if user
|
|
86
|
+
@host = host.freeze if host
|
|
87
|
+
@real = real.freeze if real
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# set users nickname (should only be used by Butler::IRC::Parser)
|
|
92
|
+
def nick=(nick) #:nodoc:
|
|
93
|
+
nick = nick.to_str.strip_user_prefixes
|
|
94
|
+
raise ArgumentError, "Invalid nickname #{nick}" unless nick.valid_nickname?
|
|
95
|
+
@users.lock.synchronize {
|
|
96
|
+
old_index = @compare
|
|
97
|
+
@nick = nick
|
|
98
|
+
@compare = @nick.downcase
|
|
99
|
+
@users.rehash(old_index, @compare)
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# set user-status (should only be used by Butler::IRC::Parser)
|
|
104
|
+
def status=(status) #:nodoc:
|
|
105
|
+
raise ArgumentError, "Invalid status #{status}" unless USER_STATUS.include?(status)
|
|
106
|
+
@status = status unless @status == :myself
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# set away status (should only be used by Butler::IRC::Parser)
|
|
110
|
+
def away=(is_away) #:nodoc:
|
|
111
|
+
@is_away = is_away
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# check if user has op (+o) in given channel (String or Butler::IRC::Channel)
|
|
115
|
+
def op?(in_channel)
|
|
116
|
+
!(@channels[in_channel.to_str.downcase] & OP).zero?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# check if user has voice (+v) in given channel (String or Butler::IRC::Channel)
|
|
120
|
+
def voice?(in_channel)
|
|
121
|
+
!(@channels[in_channel.to_str.downcase] & VOICE).zero?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# check if user has uop (+u) in given channel (String or Butler::IRC::Channel)
|
|
125
|
+
def uop?(in_channel)
|
|
126
|
+
!(@channels[in_channel.to_str.downcase] & UOP).zero?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def in_channel?(channel)
|
|
130
|
+
@channels.has_key?(channel.to_str.downcase)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# check if user is away
|
|
134
|
+
def is_away?
|
|
135
|
+
@is_away
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# retrieve away message
|
|
139
|
+
def away_message
|
|
140
|
+
@away_message
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# set away message (should only be used by Butler::IRC::Parser)
|
|
144
|
+
def away_message=(message) #:nodoc:
|
|
145
|
+
@away_message = message.freeze
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def common_channels(with_other_user)
|
|
149
|
+
@channels.keys & with_other_user.channel_names
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def common_channels?(with_other_user)
|
|
153
|
+
!common_channels(with_other_user).empty?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# FIXME
|
|
157
|
+
# add a channel to the user (should only be used by Butler::IRC::Parser)
|
|
158
|
+
def add_channel(channel, reason)
|
|
159
|
+
@channels[channel.to_str.downcase] ||= 0
|
|
160
|
+
if @status != :myself && [:out_of_sight, :unknown, :offline].include?(@status) && common_channels?(@users.myself) then
|
|
161
|
+
self.status = :online
|
|
162
|
+
end
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# FIXME
|
|
167
|
+
# remove a channel from the user (should only be used by Butler::IRC::Parser)
|
|
168
|
+
def delete_channel(channel, reason)
|
|
169
|
+
@channels.delete(channel.to_str.downcase)
|
|
170
|
+
if @channels.empty? && ![:out_of_sight, :unknown, :myself].include?(@status) then
|
|
171
|
+
self.status = :out_of_sight
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# FIXME
|
|
176
|
+
def add_flags(channel, flags)
|
|
177
|
+
channel = channel.to_str.downcase
|
|
178
|
+
raise ArgumentError, "User #{self} is not listed in #{channel}" unless @channels.include?(channel)
|
|
179
|
+
@channels[channel] |= flags
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# FIXME
|
|
183
|
+
def delete_flags(channel, flags)
|
|
184
|
+
channel = channel.to_str.downcase
|
|
185
|
+
raise ArgumentError, "User #{self} is not listed in #{channel}" unless @channels.include?(channel)
|
|
186
|
+
@channels[channel] &= ~flags
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# FIXME
|
|
190
|
+
def quit
|
|
191
|
+
@channels.each { |channel, _| delete_channel(channel, :quit) }
|
|
192
|
+
self.status = :offline
|
|
193
|
+
@users.delete(self, :quit)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# FIXME
|
|
197
|
+
def kill
|
|
198
|
+
@channels.each { |channel, _| delete_channel(channel, :kill) }
|
|
199
|
+
self.status = :offline
|
|
200
|
+
@users.delete(self, :kill)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Returns the (frozen!) nickname of the user
|
|
204
|
+
def to_s
|
|
205
|
+
@nick
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# :nodoc: string representation of Butler::IRC::User (nickname)
|
|
209
|
+
def to_str
|
|
210
|
+
@compare
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# same Butler::IRC::User?
|
|
214
|
+
# FIXME, might require stricter comparison for IRC::User objects
|
|
215
|
+
def ==(other)
|
|
216
|
+
if other.kind_of?(User) then
|
|
217
|
+
(
|
|
218
|
+
(!(@nick && other.nick) || @compare == other.compare) &&
|
|
219
|
+
(!(@user && other.user) || @user == other.user) &&
|
|
220
|
+
(!(@host && other.host) || @host == other.host) &&
|
|
221
|
+
(!(@real && other.real) || @real == other.real)
|
|
222
|
+
)
|
|
223
|
+
else
|
|
224
|
+
@compare == other.to_str.downcase
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Matching nickname to a regular expression.
|
|
229
|
+
# Same as User#to_s =~ /regular expression/
|
|
230
|
+
def =~(regex)
|
|
231
|
+
@nick =~ regex
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Lowercase String-comparison of nicknames.
|
|
235
|
+
# accepts everything that responds to :to_str
|
|
236
|
+
def ===(other)
|
|
237
|
+
@nick && @compare === other
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Compares nicknames
|
|
241
|
+
def <=>(other)
|
|
242
|
+
@compare <=> other.to_str.downcase
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def inspect
|
|
246
|
+
"#<%s:0x%x %s!%s@%s (%s)>" % [self.class, object_id, @nick || "?", @user || "?", @host || "?", @real || "?"]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
undef_method(:dup)
|
|
250
|
+
undef_method(:clone)
|
|
251
|
+
end # User
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
class Fixnum
|
|
256
|
+
# Return a String containing the user-prefixes for a flagset, e.g.
|
|
257
|
+
# (Flags::OP | Flags::Voice).to_user_prefixes # => "@-"
|
|
258
|
+
def to_user_prefixes
|
|
259
|
+
result = ""
|
|
260
|
+
Butler::IRC::User::PREFIXES.each { |char, value|
|
|
261
|
+
result += char unless (self & value).zero?
|
|
262
|
+
}
|
|
263
|
+
return result
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
# See LICENSE.txt for permissions.
|
|
5
|
+
#++
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
require 'butler/irc/channels'
|
|
10
|
+
require 'butler/irc/string'
|
|
11
|
+
require 'butler/irc/user'
|
|
12
|
+
require 'thread'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Butler
|
|
17
|
+
module IRC
|
|
18
|
+
# Enumerable: all known & visible users
|
|
19
|
+
class Users
|
|
20
|
+
include Enumerable
|
|
21
|
+
|
|
22
|
+
# the user that represents the clients user
|
|
23
|
+
attr_reader :myself
|
|
24
|
+
# provides a global (within Butler::IRC::User) mutex
|
|
25
|
+
attr_reader :lock
|
|
26
|
+
|
|
27
|
+
def initialize(client)
|
|
28
|
+
@client = client
|
|
29
|
+
@users = {}
|
|
30
|
+
@lost = {}
|
|
31
|
+
@lock = Mutex.new
|
|
32
|
+
@myself = nil
|
|
33
|
+
@channels = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def channels=(channels)
|
|
37
|
+
raise "Can't set @channels twice" if @channels
|
|
38
|
+
@channels = channels
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def map_nicks(*nicks)
|
|
42
|
+
@users.values_at(*nicks)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_myself(nick, user, real)
|
|
46
|
+
@myself = create(nick, nil, nil, nil)
|
|
47
|
+
@myself.status = :myself
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def create(nick, *args)
|
|
51
|
+
@lock.synchronize {
|
|
52
|
+
new_user = User.new(self, @channels, nick, *args)
|
|
53
|
+
old_user = self[nick]
|
|
54
|
+
if old_user == new_user then
|
|
55
|
+
old_user.update(*args)
|
|
56
|
+
old_user
|
|
57
|
+
|
|
58
|
+
elsif !old_user then
|
|
59
|
+
@users[new_user.compare] = new_user
|
|
60
|
+
|
|
61
|
+
# if old user was out of sight the new user overrides the old
|
|
62
|
+
elsif old_user.status == :out_of_sight || old_user.status == :unknown then
|
|
63
|
+
@users.delete(old_user.compare)
|
|
64
|
+
@users[new_user.compare] = new_user
|
|
65
|
+
|
|
66
|
+
else # Should NOT happen
|
|
67
|
+
old_user.status = :mutated
|
|
68
|
+
@users.delete(old_user.compare)
|
|
69
|
+
@users[new_user.compare] = new_user
|
|
70
|
+
raise "A user mutated #{old_user.inspect} -> #{new_user.inspect}"
|
|
71
|
+
end
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# list clones (comparing all known users)
|
|
76
|
+
def clones(strong=false, min=2)
|
|
77
|
+
sieve = Hash.new { |h,k| h[k] = [] }
|
|
78
|
+
if strong then
|
|
79
|
+
each { |user|
|
|
80
|
+
sieve["#{user.user}@#{user.host}"] << user if (user.host && user.user)
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
each { |user|
|
|
84
|
+
sieve[user.host] << user if user.host
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
sieve.reject { |host, users| users.length < min }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def each(&block)
|
|
91
|
+
@users.each_value(&block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def rehash(old_index, new_index)
|
|
95
|
+
@users[new_index] = @users.delete(old_index)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def delete(user, reason)
|
|
99
|
+
@users.delete(user.to_str.downcase)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def delete_channel(channel, reason)
|
|
103
|
+
@users.each_value { |user| user.delete_channel(channel, reason) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# get a user by nick (may include user prefixes)
|
|
107
|
+
def [](nick)
|
|
108
|
+
@users[nick.to_str.strip_user_prefixes.downcase]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
# See LICENSE.txt for permissions.
|
|
5
|
+
#++
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
require 'butler/plugin/configproxy'
|
|
10
|
+
require 'butler/plugin/onhandlers'
|
|
11
|
+
require 'butler/plugin/mapper'
|
|
12
|
+
require 'butler/plugin/matcher'
|
|
13
|
+
require 'butler/plugin/trigger'
|
|
14
|
+
require 'log/comfort'
|
|
15
|
+
require 'ostructfixed'
|
|
16
|
+
require 'ruby/exception/detailed'
|
|
17
|
+
require 'scriptfile'
|
|
18
|
+
require 'templater'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Butler
|
|
23
|
+
class Plugin
|
|
24
|
+
class <<self
|
|
25
|
+
attr_reader :about
|
|
26
|
+
attr_reader :base
|
|
27
|
+
attr_reader :name
|
|
28
|
+
attr_reader :butler
|
|
29
|
+
attr_reader :config
|
|
30
|
+
attr_reader :help
|
|
31
|
+
attr_reader :path
|
|
32
|
+
attr_reader :strings
|
|
33
|
+
attr_reader :summary
|
|
34
|
+
attr_reader :usage
|
|
35
|
+
|
|
36
|
+
# this method is called to initialize the plugin-class,
|
|
37
|
+
# do not override
|
|
38
|
+
def load_plugin(butler, base, path) # :nodoc:
|
|
39
|
+
@butler = butler
|
|
40
|
+
@base = base.dup.freeze
|
|
41
|
+
@name = File.basename(base).freeze
|
|
42
|
+
@commands = []
|
|
43
|
+
@listener = []
|
|
44
|
+
@config = ConfigProxy.new(@butler.config, "plugin.#{name}")
|
|
45
|
+
|
|
46
|
+
if File.directory?(path) then
|
|
47
|
+
raise "Not supported yet" # FIXME
|
|
48
|
+
@path = OpenStruct.new(
|
|
49
|
+
:data => (@path+"/data").freeze,
|
|
50
|
+
:strings => (@path+"/strings").freeze,
|
|
51
|
+
:requires => (@path+"/requires").freeze
|
|
52
|
+
)
|
|
53
|
+
else
|
|
54
|
+
data = YAML.load(ScriptFile.read(path)) || {}
|
|
55
|
+
@strings = data[:strings] || {}
|
|
56
|
+
@about = data[:about] || {}
|
|
57
|
+
@help = data[:help] || {}
|
|
58
|
+
mappers = data[:map] || {}
|
|
59
|
+
@summary = data[:summary] || {}
|
|
60
|
+
triggers = data[:trigger] || nil
|
|
61
|
+
@usage = data[:usage] || {}
|
|
62
|
+
@path = OpenStruct.new(
|
|
63
|
+
:data => nil,
|
|
64
|
+
:strings => nil,
|
|
65
|
+
:requires => nil
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
{:usage=>[@usage],:strings=>@strings.values,:help=>@help.values}.each { |key,tmpls|
|
|
70
|
+
tmpls.each { |tmpl|
|
|
71
|
+
create_templates(tmpl, key)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
mappers.each { |meth, expressions|
|
|
76
|
+
map(meth, expressions)
|
|
77
|
+
}
|
|
78
|
+
trigger(triggers) if triggers
|
|
79
|
+
|
|
80
|
+
info("Loaded plugin '#{@base}' (#{self})")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def create_templates(tmpl, name)
|
|
84
|
+
tmpl.each { |key,value|
|
|
85
|
+
begin
|
|
86
|
+
tmpl[key] = Templater.new(value)
|
|
87
|
+
rescue Exception => e
|
|
88
|
+
e.extend Exception::Detail
|
|
89
|
+
e.prepend "Could not map #{key} in #{@base} (#{name})."
|
|
90
|
+
exception(e)
|
|
91
|
+
end
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# initialize a configuration
|
|
96
|
+
def configuration(settings)
|
|
97
|
+
settings.each { |key, value|
|
|
98
|
+
@config[key] = value unless @config.has_key?(key)
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Will create an instance of the plugin and call usage on it
|
|
103
|
+
def plugin_usage(message, data={})
|
|
104
|
+
new(message).usage(data)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def on_load
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def trigger(commands)
|
|
111
|
+
commands = { "en" => commands } unless commands.kind_of?(Hash)
|
|
112
|
+
commands.each { |lang, command|
|
|
113
|
+
trigger = Trigger.new(self, lang, command)
|
|
114
|
+
@butler.add_command(trigger)
|
|
115
|
+
@commands << trigger
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def map(meth, expressions)
|
|
120
|
+
expressions = { "en" => expressions } unless expressions.kind_of?(Hash)
|
|
121
|
+
expressions.each { |lang, expression|
|
|
122
|
+
mapper = Mapper.new(self, meth, lang, expression)
|
|
123
|
+
@butler.add_command(mapper)
|
|
124
|
+
@commands << mapper
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def every(*args, &block)
|
|
129
|
+
@butler.scheduler.every(*args, &block)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def at(*args, &block)
|
|
133
|
+
@butler.scheduler.at(*args, &block)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def timed(*args, &block)
|
|
137
|
+
@butler.scheduler.timed(*args, &block)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def subscribe(*args, &block)
|
|
141
|
+
listener = @butler.subscribe(*args, &block)
|
|
142
|
+
@listener << listener
|
|
143
|
+
listener
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def on_unload
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def unload_plugin
|
|
150
|
+
info("Unloading plugin '#{@base}' (#{self})")
|
|
151
|
+
@commands.each { |command| @butler.delete_command(command) }
|
|
152
|
+
@listener.each { |listener| listener.unsubscribe }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
include Log::Comfort
|
|
158
|
+
extend Log::Comfort
|
|
159
|
+
|
|
160
|
+
attr_reader :butler
|
|
161
|
+
attr_reader :message
|
|
162
|
+
|
|
163
|
+
def initialize(message)
|
|
164
|
+
@butler = plugin.butler
|
|
165
|
+
@message = message
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
alias plugin class
|
|
169
|
+
|
|
170
|
+
# gets the arguments and downcases if possible
|
|
171
|
+
def arguments(*params)
|
|
172
|
+
args = @message.arguments[*params]
|
|
173
|
+
if args.kind_of?(Array) then
|
|
174
|
+
args.map { |arg| arg.downcase }
|
|
175
|
+
elsif args then
|
|
176
|
+
args.downcase
|
|
177
|
+
else
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def localize(index, data={})
|
|
183
|
+
string = plugin.strings[index]
|
|
184
|
+
string &&= (string[@message.language] || string["en"])
|
|
185
|
+
return "Unknown String '#{index}'" unless string
|
|
186
|
+
string.result(self, data)
|
|
187
|
+
end
|
|
188
|
+
alias _ localize
|
|
189
|
+
|
|
190
|
+
def usage(data={})
|
|
191
|
+
string = plugin.usage
|
|
192
|
+
if string = string[@message.language] || string["en"] then
|
|
193
|
+
("Usage '#{plugin.name}': #{@message.invocation}"+string.result(self, data)).mirc_formatted
|
|
194
|
+
else
|
|
195
|
+
"No usage provided"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def unknown(n=-1)
|
|
200
|
+
"Unknown argument #{@message.arguments[n]} for #{@message.arguments[0...n].join(' ')}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Only works if @message is set and conditions of Messag#answer are met.
|
|
204
|
+
# If string is a Symbol it considers it a key for localization, looks it up
|
|
205
|
+
# and translates it using vars as variables for the string interpolation.
|
|
206
|
+
# The string is subsequently mirc_formatted (see String#mirc_formatted).
|
|
207
|
+
# Besides that it works like Message#answer.
|
|
208
|
+
def answer(string, vars={})
|
|
209
|
+
string = localize(string, vars) if string.kind_of?(Symbol)
|
|
210
|
+
@message.answer(string.mirc_formatted)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# == About
|
|
214
|
+
# Sends a privmsg (localized and formatted) to any number of recipients
|
|
215
|
+
# (Users or Channels)
|
|
216
|
+
#
|
|
217
|
+
# == Synopsis
|
|
218
|
+
# privmsg(:greet, "#some_channel", :from => @butler.myself.nick)
|
|
219
|
+
#
|
|
220
|
+
# == Description
|
|
221
|
+
# If string is a Symbol it considers it a key for localization, looks it up
|
|
222
|
+
# and translates it using the last argument as variables for the string
|
|
223
|
+
# interpolation, iff the last argument is a Hash.
|
|
224
|
+
# The string is subsequently mirc_formatted (see String#mirc_formatted).
|
|
225
|
+
def privmsg(string, *args)
|
|
226
|
+
vars = args.last.kind_of?(Hash) ? args.pop : {}
|
|
227
|
+
string = localize(string, vars) if string.kind_of?(Symbol)
|
|
228
|
+
@butler.privmsg(string.mirc_formatted, *args)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# == About
|
|
232
|
+
# Sends a notice (localized and formatted) to any number of recipients
|
|
233
|
+
# (Users or Channels)
|
|
234
|
+
#
|
|
235
|
+
# == Synopsis
|
|
236
|
+
# notice(:greet, "#some_channel", :from => @butler.myself.nick)
|
|
237
|
+
#
|
|
238
|
+
# == Description
|
|
239
|
+
# If string is a Symbol it considers it a key for localization, looks it up
|
|
240
|
+
# and translates it using the last argument as variables for the string
|
|
241
|
+
# interpolation, iff the last argument is a Hash.
|
|
242
|
+
# The string is subsequently mirc_formatted (see String#mirc_formatted).
|
|
243
|
+
def notice(string, *args)
|
|
244
|
+
vars = args.last.kind_of?(Hash) ? args.pop : {}
|
|
245
|
+
string = localize(string, vars) if string.kind_of?(Symbol)
|
|
246
|
+
@butler.notice(string.mirc_formatted, *args)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|