pidgin2adium 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2009-09-27
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,10 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile.rb
5
+ bin/pidgin2adium
6
+ lib/pidgin2adium.rb
7
+ lib/pidgin2adium/balance_tags.rb
8
+ lib/pidgin2adium/log_converter.rb
9
+ lib/pidgin2adium/log_file.rb
10
+ lib/pidgin2adium/log_parser.rb
data/README.rdoc ADDED
@@ -0,0 +1,106 @@
1
+ == pidgin2adium
2
+ * http://rubyforge.org/projects/pidgin2adium/
3
+
4
+ == DESCRIPTION:
5
+ Pidgin2Adium is a fast, easy way to convert Pidgin (formerly gaim) logs to the
6
+ Adium format.
7
+ Note that it assumes a Mac OS X environment with Adium installed.
8
+
9
+ == FEATURES/PROBLEMS:
10
+ * No problems (well, hopefully).
11
+
12
+ == SYNOPSIS:
13
+
14
+ There are two ways you can use this gem: as a script or as a library.
15
+ Both require you to provide aliases, which may require a bit of explanation.
16
+ Adium and Pidgin allow you to set aliases for buddies as well as for yourself,
17
+ so that you show up in chats as (for example) "Me" instead of as
18
+ "best_screen_name_ever_018845".
19
+
20
+ However, Pidgin then uses aliases in the log file instead of the actual screen
21
+ name, which complicates things. To parse properly, this gem needs to know which
22
+ aliases belong to you so it can map them to the correct screen name.
23
+ If it encounters an alias that you did not list, it assumes that it belongs to
24
+ the person to whom you are chatting.
25
+ Note that aliases are lower-cased and space is removed, so providing "Gabe B-W,
26
+ GBW" is the same as providing "gabeb-w,gbw".
27
+
28
+ ===Example (using script)
29
+ Assuming that:
30
+ * your Pidgin log files are in the "pidgin-logs" folder
31
+ * your various aliases in your chats are "Gabe", "Gabe B-W", and "gbw"
32
+ Then run:
33
+ pidgin2adium -i pidgin-logs -a "Gabe, Gabe B-W, gbw"
34
+
35
+ ===Example (using library)
36
+ The library style allows you to parse a log file and get back a
37
+ LogFile instance for easy reading, manipulation, etc.
38
+ You can also create log files yourself using Pidgin2Adium.parse_and_generate.
39
+
40
+ require 'pidgin2adium'
41
+ logfile = Pidgin2Adium.parse("/path/to/log/file.html", "gabe,gbw,gabeb-w")
42
+ if logfile == false
43
+ puts "Oh no! Could not parse!"
44
+ else
45
+ logfile.each do |message|
46
+ # Every message has these properties
47
+ puts "Sender's screen name: #{message.sender}"
48
+ puts "Time message was sent: #{message.time}"
49
+ puts "Sender's alias: #{message.buddy_alias}"
50
+ if [Pidgin2Adium::XMLMessage, Pidgin2Adium::AutoReplyMessage, Pidgin2Adium::Event].include?(message.class)
51
+ # All of these have a body
52
+ puts "Message body: #{message.body}"
53
+ if message.class == Pidgin2Adium::Event
54
+ puts "Event type: #{message.event_type}"
55
+ end
56
+ elsif message.class == Pidgin2Adium::StatusMessage
57
+ # Only StatusMessage has status
58
+ puts "Status: #{message.status}"
59
+ end
60
+ # Prints out the message in Adium log format
61
+ puts message.to_s
62
+ end
63
+ end
64
+
65
+ ===Example 2 (using library)
66
+ If you want to output the logfile to an output dir instead of just parsing it, use Pidgin2Adium.parse_and_generate:
67
+
68
+ require 'pidgin2adium'
69
+ opts = {:overwrite => true, :output_dir => "/my/output/dir"}
70
+ path_to_converted_log = Pidgin2Adium.parse_and_generate("/path/to/log/file.html", "gabe,gbw,gabeb-w", opts)
71
+
72
+ == REQUIREMENTS:
73
+ * None
74
+
75
+ == INSTALL:
76
+ * sudo gem install pidgin2adium
77
+
78
+ == THANKS
79
+ With thanks to Li Ma, whose blog post at
80
+ http://li-ma.blogspot.com/2008/10/pidgin-log-file-to-adium-log-converter.html
81
+ helped tremendously.
82
+
83
+ == LICENSE:
84
+
85
+ (The MIT License)
86
+
87
+ Copyright (c) 2009 Gabriel Berke-Williams
88
+
89
+ Permission is hereby granted, free of charge, to any person obtaining
90
+ a copy of this software and associated documentation files (the
91
+ 'Software'), to deal in the Software without restriction, including
92
+ without limitation the rights to use, copy, modify, merge, publish,
93
+ distribute, sublicense, and/or sell copies of the Software, and to
94
+ permit persons to whom the Software is furnished to do so, subject to
95
+ the following conditions:
96
+
97
+ The above copyright notice and this permission notice shall be
98
+ included in all copies or substantial portions of the Software.
99
+
100
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
101
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
102
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
103
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
104
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
105
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
106
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+ require './lib/pidgin2adium.rb'
6
+
7
+ Hoe.plugin :newgem
8
+ # Hoe.plugin :website
9
+ # Hoe.plugin :cucumberfeatures
10
+
11
+ # Generate all the Rake tasks
12
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
13
+ $hoe = Hoe.spec 'pidgin2adium' do
14
+ self.developer('Gabe B-W', 'gbw@brandeis.edu')
15
+ self.extra_rdoc_files = %w{README.rdoc}
16
+ #self.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
17
+ self.rubyforge_name = self.name # this is default value
18
+ # self.extra_deps = [['activesupport','>= 2.0.2']]
19
+ end
20
+
21
+ require 'newgem/tasks'
22
+ Dir['tasks/**/*.rake'].each { |t| load t }
23
+
24
+ # TODO - want other tests/tasks run by default? Add them to the list
25
+ # remove_task :default
26
+ # task :default => [:spec, :features]
data/bin/pidgin2adium ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/ruby -w
2
+
3
+ =begin
4
+ Author: Gabe Berke-Williams, 2008
5
+ Usage:
6
+ This is the shell script, which is a wrapper around Pidgin2Adium::LogConverter.
7
+ Call it like so:
8
+ <tt>pidgin2adium -i ~/in_logs/ -a "me,my_pidgin_alias,other_pidgin_alias"</tt>
9
+ For <tt>-a/--aliases</tt>, there is no need to use spaces or capitalization,
10
+ since spaces will be stripped out and the aliases will be lowercased anyway.
11
+ Aliases doesn't have to include screennames, either, since these are
12
+ automatically recognized.
13
+ =end
14
+
15
+ require 'pidgin2adium/log_converter'
16
+ require 'optparse'
17
+
18
+ options = {}
19
+ oparser = OptionParser.new do |opts|
20
+ opts.banner = "Usage: #{File.basename($0)} [options]"
21
+ opts.on('-i', '--in=IN_DIR', String, 'Specify directory where pidgin logs are stored') do |i|
22
+ options[:in] = i
23
+ end
24
+ opts.on('-a alias1,alias2', "--aliases alias1,alias2",
25
+ "A comma-separated list of your alias(es) so this script knows
26
+ which person in a chat is you.", "Whitespace and case do not matter.") do |aliases|
27
+ options[:aliases] = aliases
28
+ end
29
+ opts.on('-f', '--force', 'If this is set, then logs in the Adium log folder that have the same name as converted logs will be overwritten.') do |f|
30
+ options[:force] = f
31
+ end
32
+ opts.on_tail("-h", "--help", "Show this message") do
33
+ puts opts
34
+ exit
35
+ end
36
+ end
37
+ begin
38
+ oparser.parse!
39
+ rescue => bang
40
+ if bang.class == OptionParser::MissingArgument
41
+ # No argument provided for a switch that requires an argument.
42
+ puts '"%s" requires an argument.' % bang.args[0]
43
+ exit 1
44
+ elsif bang.class == OptionParser::InvalidOption
45
+ # Provided a switch that we don't handle.
46
+ puts '"%s" is not a valid switch.' % bang.args[0]
47
+ elsif bang.class == OptionParser::NeedlessArgument
48
+ # Raised when argument provided for a switch that doesn't take an argument.
49
+ puts bang.message
50
+ end
51
+ end
52
+
53
+ need_opts = false
54
+ required_opts = [[:i, :in], [:a, :aliases]]
55
+ required_opts.each do |short, long|
56
+ if options.has_key?(long)
57
+ next
58
+ else
59
+ need_opts = true
60
+ puts "Required option -#{short}/--#{long} missing."
61
+ end
62
+ end
63
+ if need_opts
64
+ puts oparser.to_s
65
+ exit 1
66
+ end
67
+
68
+ log_converter = Pidgin2Adium::LogConverter.new(options[:in],
69
+ options[:aliases],
70
+ {:overwrite => options[:force]}
71
+ )
72
+ log_converter.start
@@ -0,0 +1,120 @@
1
+ # Author: Gabe Berke-Williams, 2008
2
+ #
3
+ # A ruby program to convert Pidgin log files to Adium log files, then place
4
+ # them in the Adium log directory.
5
+
6
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
7
+
8
+ require 'fileutils'
9
+ require 'pidgin2adium/log_parser'
10
+
11
+ module Pidgin2Adium
12
+ # Returned by LogFile.write_out if the output logfile already exists.
13
+ FILE_EXISTS = 42
14
+ ADIUM_LOG_DIR = File.expand_path('~/Library/Application Support/Adium 2.0/Users/Default/Logs/') << '/'
15
+ # These files/directories show up in Dir.entries()
16
+ BAD_DIRS = %w{. .. .DS_Store Thumbs.db .system}
17
+ VERSION = "2.0.0"
18
+
19
+ def log_msg(str) #:nodoc
20
+ puts str.to_s
21
+ end
22
+
23
+ def oops(str) #:nodoc
24
+ warn("Oops: #{str}")
25
+ end
26
+
27
+ def error(str) #:nodoc
28
+ warn("Error: #{str}")
29
+ end
30
+
31
+ #######################
32
+ private :log_msg, :oops, :error
33
+ #######################
34
+
35
+ # Returns a LogFile instance or false if an error occurred.
36
+ def parse(logfile_path, my_aliases)
37
+ logfile_path = File.expand_path(logfile_path)
38
+ ext = File.extname(logfile_path).sub('.', '').downcase
39
+
40
+ if(ext == "html" || ext == "htm")
41
+ parser = HtmlLogParser.new(logfile_path, my_aliases)
42
+ elsif(ext == "txt")
43
+ parser = TextLogParser.new(logfile_path, my_aliases)
44
+ else
45
+ error("logfile (#{logfile_path}) is not a text or html file. Doing nothing.")
46
+ return false
47
+ end
48
+
49
+ return parser.parse()
50
+ end
51
+
52
+ # Returns the path to the converted log, false if an error occurred, or
53
+ # Pidgin2Adium::FILE_EXISTS if file already exists AND opts[:overwrite] =
54
+ # false.
55
+ #
56
+ # You can add options using the _opts_ hash, which can have the following
57
+ # keys, all of which are optional:
58
+ # * *overwrite*: If true, then overwrite even if log is found.
59
+ # Defaults to false.
60
+ # * *output_dir*: The top-level dir to put the logs in.
61
+ # Logs under output_dir are still each in their own folders, etc.
62
+ # Defaults to Pidgin2Adium::ADIUM_LOG_DIR
63
+ def parse_and_generate(logfile_path, my_aliases, opts = {})
64
+ opts = {} unless opts.is_a?(Hash)
65
+ overwrite = !!opts[:overwrite]
66
+ if opts.key?(:output_dir)
67
+ output_dir = opts[:output_dir]
68
+ else
69
+ output_dir = ADIUM_LOG_DIR
70
+ end
71
+
72
+ unless File.directory?(output_dir)
73
+ puts "Output log directory (#{output_dir}) does not exist or is not a directory."
74
+ raise Errno::ENOENT
75
+ end
76
+
77
+ logfile_obj = parse(logfile_path, my_aliases)
78
+ return false if logfile_obj == false
79
+ dest_file_path = logfile_obj.write_out(overwrite, output_dir)
80
+ if dest_file_path == false
81
+ error("Converting #{logfile_path} failed.")
82
+ return false
83
+ elsif dest_file_path == FILE_EXISTS
84
+ log_msg("File already exists.")
85
+ return FILE_EXISTS
86
+ else
87
+ log_msg("Output to: #{dest_file_path}")
88
+ return true
89
+ end
90
+ end
91
+
92
+ # Newly-converted logs are viewable in the Adium Chat Transcript
93
+ # Viewer, but are not indexed, so a search of the logs doesn't give
94
+ # results from the converted logs. To fix this, we delete the cached log
95
+ # indexes, which forces Adium to re-index.
96
+ #
97
+ # Note: This function is run by LogConverter after converting all of its
98
+ # files. LogFile.write_out intentionally does _not_ run it in order to
99
+ # allow for batch-processing of files. Thus, you will probably want to run
100
+ # Pidgin2Adium.delete_search_indexes after running LogFile.write_out in
101
+ # your own scripts.
102
+ def delete_search_indexes()
103
+ log_msg "Deleting log search indexes in order to force re-indexing of imported logs..."
104
+ dirty_file = File.expand_path("~/Library/Caches/Adium/Default/DirtyLogs.plist")
105
+ log_index_file = File.expand_path("~/Library/Caches/Adium/Default/Logs.index")
106
+ [dirty_file, log_index_file].each do |f|
107
+ if File.exist?(f)
108
+ if File.writable?(f)
109
+ File.delete(f)
110
+ else
111
+ error("#{f} exists but is not writable. Please delete it yourself.")
112
+ end
113
+ end
114
+ end
115
+ log_msg "...done."
116
+ log_msg "When you next start the Adium Chat Transcript Viewer, it will re-index the logs, which may take a while."
117
+ end
118
+
119
+ module_function :parse, :parse_and_generate, :delete_search_indexes
120
+ end
@@ -1,19 +1,23 @@
1
-
2
1
  module Pidgin2Adium
3
- #From Wordpress's formatting.php; rewritten in Ruby by Gabe Berke-Williams, 2009.
4
- #Balances tags of string using a modified stack.
5
- #
6
- # @author Leonard Lin <leonard@acm.org>
7
- # @license GPL v2.0
8
- # @copyright November 4, 2001
9
- # @return string Balanced text.
10
- def Pidgin2Adium.balanceTags( text )
2
+ # Balances tags of string using a modified stack. Returns a balanced
3
+ # string, but also affects the text passed into it!
4
+ # Use text = balance_tags(text).
5
+
6
+ # From Wordpress's formatting.php; rewritten in Ruby by Gabe
7
+ # Berke-Williams, 2009.
8
+ # Author:: Leonard Lin <leonard@acm.org>
9
+ # License:: GPL v2.0
10
+ # Copyright:: November 4, 2001
11
+ def balance_tags( text )
11
12
  tagstack = []
12
13
  stacksize = 0
13
14
  tagqueue = ''
14
15
  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
16
+ single_tags = %w{br hr img input meta} # Known single-entity/self-closing tags
17
+ #nestable_tags = %w{blockquote div span} # Tags that can be immediately nested within themselves
18
+ nestable_tags = %w{blockquote div span font} # Tags that can be immediately nested within themselves
19
+ # 1: tagname, with possible leading "/"
20
+ # 2: attributes
17
21
  tag_regex = /<(\/?\w*)\s*([^>]*)>/
18
22
 
19
23
  # WP bug fix for comments - in case you REALLY meant to type '< !--'
@@ -22,24 +26,24 @@ module Pidgin2Adium
22
26
  # WP bug fix for LOVE <3 (and other situations with '<' before a number)
23
27
  text.gsub!(/<([0-9]{1})/, '&lt;\1')
24
28
 
25
- while ( regex = text.match(tag_regex) )
26
- regex = regex.to_a
29
+ while ( pos = (text =~ tag_regex) )
27
30
  newtext << tagqueue
28
- i = text.index(regex[0])
29
- l = regex[0].length
31
+ tag = $1.downcase
32
+ attributes = $2
33
+ matchlen = $~[0].size
30
34
 
31
35
  # clear the shifter
32
36
  tagqueue = ''
33
37
  # Pop or Push
34
- if (regex[1][0,1] == "/") # End Tag
35
- tag = regex[1][1,regex[1].length].downcase
38
+ if (tag[0,1] == "/") # End Tag
39
+ tag.slice!(0,1)
36
40
  # if too many closing tags
37
41
  if(stacksize <= 0)
38
42
  tag = ''
39
- #or close to be safe tag = '/' . tag
40
- # if stacktop value = tag close value then pop
43
+ #or close to be safe: tag = '/' << tag
41
44
  elsif (tagstack[stacksize - 1] == tag) # found closing tag
42
- tag = '</' << tag << '>'; # Close Tag
45
+ # if stacktop value == tag close value then pop
46
+ tag = '</' << tag << '>' # Close Tag
43
47
  # Pop
44
48
  tagstack.pop
45
49
  stacksize -= 1
@@ -59,14 +63,13 @@ module Pidgin2Adium
59
63
  end
60
64
  else
61
65
  # Begin Tag
62
- tag = regex[1].downcase
63
66
 
64
67
  # Tag Cleaning
65
- if( (regex[2].slice(-1,1) == '/') || (tag == '') )
68
+ if( (attributes[-1,1] == '/') || (tag == '') )
66
69
  # If: self-closing or '', don't do anything.
67
70
  elsif ( single_tags.include?(tag) )
68
71
  # ElseIf: it's a known single-entity tag but it doesn't close itself, do so
69
- regex[2] << '/'
72
+ attributes << '/'
70
73
  else
71
74
  # Push the tag onto the stack
72
75
  # If the top of the stack is the same as the tag we want to push, close previous tag
@@ -76,11 +79,11 @@ module Pidgin2Adium
76
79
  tagqueue = '</' << tagstack.pop << '>'
77
80
  stacksize -= 1
78
81
  end
79
- stacksize = tagstack.push(tag).length
82
+ tagstack.push(tag)
83
+ stacksize += 1
80
84
  end
81
85
 
82
86
  # Attributes
83
- attributes = regex[2]
84
87
  if(attributes != '')
85
88
  attributes = ' ' << attributes
86
89
  end
@@ -91,8 +94,8 @@ module Pidgin2Adium
91
94
  tag = ''
92
95
  end
93
96
  end
94
- newtext << text[0,i] << tag
95
- text = text[i+l, text.length - (i+l)]
97
+ newtext << text[0,pos] << tag
98
+ text = text[pos+matchlen, text.length - (pos+matchlen)]
96
99
  end
97
100
 
98
101
  # Clear Tag Queue
@@ -102,8 +105,8 @@ module Pidgin2Adium
102
105
  newtext << text
103
106
 
104
107
  # Empty Stack
105
- while(x = tagstack.pop)
106
- newtext << '</' << x << '>'; # Add remaining tags to close
108
+ tagstack.reverse_each do |t|
109
+ newtext << '</' << t << '>' # Add remaining tags to close
107
110
  end
108
111
 
109
112
  # WP fix for the bug with HTML comments