pidgin2adium 0.0.1

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_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
+