pidgin2adium 1.0.0-universal-darwin
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/pidgin2adium_logs +67 -0
- data/bin/pidgin2adium_status +15 -0
- data/lib/pidgin2adium/ChatFileGenerator.rb +59 -0
- data/lib/pidgin2adium/SrcFileParse.rb +485 -0
- data/lib/pidgin2adium/balance-tags.rb +115 -0
- data/lib/pidgin2adium/logs.rb +250 -0
- data/lib/pidgin2adium/status.rb +113 -0
- metadata +60 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
=begin
|
4
|
+
Author: Gabe Berke-Williams, 2008
|
5
|
+
This is the shell script, which is a wrapper around Pidgin2Adium::Logs.
|
6
|
+
Call it like so:
|
7
|
+
<tt>pidgin2adium_logs.rb -i ~/in_logs/ -o ~/out_logs/ -l AIM.myscreenname -a me,screenname,my_pidgin_alias,other_pidgin_alias</tt>
|
8
|
+
For <tt>-a/--aliases</tt>, there is no need to use spaces or capitalization, since spaces will be stripped out and the aliases will
|
9
|
+
be lowercased anyway.
|
10
|
+
=end
|
11
|
+
|
12
|
+
require 'pidgin2adium/logs'
|
13
|
+
require 'optparse'
|
14
|
+
|
15
|
+
options = {}
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: #{File.basename($0)} [options]"
|
18
|
+
opts.on('-i IN_DIR', '--in IN_DIR', 'Specify directory where pidgin logs are stored') do |v|
|
19
|
+
options[:in] = v
|
20
|
+
end
|
21
|
+
opts.on('-o', '--out OUT_DIR', 'Specify directory where Adium logs will be stored (not the Adium directory in ~/Library)') do |out|
|
22
|
+
options[:out] = out
|
23
|
+
end
|
24
|
+
opts.on('-l', '--libdir LIBRARY_DIR',
|
25
|
+
'Specify dirname where Adium logs are stored (eg "AIM.<username>" for',
|
26
|
+
'~/Library/Application Support/Adium 2.0/Users/Default/Logs/AIM.<username>)') do |ld|
|
27
|
+
options[:libdir] = ld
|
28
|
+
end
|
29
|
+
opts.on('-d', '--debug', 'Turn debug on.') do |lf|
|
30
|
+
options[:debug] = true
|
31
|
+
end
|
32
|
+
opts.on('-t', "--time-zone [TIME ZONE]",
|
33
|
+
"Set time zone like \"EST\". Defaults to local time zone: #{Time.now.zone}") do |tz|
|
34
|
+
options[:timezone] = tz
|
35
|
+
end
|
36
|
+
opts.on('-a', "--aliases MY_ALIASES_AND_SNs",
|
37
|
+
"A comma-separated list of your aliases and screenname(s) so this script knows which person in a chat is you.",
|
38
|
+
"Whitespace is removed and aliases are lowercased.") do |aliases|
|
39
|
+
options[:aliases] = aliases.split(',')
|
40
|
+
end
|
41
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
42
|
+
puts opts
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
end.parse!
|
46
|
+
|
47
|
+
need_opts = false
|
48
|
+
required_opts = [[:i, :in], [:o, :out], [:l, :libdir], [:a, :aliases]]
|
49
|
+
required_opts.each do |short, long|
|
50
|
+
if options.has_key?(short) or options.has_key?(long)
|
51
|
+
next
|
52
|
+
else
|
53
|
+
need_opts = true
|
54
|
+
puts "Required option -#{short}/--#{long} missing."
|
55
|
+
end
|
56
|
+
end
|
57
|
+
exit 1 if need_opts
|
58
|
+
|
59
|
+
log_converter = Pidgin2Adium::Logs.new(options[:in],
|
60
|
+
options[:out],
|
61
|
+
options[:aliases],
|
62
|
+
options[:libdir],
|
63
|
+
options[:timezone],
|
64
|
+
options[:debug]
|
65
|
+
)
|
66
|
+
|
67
|
+
log_converter.start
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# Shell script wrapper for Pidgin2Adium::Status
|
4
|
+
|
5
|
+
require 'pidgin2adium/status'
|
6
|
+
|
7
|
+
xml_file = ARGV[0]
|
8
|
+
|
9
|
+
unless File.readable?(xml_file)
|
10
|
+
puts "XML file (#{xml_file}) is not readable. Exiting."
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
status_converter = Pidgin2Adium::Status.new(xml_file)
|
15
|
+
status_converter.start
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# ADD DOCUMENTATION
|
2
|
+
require 'pidgin2adium/balance-tags.rb'
|
3
|
+
|
4
|
+
module Pidgin2Adium
|
5
|
+
class ChatFileGenerator
|
6
|
+
def initialize(service, userSN, partnerSN, adiumChatTimeStart, destDirBase)
|
7
|
+
@service = service
|
8
|
+
@userSN = userSN
|
9
|
+
@partnerSN = partnerSN
|
10
|
+
@adiumChatTimeStart = adiumChatTimeStart
|
11
|
+
@destDirBase = destDirBase
|
12
|
+
|
13
|
+
# @chatLines is an array of Message, Status, and Event objects
|
14
|
+
@chatLines = []
|
15
|
+
# key is for Pidgin, value is for Adium
|
16
|
+
# Just used for <service>.<screenname> in directory structure
|
17
|
+
@SERVICE_NAME_MAP = {'aim' => 'AIM',
|
18
|
+
'jabber' =>'jabber',
|
19
|
+
'gtalk'=> 'GTalk',
|
20
|
+
'icq' => 'ICQ',
|
21
|
+
'qq' => 'QQ',
|
22
|
+
'msn' => 'MSN',
|
23
|
+
'yahoo' => 'Yahoo'}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add a line to @chatLines.
|
27
|
+
# It is its own method because attr_writer creates the method
|
28
|
+
# 'chatMessage=', which doesn't help for chatMessage.push
|
29
|
+
def appendLine(line)
|
30
|
+
@chatLines.push(line)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns path of output file
|
34
|
+
def convert()
|
35
|
+
serviceName = @SERVICE_NAME_MAP[@service.downcase]
|
36
|
+
destDirReal = File.join(@destDirBase, "#{serviceName}.#{@userSN}", @partnerSN, "#{@partnerSN} (#{@adiumChatTimeStart}).chatlog")
|
37
|
+
FileUtils.mkdir_p(destDirReal)
|
38
|
+
destFilePath = destDirReal << '/' << "#{@partnerSN} (#{@adiumChatTimeStart}).xml"
|
39
|
+
if File.exist?(destFilePath)
|
40
|
+
return Pidgin2Adium::Logs::FILE_EXISTS
|
41
|
+
end
|
42
|
+
|
43
|
+
allMsgs = ""
|
44
|
+
# TODO: inject?
|
45
|
+
@chatLines.each { |obj| allMsgs << obj.getOutput() }
|
46
|
+
# xml is done.
|
47
|
+
|
48
|
+
# no \n before </chat> because allMsgs has it already
|
49
|
+
ret = sprintf('<?xml version="1.0" encoding="UTF-8" ?>'<<"\n"+
|
50
|
+
'<chat xmlns="http://purl.org/net/ulf/ns/0.4-02" account="%s" service="%s">'<<"\n"<<'%s</chat>', @userSN, serviceName, allMsgs)
|
51
|
+
|
52
|
+
# we already checked to see if the file previously existed.
|
53
|
+
outfile = File.new(destFilePath, 'w')
|
54
|
+
outfile.puts(ret)
|
55
|
+
outfile.close
|
56
|
+
return destFilePath
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,485 @@
|
|
1
|
+
# =SrcFileParse
|
2
|
+
# The class +SrcFileParse+ has 2 subclasses, +SrcTxtFileParse+ and +SrcHtmlFileParse+
|
3
|
+
# It parses the file passed into it and extracts the following
|
4
|
+
# from each line in the chat: time, alias, and message and/or status.
|
5
|
+
|
6
|
+
require 'parsedate'
|
7
|
+
|
8
|
+
module Pidgin2Adium
|
9
|
+
# The two subclasses of +SrcFileParse+,
|
10
|
+
# +SrcTxtFileParse+ and +SrcHtmlFileParse+, only differ
|
11
|
+
# in that they have their own @lineRegex, @lineRegexStatus,
|
12
|
+
# and most importantly, createMsg and createStatusOrEventMsg, which take
|
13
|
+
# the +MatchData+ objects from matching against @lineRegex or
|
14
|
+
# @lineRegexStatus, respectively and return object instances.
|
15
|
+
# +createMsg+ returns a +Message+ instance (or one of its subclasses).
|
16
|
+
# +createStatusOrEventMsg+ returns a +Status+ or +Event+ instance.
|
17
|
+
class SrcFileParse
|
18
|
+
def initialize(srcPath, destDirBase, userAliases, userTZ, userTZOffset)
|
19
|
+
@srcPath = srcPath
|
20
|
+
# these two are to pass to chatFG in parseFile
|
21
|
+
@destDirBase = destDirBase
|
22
|
+
@userAliases = userAliases
|
23
|
+
@userTZ = userTZ
|
24
|
+
@userTZOffset = userTZOffset
|
25
|
+
@tzOffset = getTimeZoneOffset()
|
26
|
+
|
27
|
+
# Used in @lineRegex{,Status}. Only one group: the entire timestamp.
|
28
|
+
@timestampRegexStr = '\(((?:\d{4}-\d{2}-\d{2} )?\d{1,2}:\d{1,2}:\d{1,2}(?: .{1,2})?)\)'
|
29
|
+
# the first line is special: it tells us
|
30
|
+
# 1) who we're talking to
|
31
|
+
# 2) what time/date
|
32
|
+
# 3) what SN we used
|
33
|
+
# 4) what protocol (AIM, icq, jabber...)
|
34
|
+
@firstLineRegex = /Conversation with (.+?) at (.+?) on (.+?) \((.+?)\)/
|
35
|
+
|
36
|
+
# Possible formats for timestamps:
|
37
|
+
# "2007-04-17 12:33:13" => %w{2007, 04, 17, 12, 33, 13}
|
38
|
+
@timeRegexOne = /(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/
|
39
|
+
# "4/18/2007 11:02:00 AM" => %w{4, 18, 2007, 11, 02, 00, AM}
|
40
|
+
@timeRegexTwo = %r{(\d{1,2})/(\d{1,2})/(\d{4}) (\d{1,2}):(\d{2}):(\d{2}) ([AP]M)}
|
41
|
+
# sometimes a line in a chat doesn't have a full timestamp
|
42
|
+
# "04:22:05 AM" => %w{04 22 05 AM}
|
43
|
+
@minimalTimeRegex = /(\d{1,2}):(\d{2}):(\d{2}) ?([AP]M)?/
|
44
|
+
|
45
|
+
# {user,partner}SN set in parseFile() after reading the first line
|
46
|
+
@userSN = nil
|
47
|
+
@partnerSN = nil
|
48
|
+
|
49
|
+
# @basicTimeInfo is for files that only have the full timestamp at
|
50
|
+
# the top; we can use it to fill in the minimal per-line timestamps.
|
51
|
+
# It has only 3 elements (year, month, dayofmonth) because
|
52
|
+
# you should be able to fill everything else in.
|
53
|
+
# If you can't, something's wrong.
|
54
|
+
@basicTimeInfo = []
|
55
|
+
|
56
|
+
# @userAlias is set each time getSenderByAlias is called. Set an
|
57
|
+
# initial value just in case the first message doesn't give us an
|
58
|
+
# alias.
|
59
|
+
@userAlias = @userAliases[0]
|
60
|
+
|
61
|
+
# @statusMap, @libPurpleEvents, and @events are used in
|
62
|
+
# createStatusOrEventMessage.
|
63
|
+
@statusMap = {
|
64
|
+
/(.+) logged in\.$/ => 'online',
|
65
|
+
/(.+) logged out\.$/ => 'offline',
|
66
|
+
/(.+) has signed on\.$/ => 'online',
|
67
|
+
/(.+) has signed off\.$/ => 'offline',
|
68
|
+
/(.+) has gone away\.$/ => 'away',
|
69
|
+
/(.+) is no longer away\.$/ => 'available',
|
70
|
+
/(.+) has become idle\.$/ => 'idle',
|
71
|
+
/(.+) is no longer idle\.$/ => 'available'
|
72
|
+
}
|
73
|
+
|
74
|
+
# libPurpleEvents are all of eventType libPurple
|
75
|
+
@libPurpleEvents = [
|
76
|
+
# file transfer
|
77
|
+
/Starting transfer of .+ from (.+)/,
|
78
|
+
/^Offering to send .+ to (.+)$/,
|
79
|
+
/(.+) is offering to send file/,
|
80
|
+
/^Transfer of file .+ complete$/,
|
81
|
+
/Error reading|writing|accessing .+: .+/,
|
82
|
+
/You cancelled the transfer of/,
|
83
|
+
/File transfer cancelled/,
|
84
|
+
/(.+) cancelled the transfer of/,
|
85
|
+
/(.+) cancelled the file transfer/,
|
86
|
+
# Direct IM - actual (dis)connect events are their own types
|
87
|
+
/^Attempting to connect to (.+) at .+ for Direct IM\./,
|
88
|
+
/^Asking (.+) to connect to us at .+ for Direct IM\./,
|
89
|
+
/^Attempting to connect via proxy server\.$/,
|
90
|
+
/^Direct IM with (.+) failed/,
|
91
|
+
# encryption
|
92
|
+
/Received message encrypted with wrong key/,
|
93
|
+
/^Requesting key\.\.\.$/,
|
94
|
+
/^Outgoing message lost\.$/,
|
95
|
+
/^Conflicting Key Received!$/,
|
96
|
+
/^Error in decryption- asking for resend\.\.\.$/,
|
97
|
+
/^Making new key pair\.\.\.$/,
|
98
|
+
# file transfer - these are in this (non-used) list because you can't get the alias out of matchData[1]
|
99
|
+
/^You canceled the transfer of .+$/,
|
100
|
+
# sending errors
|
101
|
+
/^Last outgoing message not received properly- resetting$/,
|
102
|
+
/'Resending\.\.\./,
|
103
|
+
# connection errors
|
104
|
+
/Lost connection with the remote user:.+/,
|
105
|
+
# chats
|
106
|
+
/^.+ entered the room\.$/,
|
107
|
+
/^.+ left the room\.$/
|
108
|
+
]
|
109
|
+
|
110
|
+
# non-libpurple events
|
111
|
+
# Each key maps to an eventType string. The keys will be matched against a line of chat
|
112
|
+
# and the partner's alias will be in regex group 1, IF the alias is matched.
|
113
|
+
@eventMap = {
|
114
|
+
# .+ is not an alias, it's a proxy server so no grouping
|
115
|
+
/^Attempting to connect to .+\.$/ => 'direct-im-connect',
|
116
|
+
# NB: pidgin doesn't track when Direct IM is disconnected, AFAIK
|
117
|
+
/^Direct IM established$/ => 'directIMConnected',
|
118
|
+
/Unable to send message. The message is too large./ => 'chat-error',
|
119
|
+
/You missed .+ messages from (.+) because they were too large./ => 'chat-error'
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def getTimeZoneOffset()
|
124
|
+
tzMatch = /([-\+]\d+)[A-Z]{3}\.txt|html?/.match(@srcPath)
|
125
|
+
tzOffset = tzMatch[1] rescue @userTZOffset
|
126
|
+
return tzOffset
|
127
|
+
end
|
128
|
+
|
129
|
+
# Adium time format: YYYY-MM-DD\THH.MM.SS[+-]TZ_HRS like:
|
130
|
+
# 2008-10-05T22.26.20-0800
|
131
|
+
def createAdiumTime(time)
|
132
|
+
# parsedDate = [year, month, day, hour, min, sec]
|
133
|
+
parsedDate = case time
|
134
|
+
when @timeRegexOne
|
135
|
+
[$~[1].to_i, # year
|
136
|
+
$~[2].to_i, # month
|
137
|
+
$~[3].to_i, # day
|
138
|
+
$~[4].to_i, # hour
|
139
|
+
$~[5].to_i, # minute
|
140
|
+
$~[6].to_i] # seconds
|
141
|
+
when @timeRegexTwo
|
142
|
+
hours = $~[4].to_i
|
143
|
+
if $~[7] == 'PM' and hours != 12
|
144
|
+
hours += 12
|
145
|
+
end
|
146
|
+
[$~[3].to_i, # year
|
147
|
+
$~[1].to_i, # month
|
148
|
+
$~[2].to_i, # day
|
149
|
+
hours,
|
150
|
+
$~[5].to_i, # minutes
|
151
|
+
$~[6].to_i] # seconds
|
152
|
+
when @minimalTimeRegex
|
153
|
+
# "04:22:05" => %w{04 22 05}
|
154
|
+
hours = $~[1].to_i
|
155
|
+
if $~[4] == 'PM' and hours != 12
|
156
|
+
hours += 12
|
157
|
+
end
|
158
|
+
@basicTimeInfo + # [year, month, day]
|
159
|
+
[hours,
|
160
|
+
$~[2].to_i, # minutes
|
161
|
+
$~[3].to_i] # seconds
|
162
|
+
else
|
163
|
+
Pidgin2Adium.logMsg("You have found an odd timestamp.", true)
|
164
|
+
Pidgin2Adium.logMsg("Please report it to the developer.")
|
165
|
+
Pidgin2Adium.logMsg("The timestamp: #{time}")
|
166
|
+
Pidgin2Adium.logMsg("Continuing...")
|
167
|
+
|
168
|
+
ParseDate.parsedate(time)
|
169
|
+
end
|
170
|
+
return Time.local(*parsedDate).strftime("%Y-%m-%dT%H.%M.%S#{@tzOffset}")
|
171
|
+
end
|
172
|
+
|
173
|
+
# parseFile slurps up @srcPath into one big string and runs
|
174
|
+
# SrcHtmlFileParse.cleanup if it's an HTML file.
|
175
|
+
# It then uses regexes to break up the string, uses create(Status)Msg
|
176
|
+
# to turn the regex MatchData into data hashes, and feeds it to
|
177
|
+
# ChatFileGenerator, which creates the XML data string.
|
178
|
+
# This method returns a ChatFileGenerator object.
|
179
|
+
def parseFile()
|
180
|
+
file = File.new(@srcPath, 'r')
|
181
|
+
# Deal with first line.
|
182
|
+
firstLine = file.readline()
|
183
|
+
firstLineMatch = @firstLineRegex.match(firstLine)
|
184
|
+
if firstLineMatch.nil?
|
185
|
+
file.close()
|
186
|
+
Pidgin2Adium.logMsg("Parsing of #{@srcPath} failed (could not find valid first line).", true)
|
187
|
+
return false
|
188
|
+
else
|
189
|
+
# one big string, without the first line
|
190
|
+
if self.class == SrcHtmlFileParse
|
191
|
+
fileContent = self.cleanup(file.read())
|
192
|
+
else
|
193
|
+
fileContent = file.read()
|
194
|
+
end
|
195
|
+
file.close()
|
196
|
+
end
|
197
|
+
|
198
|
+
service = firstLineMatch[4]
|
199
|
+
# userSN is standardized to avoid "AIM.name" and "AIM.na me" folders
|
200
|
+
@userSN = firstLineMatch[3].downcase.gsub(' ', '')
|
201
|
+
@partnerSN = firstLineMatch[1]
|
202
|
+
pidginChatTimeStart = firstLineMatch[2]
|
203
|
+
@basicTimeInfo = case firstLine
|
204
|
+
when @timeRegexOne: [$1.to_i, $2.to_i, $3.to_i]
|
205
|
+
when @timeRegexTwo: [$3.to_i, $1.to_i, $2.to_i]
|
206
|
+
end
|
207
|
+
|
208
|
+
chatFG = ChatFileGenerator.new(service,
|
209
|
+
@userSN,
|
210
|
+
@partnerSN,
|
211
|
+
createAdiumTime(pidginChatTimeStart),
|
212
|
+
@destDirBase)
|
213
|
+
fileContent.each_line do |line|
|
214
|
+
case line
|
215
|
+
when @lineRegex
|
216
|
+
chatFG.appendLine( createMsg($~.captures) )
|
217
|
+
when @lineRegexStatus
|
218
|
+
msg = createStatusOrEventMsg($~.captures)
|
219
|
+
# msg is nil if we couldn't parse the status line
|
220
|
+
chatFG.appendLine(msg) unless msg.nil?
|
221
|
+
end
|
222
|
+
end
|
223
|
+
return chatFG
|
224
|
+
end
|
225
|
+
|
226
|
+
def getSenderByAlias(aliasName)
|
227
|
+
if @userAliases.include? aliasName.downcase.sub(/^\*{3}/,'').gsub(/\s+/, '')
|
228
|
+
# Set the current alias being used of the ones in @userAliases
|
229
|
+
@userAlias = aliasName.sub(/^\*{3}/, '')
|
230
|
+
return @userSN
|
231
|
+
else
|
232
|
+
return @partnerSN
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# createMsg takes an array of captures from matching against @lineRegex
|
237
|
+
# and returns a Message object or one of its subclasses.
|
238
|
+
# It can be used for SrcTxtFileParse and SrcHtmlFileParse because
|
239
|
+
# both of them return data in the same indexes in the matches array.
|
240
|
+
def createMsg(matches)
|
241
|
+
msg = nil
|
242
|
+
# Either a regular message line or an auto-reply/away message.
|
243
|
+
time = createAdiumTime(matches[0])
|
244
|
+
aliasStr = matches[1]
|
245
|
+
sender = getSenderByAlias(aliasStr)
|
246
|
+
body = matches[3]
|
247
|
+
if matches[2] # auto-reply
|
248
|
+
msg = AutoReplyMessage.new(sender, time, aliasStr, body)
|
249
|
+
else
|
250
|
+
# normal message
|
251
|
+
msg = XMLMessage.new(sender, time, aliasStr, body)
|
252
|
+
end
|
253
|
+
return msg
|
254
|
+
end
|
255
|
+
|
256
|
+
# createStatusOrEventMsg takes an array of +MatchData+ captures from
|
257
|
+
# matching against @lineRegexStatus and returns an Event or Status.
|
258
|
+
def createStatusOrEventMsg(matches)
|
259
|
+
# ["22:58:00", "BuddyName logged in."]
|
260
|
+
# 0: time
|
261
|
+
# 1: status message or event
|
262
|
+
msg = nil
|
263
|
+
time = createAdiumTime(matches[0])
|
264
|
+
str = matches[1]
|
265
|
+
regex, status = @statusMap.detect{|regex, status| str =~ regex}
|
266
|
+
if regex and status
|
267
|
+
# Status message
|
268
|
+
aliasStr = regex.match(str)[1]
|
269
|
+
sender = getSenderByAlias(aliasStr)
|
270
|
+
msg = StatusMessage.new(sender, time, aliasStr, status)
|
271
|
+
else
|
272
|
+
# Test for event
|
273
|
+
regex = @libPurpleEvents.detect{|regex| str =~ regex }
|
274
|
+
eventType = 'libpurpleEvent' if regex
|
275
|
+
unless regex and eventType
|
276
|
+
# not a libpurple event, try others
|
277
|
+
regexAndEventType = @eventMap.detect{|regex,eventType| str =~ regex}
|
278
|
+
if regexAndEventType.nil?
|
279
|
+
Pidgin2Adium.logMsg("You have found an odd status line. Please send this line to the developer.", true)
|
280
|
+
Pidgin2Adium.logMsg("The line is: #{str}", true)
|
281
|
+
return nil
|
282
|
+
else
|
283
|
+
regex = regexAndEventType[0]
|
284
|
+
eventType = regexAndEventType[1]
|
285
|
+
end
|
286
|
+
end
|
287
|
+
if regex and eventType
|
288
|
+
regexMatches = regex.match(str)
|
289
|
+
# Event message
|
290
|
+
if regexMatches.size == 1
|
291
|
+
# No alias - this means it's the user
|
292
|
+
aliasStr = @userAlias
|
293
|
+
sender = @userSN
|
294
|
+
else
|
295
|
+
aliasStr = regex.match(str)[1]
|
296
|
+
sender = getSenderByAlias(aliasStr)
|
297
|
+
end
|
298
|
+
msg = Event.new(sender, time, aliasStr, str, eventType)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
return msg
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
class SrcTxtFileParse < SrcFileParse
|
306
|
+
def initialize(srcPath, destDirBase, userAliases, userTZ, userTZOffset)
|
307
|
+
super(srcPath, destDirBase, userAliases, userTZ, userTZOffset)
|
308
|
+
# @lineRegex matches a line in a TXT log file other than the first
|
309
|
+
# @lineRegex matchdata:
|
310
|
+
# 0: timestamp
|
311
|
+
# 1: screen name or alias, if alias set
|
312
|
+
# 2: "<AUTO-REPLY>" or nil
|
313
|
+
# 3: message body
|
314
|
+
@lineRegex = /#{@timestampRegexStr} (.*?) ?(<AUTO-REPLY>)?: (.*)$/o
|
315
|
+
# @lineRegexStatus matches a status line
|
316
|
+
# @lineRegexStatus matchdata:
|
317
|
+
# 0: timestamp
|
318
|
+
# 1: status message
|
319
|
+
@lineRegexStatus = /#{@timestampRegexStr} ([^:]+?)[\r\n]/o
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
|
324
|
+
class SrcHtmlFileParse < SrcFileParse
|
325
|
+
def initialize(srcPath, destDirBase, userAliases, userTZ, userTZOffset)
|
326
|
+
super(srcPath, destDirBase, userAliases, userTZ, userTZOffset)
|
327
|
+
# @lineRegex matches a line in an HTML log file other than the first
|
328
|
+
# time matches on either "2008-11-17 14:12" or "14:12"
|
329
|
+
# @lineRegex match obj:
|
330
|
+
# 0: timestamp, extended or not
|
331
|
+
# 1: screen name or alias, if alias set
|
332
|
+
# 2: "<AUTO-REPLY>" or nil
|
333
|
+
# 3: message body
|
334
|
+
# <span style='color: #000000;'>test sms</span>
|
335
|
+
@lineRegex = /#{@timestampRegexStr} ?<b>(.*?) ?(<AUTO-REPLY>)?:?<\/b> ?(.*)<br ?\/>/o
|
336
|
+
# @lineRegexStatus matches a status line
|
337
|
+
# @lineRegexStatus match obj:
|
338
|
+
# 0: timestamp
|
339
|
+
# 1: status message
|
340
|
+
@lineRegexStatus = /#{@timestampRegexStr} ?<b> (.*?)<\/b><br ?\/>/o
|
341
|
+
end
|
342
|
+
|
343
|
+
# Removes <font> tags, empty <a>s, and spans with either no color
|
344
|
+
# information or color information that just turns the text black.
|
345
|
+
# Returns a string.
|
346
|
+
def cleanup(text)
|
347
|
+
# Pidgin and Adium both show bold using
|
348
|
+
# <span style="font-weight: bold;"> except Pidgin uses single quotes
|
349
|
+
# and Adium uses double quotes
|
350
|
+
text.gsub!(/<\/?(html|body|font).*?>/, '')
|
351
|
+
# These empty links are sometimes appended to every line in a chat,
|
352
|
+
# for some weird reason. Remove them.
|
353
|
+
text.gsub!(%r{<a href='.+?'>\s*?</a>}, '')
|
354
|
+
text.gsub!(%r{(.*?)<span.+style='(.+?)'>(.*?)</span>(.*)}) do |s|
|
355
|
+
# before = text before match
|
356
|
+
# style = style declaration
|
357
|
+
# innertext = text inside <span>
|
358
|
+
# after = text after match
|
359
|
+
before, style, innertext, after = *($~[1..4])
|
360
|
+
# TODO: remove after from string then see what balanceTags does
|
361
|
+
# Remove empty spans.
|
362
|
+
nil if innertext == ''
|
363
|
+
# Only allow some style declarations
|
364
|
+
# We keep:
|
365
|
+
# font-weight: bold
|
366
|
+
# color (except #000000)
|
367
|
+
# text-decoration: underline
|
368
|
+
styleparts = style.split(/; ?/)
|
369
|
+
styleparts.map! do |p|
|
370
|
+
# Short-circuit for common declaration
|
371
|
+
# Yes, sometimes there's a ">" before the ";".
|
372
|
+
if p == 'color: #000000;' or p == 'color: #000000>;'
|
373
|
+
nil
|
374
|
+
else
|
375
|
+
case p
|
376
|
+
when /font-family/: nil
|
377
|
+
when /font-size/: nil
|
378
|
+
when /background/: nil
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
styleparts.compact!
|
383
|
+
if styleparts.empty?
|
384
|
+
style = ''
|
385
|
+
elsif styleparts.size == 1
|
386
|
+
style = styleparts[0] << ';'
|
387
|
+
else
|
388
|
+
style = styleparts.join('; ') << ';'
|
389
|
+
end
|
390
|
+
if style != ''
|
391
|
+
innertext = "<span style=\"#{style}\">#{innertext}</span>"
|
392
|
+
end
|
393
|
+
before + innertext + after
|
394
|
+
end
|
395
|
+
# Pidgin uses <em>, Adium uses <span>
|
396
|
+
if text.gsub!('<em>', '<span style="italic">')
|
397
|
+
text.gsub!('</em>', '</span>')
|
398
|
+
end
|
399
|
+
return text
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# A holding object for each line of the chat.
|
404
|
+
# It is subclassed as appropriate (eg AutoReplyMessage).
|
405
|
+
# All Messages have senders, times, and aliases.
|
406
|
+
class Message
|
407
|
+
def initialize(sender, time, aliasStr)
|
408
|
+
@sender = sender
|
409
|
+
@time = time
|
410
|
+
@aliasStr = aliasStr
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# Basic message with body text (as opposed to pure status messages, which
|
415
|
+
# have no body).
|
416
|
+
class XMLMessage < Message
|
417
|
+
def initialize(sender, time, aliasStr, body)
|
418
|
+
super(sender, time, aliasStr)
|
419
|
+
@body = body
|
420
|
+
normalizeBody!()
|
421
|
+
end
|
422
|
+
|
423
|
+
def getOutput
|
424
|
+
return sprintf('<message sender="%s" time="%s" alias="%s">%s</message>' << "\n",
|
425
|
+
@sender, @time, @aliasStr, @body)
|
426
|
+
end
|
427
|
+
|
428
|
+
def normalizeBody!
|
429
|
+
normalizeBodyEntities!()
|
430
|
+
# Fix mismatched tags. Yes, it's faster to do it per-message
|
431
|
+
# than all at once.
|
432
|
+
@body = Pidgin2Adium.balanceTags(@body)
|
433
|
+
if @aliasStr[0,3] == '***'
|
434
|
+
# "***<alias>" is what pidgin sets as the alias for a /me action
|
435
|
+
@aliasStr.slice!(0,3)
|
436
|
+
@body = '*' << @body << '*'
|
437
|
+
end
|
438
|
+
@body = '<div><span style="font-family: Helvetica; font-size: 12pt;">' <<
|
439
|
+
@body <<
|
440
|
+
'</span></div>'
|
441
|
+
end
|
442
|
+
|
443
|
+
def normalizeBodyEntities!
|
444
|
+
# Convert '&' to '&' only if it's not followed by an entity.
|
445
|
+
@body.gsub!(/&(?!lt|gt|amp|quot|apos)/, '&')
|
446
|
+
# replace single quotes with ''' but only outside <span>s.
|
447
|
+
@body.gsub!(/(.*?)(<span.*?>.*?<\/span>)(.*?)/) do
|
448
|
+
before, span, after = $1, ($2||''), $3||''
|
449
|
+
before.gsub("'", '&aquot;') <<
|
450
|
+
span <<
|
451
|
+
after.gsub("'", '&aquot;')
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
# An auto reply message, meaning it has a body.
|
457
|
+
class AutoReplyMessage < XMLMessage
|
458
|
+
def getOutput
|
459
|
+
return sprintf('<message sender="%s" time="%s" auto="true" alias="%s">%s</message>' << "\n", @sender, @time, @aliasStr, @body)
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# A message saying e.g. "Blahblah has gone away."
|
464
|
+
class StatusMessage < Message
|
465
|
+
def initialize(sender, time, aliasStr, status)
|
466
|
+
super(sender, time, aliasStr)
|
467
|
+
@status = status
|
468
|
+
end
|
469
|
+
def getOutput
|
470
|
+
return sprintf('<status type="%s" sender="%s" time="%s" alias="%s"/>' << "\n", @status, @sender, @time, @aliasStr)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
# An <event> line of the chat
|
475
|
+
class Event < XMLMessage
|
476
|
+
def initialize(sender, time, aliasStr, body, type="libpurpleMessage")
|
477
|
+
super(sender, time, aliasStr, body)
|
478
|
+
@type = type
|
479
|
+
end
|
480
|
+
|
481
|
+
def getOutput
|
482
|
+
return sprintf('<event type="%s" sender="%s" time="%s" alias="%s">%s</event>', @type, @sender, @time, @aliasStr, @body)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end # end module
|
@@ -0,0 +1,115 @@
|
|
1
|
+
|
2
|
+
module Pidgin2Adium
|
3
|
+
#From Wordpress's formatting.php; rewritten in Ruby by Gabe Berke-Williams, 2009.
|
4
|
+
#Balances tags of string using a modified stack.
|
5
|
+
#
|
6
|
+
# @author Leonard Lin <leonard@acm.org>
|
7
|
+
# @license GPL v2.0
|
8
|
+
# @copyright November 4, 2001
|
9
|
+
# @return string Balanced text.
|
10
|
+
def Pidgin2Adium.balanceTags( text )
|
11
|
+
tagstack = []
|
12
|
+
stacksize = 0
|
13
|
+
tagqueue = ''
|
14
|
+
newtext = ''
|
15
|
+
single_tags = ['br', 'hr', 'img', 'input', 'meta'] # Known single-entity/self-closing tags
|
16
|
+
nestable_tags = ['blockquote', 'div', 'span'] # Tags that can be immediately nested within themselves
|
17
|
+
tag_regex = /<(\/?\w*)\s*([^>]*)>/
|
18
|
+
|
19
|
+
# WP bug fix for comments - in case you REALLY meant to type '< !--'
|
20
|
+
text.gsub!('< !--', '< !--')
|
21
|
+
|
22
|
+
# WP bug fix for LOVE <3 (and other situations with '<' before a number)
|
23
|
+
text.gsub!(/<([0-9]{1})/, '<\1')
|
24
|
+
|
25
|
+
while ( regex = text.match(tag_regex) )
|
26
|
+
regex = regex.to_a
|
27
|
+
newtext << tagqueue
|
28
|
+
i = text.index(regex[0])
|
29
|
+
l = regex[0].length
|
30
|
+
|
31
|
+
# clear the shifter
|
32
|
+
tagqueue = ''
|
33
|
+
# Pop or Push
|
34
|
+
if (regex[1][0,1] == "/") # End Tag
|
35
|
+
tag = regex[1][1,regex[1].length].downcase
|
36
|
+
# if too many closing tags
|
37
|
+
if(stacksize <= 0)
|
38
|
+
tag = ''
|
39
|
+
#or close to be safe tag = '/' . tag
|
40
|
+
# if stacktop value = tag close value then pop
|
41
|
+
elsif (tagstack[stacksize - 1] == tag) # found closing tag
|
42
|
+
tag = '</' << tag << '>'; # Close Tag
|
43
|
+
# Pop
|
44
|
+
tagstack.pop
|
45
|
+
stacksize -= 1
|
46
|
+
else # closing tag not at top, search for it
|
47
|
+
(stacksize-1).downto(0) do |j|
|
48
|
+
if (tagstack[j] == tag)
|
49
|
+
# add tag to tagqueue
|
50
|
+
ss = stacksize - 1
|
51
|
+
ss.downto(j) do |k|
|
52
|
+
tagqueue << '</' << tagstack.pop << '>'
|
53
|
+
stacksize -= 1
|
54
|
+
end
|
55
|
+
break
|
56
|
+
end
|
57
|
+
end
|
58
|
+
tag = ''
|
59
|
+
end
|
60
|
+
else
|
61
|
+
# Begin Tag
|
62
|
+
tag = regex[1].downcase
|
63
|
+
|
64
|
+
# Tag Cleaning
|
65
|
+
if( (regex[2].slice(-1,1) == '/') || (tag == '') )
|
66
|
+
# If: self-closing or '', don't do anything.
|
67
|
+
elsif ( single_tags.include?(tag) )
|
68
|
+
# ElseIf: it's a known single-entity tag but it doesn't close itself, do so
|
69
|
+
regex[2] << '/'
|
70
|
+
else
|
71
|
+
# Push the tag onto the stack
|
72
|
+
# If the top of the stack is the same as the tag we want to push, close previous tag
|
73
|
+
if ((stacksize > 0) &&
|
74
|
+
! nestable_tags.include?(tag) &&
|
75
|
+
(tagstack[stacksize - 1] == tag))
|
76
|
+
tagqueue = '</' << tagstack.pop << '>'
|
77
|
+
stacksize -= 1
|
78
|
+
end
|
79
|
+
stacksize = tagstack.push(tag).length
|
80
|
+
end
|
81
|
+
|
82
|
+
# Attributes
|
83
|
+
attributes = regex[2]
|
84
|
+
if(attributes != '')
|
85
|
+
attributes = ' ' << attributes
|
86
|
+
end
|
87
|
+
tag = '<' << tag << attributes << '>'
|
88
|
+
#If already queuing a close tag, then put this tag on, too
|
89
|
+
if (tagqueue)
|
90
|
+
tagqueue << tag
|
91
|
+
tag = ''
|
92
|
+
end
|
93
|
+
end
|
94
|
+
newtext << text[0,i] << tag
|
95
|
+
text = text[i+l, text.length - (i+l)]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Clear Tag Queue
|
99
|
+
newtext << tagqueue
|
100
|
+
|
101
|
+
# Add Remaining text
|
102
|
+
newtext << text
|
103
|
+
|
104
|
+
# Empty Stack
|
105
|
+
while(x = tagstack.pop)
|
106
|
+
newtext << '</' << x << '>'; # Add remaining tags to close
|
107
|
+
end
|
108
|
+
|
109
|
+
# WP fix for the bug with HTML comments
|
110
|
+
newtext.gsub!("< !--", "<!--")
|
111
|
+
newtext.gsub!("< !--", "< !--")
|
112
|
+
|
113
|
+
return newtext
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
#Author: Gabe Berke-Williams, 2008
|
4
|
+
#With thanks to Li Ma, whose blog post at
|
5
|
+
#http://li-ma.blogspot.com/2008/10/pidgin-log-file-to-adium-log-converter.html
|
6
|
+
#helped tremendously.
|
7
|
+
#
|
8
|
+
#A ruby program to convert Pidgin log files to Adium log files, then place
|
9
|
+
#them in the Adium log directory with allowances for time zone differences.
|
10
|
+
|
11
|
+
require 'pidgin2adium/SrcFileParse'
|
12
|
+
require 'pidgin2adium/ChatFileGenerator'
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
class Time
|
16
|
+
ZoneOffset = {
|
17
|
+
'UTC' => 0,
|
18
|
+
# ISO 8601
|
19
|
+
'Z' => 0,
|
20
|
+
# RFC 822
|
21
|
+
'UT' => 0, 'GMT' => 0,
|
22
|
+
'EST' => -5, 'EDT' => -4,
|
23
|
+
'CST' => -6, 'CDT' => -5,
|
24
|
+
'MST' => -7, 'MDT' => -6,
|
25
|
+
'PST' => -8, 'PDT' => -7,
|
26
|
+
# Following definition of military zones is original one.
|
27
|
+
# See RFC 1123 and RFC 2822 for the error in RFC 822.
|
28
|
+
'A' => +1, 'B' => +2, 'C' => +3, 'D' => +4, 'E' => +5, 'F' => +6,
|
29
|
+
'G' => +7, 'H' => +8, 'I' => +9, 'K' => +10, 'L' => +11, 'M' => +12,
|
30
|
+
'N' => -1, 'O' => -2, 'P' => -3, 'Q' => -4, 'R' => -5, 'S' => -6,
|
31
|
+
'T' => -7, 'U' => -8, 'V' => -9, 'W' => -10, 'X' => -11, 'Y' => -12
|
32
|
+
}
|
33
|
+
# Returns offset in hours, e.g. '+0900'
|
34
|
+
def Time.zone_offset(zone, year=Time.now.year)
|
35
|
+
off = nil
|
36
|
+
zone = zone.upcase
|
37
|
+
if /\A([+-])(\d\d):?(\d\d)\z/ =~ zone
|
38
|
+
off = ($1 == '-' ? -1 : 1) * ($2.to_i * 60 + $3.to_i) * 60
|
39
|
+
elsif /\A[+-]\d\d\z/ =~ zone
|
40
|
+
off = zone.to_i
|
41
|
+
elsif ZoneOffset.include?(zone)
|
42
|
+
off = ZoneOffset[zone]
|
43
|
+
elsif ((t = Time.local(year, 1, 1)).zone.upcase == zone rescue false)
|
44
|
+
off = t.utc_offset / 3600
|
45
|
+
elsif ((t = Time.local(year, 7, 1)).zone.upcase == zone rescue false)
|
46
|
+
off = t.utc_offset / 3600
|
47
|
+
end
|
48
|
+
off
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module Pidgin2Adium
|
53
|
+
# put's content. Also put's to @LOG_FILE_FH if @debug == true.
|
54
|
+
def Pidgin2Adium.logMsg(str, isError=false)
|
55
|
+
content = str.to_s
|
56
|
+
if isError == true
|
57
|
+
content= "ERROR: #{str}"
|
58
|
+
end
|
59
|
+
puts content
|
60
|
+
end
|
61
|
+
|
62
|
+
class Logs
|
63
|
+
# FILE_EXISTS is returned by ChatFileGenerator.buildDomAndOutput() if the output logfile already exists.
|
64
|
+
FILE_EXISTS = 42
|
65
|
+
def initialize(src, out, aliases, libdir, tz=nil, debug=false)
|
66
|
+
# These files/directories show up in Dir.entries(x)
|
67
|
+
@BAD_DIRS = %w{. .. .DS_Store Thumbs.db .system}
|
68
|
+
@src_dir = File.expand_path(src)
|
69
|
+
@out_dir = File.expand_path(out)
|
70
|
+
# Whitespace is removed for easy matching later on.
|
71
|
+
@my_aliases = aliases.map{|x| x.downcase.gsub(/\s+/,'') }.uniq
|
72
|
+
# @libdir is the directory in
|
73
|
+
# ~/Library/Application Support/Adium 2.0/Users/Default/Logs/.
|
74
|
+
# For AIM, it's like "AIM.<screenname>"
|
75
|
+
# FIXME: don't make the user pass in libdir - we can and SHOULD change it on a per-service/screenname basis
|
76
|
+
@libdir = libdir
|
77
|
+
@DEFAULT_TIME_ZONE = tz || Time.now.zone
|
78
|
+
@debug = debug
|
79
|
+
unless File.directory?(@src_dir)
|
80
|
+
puts "Source directory #{@src_dir} does not exist or is not a directory."
|
81
|
+
raise Errno::ENOENT
|
82
|
+
end
|
83
|
+
unless File.directory?(@out_dir)
|
84
|
+
begin
|
85
|
+
FileUtils.mkdir_p(@out_dir)
|
86
|
+
rescue
|
87
|
+
puts "Output directory #{@out_dir} does not exist or is not a directory and could not be created."
|
88
|
+
raise Errno::ENOENT
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# local offset, like "-0800" or "+1000"
|
93
|
+
@DEFAULT_TZ_OFFSET = '%+03d00'%Time.zone_offset(@DEFAULT_TIME_ZONE)
|
94
|
+
end
|
95
|
+
|
96
|
+
def start
|
97
|
+
Pidgin2Adium.logMsg "Begin converting."
|
98
|
+
begin
|
99
|
+
filesPath = getAllChatFiles(@src_dir)
|
100
|
+
rescue Errno::EACCES => bang
|
101
|
+
Pidgin2Adium.logMsg("Sorry, permission denied for getting Pidgin chat files from #{@src_dir}.", true)
|
102
|
+
Pidgin2Adium.logMsg("Details: #{bang.message}", true)
|
103
|
+
raise Errno::EACCES
|
104
|
+
end
|
105
|
+
|
106
|
+
Pidgin2Adium.logMsg("#{filesPath.length} files to convert.")
|
107
|
+
totalFiles = filesPath.size
|
108
|
+
filesPath.each_with_index do |fname, i|
|
109
|
+
Pidgin2Adium.logMsg(
|
110
|
+
sprintf("[%d/%d] Converting %s...",
|
111
|
+
(i+1), totalFiles, fname)
|
112
|
+
)
|
113
|
+
convert(fname)
|
114
|
+
end
|
115
|
+
|
116
|
+
copyLogs()
|
117
|
+
deleteSearchIndexes()
|
118
|
+
|
119
|
+
Pidgin2Adium.logMsg "Finished converting! Converted #{filesPath.length} files."
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# Problem: imported logs are viewable in the Chat Transcript Viewer, but are not indexed,
|
124
|
+
# so a search of the logs doesn't give results from the imported logs.
|
125
|
+
# To fix this, we delete the cached log indexes, which forces Adium to re-index.
|
126
|
+
def deleteSearchIndexes()
|
127
|
+
Pidgin2Adium.logMsg "Deleting log search indexes in order to force re-indexing of imported logs..."
|
128
|
+
dirtyFile=File.expand_path("~/Library/Caches/Adium/Default/DirtyLogs.plist")
|
129
|
+
logIndexFile=File.expand_path("~/Library/Caches/Adium/Default/Logs.index")
|
130
|
+
[dirtyFile, logIndexFile].each do |f|
|
131
|
+
if File.exist?(f)
|
132
|
+
if File.writable?(f)
|
133
|
+
File.delete(f)
|
134
|
+
else
|
135
|
+
Pidgin2Adium.logMsg("#{f} exists but is not writable. Please delete it yourself.", true)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
Pidgin2Adium.logMsg "...done."
|
140
|
+
Pidgin2Adium.logMsg "When you next start the Adium Chat Transcript Viewer, it will re-index the logs, which may take a while."
|
141
|
+
end
|
142
|
+
|
143
|
+
# <tt>convert</tt> creates a new SrcHtmlFileParse or SrcTxtFileParse object,
|
144
|
+
# as appropriate, and calls its parse() method.
|
145
|
+
# Returns false if there was a problem, true otherwise
|
146
|
+
def convert(srcPath)
|
147
|
+
ext = File.extname(srcPath).sub('.', '').downcase
|
148
|
+
if(ext == "html" || ext == "htm")
|
149
|
+
srcFileParse = SrcHtmlFileParse.new(srcPath, @out_dir, @my_aliases, @DEFAULT_TIME_ZONE, @DEFAULT_TZ_OFFSET)
|
150
|
+
elsif(ext == "txt")
|
151
|
+
srcFileParse = SrcTxtFileParse.new(srcPath, @out_dir, @my_aliases, @DEFAULT_TIME_ZONE, @DEFAULT_TZ_OFFSET)
|
152
|
+
elsif(ext == "chatlog")
|
153
|
+
# chatlog FILE, not directory
|
154
|
+
Pidgin2Adium.logMsg("Found chatlog FILE - moving to chatlog DIRECTORY.")
|
155
|
+
# Create out_dir/log.chatlog/
|
156
|
+
begin
|
157
|
+
toCreate = "#{@out_dir}/#{srcPath}"
|
158
|
+
Dir.mkdir(toCreate)
|
159
|
+
rescue => bang
|
160
|
+
Pidgin2Adium.logMsg("Could not create #{toCreate}: #{bang.class} #{bang.message}", true)
|
161
|
+
return false
|
162
|
+
end
|
163
|
+
fileWithXmlExt = srcPath[0, srcPath.size-File.extname(srcPath).size] << ".xml"
|
164
|
+
# @src_dir/log.chatlog (file) -> @out_dir/log.chatlog/log.xml
|
165
|
+
File.cp(srcPath, File.join(@out_dir, srcPath, fileWithXmlExt))
|
166
|
+
Pidgin2Adium.logMsg("Copied #{srcPath} to " << File.join(@out_dir, srcPath, fileWithXmlExt))
|
167
|
+
return true
|
168
|
+
else
|
169
|
+
Pidgin2Adium.logMsg("srcPath (#{srcPath}) is not a txt, html, or chatlog file. Doing nothing.")
|
170
|
+
return false
|
171
|
+
end
|
172
|
+
|
173
|
+
chatFG = srcFileParse.parseFile()
|
174
|
+
return false if chatFG == false
|
175
|
+
|
176
|
+
destFilePath = chatFG.convert()
|
177
|
+
return \
|
178
|
+
case destFilePath
|
179
|
+
when false
|
180
|
+
Pidgin2Adium.logMsg("Converting #{srcPath} failed.", true);
|
181
|
+
false
|
182
|
+
when FILE_EXISTS
|
183
|
+
Pidgin2Adium.logMsg("File already exists.")
|
184
|
+
true
|
185
|
+
else
|
186
|
+
Pidgin2Adium.logMsg("Output to: #{destFilePath}")
|
187
|
+
true
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def getAllChatFiles(dir)
|
192
|
+
return [] if File.basename(dir) == ".system"
|
193
|
+
# recurse into each subdir
|
194
|
+
return (Dir.glob(File.join(@src_dir, '**', '*.{htm,html,txt}')) - @BAD_DIRS)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Copies logs, accounting for timezone changes
|
198
|
+
def copyLogs
|
199
|
+
Pidgin2Adium.logMsg "Copying logs with accounting for different time zones..."
|
200
|
+
# FIXME: not all logs are AIM logs, libdir may change
|
201
|
+
realSrcDir = File.expand_path('~/Library/Application Support/Adium 2.0/Users/Default/Logs/') << "/#{@libdir}/"
|
202
|
+
realDestDir = File.join(@out_dir, @libdir) << '/'
|
203
|
+
|
204
|
+
src_entries = Dir.entries(realSrcDir)
|
205
|
+
dest_entries = Dir.entries(realDestDir)
|
206
|
+
both_entries = (src_entries & dest_entries) - @BAD_DIRS
|
207
|
+
|
208
|
+
both_entries.each do |name|
|
209
|
+
my_src_entries = Dir.entries(realSrcDir << name) - @BAD_DIRS
|
210
|
+
my_dest_entries = Dir.entries(realDestDir << name) - @BAD_DIRS
|
211
|
+
|
212
|
+
in_both = my_src_entries & my_dest_entries
|
213
|
+
in_both.each do |logdir|
|
214
|
+
FileUtils.cp(
|
215
|
+
File.join(realSrcDir, name, logdir, logdir.sub('chatlog', 'xml')),
|
216
|
+
File.join(realDestDir, name, logdir) << '/',
|
217
|
+
:verbose => false)
|
218
|
+
end
|
219
|
+
# The logs that are only in one of the dirs are not necessarily
|
220
|
+
# different logs than the dest. They might just have different
|
221
|
+
# timestamps. Thus, we use regexes.
|
222
|
+
only_in_src = my_src_entries - in_both
|
223
|
+
only_in_dest = my_dest_entries - in_both
|
224
|
+
# Move files from realSrcDir that are actually in both, but
|
225
|
+
# just have different time zones.
|
226
|
+
only_in_src.each do |srcLogDir|
|
227
|
+
# Match on everything except the timezone ("-0400.chatlog")
|
228
|
+
fileBeginRegex = Regexp.new('^'<<Regexp.escape(srcLogDir.sub(/-\d{4}.\.chatlog$/, '')) )
|
229
|
+
targetChatlogDir = only_in_dest.find{|d| d =~ fileBeginRegex}
|
230
|
+
if targetChatlogDir.nil?
|
231
|
+
# Only in source, so we can copy it without fear of
|
232
|
+
# overwriting.
|
233
|
+
targetChatlogDir = srcLogDir
|
234
|
+
FileUtils.mkdir_p(File.join(realDestDir, name, targetChatlogDir))
|
235
|
+
end
|
236
|
+
# Move to targetChatlogDir so we overwrite the destination
|
237
|
+
# file but still use its timestamp
|
238
|
+
# (if it exists; if it doesn't, then we're using our own
|
239
|
+
# timestamp).
|
240
|
+
FileUtils.cp(
|
241
|
+
File.join(realSrcDir, name, srcLogDir, srcLogDir.sub('chatlog', 'xml')),
|
242
|
+
File.join(realDestDir, name, targetChatlogDir, targetChatlogDir.sub('chatlog', 'xml')),
|
243
|
+
:verbose => false
|
244
|
+
)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
Pidgin2Adium.logMsg "Log files copied!"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# Author: Gabe Berke-Williams 2008-11-25
|
4
|
+
# Requires rubygems and hpricot (http://wiki.github.com/why/hpricot)
|
5
|
+
|
6
|
+
# Script to import pidgin logs into Adium. It uses Applescript to create new statuses in Adium.
|
7
|
+
# Stupid binary status file format. Thanks a lot, Adium.
|
8
|
+
# It doesn't work in Mac OSX 10.5 (Leopard).
|
9
|
+
# See: http://trac.adiumx.com/ticket/8863
|
10
|
+
# It should work in Mac OSX 10.4 (Tiger), but is untested.
|
11
|
+
#
|
12
|
+
# TODO: check adium version in
|
13
|
+
# /Applications/Adium.app/Contents
|
14
|
+
# with this:
|
15
|
+
# <key>CFBundleShortVersionString</key>
|
16
|
+
# <string>1.3.4</string>
|
17
|
+
# For Mac 10.5+, needs to be 1.4; should work for 10.4 with 1.3.x
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'hpricot'
|
21
|
+
|
22
|
+
module Pidgin2Adium
|
23
|
+
class Status
|
24
|
+
def initialize(xml_file)
|
25
|
+
@xml_file = File.expand_path(xml_file)
|
26
|
+
#xml_file=File.expand_path("~/Desktop/purple/status.xml")
|
27
|
+
# Unescape for Adium.
|
28
|
+
@TRANSLATIONS = {
|
29
|
+
'&' => '&',
|
30
|
+
'<' => '<',
|
31
|
+
'>' => '>',
|
32
|
+
# escape quotes for shell quoting in tell -e 'blah' below
|
33
|
+
'"' => '\"',
|
34
|
+
''' => "\\'",
|
35
|
+
"<br>" => "\n"
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def start
|
40
|
+
# For some reason Hpricot doesn't like entities in attributes,
|
41
|
+
# but since that only affects the status name, which only we see,
|
42
|
+
# that's not really a problem.
|
43
|
+
doc = Hpricot( File.read(xml_file) )
|
44
|
+
$max_id = get_max_status_id
|
45
|
+
# remove <substatus>'s because sometimes their message is different
|
46
|
+
# from the actual message, and we don't want to grab them accidentally
|
47
|
+
doc.search('substatus').remove
|
48
|
+
|
49
|
+
doc.search('status').each do |status|
|
50
|
+
next if status.search('message').empty?
|
51
|
+
add_status_to_adium(status)
|
52
|
+
end
|
53
|
+
|
54
|
+
puts "All statuses have been migrated. Enjoy!"
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def unescape(str)
|
59
|
+
unescaped_str = str.clone
|
60
|
+
# Unescape the escaped entities in Pidgin's XML.
|
61
|
+
# translate "&" first because sometimes the entities are
|
62
|
+
# like "&gt;"
|
63
|
+
unescaped_str.gsub!('&', '&')
|
64
|
+
TRANSLATIONS.each do |k,v|
|
65
|
+
unescaped_str.gsub!(k, v)
|
66
|
+
end
|
67
|
+
return unescaped_str
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_max_status_id
|
71
|
+
# osascript line returns like so: "-1000, -8000, -1001, 24, -1002\n"
|
72
|
+
# Turn the single String into an array of Fixnums.
|
73
|
+
script = `osascript -e 'tell application "Adium" to get id of every status'`
|
74
|
+
id = script.split(',').map{ |x| x.to_i }.max
|
75
|
+
return id
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_status_to_adium(elem)
|
79
|
+
# pass in <status> element
|
80
|
+
id = ($max_id += 1)
|
81
|
+
# status_type is invisible/available/away
|
82
|
+
status_type = elem.search('state').inner_html
|
83
|
+
title = unescape( elem[:name] )
|
84
|
+
status_message = unescape( elem.search(:message).inner_html )
|
85
|
+
puts '-' * 80
|
86
|
+
puts "status_type: #{status_type}"
|
87
|
+
puts "title: #{title}"
|
88
|
+
puts "status_message: #{status_message}"
|
89
|
+
puts '-' * 80
|
90
|
+
# TODO: when it actually works, remove this line
|
91
|
+
command="osascript -e 'tell application \"Adium\" to set myStat to (make new status with properties {id:#{id}, saved:true, status type:#{status_type}, title:\"#{title}\", message:\"#{status_message}\", autoreply:\"#{status_message}\"})'"
|
92
|
+
# TODO: popen[123]?
|
93
|
+
p `#{command}`
|
94
|
+
if $? != 0
|
95
|
+
puts "*" * 80
|
96
|
+
puts "command: #{command}"
|
97
|
+
puts "Uh-oh. Something went wrong."
|
98
|
+
puts "The command that failed is above."
|
99
|
+
# given 10.x.y, to_f leaves off y
|
100
|
+
if `sw_vers -productVersion`.to_f == 10.5
|
101
|
+
puts "You are running Mac OS X 10.5 (Leopard)."
|
102
|
+
puts "This script does not work for that version."
|
103
|
+
puts "It should work for Mac OS X 10.4 (Tiger),"
|
104
|
+
puts "but is untested."
|
105
|
+
puts "See: http://trac.adiumx.com/ticket/8863"
|
106
|
+
end
|
107
|
+
puts "Return status: #{$?}"
|
108
|
+
puts "Error, exiting."
|
109
|
+
raise "You need Mac OS X Tiger (10.4)"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pidgin2adium
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: universal-darwin
|
6
|
+
authors:
|
7
|
+
- Gabe B-W
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-24 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Converts Pidgin logs and statuses to Adium format and makes them available to Adium. Also installstwo shell scripts, pidgin2adium_logs and pidgin2adium_status.
|
17
|
+
email: gbw@rubyforge.org
|
18
|
+
executables:
|
19
|
+
- pidgin2adium_logs
|
20
|
+
- pidgin2adium_status
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- lib/pidgin2adium/balance-tags.rb
|
27
|
+
- lib/pidgin2adium/ChatFileGenerator.rb
|
28
|
+
- lib/pidgin2adium/logs.rb
|
29
|
+
- lib/pidgin2adium/SrcFileParse.rb
|
30
|
+
- lib/pidgin2adium/status.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: http://pidgin2adium.rubyforge.org
|
33
|
+
licenses: []
|
34
|
+
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
rubyforge_project: pidgin2adium
|
55
|
+
rubygems_version: 1.3.5
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: Converts Pidgin logs and statuses to Adium format and makes them available to Adium.
|
59
|
+
test_files: []
|
60
|
+
|