pidgin2adium 1.0.0-universal-darwin
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/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
|
+
|