Module: Pidgin2Adium

Included in:
BasicParser, LogConverter, LogFile
Defined in:
lib/pidgin2adium/messages/message.rb,
lib/pidgin2adium.rb,
lib/pidgin2adium/version.rb,
lib/pidgin2adium/log_file.rb,
lib/pidgin2adium/log_converter.rb,
lib/pidgin2adium/messages/event.rb,
lib/pidgin2adium/messages/xml_message.rb,
lib/pidgin2adium/parsers/basic_parser.rb,
lib/pidgin2adium/messages/status_message.rb,
lib/pidgin2adium/parsers/html_log_parser.rb,
lib/pidgin2adium/parsers/text_log_parser.rb,
lib/pidgin2adium/messages/auto_reply_message.rb,
ext/balance_tags_c/balance_tags_c.c

Overview

The Message class’s subclasses, each used for holding one line of a chat.

Defined Under Namespace

Classes: AutoReplyMessage, BasicParser, Event, HtmlLogParser, InvalidFirstLineError, LogConverter, LogFile, Message, StatusMessage, TextLogParser, XMLMessage

Constant Summary collapse

FILE_EXISTS =

Returned by LogFile.write_out if the output logfile already exists.

42
ADIUM_LOG_DIR =
File.expand_path('~/Library/Application Support/Adium 2.0/Users/Default/Logs/') << '/'
BAD_DIRS =

These files/directories show up in Dir.entries()

%w{. .. .DS_Store Thumbs.db .system}
VERSION =
"3.3.0"
@@oops_messages =

For displaying after we finish converting

[]
@@error_messages =
[]

Class Method Summary collapse

Class Method Details

.balance_tags_c(text) ⇒ Object

Balances tags of text. Returns modified text.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'ext/balance_tags_c/balance_tags_c.c', line 30

VALUE balance_tags_c(VALUE mod, VALUE text){
  if( TYPE(text) != T_STRING ){
    rb_raise(rb_eArgError, "bad argument to balance_tags_c, String only please.");
  }
  VALUE tagstack = rb_ary_new2(1);
  int stacksize = 0;
  VALUE tagqueue = rb_str_new2("");
  VALUE ZERO = INT2FIX(0),
        ONE = INT2FIX(1);
  VALUE newtext = rb_str_new2("");
  // Known single-entity/self-closing tags
  VALUE single_tags = rb_ary_new3(5,
      rb_str_new2("br"),
      rb_str_new2("hr"),
      rb_str_new2("img"),
      rb_str_new2("input"),
      rb_str_new2("meta"));
  // Tags that can be immediately nested within themselves
  VALUE nestable_tags = rb_ary_new3(4,
      rb_str_new2("blockquote"),
      rb_str_new2("div"),
      rb_str_new2("span"),
      rb_str_new2("font"));
  // 1: tagname, with possible leading "/"
  // 2: attributes
  VALUE tag_regex = rb_eval_string("/<(\\/?\\w*)\\s*([^>]*)>/");
  VALUE pos;  // position in text
  VALUE match;
  VALUE tag;
  VALUE attributes;
  VALUE t; // loop variable when iterating over tagstack at end of while loop
  int matchlen;
  int done = 0;
  int j, k, i; // loop counters

  // WP bug fix for comments - in case you REALLY meant to type '< !--'
  rb_funcall(text, rb_intern("gsub!"), 2,
      rb_str_new2("< !--"),
      rb_str_new2("<    !--"));

  // WP bug fix for LOVE <3 (and other situations with '<' before a number)
  rb_funcall(text,rb_intern("gsub!"), 2,
      rb_eval_string("/<([0-9]{1})/"),
      rb_str_new2("&lt;\\1"));

  pos = rb_funcall(text, rb_intern("=~"), 1, tag_regex);
  done = (pos == Qnil);
  while ( ! done ){
    rb_str_concat(newtext, tagqueue);
    match = rb_funcall(text, rb_intern("match"), 1, tag_regex);
    tag = rb_funcall(rb_reg_nth_match(1, match), rb_intern("downcase"), 0);
    attributes = rb_reg_nth_match(2, match);

    matchlen = NUM2INT(rb_funcall(rb_reg_nth_match(0, match), rb_intern("size"), 0));

    // clear the shifter
    tagqueue = rb_str_new2("");
    // Pop or Push
    if (0 == rb_str_cmp(rb_str_substr(tag, 0, 1), rb_str_new2("/"))){ // End Tag
      rb_funcall(tag, rb_intern("slice!"), 2, ZERO, ONE);
      // if too many closing tags
      if(stacksize <= 0){
        tag = rb_str_new2("");
        //or close to be safe: tag = '/' << tag
      } else if (0 == rb_str_cmp(RARRAY_PTR(tagstack)[stacksize - 1], tag)){
        // found closing tag
        // if stacktop value == tag close value then pop
        // Close Tag
        tag = rb_str_append(rb_str_new2("</"), tag);
        rb_str_concat(tag, rb_str_new2(">"));
        // Pop
        rb_ary_pop(tagstack);
        stacksize--;
      } else { // closing tag not at top, search for it
        for(j=stacksize-1; j>=0; j--){
          if(0 == rb_str_cmp(RARRAY_PTR(tagstack)[j], tag) ){
            // add tag to tagqueue
            for(k = stacksize-1;k>=j;k--){
              rb_str_concat(tagqueue, rb_str_new2("</"));
              rb_str_concat(tagqueue, rb_ary_pop(tagstack));
              rb_str_concat(tagqueue, rb_str_new2(">"));
              stacksize--;
            }
            break;
          }
        }
        tag = rb_str_new2("");
      }
    } else {
      // Begin Tag

      // Tag Cleaning

      if( ( RSTRING_LEN(attributes) > 0 && // test length before rb_str_substr
            (0 == rb_str_cmp(rb_str_substr(attributes, -1, 1), rb_str_new2("/"))) ) ||
          (0 == rb_str_cmp(tag, rb_str_new2(""))) ){
        // If: self-closing or '', don't do anything.
      } else if ( rb_ary_includes(single_tags, tag) ) {
        // ElseIf: it's a known single-entity tag but it doesn't close itself, do so
        rb_str_concat(attributes, rb_str_new2("/"));
      } else {
        // Push the tag onto the stack
        // If the top of the stack is the same as the tag we want
        // to push, close previous tag
        if ( (stacksize > 0) &&
            (Qfalse == rb_ary_includes(nestable_tags, tag)) &&
            (0 == rb_str_cmp(rb_ary_entry(tagstack, stacksize - 1), tag))){
          tagqueue = rb_str_new2("</");
          rb_str_concat(tagqueue, rb_ary_pop(tagstack));
          rb_str_concat(tagqueue, rb_str_new2(">"));
          stacksize--;
        }
        rb_ary_push(tagstack, tag);
        stacksize++;
      }

      // Attributes
      if( 0 != rb_str_cmp(attributes, rb_str_new2("")) ){
        attributes = rb_str_plus(rb_str_new2(" "), attributes);
      }
      tag = rb_str_plus(rb_str_new2("<"), tag);
      rb_str_concat(tag, attributes);
      rb_str_concat(tag, rb_str_new2(">"));
      //If already queuing a close tag, then put this tag on, too
      if( RSTRING_LEN(tagqueue) > 0 ){
        rb_str_concat(tagqueue, tag);
        tag = rb_str_new2("");
      }
    }
    rb_str_concat(newtext,
        rb_str_plus(rb_str_substr(text, 0, NUM2INT(pos)), tag));
    text = rb_str_substr(text,
        NUM2INT(pos)+matchlen,
        RSTRING_LEN(text) - (NUM2INT(pos)+matchlen));
    pos = rb_funcall(text, rb_intern("=~"), 1, tag_regex);
    done = (pos == Qnil);
  }

  // Clear Tag Queue
  rb_str_concat(newtext, tagqueue);

  // Add Remaining text
  rb_str_concat(newtext, text);

  i = NUM2INT(rb_funcall(tagstack, rb_intern("length"), 0)) - 1;
  // Empty Stack
  for(; i >= 0; i--){
    // Add remaining tags to close
    t = RARRAY_PTR(tagstack)[i];
    rb_str_concat(newtext, rb_str_new2("</"));
    rb_str_concat(newtext, t);
    rb_str_concat(newtext, rb_str_new2(">"));
  }

  // WP fix for the bug with HTML comments
  rb_funcall(newtext, rb_intern("gsub!"), 2,
      rb_str_new2("< !--"),
      rb_str_new2("<!--"));
  rb_funcall(newtext, rb_intern("gsub!"), 2,
      rb_str_new2("<    !--"),
      rb_str_new2("< !--"));

  return newtext;
}

.delete_search_indexesObject

Newly-converted logs are viewable in the Adium Chat Transcript Viewer, but are not indexed, so a search of the logs doesn’t give results from the converted logs. To fix this, we delete the cached log indexes, which forces Adium to re-index.

Note: This function is run by LogConverter after converting all of its files. LogFile.write_out intentionally does not run it in order to allow for batch-processing of files. Thus, you will probably want to run Pidgin2Adium.delete_search_indexes after running LogFile.write_out in your own scripts.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/pidgin2adium.rb', line 119

def delete_search_indexes()
  log_msg "Deleting log search indexes in order to force re-indexing of imported logs..."
  dirty_file = File.expand_path("~/Library/Caches/Adium/Default/DirtyLogs.plist")
  log_index_file = File.expand_path("~/Library/Caches/Adium/Default/Logs.index")
  [dirty_file, log_index_file].each do |f|
    if File.exist?(f)
      if File.writable?(f)
        File.delete(f)
      else
        error("File exists but is not writable. Please delete it yourself: #{f}")
      end
    end
  end
  log_msg "...done."
  log_msg "When you next start the Adium Chat Transcript Viewer, it will re-index the logs, which may take a while."
end

.error(str) ⇒ Object

:nodoc:



30
31
32
33
# File 'lib/pidgin2adium.rb', line 30

def error(str) #:nodoc:
  @@error_messages << str
  warn("Error: #{str}")
end

.log_msg(str) ⇒ Object

:nodoc:



21
22
23
# File 'lib/pidgin2adium.rb', line 21

def log_msg(str) #:nodoc:
  puts str.to_s
end

.oops(str) ⇒ Object

:nodoc:



25
26
27
28
# File 'lib/pidgin2adium.rb', line 25

def oops(str) #:nodoc:
  @@oops_messages << str
  warn("Oops: #{str}")
end

.parse(logfile_path, my_aliases, force_conversion) ⇒ Object

Parses the provided log. Returns a LogFile instance or false if an error occurred.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/pidgin2adium.rb', line 42

def parse(logfile_path, my_aliases, force_conversion)
  logfile_path = File.expand_path(logfile_path)
  ext = File.extname(logfile_path).sub('.', '').downcase

  if(ext == "html" || ext == "htm")
    parser_class = HtmlLogParser
  elsif(ext == "txt")
    parser_class = TextLogParser
  else
    error("Doing nothing, logfile is not a text or html file. Path: #{logfile_path}.")
    return false
  end

  parser = parser_class.new(logfile_path, my_aliases, force_conversion)
  return parser.parse()
end

.parse_and_generate(logfile_path, my_aliases, opts = {}) ⇒ Object

Parses the provided log and writes out the log in Adium format. Returns:

* true if it successfully converted and wrote out the log,
* false if an error occurred, or
* Pidgin2Adium::FILE_EXISTS if file already exists AND
  opts[:overwrite] = false.

You can add options using the opts hash, which can have the following keys, all of which are optional:

  • overwrite: If true, then overwrite even if log is found.

Defaults to false.

  • output_dir: The top-level dir to put the logs in. Logs under output_dir are still each in their own folders, etc. Defaults to Pidgin2Adium::ADIUM_LOG_DIR



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/pidgin2adium.rb', line 73

def parse_and_generate(logfile_path, my_aliases, opts = {})
  opts = {} unless opts.is_a?(Hash)
  overwrite = !!opts[:overwrite]
  force_conversion = opts[:force_conversion]

  if opts.key?(:output_dir)
    output_dir = opts[:output_dir]
  else
    output_dir = ADIUM_LOG_DIR
  end

  unless File.directory?(output_dir)
    puts "Output log directory (#{output_dir}) does not exist or is not a directory."
    begin
      FileUtils.mkdir_p(output_dir)
    rescue Errno::EACCES
      puts "Permission denied, could not create output directory (#{output_dir})"
      return false
    end
  end

  logfile_obj = parse(logfile_path, my_aliases, force_conversion)
  return false if logfile_obj == false
  dest_file_path = logfile_obj.write_out(overwrite, output_dir)
  if dest_file_path == false
    error("Successfully parsed file, but failed to write it out. Path: #{logfile_path}.")
    return false
  elsif dest_file_path == FILE_EXISTS
    log_msg("File already exists.")
    return FILE_EXISTS
  else
    log_msg("Output to: #{dest_file_path}")
    return true
  end
end