summaryrefslogtreecommitdiff
path: root/data/rbot/plugins/seen.rb
blob: 717721e290d959f63139c5a55bf19066156511c1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#-- vim:sw=2:et
#++
#
# :title: Seen Plugin
#
# Keep a database of who last said/did what

define_structure :Saw, :nick, :time, :type, :where, :message

class SeenPlugin < Plugin

  MSG_PUBLIC = N_("saying \"%{message}\" in %{where}")
  MSG_ACTION = N_("doing *%{nick} %{message}* in %{where}")
  MSG_NICK = N_("changing nick from %{nick} to %{message}")
  MSG_PART = N_("leaving %{where} (%{message})")
  MSG_PART_EMPTY = N_("leaving %{where}")
  MSG_JOIN = N_("joining %{where}")
  MSG_QUIT = N_("quitting IRC (%{message})")
  MSG_TOPIC = N_("changing the topic of %{where} to \"%{message}\"")

  CHANPRIV_CHAN      = N_("a private channel")
  CHANPRIV_MSG_TOPIC  = N_("changing the topic of %{where}")

  MSGPRIV_MSG_PUBLIC = N_("speaking in %{where}")
  MSGPRIV_MSG_ACTION = N_("doing something in %{where}")

  FORMAT_NORMAL = N_("%{nick} was last seen %{when}, %{doing}")
  FORMAT_WITH_BEFORE = N_("%{nick} was last seen %{when}, %{doing} and %{time} before %{did_before}")

  Config.register Config::IntegerValue.new('seen.max_results',
    :default => 3, :validate => Proc.new{|v| v >= 0},
    :desc => _("Maximum number of seen users to return in search (0 = no limit)."))
  Config.register Config::ArrayValue.new('seen.ignore_patterns',
    :default => [ "^no u$" ],
    :desc => _("Strings/regexes that you'd like to ignore for 'last message' purposes"))

  def help(plugin, topic="")
    _("seen <nick> => have you seen, or when did you last see <nick>")
  end

  def privmsg(m)
    unless(m.params =~ /^(\S)+$/)
      m.reply "incorrect usage: " + help(m.plugin)
      return
    end

    m.params.gsub!(/\?$/, "")

    if @registry.has_key?(m.params)
      m.reply seen(@registry[m.params])
    else
      rx = Regexp.new(m.params, true)
      num_matched = 0
      @registry.each {|nick, saw|
        if nick.match(rx)
          m.reply seen(saw)
          num_matched += 1
          break if num_matched == @bot.config['seen.max_results']
        end
      }

      m.reply _("nope!") if num_matched.zero?
    end
  end

  def listen(m)
    return unless m.source
    # keep database up to date with who last said what
    now = Time.new
    case m
    when PrivMessage
      return if m.private?
      @bot.config['seen.ignore_patterns'].each { |regex|
        return if m.message =~ /#{regex}/
      }

      type = m.action? ? 'ACTION' : 'PUBLIC'
      store m, Saw.new(m.sourcenick.dup, now, type,
                       m.target.to_s, m.message.dup)
    when QuitMessage
      return if m.address?
      store m, Saw.new(m.sourcenick.dup, now, "QUIT",
                       nil, m.message.dup)
    when NickMessage
      return if m.address?
      store m, Saw.new(m.oldnick, now, "NICK", nil, m.newnick)
    when PartMessage
      return if m.address?
      store m, Saw.new(m.sourcenick.dup, Time.new, "PART",
                       m.target.to_s, m.message.dup)
    when JoinMessage
      return if m.address?
      store m, Saw.new(m.sourcenick.dup, Time.new, "JOIN",
                       m.target.to_s, m.message.dup)
    when TopicMessage
      return if m.address? or m.info_or_set == :info
      store m, Saw.new(m.sourcenick.dup, Time.new, "TOPIC",
                       m.target.to_s, m.message.dup)
    end
  end

  def seen(reg)
    saw = case reg
    when Struct::Saw
      reg # for backwards compatibility
    when Array
      reg.last
    end

    if reg.kind_of? Array
      before = reg.first
    end

    # TODO: a message should not be disclosed if:
    # - it was said in a channel that was/is invite-only, private or secret
    # - UNLESS the requester is also in the channel now, or the request is made
    #   in the channel?
    msg_privacy = false
    # TODO: a channel or it's topic should not be disclosed if:
    # - the channel was/is private or secret
    # - UNLESS the requester is also in the channel now, or the request is made
    #   in the channel?
    chan_privacy = false

    # What should be displayed for channel?
    where = chan_privacy ? _(CHANPRIV_CHAN) : saw.where

    formats = {
      :normal      => _(FORMAT_NORMAL),
      :with_before => _(FORMAT_WITH_BEFORE)
    }

    if before && [:PART, :QUIT].include?(saw.type.to_sym) &&
       [:PUBLIC, :ACTION].include?(before.type.to_sym)
      # TODO see chan_privacy
      prev_chan_privacy = false
      prev_where = prev_chan_privacy ? _(CHANPRIV_CHAN) : before.where
      did_before = case before.type.to_sym
      when :PUBLIC
        _(msg_privacy ? MSGPRIV_MSG_PUBLIC : MSG_PUBLIC)
      when :ACTION
        _(msg_privacy ? MSGPRIV_MSG_ACTION : MSG_ACTION)
      end % {
        :nick => saw.nick,
        :message => before.message,
        :where => prev_where
      }

      format = :with_before

      time_diff = saw.time - before.time
      if time_diff < 300
        time_before = _("a moment")
      elsif time_diff < 3600
        time_before = _("a while")
      else
        format = :normal
      end
    else
      format = :normal
    end

    nick = saw.nick
    ago = Time.new - saw.time

    if (ago.to_i == 0)
      time = _("just now")
    else
      time = _("%{time} ago") % { :time => Utils.secs_to_string(ago) }
    end

    doing = case saw.type.to_sym
    when :PUBLIC
      _(msg_privacy ? MSGPRIV_MSG_PUBLIC : MSG_PUBLIC)
    when :ACTION
      _(msg_privacy ? MSGPRIV_MSG_ACTION : MSG_ACTION)
    when :NICK
      _(MSG_NICK)
    when :PART
      if saw.message.empty?
        _(MSG_PART_EMPTY)
      else
        _(MSG_PART)
      end
    when :JOIN
      _(MSG_JOIN)
    when :QUIT
      _(MSG_QUIT)
    when :TOPIC
      _(chan_privacy ? CHANPRIV_MSG_TOPIC : MSG_TOPIC)
    end % { :message => saw.message, :where => where, :nick => saw.nick }

    case format
    when :normal
      formats[:normal] % {
        :nick  => saw.nick,
        :when  => time,
        :doing => doing,
      }
    when :with_before
      formats[:with_before] % {
        :nick  => saw.nick,
        :when  => time,
        :doing => doing,
        :time  => time_before,
        :did_before => did_before
      }
    end
  end

  def store(m, saw)
    # TODO: we need to store the channel state INVITE/SECRET/PRIVATE here, in
    # some symbolic form, so that we know the prior state of the channel when
    # it comes time to display.
    reg = @registry[saw.nick]

    if reg && reg.is_a?(Array)
      reg.shift if reg.size > 1
      reg.push(saw)
    else
      reg = [saw]
    end

    if m.is_a? NickMessage
      @registry[m.newnick] = reg
    end

    @registry[saw.nick] = reg
  end
end
plugin = SeenPlugin.new
plugin.register("seen")