Class: Tkellem::Backlog

Inherits:
Object
  • Object
show all
Includes:
EasyLogger
Defined in:
lib/tkellem/plugins/backlog.rb

Overview

This is implemented as a plugin – in theory, it could be switched out for a different backlog implementation. Right now, it’s always loaded though.

Defined Under Namespace

Classes: Device

Class Method Summary collapse

Instance Method Summary collapse

Methods included from EasyLogger

#failsafe, logger, logger=, trace, #trace, trace=

Constructor Details

#initialize(bouncer) ⇒ Backlog

Returns a new instance of Backlog.



81
82
83
84
85
86
87
# File 'lib/tkellem/plugins/backlog.rb', line 81

def initialize(bouncer)
  @bouncer = bouncer
  @network_user = bouncer.network_user
  @devices = {}
  @dir = Pathname.new(File.expand_path("~/.tkellem/logs/#{bouncer.user.username}/#{bouncer.network.name}"))
  @dir.mkpath()
end

Class Method Details

.client_msg(bouncer, client, msg) ⇒ Object



35
36
37
38
39
# File 'lib/tkellem/plugins/backlog.rb', line 35

def self.client_msg(bouncer, client, msg)
  instance = get_instance(bouncer)
  instance.client_msg(msg)
  true
end

.get_instance(bouncer) ⇒ Object



26
27
28
# File 'lib/tkellem/plugins/backlog.rb', line 26

def self.get_instance(bouncer)
  bouncer.data(self)[:instance] ||= self.new(bouncer)
end

.new_client_connected(bouncer, client) ⇒ Object



30
31
32
33
# File 'lib/tkellem/plugins/backlog.rb', line 30

def self.new_client_connected(bouncer, client)
  instance = get_instance(bouncer)
  instance.client_connected(client)
end

.server_msg(bouncer, msg) ⇒ Object



41
42
43
44
45
# File 'lib/tkellem/plugins/backlog.rb', line 41

def self.server_msg(bouncer, msg)
  instance = get_instance(bouncer)
  instance.server_msg(msg)
  true
end

Instance Method Details

#all_existing_ctxsObject



93
94
95
# File 'lib/tkellem/plugins/backlog.rb', line 93

def all_existing_ctxs
  @dir.entries.select { |e| e.extname == ".log" }.map { |e| e.basename(".log").to_s }
end

#client_connected(conn) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/tkellem/plugins/backlog.rb', line 118

def client_connected(conn)
  device = get_device(conn)
  behind = all_existing_ctxs.select do |ctx_name|
    eof = stream_size(ctx_name)
    # default to the end of file, rather than the beginning, for new devices
    # that way they don't get flooded the first time they connect
    device.pos(ctx_name, eof) < eof
  end
  if !behind.empty?
    # this device has missed messages, replay all the relevant backlogs
    send_connect_backlogs(conn, device, behind)
  end
end

#client_msg(msg) ⇒ Object



161
162
163
164
165
166
167
168
# File 'lib/tkellem/plugins/backlog.rb', line 161

def client_msg(msg)
  case msg.command
  when 'PRIVMSG'
    return if msg.ctcp? && !msg.action?
    ctx = msg.args.first
    write_msg(ctx, Time.now.strftime("%d-%m-%Y %H:%M:%S") + " > #{'* ' if msg.action?}#{msg.args.last}")
  end
end

#get_device(conn) ⇒ Object



114
115
116
# File 'lib/tkellem/plugins/backlog.rb', line 114

def get_device(conn)
  @devices[conn.device_name] ||= Device.new(@network_user, conn.device_name)
end

#get_stream(ctx, for_reading = false) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/tkellem/plugins/backlog.rb', line 97

def get_stream(ctx, for_reading = false)
  mode = for_reading ? 'rb:utf-8' : 'ab:utf-8'
  ctx = ctx.gsub(%r{[\./\\]}, '')
  path = stream_path(ctx)
  return nil if !path.file? && for_reading
  path.open(mode) do |stream|
    if !for_reading
      stream.seek(0, IO::SEEK_END)
    end
    yield stream
  end
end

#log_nameObject



141
142
143
# File 'lib/tkellem/plugins/backlog.rb', line 141

def log_name
  "backlog:#{@bouncer.log_name}"
end

#parse_line(line, ctx_name) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/tkellem/plugins/backlog.rb', line 235

def parse_line(line, ctx_name)
  timestamp = Time.parse(line[0, 19])
  case line[20..-1]
  when %r{^> (\* )?(.+)$}
    msg = IrcMessage.new(nil, 'PRIVMSG', [ctx_name, $2])
    if $1 == '* '
      msg.ctcp = 'ACTION'
    end
    return timestamp, msg
  when %r{^< (\* )?([^:]+): (.+)$}
    msg = IrcMessage.new($2, 'PRIVMSG', [ctx_name, $3])
    if $1 == '* '
      msg.ctcp = 'ACTION'
    end
    return timestamp, msg
  else
    nil
  end
end

#send_backlog(conn, ctx_name, stream) ⇒ Object



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
233
# File 'lib/tkellem/plugins/backlog.rb', line 205

def send_backlog(conn, ctx_name, stream)
  while line = stream.gets
    timestamp, msg = parse_line(line, ctx_name)
    next unless msg
    privmsg = msg.args.first[0] != '#'[0]
    if msg.prefix
      # to this user
      if privmsg
        msg.args[0] = @bouncer.nick
      else
        # do nothing, it's good to send
      end
    else
      # from this user
      if privmsg
        # a one-on-one chat -- every client i've seen doesn't know how to
        # display messages from themselves here, so we fake it by just
        # adding an arrow and pretending the other user said it. shame.
        msg.prefix = msg.args.first
        msg.args[0] = @bouncer.nick
        msg.args[-1] = "-> #{msg.args.last}"
      else
        # it's a room, we can just replay
        msg.prefix = @bouncer.nick
      end
    end
    conn.send_msg(msg.with_timestamp(timestamp))
  end
end

#send_backlog_since(conn, start_time, contexts) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/tkellem/plugins/backlog.rb', line 188

def send_backlog_since(conn, start_time, contexts)
  debug "scanning for backlog from #{start_time.inspect}"
  contexts.each do |ctx_name|
    get_stream(ctx_name, true) do |stream|
      last_line_len = 0
      BackwardsFileReader.scan(stream) do |line|
        # remember this last line length so we can scan past it
        last_line_len = line.length
        timestamp = Time.parse(line[0,19]) rescue nil
        !timestamp || timestamp >= start_time
      end
      stream.seek(last_line_len, IO::SEEK_CUR)
      send_backlog(conn, ctx_name, stream)
    end
  end
end

#send_connect_backlogs(conn, device, contexts) ⇒ Object



177
178
179
180
181
182
183
184
185
186
# File 'lib/tkellem/plugins/backlog.rb', line 177

def send_connect_backlogs(conn, device, contexts)
  contexts.each do |ctx_name|
    start_pos = device.pos(ctx_name)
    get_stream(ctx_name, true) do |stream|
      stream.seek(start_pos)
      send_backlog(conn, ctx_name, stream)
      device.update_pos(ctx_name, stream.pos)
    end
  end
end

#server_msg(msg) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/tkellem/plugins/backlog.rb', line 145

def server_msg(msg)
  case msg.command
  when /3\d\d/, 'JOIN', 'PART'
    # transient messages
    return
  when 'PRIVMSG'
    return if msg.ctcp? && !msg.action?
    ctx = msg.args.first
    if ctx == @bouncer.nick
      # incoming pm, fake ctx to be the sender's nick
      ctx = msg.prefix.split(/[!~@]/, 2).first
    end
    write_msg(ctx, Time.now.strftime("%d-%m-%Y %H:%M:%S") + " < #{'* ' if msg.action?}#{msg.prefix}: #{msg.args.last}")
  end
end

#stream_path(ctx) ⇒ Object



89
90
91
# File 'lib/tkellem/plugins/backlog.rb', line 89

def stream_path(ctx)
  @dir + "#{ctx}.log"
end

#stream_size(ctx) ⇒ Object



110
111
112
# File 'lib/tkellem/plugins/backlog.rb', line 110

def stream_size(ctx)
  stream_path(ctx).size
end

#update_pos(ctx_name, pos) ⇒ Object



132
133
134
135
136
137
138
139
# File 'lib/tkellem/plugins/backlog.rb', line 132

def update_pos(ctx_name, pos)
  # don't just iterate @devices here, because that may contain devices that
  # have since been disconnected
  @bouncer.active_conns.each do |conn|
    device = get_device(conn)
    device.update_pos(ctx_name, pos)
  end
end

#write_msg(ctx, processed_msg) ⇒ Object



170
171
172
173
174
175
# File 'lib/tkellem/plugins/backlog.rb', line 170

def write_msg(ctx, processed_msg)
  get_stream(ctx) do |stream|
    stream.puts(processed_msg)
    update_pos(ctx, stream.pos)
  end
end