pidgin2adium 0.0.1

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_DIR', '--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 LIBRARY_DIR', '--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("--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 MY_ALIASES_AND_SNs', "--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(src=options[:in],
60
+ out = options[:out],
61
+ aliases = options[:aliases],
62
+ libdir = options[:libdir],
63
+ tz = options[:timezone],
64
+ debug = 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,243 @@
1
+ # ADD DOCUMENTATION
2
+ require 'pidgin2adium/balance-tags.rb'
3
+ require 'hpricot'
4
+
5
+ module Pidgin2Adium
6
+ def Pidgin2Adium.normalizeBodyEntities!(body)
7
+ # Convert '&' to '&amp;' only if it's not followed by an entity.
8
+ body.gsub!(/&(?!lt|gt|amp|quot|apos)/, '&amp;')
9
+ # replace single quotes with '&apos;' but only outside <span>s.
10
+ parts = body.split(/(<\/?span.*?>)/)
11
+ body = parts.map{ |part| part.match(/<\/?span/) ? part : part.gsub("'", '&apos;') }.join('')
12
+ end
13
+
14
+ def Pidgin2Adium.normalizeBody!(body, aliasStr)
15
+ # Fix mismatched tags.
16
+ body = Pidgin2Adium.balance_tags(body)
17
+ normalizeBodyEntities!(body)
18
+ if aliasStr[0,3] == '***'
19
+ # "***<alias>" is what pidgin sets as the alias for a /me action
20
+ aliasStr.slice!(0,3)
21
+ body = '*' + body + '*'
22
+ end
23
+ body = '<div><span style="font-family: Helvetica; font-size: 12pt;">' +
24
+ body +
25
+ '</span></div>'
26
+ end
27
+
28
+ class ChatFileGenerator
29
+ def initialize(service, mySN, otherPersonsSN, chatTimePidgin_start, tzOffset, masterAlias, destDirBase)
30
+ # basicTimeInfo is for files that only have the full timestamp at
31
+ # the top; we can use it to fill in the minimal per-line timestamps.
32
+ # It has only 3 elements ([year, month, dayofmonth]) because
33
+ # you should be able to fill everything else in.
34
+ # If you can't, something's wrong.
35
+ @basicTimeInfo = nil
36
+ # @chatMessage is a 2D array composed of arrays like so (e.g.):
37
+ # ['time'=>'2:23:48 PM', 'alias'=>'Me', 'status' => 'available', 'body'=>'abcdefg', auto-reply=true]
38
+ @chatMessage=[]
39
+ # chatTimeAdium_start format: YYYY-MM-DD\THH.MM.SS[+-]TZ_HRS like so:
40
+ # 2008-10-05T22.26.20-0800
41
+ @chatTimeAdium_start=nil
42
+ @chatTimePidgin_start=chatTimePidgin_start
43
+ @destDirBase=destDirBase
44
+ @masterAlias=masterAlias
45
+ @mySN=mySN
46
+ @otherPersonsSN=otherPersonsSN
47
+ @service=service
48
+ @tzOffset=tzOffset
49
+ # key is for Pidgin, value is for Adium
50
+ # Just used for <service>.<screenname> in directory structure
51
+ @SERVICE_NAME_MAP={'aim'=>'AIM',
52
+ 'jabber'=>'jabber',
53
+ 'gtalk'=>'GTalk',
54
+ 'icq' => 'ICQ',
55
+ 'qq'=>'QQ',
56
+ 'msn'=>'MSN',
57
+ 'yahoo'=>'Yahoo'}
58
+ end
59
+
60
+ def convert()
61
+ initChatTime()
62
+ return buildDomAndOutput()
63
+ end
64
+
65
+ def initChatTime()
66
+ # ParseDate.parsedate "Tuesday, July 5th, 2007, 18:35:20 UTC"
67
+ # # => [2007, 7, 5, 18, 35, 20, "UTC", 2]
68
+ # [year, month, day of month, hour, minute, sec, timezone, day of week]
69
+ # strtotime returns seconds since the epoch
70
+ @chatTimeAdium_start = createAdiumDate(@chatTimePidgin_start)
71
+ @basicTimeInfo = ParseDate.parsedate(@chatTimePidgin_start)[0..2]
72
+ end
73
+
74
+ # Add a line to @chatMessage.
75
+ # It is its own method because attr_writer creates the method 'chatMessage=', which doesn't help for chatMessage.push
76
+ def appendLine(line)
77
+ @chatMessage.push(line)
78
+ end
79
+
80
+ #
81
+ def createAdiumDate(date)
82
+ epochSecs = getEpochSeconds(date)
83
+ if @tzOffset.nil?
84
+ Pidgin2Adium.logMsg("@tzOffset is nil. This really shouldn't happen.", true)
85
+ @tzOffset = "+0"
86
+ end
87
+ return Time.at(epochSecs).strftime("%Y-%m-%dT%H.%M.%S#{@tzOffset}")
88
+ end
89
+
90
+ def getEpochSeconds(timestr)
91
+ parsed_date = ParseDate.parsedate(timestr)
92
+ [0, 1, 2].each do |i|
93
+ parsed_date[i] = @basicTimeInfo[i] if parsed_date[i].nil?
94
+ end
95
+ return Time.local(*parsed_date).tv_sec
96
+ end
97
+
98
+ def getScreenNameByAlias(aliasStr)
99
+ myAliasStr = aliasStr.clone
100
+ myAliasStr.slice!(0,3) if myAliasStr[0,3] == '***'
101
+ if aliasStr==""
102
+ return ""
103
+ else
104
+ return @masterAlias.include?(myAliasStr.downcase.gsub(/\s*/, '')) ? @mySN : @otherPersonsSN
105
+ end
106
+ end
107
+
108
+ # returns path of output file
109
+ def buildDomAndOutput()
110
+ serviceName = @SERVICE_NAME_MAP[@service.downcase]
111
+ destDirReal = File.join(@destDirBase, "#{serviceName}.#{@mySN}", @otherPersonsSN, "#{@otherPersonsSN} (#{@chatTimeAdium_start}).chatlog")
112
+ FileUtils.mkdir_p(destDirReal)
113
+ destFilePath = destDirReal + '/' + "#{@otherPersonsSN} (#{@chatTimeAdium_start}).xml"
114
+ if File.exist?(destFilePath)
115
+ return Pidgin2Adium::Logs::FILE_EXISTS
116
+ end
117
+
118
+ # no \n before </chat> because {body} has it already
119
+ chatLogTemplate = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
120
+ "<chat xmlns=\"http://purl.org/net/ulf/ns/0.4-02\" account=\"#{@mySN}\" service=\"#{serviceName}\">\n{body}</chat>"
121
+
122
+ allMsgs = ""
123
+ @chatMessage.each do |msg|
124
+ # template is set to a copy of one of the three templates,
125
+ # the {...} vars are subbed, and then it's added to allMsgs
126
+ template = nil
127
+ # Note:
128
+ # away/auto message has both body and status set
129
+ # pure status has status but not body set
130
+ # pure message has body set but not status
131
+ begin
132
+ chatTimeAdium = createAdiumDate(msg['time'])
133
+ rescue TypeError => bang
134
+ puts '*' * 80
135
+ @chatMessage.each { |m| p m }
136
+ puts "Oops! Time error! on msg:"
137
+ p msg
138
+ puts "Rest of message is above, just below the stars."
139
+ return false
140
+ end
141
+ sender = getScreenNameByAlias(msg['alias'])
142
+ time = chatTimeAdium
143
+ aliasStr = msg['alias']
144
+ if msg['body']
145
+ body = msg['body']
146
+ if msg['status'].nil?
147
+ # Body with no status
148
+ if msg['auto-reply'] == true
149
+ # auto-reply from away message
150
+ template = AutoReplyMessage.new(sender, time, aliasStr, body)
151
+ else
152
+ # pure regular message
153
+ template = XMLMessage.new(sender, time, aliasStr, body)
154
+ end
155
+ else
156
+ # Body with status message
157
+ template = AwayMessage.new(sender, time, aliasStr, body)
158
+ end
159
+ elsif msg['status']
160
+ # Status message, no body
161
+ template = StatusMessage.new(sender, time, aliasStr, msg['status'])
162
+ else
163
+ Pidgin2Adium.logMsg("msg has neither status nor body key set. Unsure what to do. msg is as follows:", true)
164
+ Pidgin2Adium.logMsg(sprintf('%p', msg), true)
165
+ return false
166
+ end
167
+ begin
168
+ allMsgs += template.getOutput()
169
+ rescue TypeError => bang
170
+ Pidgin2Adium.logMsg "TypeError: #{bang.message}"
171
+ Pidgin2Adium.logMsg "This is probably caused by an unrecognized status string."
172
+ Pidgin2Adium.logMsg "Go to the file currently being worked on (displayed above) at time #{msg['time']}"
173
+ Pidgin2Adium.logMsg "and add the status message there to one of the hashes in SrcHtmlFileParse.getAliasAndStatus."
174
+ Pidgin2Adium.logMsg "**Debug info**"
175
+ Pidgin2Adium.logMsg "msg: #{msg.inspect}"
176
+ Pidgin2Adium.logMsg "--"
177
+ Pidgin2Adium.logMsg "Exiting."
178
+ return false
179
+ end
180
+ end
181
+ ret = chatLogTemplate.sub("{body}", allMsgs)
182
+ # xml is ok.
183
+
184
+ # we already checked to see if the file previously existed.
185
+ outfile = File.new(destFilePath, 'w')
186
+ outfile.puts(ret)
187
+ outfile.close
188
+ return destFilePath
189
+ end
190
+
191
+ # A holding object for each line of the chat.
192
+ # It is subclassed as appropriate (eg AutoReplyMessage).
193
+ # All Messages have senders, times, and aliases.
194
+ class Message
195
+ def initialize(sender, time, aliasStr)
196
+ @sender = sender
197
+ @time = time
198
+ @aliasStr = aliasStr
199
+ end
200
+ end
201
+
202
+ # Basic message with body text (as opposed to pure status messages which have no body).
203
+ class XMLMessage < Message
204
+ def initialize(sender, time, aliasStr, body)
205
+ super(sender, time, aliasStr)
206
+ @body = Pidgin2Adium.normalizeBody!(body, @aliasStr)
207
+ end
208
+
209
+ def getOutput
210
+ return sprintf('<message sender="%s" time="%s" alias="%s">%s</message>' + "\n",
211
+ @sender, @time, @aliasStr, @body)
212
+ end
213
+
214
+ end
215
+
216
+ # An auto reply message, meaning it has a body.
217
+ class AutoReplyMessage < XMLMessage
218
+ def getOutput
219
+ return sprintf('<message sender="%s" time="%s" alias="%s" auto="true">%s</message>' + "\n",
220
+ @sender, @time, @aliasStr, @body)
221
+ end
222
+ end
223
+
224
+ class AwayMessage < XMLMessage
225
+ def getOutput
226
+ return sprintf('<status type="away" sender="%s" time="%s" alias="%s">%s</status>' + "\n",
227
+ @sender, @time, @aliasStr, @body)
228
+ end
229
+ end
230
+
231
+ # A message saying e.g. "Blahblah has gone away."
232
+ class StatusMessage < Message
233
+ def initialize(sender, time, aliasStr, status)
234
+ super(sender, time, aliasStr)
235
+ @status = status
236
+ end
237
+ def getOutput
238
+ return sprintf('<status type="%s" sender="%s" time="%s" alias="%s"/>' + "\n",
239
+ @status, @sender, @time, @aliasStr)
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,280 @@
1
+ # =SrcFileParse
2
+ # The class SrcFileParse has two 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
+ module Pidgin2Adium
6
+ # The two subclasses of SrcFileParse,
7
+ # SrcTxtFileParse and SrcHtmlFileParse, only differ
8
+ # in that they have their own @line_regex, @line_regex_status,
9
+ # and most importantly, createMsgData, which takes the
10
+ # +MatchData+ objects from matching against @line_regex and
11
+ # fits them into hashes.
12
+ class SrcFileParse
13
+ def initialize(srcPath, destDirBase, masterAlias, userTZ, userTZOffset)
14
+ @srcPath = srcPath
15
+ # these two are to pass to chatFG in parseFile
16
+ @destDirBase = destDirBase
17
+ @masterAlias = masterAlias
18
+ @userTZ = userTZ
19
+ @userTZOffset = userTZOffset
20
+ # Automagically does grouping for you. Will be inserted in @line_regex{,_status}
21
+ @timestamp_regex_str = '\(((?:\d{4}-\d{2}-\d{2} )?\d{1,2}:\d{1,2}:\d{1,2}(?: .{1,2})?)\)'
22
+ # the first line is special: it tells us
23
+ # 1) who we're talking to
24
+ # 2) what time/date
25
+ # 3) what SN we used
26
+ # 4) what protocol (AIM, jabber...)
27
+ @first_line_regex = /Conversation with (.*?) at (.*?) on (.*?) \((.*?)\)/s
28
+ end
29
+
30
+ # Takes the body of a line of a chat and returns the [username, status] as a 2-element array.
31
+ # Example:
32
+ # Pass in "Generic Screenname228 has signed off" and it returns <tt>["Generic Screenname228", "offline"]</tt>
33
+ def getAliasAndStatus(str)
34
+ alias_and_status = [nil, nil]
35
+
36
+ # Screen name is in regex group 1.
37
+ status_map = {
38
+ /(.+) logged in\.$/ => 'online',
39
+ /(.+) logged out\.$/ => 'offline',
40
+ /(.+) has signed on\.$/ => 'online',
41
+ /(.+) has signed off\.$/ => 'offline',
42
+ /(.+) has gone away\.$/ => 'away',
43
+ /(.+) is no longer away\.$/ => 'available',
44
+ /(.+) has become idle\.$/ => 'idle',
45
+ /(.+) is no longer idle\.$/ => 'available',
46
+ # file transfer
47
+ /Starting transfer of .+ from (.+)/ => 'file-transfer-start',
48
+ /^Offering to send .+ to (.+)$/ => 'fileTransferRequested',
49
+ /(.+) is offering to send file/ => 'fileTransferRequested',
50
+ }
51
+
52
+ # statuses that come from my end. I totally made up these status names.
53
+ my_status_map = {
54
+ # encryption
55
+ /^Received message encrypted with wrong key$/ => 'encrypt-error',
56
+ /^Requesting key\.\.\.$/ => 'encrypt-error',
57
+ /^Outgoing message lost\.$/ => 'encrypt-error',
58
+ /^Conflicting Key Received!$/ => 'encrypt-error',
59
+ /^Error in decryption- asking for resend\.\.\.$/ => 'encrypt-error',
60
+ /^Making new key pair\.\.\.$/ => 'encrypt-key-create',
61
+ # file transfer - these are in this (non-used) list because you can't get the alias out of matchData[1]
62
+ /^You canceled the transfer of .+$/ => 'file-transfer-cancel',
63
+ /^Transfer of file .+ complete$/ => 'fileTransferCompleted',
64
+ # sending errors
65
+ /^Last outgoing message not received properly- resetting$/ => 'sending-error',
66
+ /^Resending\.\.\.$/ => 'sending-error',
67
+ # connection errors
68
+ /^Lost connection with the remote user:<br\/>Remote host closed connection\.$/ => 'lost-remote-conn',
69
+ # direct IM stuff
70
+ /^Attempting to connect to .+ at .+ for Direct IM\./ => 'direct-im-connect',
71
+ /^Asking .+ to connect to us at .+ for Direct IM\./ => 'direct-im-ask',
72
+ /^Direct IM with .+ failed/ => 'direct-im-failed',
73
+ /^Attempting to connect to .+\.$/ => 'direct-im-connect',
74
+ /^Attempting to connect via proxy server\.$/ => 'direct-im-proxy',
75
+ /^Direct IM established$/ => 'direct-im-established',
76
+ /^Lost connection with the remote user:<br\/>Windows socket error/ => 'direct-im-lost-conn',
77
+ # chats
78
+ /^.+ entered the room\.$/ => 'chat-entered-room',
79
+ /^.+ left the room\.$/ => 'chat-left-room'
80
+ }
81
+
82
+ regex, status = status_map.detect{ |regex, status| regex.match(str) }
83
+ if regex and status
84
+ alias_and_status = [regex.match(str)[1], status]
85
+ else
86
+ # not one of the regular statuses, try my statuses.
87
+ regex, status = my_status_map.detect{ |regex, status| regex.match(str) }
88
+ alias_and_status = ['System Message', status]
89
+ end
90
+ return alias_and_status
91
+ end
92
+
93
+ def getTimeZoneOffset()
94
+ tz_regex = /([-+]\d+)[A-Z]{3}\.(txt|html?)/
95
+ tz_match = tz_regex.match(@srcPath)
96
+ tz_offset = tz_match.nil? ? @userTZOffset : tz_match[1]
97
+ return tz_offset
98
+ end
99
+
100
+ # parseFile slurps up @srcPath into one big string and runs
101
+ # SrcHtmlFileParse.cleanup if it's an HTML file.
102
+ # It then uses regexes to break up the string, uses createMsgData
103
+ # to turn the regex MatchData into data hashes, and feeds it to
104
+ # ChatFileGenerator, which creates the XML data string.
105
+ # This method returns a ChatFileGenerator object.
106
+ def parseFile()
107
+ fileContent = File.read(@srcPath) # one big string
108
+ if self.class == SrcHtmlFileParse
109
+ fileContent = self.cleanup(fileContent)
110
+ end
111
+ # Deal with first line.
112
+ first_line_match = @first_line_regex.match(fileContent)
113
+
114
+ if first_line_match.nil?
115
+ Pidgin2Adium.logMsg("Parsing of #{@srcPath} failed (could not find first line).", true)
116
+ return false
117
+ end
118
+ service = first_line_match[4]
119
+ # mySN is standardized to avoid "AIM.name" and "AIM.na me" folders
120
+ mySN = first_line_match[3].downcase.sub(' ', '')
121
+ otherPersonsSN = first_line_match[1]
122
+ chatTimePidgin_start = first_line_match[2]
123
+ chatFG = ChatFileGenerator.new(service,
124
+ mySN,
125
+ otherPersonsSN,
126
+ chatTimePidgin_start,
127
+ getTimeZoneOffset(),
128
+ @masterAlias,
129
+ @destDirBase)
130
+ all_line_matches = fileContent.scan( Regexp.union(@line_regex, @line_regex_status) )
131
+
132
+ # an empty chat window that got saved
133
+ if all_line_matches.empty?
134
+ return chatFG
135
+ end
136
+
137
+ all_line_matches.each do |line|
138
+ chatFG.appendLine( createMsgData(line) )
139
+ end
140
+ return chatFG
141
+ end
142
+ end
143
+
144
+ class SrcTxtFileParse < SrcFileParse
145
+ def initialize(srcPath, destDirBase, masterAlias, userTZ, userTZOffset)
146
+ super(srcPath, destDirBase, masterAlias, userTZ, userTZOffset)
147
+ # @line_regex matches a line in an HTML log file other than the first
148
+ # @line_regex matchdata:
149
+ # 0: timestamp
150
+ # 1: screen name
151
+ # 2: "<AUTO-REPLY>" or nil
152
+ # 3: message
153
+ @line_regex = /#{@timestamp_regex_str} (.*?) ?(<AUTO-REPLY>)?: (.*)$/
154
+ # @line_regex_status matches a status line
155
+ # @line_regex_status matchdata:
156
+ # 0: timestamp
157
+ # 1: message
158
+ @line_regex_status = /#{@timestamp_regex_str} ([^:]+?)[\r\n]{1,2}/
159
+ end
160
+
161
+ # createMsgData takes a +MatchData+ object (from @line_regex or @line_regex_status) and returns a hash
162
+ # with the following keys: time, alias, and message and/or status.
163
+ def createMsgData(matchObj)
164
+ msg_data_hash = { 'time' => nil, 'alias' => nil, 'status' => nil, 'body' => nil, 'auto-reply' => nil }
165
+ if matchObj[4..5] == [nil, nil]
166
+ # regular message
167
+ # ["10:58:29", "BuddyName", "<AUTO-REPLY>", "hello!\r", nil, nil]
168
+ msg_data_hash['time'] = matchObj[0]
169
+ msg_data_hash['alias'] = matchObj[1]
170
+ msg_data_hash['auto-reply'] = (matchObj[2] != nil)
171
+ # strip() to remove "\r" from end
172
+ msg_data_hash['body'] = matchObj[3].strip
173
+ elsif matchObj[0..3] == [nil, nil, nil, nil]
174
+ # status message
175
+ # [nil, nil, nil, nil, "22:58:00", "BuddyName logged in."]
176
+ alias_and_status = getAliasAndStatus(matchObj[5])
177
+ msg_data_hash['time'] = matchObj[4]
178
+ msg_data_hash['alias'] = alias_and_status[0]
179
+ msg_data_hash['status'] = alias_and_status[1]
180
+ end
181
+ return msg_data_hash
182
+ end
183
+ end
184
+
185
+ class SrcHtmlFileParse < SrcFileParse
186
+ def initialize(srcPath, destDirBase, masterAlias, userTZ, userTZOffset)
187
+ super(srcPath, destDirBase, masterAlias, userTZ, userTZOffset)
188
+ # @line_regex matches a line in an HTML log file other than the first
189
+ # time matches on either "2008-11-17 14:12" or "14:12"
190
+ # @line_regex match obj:
191
+ # 0: timestamp, extended or not
192
+ # 1: alias
193
+ # 2: "&lt;AUTO-REPLY&gt;" or nil
194
+ # 3: message body
195
+ # <span style='color: #000000;'>test sms</span>
196
+ @line_regex = /#{@timestamp_regex_str} ?<b>(.*?) ?(&lt;AUTO-REPLY&gt;)?:?<\/b> ?(.*)<br ?\/>/ #(?:[\n\r]{1,2}<(?:font|\/body))/s
197
+ # @line_regex_status matches a status line
198
+ # @line_regex_status match obj:
199
+ # 0: timestamp
200
+ # 1: status message
201
+ @line_regex_status = /#{@timestamp_regex_str} ?<b> (.*?)<\/b><br\/>/
202
+ end
203
+
204
+ # createMsgData takes a +MatchData+ object (from @line_regex or @line_regex_status) and returns a hash
205
+ # with the following keys: time, alias, and message and/or status.
206
+ def createMsgData(matchObj)
207
+ msg_data_hash = { 'time' => nil,
208
+ 'alias' => nil,
209
+ 'auto-reply' => nil,
210
+ 'body' => nil,
211
+ 'status' => nil}
212
+ # the Regexp.union leaves nil where one of the regexes didn't match.
213
+ # (Is there any way to have it not do this?)
214
+ # ie
215
+ # the first one matches: ['foo', 'bar', 'baz', 'bash', nil, nil]
216
+ # second one matches: [nil, nil, nil, nil, 'bim', 'bam']
217
+ if matchObj[0..3] == [nil, nil, nil, nil]
218
+ # This is a status message.
219
+ # slice off results from other Regexp
220
+ # becomes: ["11:27:53", "Generic Screenname228 logged in."]
221
+ matchObj = matchObj[4..5]
222
+ alias_and_status = getAliasAndStatus(matchObj[1])
223
+ msg_data_hash['time'] = matchObj[0]
224
+ msg_data_hash['alias'] = alias_and_status[0]
225
+ msg_data_hash['status'] = alias_and_status[1]
226
+ elsif matchObj[4..5] == [nil, nil]
227
+ # Either a regular message line or an auto-reply/away message.
228
+ # slice off results from other Regexp
229
+ matchObj = matchObj[0..3]
230
+ msg_data_hash['time'] = matchObj[0]
231
+ msg_data_hash['alias'] = matchObj[1]
232
+ msg_data_hash['body'] = matchObj[3]
233
+ if not matchObj[2].nil?
234
+ # an auto-reply message
235
+ msg_data_hash['auto-reply'] = true
236
+ end
237
+ end
238
+ return msg_data_hash
239
+ end
240
+
241
+ # Removes <font> tags, empty <a>s, spans with either no color
242
+ # information or color information that just turns the text black.
243
+ # Returns a string.
244
+ def cleanup(text)
245
+ color_regex = /.*(color: ?#[[:alnum:]]{6}; ?).*/
246
+ # For some reason, Hpricot doesn't work well with
247
+ # elem.swap(elem.innerHTML) when the elements are nested
248
+ # (eg doc.search('font') only returns the outside <font> tags,
249
+ # not "font font") and also it appears that it doesn't reinterpret
250
+ # the doc when outside tags are swapped with their innerHTML (so
251
+ # when <html> tags are replaced with their innerHTML, then
252
+ # a search for <font> tags in the new HTML fails).
253
+ # Long story short, we use gsub.
254
+ text.gsub!(/<\/?(html|body|font).*?>/, '')
255
+ doc = Hpricot(text)
256
+ # These empty links sometimes are appended to every line in a chat,
257
+ # for some weird reason. Remove them.
258
+ doc.search("a[text()='']").remove
259
+ spans = doc.search('span')
260
+ spans.each do |span|
261
+ if span.empty?
262
+ Hpricot::Elements[span].remove
263
+ else
264
+ # No need to check for the span.attributes.key?('style')
265
+ if span[:style] =~ color_regex
266
+ # Remove black-text spans after other processing because
267
+ # the processing can reduce spans to that
268
+ span[:style] = span[:style].gsub(color_regex, '\1').
269
+ gsub(/color: ?#000000; ?/,'')
270
+ # Remove span but keep its contents
271
+ span.swap(span.innerHTML) if span[:style] == ''
272
+ else
273
+ span.swap(span.innerHTML)
274
+ end
275
+ end
276
+ end
277
+ return doc.to_html
278
+ end
279
+ end
280
+ 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.balance_tags( 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
+ # WP bug fix for LOVE <3 (and other situations with '<' before a number)
22
+ text.gsub!(/<([0-9]{1})/, '&lt;\1')
23
+
24
+ while ( regex = text.match(tag_regex) )
25
+ regex = regex.to_a
26
+ newtext += tagqueue
27
+ i = text.index(regex[0])
28
+ l = regex[0].length
29
+
30
+ # clear the shifter
31
+ tagqueue = ''
32
+ # Pop or Push
33
+ if (regex[1][0,1] == "/") # End Tag
34
+ tag = regex[1][1,regex[1].length].downcase
35
+ # if too many closing tags
36
+ if(stacksize <= 0)
37
+ tag = ''
38
+ #or close to be safe tag = '/' . tag
39
+ # if stacktop value = tag close value then pop
40
+ elsif (tagstack[stacksize - 1] == tag) # found closing tag
41
+ tag = '</' + tag + '>'; # Close Tag
42
+ # Pop
43
+ tagstack.pop
44
+ stacksize -= 1
45
+ else # closing tag not at top, search for it
46
+ (stacksize-1).downto(0) do |j|
47
+ if (tagstack[j] == tag)
48
+ # add tag to tagqueue
49
+ ss = stacksize - 1
50
+ ss.downto(j) do |k|
51
+ tagqueue += '</' + tagstack.pop + '>'
52
+ stacksize -= 1
53
+ end
54
+ break
55
+ end
56
+ end
57
+ tag = ''
58
+ end
59
+ else
60
+ # Begin Tag
61
+ tag = regex[1].downcase
62
+
63
+ # Tag Cleaning
64
+ if( (regex[2].slice(-1,1) == '/') || (tag == '') )
65
+ # If: self-closing or '', don't do anything.
66
+ elsif ( single_tags.include?(tag) )
67
+ # ElseIf: it's a known single-entity tag but it doesn't close itself, do so
68
+ regex[2] += '/'
69
+ else
70
+ # Push the tag onto the stack
71
+ # If the top of the stack is the same as the tag we want to push, close previous tag
72
+ if ((stacksize > 0) &&
73
+ ! nestable_tags.include?(tag) &&
74
+ (tagstack[stacksize - 1] == tag))
75
+ tagqueue = '</' + tagstack.pop + '>'
76
+ stacksize -= 1
77
+ end
78
+ stacksize = tagstack.push(tag).length
79
+ end
80
+
81
+ # Attributes
82
+ attributes = regex[2]
83
+ if(attributes != '')
84
+ attributes = ' ' + attributes
85
+ end
86
+ tag = '<' + tag + attributes + '>'
87
+ #If already queuing a close tag, then put this tag on, too
88
+ if (tagqueue)
89
+ tagqueue += tag
90
+ tag = ''
91
+ end
92
+ end
93
+ newtext += text[0,i] + tag
94
+ # text = substr(text,i+l)
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
+ class Time
12
+ ZoneOffset = {
13
+ 'UTC' => 0,
14
+ # ISO 8601
15
+ 'Z' => 0,
16
+ # RFC 822
17
+ 'UT' => 0, 'GMT' => 0,
18
+ 'EST' => -5, 'EDT' => -4,
19
+ 'CST' => -6, 'CDT' => -5,
20
+ 'MST' => -7, 'MDT' => -6,
21
+ 'PST' => -8, 'PDT' => -7,
22
+ # Following definition of military zones is original one.
23
+ # See RFC 1123 and RFC 2822 for the error in RFC 822.
24
+ 'A' => +1, 'B' => +2, 'C' => +3, 'D' => +4, 'E' => +5, 'F' => +6,
25
+ 'G' => +7, 'H' => +8, 'I' => +9, 'K' => +10, 'L' => +11, 'M' => +12,
26
+ 'N' => -1, 'O' => -2, 'P' => -3, 'Q' => -4, 'R' => -5, 'S' => -6,
27
+ 'T' => -7, 'U' => -8, 'V' => -9, 'W' => -10, 'X' => -11, 'Y' => -12
28
+ }
29
+ # Returns offset in hours, e.g. '+0900'
30
+ def Time.zone_offset(zone, year=Time.now.year)
31
+ off = nil
32
+ zone = zone.upcase
33
+ if /\A([+-])(\d\d):?(\d\d)\z/ =~ zone
34
+ off = ($1 == '-' ? -1 : 1) * ($2.to_i * 60 + $3.to_i) * 60
35
+ elsif /\A[+-]\d\d\z/ =~ zone
36
+ off = zone.to_i
37
+ elsif ZoneOffset.include?(zone)
38
+ off = ZoneOffset[zone]
39
+ elsif ((t = Time.local(year, 1, 1)).zone.upcase == zone rescue false)
40
+ off = t.utc_offset / 3600
41
+ elsif ((t = Time.local(year, 7, 1)).zone.upcase == zone rescue false)
42
+ off = t.utc_offset / 3600
43
+ end
44
+ off
45
+ end
46
+ end
47
+
48
+ module Pidgin2Adium
49
+ require 'pidgin2adium/SrcFileParse'
50
+ require 'pidgin2adium/ChatFileGenerator'
51
+ require 'parsedate'
52
+ require 'fileutils'
53
+
54
+ # put's content. Also put's to @LOG_FILE_FH if @debug == true.
55
+ def Pidgin2Adium.logMsg(str, isError=false)
56
+ content = str.to_s
57
+ if isError == true
58
+ content= "ERROR: #{str}"
59
+ end
60
+ puts content
61
+ end
62
+
63
+ class Logs
64
+ # FILE_EXISTS is returned by ChatFileGenerator.buildDomAndOutput() if the output logfile already exists.
65
+ FILE_EXISTS = 42
66
+ def initialize(src, out, aliases, libdir, tz=nil, debug=false)
67
+ # These files/directories show up in Dir.entries(x)
68
+ @BAD_DIRS = %w{. .. .DS_Store Thumbs.db .system}
69
+ src = File.expand_path(src)
70
+ out = File.expand_path(out)
71
+ unless File.directory?(src)
72
+ puts "Source directory #{src} does not exist or is not a directory."
73
+ raise Errno::ENOENT
74
+ end
75
+ unless File.directory?(out)
76
+ begin
77
+ FileUtils.mkdir_p(out)
78
+ rescue
79
+ puts "Output directory #{out} does not exist or is not a directory and could not be created."
80
+ raise Errno::ENOENT
81
+ end
82
+ end
83
+
84
+ if libdir.nil?
85
+ puts "You must provide libdir."
86
+ raise Error
87
+ end
88
+
89
+ @src_dir = src
90
+ @out_dir = out
91
+
92
+ # Whitespace is removed for easy matching later on.
93
+ @my_aliases = aliases.map{|x| x.downcase.gsub(/\s+/,'') }.uniq
94
+ # @libdir is the directory in
95
+ # ~/Library/Application Support/Adium 2.0/Users/Default/Logs/.
96
+ # For AIM, it's like "AIM.<screenname>"
97
+ @libdir = libdir
98
+ @debug = debug
99
+ @DEFAULT_TIME_ZONE = tz || Time.now.zone
100
+ # local offset, like "-0800" or "+1000"
101
+ @DEFAULT_TZ_OFFSET = '%+03d00'%Time.zone_offset(@DEFAULT_TIME_ZONE)
102
+ end
103
+
104
+ def start
105
+ Pidgin2Adium.logMsg "Begin converting."
106
+ begin
107
+ filesPath = getAllChatFilesPath(@src_dir)
108
+ rescue Errno::EACCES => bang
109
+ Pidgin2Adium.logMsg("Sorry, permission denied for getting chat files from #{@src_dir}.", true)
110
+ Pidgin2Adium.logMsg("Details: #{bang.message}", true)
111
+ raise Errno::EACCES
112
+ end
113
+
114
+ Pidgin2Adium.logMsg(filesPath.length.to_s + " files to convert.")
115
+ filesPath.each do |fname|
116
+ Pidgin2Adium.logMsg("Converting #{fname}...")
117
+ convert(fname)
118
+ end
119
+
120
+ copyLogs()
121
+ deleteSearchIndexes()
122
+
123
+ Pidgin2Adium.logMsg "Finished converting! Converted #{filesPath.length} files."
124
+ end
125
+
126
+
127
+ # Problem: imported logs are viewable in the Chat Transcript Viewer, but are not indexed,
128
+ # so a search of the logs doesn't give results from the imported logs.
129
+ # To fix this, we delete the cached log indexes, which forces Adium to re-index.
130
+ def deleteSearchIndexes()
131
+ Pidgin2Adium.logMsg "Deleting log search indexes in order to force re-indexing of imported logs..."
132
+ dirtyFile=File.expand_path("~/Library/Caches/Adium/Default/DirtyLogs.plist")
133
+ logIndexFile=File.expand_path("~/Library/Caches/Adium/Default/Logs.index")
134
+ [dirtyFile, logIndexFile].each do |f|
135
+ if File.exist?(f)
136
+ if File.writable?(f)
137
+ File.delete(f)
138
+ else
139
+ Pidgin2Adium.logMsg("#{f} exists but is not writable. Please delete it yourself.", true)
140
+ end
141
+ end
142
+ end
143
+ Pidgin2Adium.logMsg "...done."
144
+ Pidgin2Adium.logMsg "When you next start the Adium Chat Transcript Viewer, it will re-index the logs, which may take a while."
145
+ end
146
+
147
+ # <tt>convert</tt> creates a new SrcHtmlFileParse or SrcTxtFileParse object,
148
+ # as appropriate, and calls its parse() method.
149
+ # Returns false if there was a problem, true otherwise
150
+ def convert(srcPath)
151
+ ext = File.extname(srcPath).sub('.', '').downcase
152
+ if(ext == "html" || ext == "htm")
153
+ srcFileParse = SrcHtmlFileParse.new(srcPath, @out_dir, @my_aliases, @DEFAULT_TIME_ZONE, @DEFAULT_TZ_OFFSET)
154
+ elsif(ext == "txt")
155
+ srcFileParse = SrcTxtFileParse.new(srcPath, @out_dir, @my_aliases, @DEFAULT_TIME_ZONE, @DEFAULT_TZ_OFFSET)
156
+ elsif(ext == "chatlog")
157
+ # chatlog FILE, not directory
158
+ Pidgin2Adium.logMsg("Found chatlog FILE - moving to chatlog DIRECTORY.")
159
+ # Create out_dir/log.chatlog/
160
+ begin
161
+ toCreate = "#{@out_dir}/#{srcPath}"
162
+ Dir.mkdir(toCreate)
163
+ rescue => bang
164
+ Pidgin2Adium.logMsg("Could not create #{toCreate}: #{bang.class} #{bang.message}", true)
165
+ return false
166
+ end
167
+ fileWithXmlExt = srcPath[0, srcPath.size-File.extname(srcPath).size] + ".xml"
168
+ # @src_dir/log.chatlog (file) -> @out_dir/log.chatlog/log.xml
169
+ File.cp(srcPath, File.join(@out_dir, srcPath, fileWithXmlExt))
170
+ Pidgin2Adium.logMsg("Copied #{srcPath} to " + File.join(@out_dir, srcPath, fileWithXmlExt))
171
+ return true
172
+ else
173
+ Pidgin2Adium.logMsg("srcPath (#{srcPath}) is not a txt, html, or chatlog file. Doing nothing.")
174
+ return false
175
+ end
176
+
177
+ chatFG = srcFileParse.parseFile()
178
+ return false if chatFG == false
179
+
180
+ destFilePath = chatFG.convert()
181
+ return \
182
+ case destFilePath
183
+ when false
184
+ Pidgin2Adium.logMsg("Converting #{srcPath} failed.", true);
185
+ false
186
+ when FILE_EXISTS
187
+ Pidgin2Adium.logMsg("File already exists.")
188
+ true
189
+ else
190
+ Pidgin2Adium.logMsg("Output to: #{destFilePath}")
191
+ true
192
+ end
193
+ end
194
+
195
+ def getAllChatFilesPath(dir)
196
+ return [] if File.basename(dir) == ".system"
197
+ # recurse into each subdir
198
+ return (Dir.glob(File.join(@src_dir, '**', '*.{html,txt}')) - @BAD_DIRS)
199
+ end
200
+
201
+ # Copies logs, accounting for timezone changes
202
+ def copyLogs
203
+ Pidgin2Adium.logMsg "Copying logs with accounting for different time zones..."
204
+ real_dest_dir = File.expand_path('~/Library/Application Support/Adium 2.0/Users/Default/Logs/') + '/' + @libdir + '/'
205
+ real_src_dir = File.join(@out_dir, @libdir) + '/'
206
+
207
+ src_entries = Dir.entries(real_src_dir)
208
+ dest_entries = Dir.entries(real_dest_dir)
209
+ both_entries = (src_entries & dest_entries) - @BAD_DIRS
210
+
211
+ both_entries.each do |name|
212
+ my_src_entries = Dir.entries(real_src_dir + name) - @BAD_DIRS
213
+ my_dest_entries = Dir.entries(real_dest_dir + name) - @BAD_DIRS
214
+
215
+ in_both = my_src_entries & my_dest_entries
216
+ in_both.each do |logdir|
217
+ FileUtils.cp(
218
+ File.join(real_src_dir, name, logdir, logdir.sub('chatlog', 'xml')),
219
+ File.join(real_dest_dir, name, logdir) + '/',
220
+ :verbose => false)
221
+ end
222
+ # The logs that are only in one of the dirs are not necessarily different logs than the dest.
223
+ # They might just have different timestamps. Thus, we use regexes.
224
+ only_in_src = my_src_entries - in_both
225
+ only_in_dest = my_dest_entries - in_both
226
+ # Move files from real_src_dir that are actually in both, but just have different time zones.
227
+ only_in_src.each do |srcLogDir|
228
+ # Match on everything except the timezone ("-0400.chatlog")
229
+ fname_beginning_regex = Regexp.new( Regexp.escape(srcLogDir.sub(/-\d{4}.\.chatlog$/, '')) )
230
+ target_chatlog_dir = only_in_dest.find{|d| d =~ fname_beginning_regex }
231
+ if target_chatlog_dir.nil?
232
+ # Only in source, so we can copy it without fear of overwriting.
233
+ target_chatlog_dir = srcLogDir
234
+ FileUtils.mkdir_p(File.join(real_dest_dir, name, target_chatlog_dir))
235
+ end
236
+ # Move to target_chatlog_dir 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(real_src_dir, name, srcLogDir, srcLogDir.sub('chatlog', 'xml')),
242
+ File.join(real_dest_dir, name, target_chatlog_dir, target_chatlog_dir.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,108 @@
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
+ # location of status.xml
13
+
14
+ require 'rubygems'
15
+ require 'hpricot'
16
+
17
+ module Pidgin2Adium
18
+ class Status
19
+ def initialize(xml_file)
20
+ @xml_file = File.expand_path(xml_file)
21
+ #xml_file=File.expand_path("~/Desktop/purple/status.xml")
22
+ # Unescape for Adium.
23
+ @TRANSLATIONS = {
24
+ '&amp;' => '&',
25
+ '&lt;' => '<',
26
+ '&gt;' => '>',
27
+ # escape quotes for shell quoting in tell -e 'blah' below
28
+ '&quot;' => '\"',
29
+ '&apos;' => "\\'",
30
+ "<br>" => "\n"
31
+ }
32
+ end
33
+
34
+ def start
35
+ # For some reason Hpricot doesn't like entities in attributes,
36
+ # but since that only affects the status name, which only we see,
37
+ # that's not really a problem.
38
+ doc = Hpricot( File.read(xml_file) )
39
+ $max_id = get_max_status_id
40
+ # remove <substatus>'s because sometimes their message is different
41
+ # from the actual message, and we don't want to grab them accidentally
42
+ doc.search('substatus').remove
43
+
44
+ doc.search('status').each do |status|
45
+ next if status.search('message').empty?
46
+ add_status_to_adium(status)
47
+ end
48
+
49
+ puts "All statuses have been migrated. Enjoy!"
50
+ end
51
+
52
+
53
+ def unescape(str)
54
+ unescaped_str = str.clone
55
+ # Unescape the escaped entities in Pidgin's XML.
56
+ # translate "&amp;" first because sometimes the entities are
57
+ # like "&amp;gt;"
58
+ unescaped_str.gsub!('&amp;', '&')
59
+ TRANSLATIONS.each do |k,v|
60
+ unescaped_str.gsub!(k, v)
61
+ end
62
+ return unescaped_str
63
+ end
64
+
65
+ def get_max_status_id
66
+ # osascript line returns like so: "-1000, -8000, -1001, 24, -1002\n"
67
+ # Turn the single String into an array of Fixnums.
68
+ script = `osascript -e 'tell application "Adium" to get id of every status'`
69
+ id = script.split(',').map{ |x| x.to_i }.max
70
+ return id
71
+ end
72
+
73
+ def add_status_to_adium(elem)
74
+ # pass in <status> element
75
+ id = ($max_id += 1)
76
+ # status_type is invisible/available/away
77
+ status_type = elem.search('state').inner_html
78
+ title = unescape( elem[:name] )
79
+ status_message = unescape( elem.search(:message).inner_html )
80
+ puts '-' * 80
81
+ puts "status_type: #{status_type}"
82
+ puts "title: #{title}"
83
+ puts "status_message: #{status_message}"
84
+ puts '-' * 80
85
+ # TODO: when it actually works, remove this line
86
+ 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}\"})'"
87
+ # TODO: popen[123]?
88
+ p `#{command}`
89
+ if $? != 0
90
+ puts "*" * 80
91
+ puts "command: #{command}"
92
+ puts "Uh-oh. Something went wrong."
93
+ puts "The command that failed is above."
94
+ # given 10.x.y, to_f leaves off y
95
+ if `sw_vers -productVersion`.to_f == 10.5
96
+ puts "You are running Mac OS X 10.5 (Leopard)."
97
+ puts "This script does not work for that version."
98
+ puts "It should work for Mac OS X 10.4 (Tiger),"
99
+ puts "but is untested."
100
+ puts "See: http://trac.adiumx.com/ticket/8863"
101
+ end
102
+ puts "Return status: #{$?}"
103
+ puts "Error, exiting."
104
+ raise "You need Mac OS X Tiger (10.4)"
105
+ end
106
+ end
107
+ end
108
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pidgin2adium
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Gabe B-W
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-05 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.8.1
24
+ version:
25
+ description: Converts Pidgin logs and statuses to Adium format and makes them available to Adium. Also installstwo shell scripts, pidgin2adium_logs and pidgin2adium_status.
26
+ email: gbw@rubyforge.org
27
+ executables:
28
+ - pidgin2adium_logs
29
+ - pidgin2adium_status
30
+ extensions: []
31
+
32
+ extra_rdoc_files: []
33
+
34
+ files:
35
+ - lib/pidgin2adium/balance-tags.rb
36
+ - lib/pidgin2adium/ChatFileGenerator.rb
37
+ - lib/pidgin2adium/logs.rb
38
+ - lib/pidgin2adium/SrcFileParse.rb
39
+ - lib/pidgin2adium/status.rb
40
+ has_rdoc: true
41
+ homepage: http://pidgin2adium.rubyforge.org
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options: []
46
+
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project: pidgin2adium
64
+ rubygems_version: 1.3.4
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: Converts Pidgin logs and statuses to Adium format and makes them available to Adium.
68
+ test_files: []
69
+