pidgin2adium 1.0.0 → 2.0.0

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/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