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