pidgin2adium 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/pidgin2adium_logs +67 -0
- data/bin/pidgin2adium_status +15 -0
- data/lib/pidgin2adium/ChatFileGenerator.rb +243 -0
- data/lib/pidgin2adium/SrcFileParse.rb +280 -0
- data/lib/pidgin2adium/balance-tags.rb +115 -0
- data/lib/pidgin2adium/logs.rb +250 -0
- data/lib/pidgin2adium/status.rb +108 -0
- metadata +69 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
=begin
|
4
|
+
Author: Gabe Berke-Williams, 2008
|
5
|
+
This is the shell script, which is a wrapper around Pidgin2Adium::Logs.
|
6
|
+
Call it like so:
|
7
|
+
<tt>pidgin2adium_logs.rb -i ~/in_logs/ -o ~/out_logs/ -l AIM.myscreenname -a me,screenname,my_pidgin_alias,other_pidgin_alias</tt>
|
8
|
+
For <tt>-a/--aliases</tt>, there is no need to use spaces or capitalization, since spaces will be stripped out and the aliases will
|
9
|
+
be lowercased anyway.
|
10
|
+
=end
|
11
|
+
|
12
|
+
require 'pidgin2adium/logs'
|
13
|
+
require 'optparse'
|
14
|
+
|
15
|
+
options = {}
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: #{File.basename($0)} [options]"
|
18
|
+
opts.on('-i IN_DIR', '--in IN_DIR', 'Specify directory where pidgin logs are stored') do |v|
|
19
|
+
options[:in] = v
|
20
|
+
end
|
21
|
+
opts.on('-o OUT_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 '&' only if it's not followed by an entity.
|
8
|
+
body.gsub!(/&(?!lt|gt|amp|quot|apos)/, '&')
|
9
|
+
# replace single quotes with ''' but only outside <span>s.
|
10
|
+
parts = body.split(/(<\/?span.*?>)/)
|
11
|
+
body = parts.map{ |part| part.match(/<\/?span/) ? part : part.gsub("'", ''') }.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: "<AUTO-REPLY>" or nil
|
194
|
+
# 3: message body
|
195
|
+
# <span style='color: #000000;'>test sms</span>
|
196
|
+
@line_regex = /#{@timestamp_regex_str} ?<b>(.*?) ?(<AUTO-REPLY>)?:?<\/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})/, '<\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
|
+
'&' => '&',
|
25
|
+
'<' => '<',
|
26
|
+
'>' => '>',
|
27
|
+
# escape quotes for shell quoting in tell -e 'blah' below
|
28
|
+
'"' => '\"',
|
29
|
+
''' => "\\'",
|
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 "&" first because sometimes the entities are
|
57
|
+
# like "&gt;"
|
58
|
+
unescaped_str.gsub!('&', '&')
|
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
|
+
|