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