]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/seen.rb
plugin(script): remove deprecated $SAFE
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / seen.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Seen Plugin
5 #
6 # Keep a database of who last said/did what
7
8 define_structure :Saw, :nick, :time, :type, :where, :message
9
10 class SeenPlugin < Plugin
11
12   MSG_PUBLIC = N_("saying \"%{message}\" in %{where}")
13   MSG_ACTION = N_("doing *%{nick} %{message}* in %{where}")
14   MSG_NICK = N_("changing nick from %{nick} to %{message}")
15   MSG_PART = N_("leaving %{where} (%{message})")
16   MSG_PART_EMPTY = N_("leaving %{where}")
17   MSG_JOIN = N_("joining %{where}")
18   MSG_QUIT = N_("quitting IRC (%{message})")
19   MSG_TOPIC = N_("changing the topic of %{where} to \"%{message}\"")
20
21   CHANPRIV_CHAN      = N_("a private channel")
22   CHANPRIV_MSG_TOPIC  = N_("changing the topic of %{where}")
23
24   MSGPRIV_MSG_PUBLIC = N_("speaking in %{where}")
25   MSGPRIV_MSG_ACTION = N_("doing something in %{where}")
26
27   FORMAT_NORMAL = N_("%{nick} was last seen %{when}, %{doing}")
28   FORMAT_WITH_BEFORE = N_("%{nick} was last seen %{when}, %{doing} and %{time} before %{did_before}")
29
30   Config.register Config::IntegerValue.new('seen.max_results',
31     :default => 3, :validate => Proc.new{|v| v >= 0},
32     :desc => _("Maximum number of seen users to return in search (0 = no limit)."))
33   Config.register Config::ArrayValue.new('seen.ignore_patterns',
34     :default => [ "^no u$" ],
35     :desc => _("Strings/regexes that you'd like to ignore for 'last message' purposes"))
36
37   def help(plugin, topic="")
38     _("seen <nick> => have you seen, or when did you last see <nick>")
39   end
40
41   def privmsg(m)
42     unless(m.params =~ /^(\S)+$/)
43       m.reply "incorrect usage: " + help(m.plugin)
44       return
45     end
46
47     m.params.gsub!(/\?$/, "")
48
49     if @registry.has_key?(m.params)
50       m.reply seen(@registry[m.params])
51     else
52       rx = Regexp.new(m.params, true)
53       num_matched = 0
54       @registry.each {|nick, saw|
55         if nick.match(rx)
56           m.reply seen(saw)
57           num_matched += 1
58           break if num_matched == @bot.config['seen.max_results']
59         end
60       }
61
62       m.reply _("nope!") if num_matched.zero?
63     end
64   end
65
66   def listen(m)
67     return unless m.source
68     # keep database up to date with who last said what
69     now = Time.new
70     case m
71     when PrivMessage
72       return if m.private?
73       @bot.config['seen.ignore_patterns'].each { |regex|
74         return if m.message =~ /#{regex}/
75       }
76
77       type = m.action? ? 'ACTION' : 'PUBLIC'
78       store m, Saw.new(m.sourcenick.dup, now, type,
79                        m.target.to_s, m.message.dup)
80     when QuitMessage
81       return if m.address?
82       store m, Saw.new(m.sourcenick.dup, now, "QUIT",
83                        nil, m.message.dup)
84     when NickMessage
85       return if m.address?
86       store m, Saw.new(m.oldnick, now, "NICK", nil, m.newnick)
87     when PartMessage
88       return if m.address?
89       store m, Saw.new(m.sourcenick.dup, Time.new, "PART",
90                        m.target.to_s, m.message.dup)
91     when JoinMessage
92       return if m.address?
93       store m, Saw.new(m.sourcenick.dup, Time.new, "JOIN",
94                        m.target.to_s, m.message.dup)
95     when TopicMessage
96       return if m.address? or m.info_or_set == :info
97       store m, Saw.new(m.sourcenick.dup, Time.new, "TOPIC",
98                        m.target.to_s, m.message.dup)
99     end
100   end
101
102   def seen(reg)
103     saw = case reg
104     when Struct::Saw
105       reg # for backwards compatibility
106     when Array
107       reg.last
108     end
109
110     if reg.kind_of? Array
111       before = reg.first
112     end
113
114     # TODO: a message should not be disclosed if:
115     # - it was said in a channel that was/is invite-only, private or secret
116     # - UNLESS the requester is also in the channel now, or the request is made
117     #   in the channel?
118     msg_privacy = false
119     # TODO: a channel or it's topic should not be disclosed if:
120     # - the channel was/is private or secret
121     # - UNLESS the requester is also in the channel now, or the request is made
122     #   in the channel?
123     chan_privacy = false
124
125     # What should be displayed for channel?
126     where = chan_privacy ? _(CHANPRIV_CHAN) : saw.where
127
128     formats = {
129       :normal      => _(FORMAT_NORMAL),
130       :with_before => _(FORMAT_WITH_BEFORE)
131     }
132
133     if before && [:PART, :QUIT].include?(saw.type.to_sym) &&
134        [:PUBLIC, :ACTION].include?(before.type.to_sym)
135       # TODO see chan_privacy
136       prev_chan_privacy = false
137       prev_where = prev_chan_privacy ? _(CHANPRIV_CHAN) : before.where
138       did_before = case before.type.to_sym
139       when :PUBLIC
140         _(msg_privacy ? MSGPRIV_MSG_PUBLIC : MSG_PUBLIC)
141       when :ACTION
142         _(msg_privacy ? MSGPRIV_MSG_ACTION : MSG_ACTION)
143       end % {
144         :nick => saw.nick,
145         :message => before.message,
146         :where => prev_where
147       }
148
149       format = :with_before
150
151       time_diff = saw.time - before.time
152       if time_diff < 300
153         time_before = _("a moment")
154       elsif time_diff < 3600
155         time_before = _("a while")
156       else
157         format = :normal
158       end
159     else
160       format = :normal
161     end
162
163     nick = saw.nick
164     ago = Time.new - saw.time
165
166     if (ago.to_i == 0)
167       time = _("just now")
168     else
169       time = _("%{time} ago") % { :time => Utils.secs_to_string(ago) }
170     end
171
172     doing = case saw.type.to_sym
173     when :PUBLIC
174       _(msg_privacy ? MSGPRIV_MSG_PUBLIC : MSG_PUBLIC)
175     when :ACTION
176       _(msg_privacy ? MSGPRIV_MSG_ACTION : MSG_ACTION)
177     when :NICK
178       _(MSG_NICK)
179     when :PART
180       if saw.message.empty?
181         _(MSG_PART_EMPTY)
182       else
183         _(MSG_PART)
184       end
185     when :JOIN
186       _(MSG_JOIN)
187     when :QUIT
188       _(MSG_QUIT)
189     when :TOPIC
190       _(chan_privacy ? CHANPRIV_MSG_TOPIC : MSG_TOPIC)
191     end % { :message => saw.message, :where => where, :nick => saw.nick }
192
193     case format
194     when :normal
195       formats[:normal] % {
196         :nick  => saw.nick,
197         :when  => time,
198         :doing => doing,
199       }
200     when :with_before
201       formats[:with_before] % {
202         :nick  => saw.nick,
203         :when  => time,
204         :doing => doing,
205         :time  => time_before,
206         :did_before => did_before
207       }
208     end
209   end
210
211   def store(m, saw)
212     # TODO: we need to store the channel state INVITE/SECRET/PRIVATE here, in
213     # some symbolic form, so that we know the prior state of the channel when
214     # it comes time to display.
215     reg = @registry[saw.nick]
216
217     if reg && reg.is_a?(Array)
218       reg.shift if reg.size > 1
219       reg.push(saw)
220     else
221       reg = [saw]
222     end
223
224     if m.is_a? NickMessage
225       @registry[m.newnick] = reg
226     end
227
228     @registry[saw.nick] = reg
229   end
230 end
231 plugin = SeenPlugin.new
232 plugin.register("seen")