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.



96
97
98
99
100
101
102
# File 'lib/tkellem/plugins/backlog.rb', line 96

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



37
38
39
40
41
# File 'lib/tkellem/plugins/backlog.rb', line 37

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

.geoip(conn) ⇒ Object

IMPL



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/tkellem/plugins/backlog.rb', line 51

def self.geoip(conn)
  if !defined?(@geoip)
    begin
      @geoip = GeoIP.new('/usr/share/GeoIP/GeoIPCity.dat')
    rescue Errno::ENOENT
      @geoip = nil
    end
  end
  geoip_info = @geoip && @geoip.country(Socket.unpack_sockaddr_in(conn.get_peername).last)
  tz = geoip_info.respond_to?(:timezone) && geoip_info.timezone && ActiveSupport::TimeZone[geoip_info.timezone]
  country = geoip_info.respond_to?(:country_code2) && geoip_info.country_code2
  [tz, country]
end

.get_instance(bouncer) ⇒ Object



28
29
30
# File 'lib/tkellem/plugins/backlog.rb', line 28

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

.new_client_connected(bouncer, client) ⇒ Object



32
33
34
35
# File 'lib/tkellem/plugins/backlog.rb', line 32

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

.parse_line(line, ctx_name) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/tkellem/plugins/backlog.rb', line 238

def self.parse_line(line, ctx_name)
  timestamp = Time.parse(line[0, 20])
  # older log lines have a timestamp that is one character shorter, which is
  # why the regular expressions below optionally allow a space in front of
  # the directional < >
  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

.server_msg(bouncer, msg) ⇒ Object



43
44
45
46
47
# File 'lib/tkellem/plugins/backlog.rb', line 43

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

Instance Method Details

#all_existing_ctxsObject



108
109
110
# File 'lib/tkellem/plugins/backlog.rb', line 108

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

#client_connected(conn) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/tkellem/plugins/backlog.rb', line 133

def client_connected(conn)
  device = get_device(conn)
  tz, country = self.class.geoip(conn)
  device.time_zone = tz || device.time_zone
  device.country = country || device.country
  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



183
184
185
186
187
188
189
190
# File 'lib/tkellem/plugins/backlog.rb', line 183

def client_msg(msg)
  case msg.command
  when 'PRIVMSG'
    return if msg.ctcp? && !msg.action?
    ctx = msg.args.first
    write_msg(ctx, "#{now_timestamp} > #{'* ' if msg.action?}#{msg.args.last}")
  end
end

#get_device(conn) ⇒ Object



129
130
131
# File 'lib/tkellem/plugins/backlog.rb', line 129

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

#get_stream(ctx, for_reading = false) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/tkellem/plugins/backlog.rb', line 112

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



159
160
161
# File 'lib/tkellem/plugins/backlog.rb', line 159

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

#now_timestampObject



163
164
165
# File 'lib/tkellem/plugins/backlog.rb', line 163

def now_timestamp
  Time.now.utc.iso8601
end

#send_backlog(conn, ctx_name, stream, time_zone) ⇒ Object



227
228
229
230
231
232
233
234
235
236
# File 'lib/tkellem/plugins/backlog.rb', line 227

def send_backlog(conn, ctx_name, stream, time_zone)
  while line = stream.gets
    timestamp, msg = self.class.parse_line(line, ctx_name)
    timestamp = timestamp.in_time_zone(time_zone) if timestamp && time_zone
    timestamp = timestamp.localtime if timestamp && !time_zone
    next unless msg
    msg.readdress_to(@bouncer.nick)
    conn.send_msg(msg.with_timestamp(timestamp))
  end
end

#send_backlog_since(conn, start_time, contexts) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/tkellem/plugins/backlog.rb', line 210

def send_backlog_since(conn, start_time, contexts)
  debug "scanning for backlog from #{start_time.iso8601}"
  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,20]) rescue nil
        !timestamp || timestamp >= start_time
      end
      stream.seek(last_line_len, IO::SEEK_CUR)
      send_backlog(conn, ctx_name, stream, get_device(conn).time_zone)
    end
  end
end

#send_connect_backlogs(conn, device, contexts) ⇒ Object



199
200
201
202
203
204
205
206
207
208
# File 'lib/tkellem/plugins/backlog.rb', line 199

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.time_zone)
      device.update_pos(ctx_name, stream.pos)
    end
  end
end

#server_msg(msg) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/tkellem/plugins/backlog.rb', line 167

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, "#{now_timestamp} < #{'* ' if msg.action?}#{msg.prefix}: #{msg.args.last}")
  end
end

#stream_path(ctx) ⇒ Object



104
105
106
# File 'lib/tkellem/plugins/backlog.rb', line 104

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

#stream_size(ctx) ⇒ Object



125
126
127
# File 'lib/tkellem/plugins/backlog.rb', line 125

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

#update_pos(ctx_name, pos) ⇒ Object



150
151
152
153
154
155
156
157
# File 'lib/tkellem/plugins/backlog.rb', line 150

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



192
193
194
195
196
197
# File 'lib/tkellem/plugins/backlog.rb', line 192

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