pidgin2adium 3.0.1 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +22 -0
- data/.gitignore +7 -0
- data/{History.txt → ChangeLog} +11 -0
- data/Gemfile +1 -9
- data/README.rdoc +38 -39
- data/Rakefile +4 -2
- data/VERSION +1 -1
- data/bin/pidgin2adium +63 -54
- data/ext/balance_tags_c/balance_tags_c.c +161 -161
- data/lib/pidgin2adium.rb +97 -97
- data/lib/pidgin2adium/balance_tags.rb +2 -2
- data/lib/pidgin2adium/basic_parser.rb +412 -0
- data/lib/pidgin2adium/html_log_parser.rb +125 -0
- data/lib/pidgin2adium/log_converter.rb +12 -13
- data/lib/pidgin2adium/log_file.rb +1 -1
- data/lib/pidgin2adium/log_parser.rb +3 -618
- data/lib/pidgin2adium/message.rb +97 -0
- data/lib/pidgin2adium/text_log_parser.rb +39 -0
- data/pidgin2adium.gemspec +31 -9
- data/spec/balance_tags_c_extn_spec.rb +47 -0
- data/spec/basic_parser_spec.rb +217 -0
- data/spec/html_log_parser_spec.rb +150 -0
- data/spec/log_converter_spec.rb +48 -0
- data/spec/log_file_spec.rb +168 -0
- data/spec/logfiles/2006-12-21.223606.txt +3 -0
- data/spec/logfiles/2008-01-15.071445-0500PST.htm +5 -0
- data/spec/logfiles/2008-01-15.071445-0500PST.html +5 -0
- data/spec/pidgin2adium_spec.rb +248 -3
- data/spec/spec_helper.rb +69 -16
- data/spec/test-output/README.md +1 -0
- data/spec/test-output/html_log_output.xml +6 -0
- data/spec/test-output/text_log_output.xml +4 -0
- data/spec/text_log_parser_spec.rb +42 -0
- data/tasks/extconf/balance_tags_c.rake +5 -1
- metadata +40 -26
- data/bin/pidgin2adium_profiler +0 -1
- data/tasks/build_profiler.rake +0 -49
data/lib/pidgin2adium.rb
CHANGED
@@ -3,8 +3,6 @@
|
|
3
3
|
# A ruby program to convert Pidgin log files to Adium log files, then place
|
4
4
|
# them in the Adium log directory.
|
5
5
|
|
6
|
-
$:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
7
|
-
|
8
6
|
require 'fileutils'
|
9
7
|
require 'pidgin2adium/log_parser'
|
10
8
|
|
@@ -14,120 +12,122 @@ module Pidgin2Adium
|
|
14
12
|
ADIUM_LOG_DIR = File.expand_path('~/Library/Application Support/Adium 2.0/Users/Default/Logs/') << '/'
|
15
13
|
# These files/directories show up in Dir.entries()
|
16
14
|
BAD_DIRS = %w{. .. .DS_Store Thumbs.db .system}
|
17
|
-
VERSION = "3.0
|
15
|
+
VERSION = "3.1.0"
|
18
16
|
# For displaying after we finish converting
|
19
17
|
@@oops_messages = []
|
20
18
|
@@error_messages = []
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
def log_msg(str) #:nodoc:
|
21
|
+
puts str.to_s
|
22
|
+
end
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
def oops(str) #:nodoc:
|
25
|
+
@@oops_messages << str
|
26
|
+
warn("Oops: #{str}")
|
27
|
+
end
|
30
28
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
def error(str) #:nodoc:
|
30
|
+
@@error_messages << str
|
31
|
+
warn("Error: #{str}")
|
32
|
+
end
|
35
33
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
#######################
|
35
|
+
#So that we can use log_msg when calling delete_search_indexes() by itself
|
36
|
+
module_function :log_msg, :oops, :error
|
37
|
+
#######################
|
40
38
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
39
|
+
# Parses the provided log.
|
40
|
+
# Returns a LogFile instance or false if an error occurred.
|
41
|
+
def parse(logfile_path, my_aliases)
|
42
|
+
logfile_path = File.expand_path(logfile_path)
|
43
|
+
ext = File.extname(logfile_path).sub('.', '').downcase
|
44
|
+
|
45
|
+
if(ext == "html" || ext == "htm")
|
46
|
+
parser = HtmlLogParser.new(logfile_path, my_aliases)
|
47
|
+
elsif(ext == "txt")
|
48
|
+
parser = TextLogParser.new(logfile_path, my_aliases)
|
49
|
+
else
|
50
|
+
error("Doing nothing, logfile is not a text or html file. Path: #{logfile_path}.")
|
51
|
+
return false
|
52
|
+
end
|
46
53
|
|
47
|
-
|
48
|
-
parser = HtmlLogParser.new(logfile_path, my_aliases)
|
49
|
-
elsif(ext == "txt")
|
50
|
-
parser = TextLogParser.new(logfile_path, my_aliases)
|
51
|
-
else
|
52
|
-
error("Doing nothing, logfile is not a text or html file. Path: #{logfile_path}.")
|
53
|
-
return false
|
54
|
+
return parser.parse()
|
54
55
|
end
|
55
56
|
|
56
|
-
|
57
|
-
|
57
|
+
# Parses the provided log and writes out the log in Adium format.
|
58
|
+
# Returns:
|
59
|
+
# * true if it successfully converted and wrote out the log,
|
60
|
+
# * false if an error occurred, or
|
61
|
+
# * Pidgin2Adium::FILE_EXISTS if file already exists AND
|
62
|
+
# opts[:overwrite] = false.
|
63
|
+
#
|
64
|
+
# You can add options using the _opts_ hash, which can have the following
|
65
|
+
# keys, all of which are optional:
|
66
|
+
# * *overwrite*: If true, then overwrite even if log is found.
|
67
|
+
# Defaults to false.
|
68
|
+
# * *output_dir*: The top-level dir to put the logs in.
|
69
|
+
# Logs under output_dir are still each in their own folders, etc.
|
70
|
+
# Defaults to Pidgin2Adium::ADIUM_LOG_DIR
|
71
|
+
def parse_and_generate(logfile_path, my_aliases, opts = {})
|
72
|
+
opts = {} unless opts.is_a?(Hash)
|
73
|
+
overwrite = !!opts[:overwrite]
|
74
|
+
if opts.key?(:output_dir)
|
75
|
+
output_dir = opts[:output_dir]
|
76
|
+
else
|
77
|
+
output_dir = ADIUM_LOG_DIR
|
78
|
+
end
|
58
79
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
# * *output_dir*: The top-level dir to put the logs in.
|
69
|
-
# Logs under output_dir are still each in their own folders, etc.
|
70
|
-
# Defaults to Pidgin2Adium::ADIUM_LOG_DIR
|
71
|
-
def parse_and_generate(logfile_path, my_aliases, opts = {})
|
72
|
-
opts = {} unless opts.is_a?(Hash)
|
73
|
-
overwrite = !!opts[:overwrite]
|
74
|
-
if opts.key?(:output_dir)
|
75
|
-
output_dir = opts[:output_dir]
|
76
|
-
else
|
77
|
-
output_dir = ADIUM_LOG_DIR
|
78
|
-
end
|
80
|
+
unless File.directory?(output_dir)
|
81
|
+
puts "Output log directory (#{output_dir}) does not exist or is not a directory."
|
82
|
+
begin
|
83
|
+
FileUtils.mkdir_p(output_dir)
|
84
|
+
rescue Errno::EACCES
|
85
|
+
puts "Permission denied, could not create output directory (#{output_dir})"
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
end
|
79
89
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
puts "Permission denied, could not create output directory (#{output_dir})"
|
90
|
+
logfile_obj = parse(logfile_path, my_aliases)
|
91
|
+
return false if logfile_obj == false
|
92
|
+
dest_file_path = logfile_obj.write_out(overwrite, output_dir)
|
93
|
+
if dest_file_path == false
|
94
|
+
error("Successfully parsed file, but failed to write it out. Path: #{logfile_path}.")
|
86
95
|
return false
|
96
|
+
elsif dest_file_path == FILE_EXISTS
|
97
|
+
log_msg("File already exists.")
|
98
|
+
return FILE_EXISTS
|
99
|
+
else
|
100
|
+
log_msg("Output to: #{dest_file_path}")
|
101
|
+
return true
|
87
102
|
end
|
88
103
|
end
|
89
104
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
# files. LogFile.write_out intentionally does _not_ run it in order to
|
112
|
-
# allow for batch-processing of files. Thus, you will probably want to run
|
113
|
-
# Pidgin2Adium.delete_search_indexes after running LogFile.write_out in
|
114
|
-
# your own scripts.
|
115
|
-
def delete_search_indexes()
|
116
|
-
log_msg "Deleting log search indexes in order to force re-indexing of imported logs..."
|
117
|
-
dirty_file = File.expand_path("~/Library/Caches/Adium/Default/DirtyLogs.plist")
|
118
|
-
log_index_file = File.expand_path("~/Library/Caches/Adium/Default/Logs.index")
|
119
|
-
[dirty_file, log_index_file].each do |f|
|
120
|
-
if File.exist?(f)
|
121
|
-
if File.writable?(f)
|
122
|
-
File.delete(f)
|
123
|
-
else
|
124
|
-
error("File exists but is not writable. Please delete it yourself: #{f}")
|
105
|
+
# Newly-converted logs are viewable in the Adium Chat Transcript
|
106
|
+
# Viewer, but are not indexed, so a search of the logs doesn't give
|
107
|
+
# results from the converted logs. To fix this, we delete the cached log
|
108
|
+
# indexes, which forces Adium to re-index.
|
109
|
+
#
|
110
|
+
# Note: This function is run by LogConverter after converting all of its
|
111
|
+
# files. LogFile.write_out intentionally does _not_ run it in order to
|
112
|
+
# allow for batch-processing of files. Thus, you will probably want to run
|
113
|
+
# Pidgin2Adium.delete_search_indexes after running LogFile.write_out in
|
114
|
+
# your own scripts.
|
115
|
+
def delete_search_indexes()
|
116
|
+
log_msg "Deleting log search indexes in order to force re-indexing of imported logs..."
|
117
|
+
dirty_file = File.expand_path("~/Library/Caches/Adium/Default/DirtyLogs.plist")
|
118
|
+
log_index_file = File.expand_path("~/Library/Caches/Adium/Default/Logs.index")
|
119
|
+
[dirty_file, log_index_file].each do |f|
|
120
|
+
if File.exist?(f)
|
121
|
+
if File.writable?(f)
|
122
|
+
File.delete(f)
|
123
|
+
else
|
124
|
+
error("File exists but is not writable. Please delete it yourself: #{f}")
|
125
|
+
end
|
125
126
|
end
|
126
127
|
end
|
128
|
+
log_msg "...done."
|
129
|
+
log_msg "When you next start the Adium Chat Transcript Viewer, it will re-index the logs, which may take a while."
|
127
130
|
end
|
128
|
-
log_msg "...done."
|
129
|
-
log_msg "When you next start the Adium Chat Transcript Viewer, it will re-index the logs, which may take a while."
|
130
|
-
end
|
131
131
|
|
132
|
-
|
132
|
+
module_function :parse, :parse_and_generate, :delete_search_indexes
|
133
133
|
end
|
@@ -20,8 +20,8 @@ module Pidgin2Adium
|
|
20
20
|
# 2: attributes
|
21
21
|
tag_regex = /<(\/?\w*)\s*([^>]*)>/
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
# WP bug fix for comments - in case you REALLY meant to type '< !--'
|
24
|
+
text.gsub!('< !--', '< !--')
|
25
25
|
|
26
26
|
# WP bug fix for LOVE <3 (and other situations with '<' before a number)
|
27
27
|
text.gsub!(/<([0-9]{1})/, '<\1')
|
@@ -0,0 +1,412 @@
|
|
1
|
+
# Contains the BasicParser class.
|
2
|
+
# For its subclasses, see html_log_parser.rb and text_log_parser.rb.
|
3
|
+
# The subclasses parse the file passed into it and return a LogFile object.
|
4
|
+
# The BasicParser class just provides some common functionality.
|
5
|
+
#
|
6
|
+
# Please use Pidgin2Adium.parse or Pidgin2Adium.parse_and_generate instead of
|
7
|
+
# using these classes directly.
|
8
|
+
|
9
|
+
require 'date'
|
10
|
+
require 'time'
|
11
|
+
|
12
|
+
require 'pidgin2adium/log_file'
|
13
|
+
require 'pidgin2adium/message'
|
14
|
+
|
15
|
+
module Pidgin2Adium
|
16
|
+
# Empty class. Raise'd by LogParser if the first line of a log is not
|
17
|
+
# parseable.
|
18
|
+
class InvalidFirstLineError < StandardError; end
|
19
|
+
|
20
|
+
# BasicParser is a base class. Its subclasses are TextLogParser and
|
21
|
+
# HtmlLogParser.
|
22
|
+
#
|
23
|
+
# Please use Pidgin2Adium.parse or Pidgin2Adium.parse_and_generate instead of
|
24
|
+
# using this class directly.
|
25
|
+
class BasicParser
|
26
|
+
include Pidgin2Adium
|
27
|
+
def initialize(src_path, user_aliases)
|
28
|
+
@src_path = src_path
|
29
|
+
# Whitespace is removed for easy matching later on.
|
30
|
+
@user_aliases = user_aliases.split(',').map!{|x| x.downcase.gsub(/\s+/,'') }.uniq
|
31
|
+
# @user_alias is set each time get_sender_by_alias is called. It is a non-normalized
|
32
|
+
# alias.
|
33
|
+
# Set an initial value just in case the first message doesn't give
|
34
|
+
# us an alias.
|
35
|
+
@user_alias = user_aliases.split(',')[0]
|
36
|
+
|
37
|
+
@tz_offset = get_time_zone_offset()
|
38
|
+
|
39
|
+
@log_file_is_valid = true
|
40
|
+
begin
|
41
|
+
file = File.new(@src_path, 'r')
|
42
|
+
@first_line = file.readline
|
43
|
+
@file_content = file.read
|
44
|
+
file.close
|
45
|
+
rescue Errno::ENOENT
|
46
|
+
oops("#{@src_path} doesn't exist! Continuing...")
|
47
|
+
@log_file_is_valid = false
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Time regexes must be set before pre_parse().
|
52
|
+
# "4/18/2007 11:02:00 AM" => %w{4, 18, 2007, 11, 02, 00, AM}
|
53
|
+
# ONLY used (if at all) in first line of chat ("Conversation with...at...")
|
54
|
+
@time_regex_first_line = %r{^(\d{1,2})/(\d{1,2})/(\d{4}) (\d{1,2}):(\d{2}):(\d{2}) ([AP]M)$}
|
55
|
+
# "2007-04-17 12:33:13" => %w{2007, 04, 17, 12, 33, 13}
|
56
|
+
@time_regex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/
|
57
|
+
|
58
|
+
begin
|
59
|
+
@service,
|
60
|
+
@user_SN,
|
61
|
+
@partner_SN,
|
62
|
+
# @basic_time_info is for files that only have the full
|
63
|
+
# timestamp at the top; we can use it to fill in the minimal
|
64
|
+
# per-line timestamps. It is a hash with 3 keys:
|
65
|
+
# * :year
|
66
|
+
# * :mon
|
67
|
+
# * :mday (day of month)
|
68
|
+
# You should be able to fill everything else in. If you can't,
|
69
|
+
# something's wrong.
|
70
|
+
@basic_time_info,
|
71
|
+
# When the chat started, in Adium's format
|
72
|
+
@adium_chat_time_start = pre_parse()
|
73
|
+
rescue InvalidFirstLineError
|
74
|
+
# The first line isn't parseable
|
75
|
+
@log_file_is_valid = false
|
76
|
+
error("Failed to parse, invalid first line: #{@src_path}")
|
77
|
+
return # stop processing
|
78
|
+
end
|
79
|
+
|
80
|
+
# @status_map, @lib_purple_events, and @events are used in
|
81
|
+
# create_status_or_event_msg
|
82
|
+
@status_map = {
|
83
|
+
/(.+) logged in\.$/ => 'online',
|
84
|
+
/(.+) logged out\.$/ => 'offline',
|
85
|
+
/(.+) has signed on\.$/ => 'online',
|
86
|
+
/(.+) has signed off\.$/ => 'offline',
|
87
|
+
/(.+) has gone away\.$/ => 'away',
|
88
|
+
/(.+) is no longer away\.$/ => 'available',
|
89
|
+
/(.+) has become idle\.$/ => 'idle',
|
90
|
+
/(.+) is no longer idle\.$/ => 'available'
|
91
|
+
}
|
92
|
+
|
93
|
+
# lib_purple_events are all of event_type libPurple
|
94
|
+
@lib_purple_events = [
|
95
|
+
# file transfer
|
96
|
+
/Starting transfer of .+ from (.+)/,
|
97
|
+
/^Offering to send .+ to (.+)$/,
|
98
|
+
/(.+) is offering to send file/,
|
99
|
+
/^Transfer of file .+ complete$/,
|
100
|
+
/Error reading|writing|accessing .+: .+/,
|
101
|
+
/You cancell?ed the transfer of/,
|
102
|
+
/File transfer cancelled/,
|
103
|
+
/(.+?) cancell?ed the transfer of/,
|
104
|
+
/(.+?) cancelled the file transfer/,
|
105
|
+
# Direct IM - actual (dis)connect events are their own types
|
106
|
+
/^Attempting to connect to (.+) at .+ for Direct IM\./,
|
107
|
+
/^Asking (.+) to connect to us at .+ for Direct IM\./,
|
108
|
+
/^Attempting to connect via proxy server\.$/,
|
109
|
+
/^Direct IM with (.+) failed/,
|
110
|
+
# encryption
|
111
|
+
/Received message encrypted with wrong key/,
|
112
|
+
/^Requesting key\.\.\.$/,
|
113
|
+
/^Outgoing message lost\.$/,
|
114
|
+
/^Conflicting Key Received!$/,
|
115
|
+
/^Error in decryption- asking for resend\.\.\.$/,
|
116
|
+
/^Making new key pair\.\.\.$/,
|
117
|
+
# sending errors
|
118
|
+
/^Last outgoing message not received properly- resetting$/,
|
119
|
+
/Resending\.\.\./,
|
120
|
+
# connection errors
|
121
|
+
/Lost connection with the remote user:.+/,
|
122
|
+
# chats
|
123
|
+
/^.+ entered the room\.$/,
|
124
|
+
/^.+ left the room\.$/
|
125
|
+
]
|
126
|
+
|
127
|
+
# non-libpurple events
|
128
|
+
# Each key maps to an event_type string. The keys will be matched against a line of chat
|
129
|
+
# and the partner's alias will be in regex group 1, IF the alias is matched.
|
130
|
+
@event_map = {
|
131
|
+
# .+ is not an alias, it's a proxy server so no grouping
|
132
|
+
/^Attempting to connect to .+\.$/ => 'direct-im-connect',
|
133
|
+
# NB: pidgin doesn't track when Direct IM is disconnected, AFAIK
|
134
|
+
/^Direct IM established$/ => 'directIMConnected',
|
135
|
+
/Unable to send message/ => 'chat-error',
|
136
|
+
/You missed .+ messages from (.+) because they were too large/ => 'chat-error',
|
137
|
+
/User information not available/ => 'chat-error'
|
138
|
+
}
|
139
|
+
|
140
|
+
@ignore_events = [
|
141
|
+
# Adium ignores SN/alias changes.
|
142
|
+
/^.+? is now known as .+?\.<br\/?>$/
|
143
|
+
]
|
144
|
+
end
|
145
|
+
|
146
|
+
# This method returns a LogFile instance, or false if an error occurred.
|
147
|
+
def parse
|
148
|
+
# Prevent parse from being called directly from BasicParser, since
|
149
|
+
# it uses subclassing magic.
|
150
|
+
if self.class == BasicParser
|
151
|
+
oops("Please don't call parse directly from BasicParser. Use a subclass :)")
|
152
|
+
return false
|
153
|
+
end
|
154
|
+
return false unless @log_file_is_valid
|
155
|
+
@file_content = cleanup(@file_content).split("\n")
|
156
|
+
|
157
|
+
@file_content.map! do |line|
|
158
|
+
# "next" returns nil which is removed by compact
|
159
|
+
next if line =~ /^\s+$/
|
160
|
+
if line =~ @line_regex
|
161
|
+
create_msg($~.captures)
|
162
|
+
elsif line =~ @line_regex_status
|
163
|
+
msg = create_status_or_event_msg($~.captures)
|
164
|
+
# Error occurred while parsing
|
165
|
+
return false if msg == false
|
166
|
+
else
|
167
|
+
error "Could not parse line:"
|
168
|
+
p line
|
169
|
+
return false
|
170
|
+
end
|
171
|
+
end
|
172
|
+
@file_content.compact!
|
173
|
+
return LogFile.new(@file_content, @service, @user_SN, @partner_SN, @adium_chat_time_start)
|
174
|
+
end
|
175
|
+
|
176
|
+
def get_time_zone_offset()
|
177
|
+
# We must have a tz_offset or else the Adium Chat Log viewer
|
178
|
+
# doesn't read the date correctly and then:
|
179
|
+
# 1) the log has an empty start date column in the viewer
|
180
|
+
# 2) The timestamps are all the same for the whole log
|
181
|
+
tz_match = /([-\+]\d+)[A-Z]{3}\.(?:txt|htm|html)/.match(@src_path)
|
182
|
+
if tz_match and tz_match[1]
|
183
|
+
tz_offset = tz_match[1]
|
184
|
+
else
|
185
|
+
# "-0500" (3d rather than 2d to allow for "+")
|
186
|
+
tz_offset = sprintf('%+03d00', Time.zone_offset(Time.now.zone) / 3600)
|
187
|
+
end
|
188
|
+
return tz_offset
|
189
|
+
end
|
190
|
+
|
191
|
+
def try_to_parse_first_line_time(first_line_time)
|
192
|
+
formats = [
|
193
|
+
"%m/%d/%Y %I:%M:%S %P", # 01/22/2008 03:01:45 PM
|
194
|
+
"%Y-%m-%d %H:%M:%S" # 2008-01-22 23:08:24
|
195
|
+
]
|
196
|
+
parsed = nil
|
197
|
+
formats.each do |format|
|
198
|
+
begin
|
199
|
+
parsed = Time.strptime(first_line_time, format)
|
200
|
+
break
|
201
|
+
rescue ArgumentError
|
202
|
+
end
|
203
|
+
end
|
204
|
+
parsed
|
205
|
+
end
|
206
|
+
|
207
|
+
def try_to_parse_time(time)
|
208
|
+
formats = [
|
209
|
+
"%Y/%m/%d %H:%M:%S", # 2008/01/22 04:01:45
|
210
|
+
"%Y-%m-%d %H:%M:%S" # 2008-01-22 04:01:45
|
211
|
+
]
|
212
|
+
parsed = nil
|
213
|
+
formats.each do |format|
|
214
|
+
begin
|
215
|
+
parsed = Time.strptime(time, format)
|
216
|
+
break
|
217
|
+
rescue ArgumentError
|
218
|
+
end
|
219
|
+
end
|
220
|
+
parsed
|
221
|
+
end
|
222
|
+
|
223
|
+
def try_to_parse_minimal_time(minimal_time)
|
224
|
+
# 04:01:45 AM
|
225
|
+
minimal_format_with_ampm = "%I:%M:%S %P"
|
226
|
+
# 23:01:45
|
227
|
+
minimal_format_without_ampm = "%H:%M:%S"
|
228
|
+
|
229
|
+
time_hash = nil
|
230
|
+
|
231
|
+
# Use Date._strptime to allow filling in the blanks on minimal
|
232
|
+
# timestamps
|
233
|
+
if minimal_time =~ /[AP]M$/
|
234
|
+
time_hash = Date._strptime(minimal_time, minimal_format_with_ampm)
|
235
|
+
else
|
236
|
+
time_hash = Date._strptime(minimal_time, minimal_format_without_ampm)
|
237
|
+
end
|
238
|
+
if time_hash.nil?
|
239
|
+
# Date._strptime returns nil on failure
|
240
|
+
return nil
|
241
|
+
end
|
242
|
+
# Fill in the blanks
|
243
|
+
time_hash[:year] = @basic_time_info[:year]
|
244
|
+
time_hash[:mon] = @basic_time_info[:mon]
|
245
|
+
time_hash[:mday] = @basic_time_info[:mday]
|
246
|
+
new_time = Time.local(time_hash[:year],
|
247
|
+
time_hash[:mon],
|
248
|
+
time_hash[:mday],
|
249
|
+
time_hash[:hour],
|
250
|
+
time_hash[:min],
|
251
|
+
time_hash[:sec])
|
252
|
+
new_time
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
#--
|
257
|
+
# Adium time format: YYYY-MM-DD\THH:MM:SS[+-]TZ_HRS like:
|
258
|
+
# 2008-10-05T22:26:20-0800
|
259
|
+
# HOWEVER:
|
260
|
+
# If it's the first line, then return it like this (note periods):
|
261
|
+
# 2008-10-05T22.26.20-0800
|
262
|
+
# because it will be used in the filename.
|
263
|
+
#++
|
264
|
+
# Converts a pidgin datestamp to an Adium one.
|
265
|
+
# Returns a string representation of _time_ or
|
266
|
+
# nil if it couldn't parse the provided _time_.
|
267
|
+
def create_adium_time(time, is_first_line = false)
|
268
|
+
return nil if time.nil?
|
269
|
+
if is_first_line
|
270
|
+
new_time = try_to_parse_first_line_time(time)
|
271
|
+
else
|
272
|
+
new_time = try_to_parse_time(time)
|
273
|
+
if new_time.nil?
|
274
|
+
new_time = try_to_parse_minimal_time(time)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
return nil if new_time.nil?
|
279
|
+
|
280
|
+
if is_first_line
|
281
|
+
adium_time = new_time.strftime("%Y-%m-%dT%H.%M.%S#{@tz_offset}")
|
282
|
+
else
|
283
|
+
adium_time = new_time.strftime("%Y-%m-%dT%H:%M:%S#{@tz_offset}")
|
284
|
+
end
|
285
|
+
adium_time
|
286
|
+
end
|
287
|
+
|
288
|
+
# Extract required data from the file. Run by parse.
|
289
|
+
def pre_parse
|
290
|
+
# Deal with first line.
|
291
|
+
|
292
|
+
# the first line is special. It tells us (in order of regex groups):
|
293
|
+
# 1) who we're talking to
|
294
|
+
# 2) what time/date
|
295
|
+
# 3) what SN we used
|
296
|
+
# 4) what protocol (AIM, icq, jabber...)
|
297
|
+
first_line_match = /Conversation with (.+?) at (.+?) on (.+?) \((.+?)\)/.match(@first_line)
|
298
|
+
if first_line_match.nil?
|
299
|
+
raise InvalidFirstLineError
|
300
|
+
else
|
301
|
+
service = first_line_match[4]
|
302
|
+
# @user_SN is normalized to avoid "AIM.name" and "AIM.na me" folders
|
303
|
+
user_SN = first_line_match[3].downcase.tr(' ', '')
|
304
|
+
partner_SN = first_line_match[1]
|
305
|
+
pidgin_chat_time_start = first_line_match[2]
|
306
|
+
basic_time_info = case pidgin_chat_time_start
|
307
|
+
when @time_regex
|
308
|
+
{:year => $1.to_i,
|
309
|
+
:mon => $2.to_i,
|
310
|
+
:mday => $3.to_i}
|
311
|
+
when @time_regex_first_line
|
312
|
+
{:year => $3.to_i,
|
313
|
+
:mon => $1.to_i,
|
314
|
+
:mday => $2.to_i}
|
315
|
+
end
|
316
|
+
adium_chat_time_start = create_adium_time(pidgin_chat_time_start, true)
|
317
|
+
return [service,
|
318
|
+
user_SN,
|
319
|
+
partner_SN,
|
320
|
+
basic_time_info,
|
321
|
+
adium_chat_time_start]
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def get_sender_by_alias(alias_name)
|
326
|
+
no_action = alias_name.sub(/^\*{3}/, '')
|
327
|
+
if @user_aliases.include? no_action.downcase.gsub(/\s+/, '')
|
328
|
+
# Set the current alias being used of the ones in @user_aliases
|
329
|
+
@user_alias = no_action
|
330
|
+
return @user_SN
|
331
|
+
else
|
332
|
+
return @partner_SN
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
#--
|
337
|
+
# create_msg takes an array of captures from matching against
|
338
|
+
# @line_regex and returns a Message object or one of its subclasses.
|
339
|
+
# It can be used for TextLogParser and HtmlLogParser because both of
|
340
|
+
# they return data in the same indexes in the matches array.
|
341
|
+
#++
|
342
|
+
def create_msg(matches)
|
343
|
+
msg = nil
|
344
|
+
# Either a regular message line or an auto-reply/away message.
|
345
|
+
time = create_adium_time(matches[0])
|
346
|
+
return nil if time.nil?
|
347
|
+
buddy_alias = matches[1]
|
348
|
+
sender = get_sender_by_alias(buddy_alias)
|
349
|
+
body = matches[3]
|
350
|
+
if matches[2] # auto-reply
|
351
|
+
msg = AutoReplyMessage.new(sender, time, buddy_alias, body)
|
352
|
+
else
|
353
|
+
# normal message
|
354
|
+
msg = XMLMessage.new(sender, time, buddy_alias, body)
|
355
|
+
end
|
356
|
+
return msg
|
357
|
+
end
|
358
|
+
|
359
|
+
#--
|
360
|
+
# create_status_or_event_msg takes an array of +MatchData+ captures from
|
361
|
+
# matching against @line_regex_status and returns an Event or Status.
|
362
|
+
# Returns nil if it's a message that should be ignored, or false if an
|
363
|
+
# error occurred.
|
364
|
+
#++
|
365
|
+
def create_status_or_event_msg(matches)
|
366
|
+
# ["22:58:00", "BuddyName logged in."]
|
367
|
+
# 0: time
|
368
|
+
# 1: status message or event
|
369
|
+
msg = nil
|
370
|
+
time = create_adium_time(matches[0])
|
371
|
+
return nil if time.nil?
|
372
|
+
str = matches[1]
|
373
|
+
# Return nil, which will get compact'ed out
|
374
|
+
return nil if @ignore_events.detect{|regex| str =~ regex }
|
375
|
+
|
376
|
+
regex, status = @status_map.detect{|regex, status| str =~ regex}
|
377
|
+
if regex and status
|
378
|
+
# Status message
|
379
|
+
buddy_alias = regex.match(str)[1]
|
380
|
+
sender = get_sender_by_alias(buddy_alias)
|
381
|
+
msg = StatusMessage.new(sender, time, buddy_alias, status)
|
382
|
+
else
|
383
|
+
# Test for event
|
384
|
+
regex = @lib_purple_events.detect{|regex| str =~ regex }
|
385
|
+
event_type = 'libpurpleEvent' if regex
|
386
|
+
unless regex and event_type
|
387
|
+
# not a libpurple event, try others
|
388
|
+
regex, event_type = @event_map.detect{|regex,event_type| str =~ regex}
|
389
|
+
unless regex and event_type
|
390
|
+
error(sprintf("Error parsing status or event message, no status or event found: %p", str))
|
391
|
+
return false
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
if regex and event_type
|
396
|
+
regex_matches = regex.match(str)
|
397
|
+
# Event message
|
398
|
+
if regex_matches.size == 1
|
399
|
+
# No alias - this means it's the user
|
400
|
+
buddy_alias = @user_alias
|
401
|
+
sender = @user_SN
|
402
|
+
else
|
403
|
+
buddy_alias = regex_matches[1]
|
404
|
+
sender = get_sender_by_alias(buddy_alias)
|
405
|
+
end
|
406
|
+
msg = Event.new(sender, time, buddy_alias, str, event_type)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
return msg
|
410
|
+
end
|
411
|
+
end # END BasicParser class
|
412
|
+
end
|