pidgin2adium 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|