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.
@@ -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
+