pidgin2adium 1.0.0-universal-darwin

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: "&lt;AUTO-REPLY&gt;" or nil
333
+ # 3: message body
334
+ # <span style='color: #000000;'>test sms</span>
335
+ @lineRegex = /#{@timestampRegexStr} ?<b>(.*?) ?(&lt;AUTO-REPLY&gt;)?:?<\/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 '&amp;' only if it's not followed by an entity.
445
+ @body.gsub!(/&(?!lt|gt|amp|quot|apos)/, '&amp;')
446
+ # replace single quotes with '&apos;' 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})/, '&lt;\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
+ '&amp;' => '&',
30
+ '&lt;' => '<',
31
+ '&gt;' => '>',
32
+ # escape quotes for shell quoting in tell -e 'blah' below
33
+ '&quot;' => '\"',
34
+ '&apos;' => "\\'",
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 "&amp;" first because sometimes the entities are
62
+ # like "&amp;gt;"
63
+ unescaped_str.gsub!('&amp;', '&')
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
+