<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>.gitignore</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -3,9 +3,7 @@ basecamp:
   domain: somedomain.projectpath.com
   user: capistrano
   password: password
+  use_ssl: true
   project_id: 123456
   category_id: 789012
-  prefix: Deploy
-  notify:
-    - 123456
-    - 789012
\ No newline at end of file
+  prefix: Deploy
\ No newline at end of file</diff>
      <filename>example/basecamp.yml</filename>
    </modified>
    <modified>
      <diff>@@ -7,18 +7,25 @@ namespace :basecamp do
     YAML.load(File.open('config/basecamp.yml'))['basecamp']
   end
 
-  set :api_wrapper do
-    Basecamp.new(basecamp_config['domain'], basecamp_config['user'], basecamp_config['password'])
-  end
-
   desc 'Post a new message to Basecamp containing the commit messages between the previous and the current deploy'
   task :notify do
     unless exists?(:stage) and stage.to_sym != :production
-      api_wrapper.post_message basecamp_config['project_id'], {
-        :title =&gt; &quot;#{ basecamp_config['prefix'] || 'Deploy' }: #{application} [#{current_revision[0..6]}]&quot;,
-        :body =&gt; grab_revision_log,
-        :category_id =&gt; basecamp_config['category_id']
-      }, basecamp_config['notify'] || []
+      domain = basecamp_config['domain']
+      user = basecamp_config['user']
+      password = basecamp_config['password']
+      use_ssl = basecamp_config['use_ssl']
+      project = basecamp_config['project_id']
+      category = basecamp_config['category_id']
+      prefix = basecamp_config['prefix'] || 'Deploy'
+
+      Basecamp.establish_connection!(domain, user, password, use_ssl)
+
+      m = Basecamp::Message.new(:project_id =&gt; project)
+      m.title = &quot;#{prefix} - #{current_revision[0..7]}&quot;
+      m.body = grab_revision_log
+      m.category_id = category
+
+      m.save
     end
   end
 </diff>
      <filename>recipes/basecamp_notify.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,3 @@
-# the following are all standard ruby libraries
 require 'net/https'
 require 'yaml'
 require 'date'
@@ -9,7 +8,7 @@ begin
 rescue LoadError
   begin
     require 'rubygems'
-    require_gem 'xml-simple'
+    require 'xmlsimple'
   rescue LoadError
     abort &lt;&lt;-ERROR
 The 'xml-simple' library could not be loaded. If you have RubyGems installed
@@ -18,34 +17,350 @@ ERROR
   end
 end
 
-# An interface to the Basecamp web-services API. Usage is straightforward:
+begin
+  require 'activeresource'
+rescue LoadError
+  begin
+    require 'rubygems'
+    require 'activeresource'
+  rescue LoadError
+    abort &lt;&lt;-ERROR
+The 'activeresource' library could not be loaded. If you have RubyGems 
+installed you can install ActiveResource by doing &quot;gem install activeresource&quot;.
+ERROR
+  end
+end
+
+# = A Ruby library for working with the Basecamp web-services API.
+#
+# For more information about the Basecamp web-services API, visit:
+#
+#   http://developer.37signals.com/basecamp
+#
+# NOTE: not all of Basecamp's web-services are accessible via REST. This
+# library provides access to RESTful services via ActiveResource. Services not
+# yet upgraded to REST are accessed via the Basecamp class. Continue reading
+# for more details.
+#
+#
+# == Establishing a Connection
+#
+# The first thing you need to do is establish a connection to Basecamp. This
+# requires your Basecamp site address and your login credentials. Example:
+#
+#   Basecamp.establish_connection!('you.grouphub.com', 'username', 'password')
+#
+# This is the same whether you're accessing using the ActiveResource interface,
+# or the legacy interface.
+#
+#
+# == Using the REST interface via ActiveResource
+#
+# The REST interface is accessed via ActiveResource, a popular Ruby library
+# that implements object-relational mapping for REST web-services. For more
+# information on working with ActiveResource, see:
+#
+#  * http://api.rubyonrails.org/files/activeresource/README.html
+#  * http://api.rubyonrails.org/classes/ActiveResource/Base.html
+#
+# === Finding a Resource
+#
+# Find a specific resource using the +find+ method. Attributes of the resource
+# are available as instance methods on the resulting object. For example, to
+# find a message with the ID of 8675309 and access its title attribute, you
+# would do the following:
+#
+#   m = Basecamp::Message.find(8675309)
+#   m.title # =&gt; 'Jenny'
+#
+# === Creating a Resource
+#
+# Create a resource by making a new instance of that resource, setting its
+# attributes, and saving it. If the resource requires a prefix to identify
+# it (as is the case with resources that belong to a sub-resource, such as a
+# project), it should be specified when instantiating the object. Examples:
+#
+#   m = Basecamp::Message.new(:project_id =&gt; 1037)
+#   m.category_id = 7301
+#   m.title = 'Message in a bottle'
+#   m.body = 'Another lonely day, with no one here but me'
+#   m.save # =&gt; true
+#
+#   c = Basecamp::Comment.new(:post_id =&gt; 25874)
+#   c.body = 'Did you get those TPS reports?'
+#   c.save # =&gt; true
+#
+# You can also create a resource using the +create+ method, which will create
+# and save it in one step. Example:
+#
+#   Basecamp::TodoItem.create(:todo_list_id =&gt; 3422, :contents =&gt; 'Do it')
+#
+# === Updating a Resource
+#
+# To update a resource, first find it by its id, change its attributes, and
+# save it. Example:
+#
+#   m = Basecamp::Message.find(8675309)
+#   m.body = 'Changed'
+#   m.save # =&gt; true
+#
+# === Deleting a Resource
+#
+# To delete a resource, use the +delete+ method with the ID of the resource
+# you want to delete. Example:
+#
+#   Basecamp::Message.delete(1037)
+#
+# === Attaching Files to a Resource
+#
+# If the resource accepts file attachments, the +attachments+ parameter should
+# be an array of Basecamp::Attachment objects. Example:
+#
+#   a1 = Basecamp::Atachment.create('primary', File.read('primary.doc'))
+#   a2 = Basecamp::Atachment.create('another', File.read('another.doc'))
+#
+#   m = Basecamp::Message.new(:project_id =&gt; 1037)
+#   ...
+#   m.attachments = [a1, a2]
+#   m.save # =&gt; true
+#
+#
+# = Using the non-REST inteface
+#
+# The non-REST interface is accessed via instance methods on the Basecamp
+# class. Ensure you've established a connection, then create a new Basecamp
+# instance and call methods on it. Examples:
+#
+#   basecamp = Basecamp.new
+#
+#   basecamp.projects.length      # =&gt; 5
+#   basecamp.person(93832)        # =&gt; #&lt;Record(person)..&gt;
+#   basecamp.file_categories(123) # =&gt; [#&lt;Record(file-category)&gt;,#&lt;Record..&gt;]
+#
+## Object attributes are accessible as methods. Example:
+#
+#   person = basecamp.person(93832)
+#   person.first_name # =&gt; &quot;Jason&quot;
 #
-#   session = Basecamp.new('your.basecamp.com', 'username', 'password')
-#   puts &quot;projects: #{session.projects.length}&quot;
 class Basecamp
-  
-  # A wrapper to encapsulate the data returned by Basecamp, for easier access.
+  class Connection #:nodoc:
+    def initialize(master)
+      @master = master
+      @connection = Net::HTTP.new(master.site, master.use_ssl ? 443 : 80)
+      @connection.use_ssl = master.use_ssl
+      @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if master.use_ssl
+    end
+
+    def post(path, body, headers = {})
+      request = Net::HTTP::Post.new(path, headers.merge('Accept' =&gt; 'application/xml'))
+      request.basic_auth(@master.user, @master.password)
+      @connection.request(request, body)
+    end
+  end
+
+  class Resource &lt; ActiveResource::Base #:nodoc:
+    class &lt;&lt; self
+      def parent_resources(*parents)
+        @parent_resources = parents
+      end
+
+      def element_name
+        name.split(/::/).last.underscore
+      end
+
+      def prefix_source
+        @parent_resources.map { |resource| &quot;/#{resource.to_s.pluralize}/:#{resource}_id/&quot; }.join
+      end
+
+      def prefix(options={})
+        if options.any?
+          options.map { |name, value| &quot;/#{name.to_s.chomp('_id').pluralize}/#{value}/&quot; }.join
+        else
+          super
+        end
+      end
+    end
+
+    def prefix_options
+      id ? {} : super
+    end
+  end
+
+  class Message &lt; Resource
+    parent_resources :project
+    self.element_name = 'post'
+
+    # Returns the most recent 25 messages in the given project (and category,
+    # if specified). If you need to retrieve older messages, use the archive
+    # method instead. Example:
+    #
+    #   Basecamp::Message.list(1037)
+    #   Basecamp::Message.list(1037, :category_id =&gt; 7301)
+    #
+    def self.list(project_id, options = {})
+      find(:all, :params =&gt; options.merge(:project_id =&gt; project_id))
+    end
+
+    # Returns a summary of all messages in the given project (and category, if
+    # specified). The summary is simply the title and category of the message,
+    # as well as the number of attachments (if any). Example:
+    #
+    #   Basecamp::Message.archive(1037)
+    #   Basecamp::Message.archive(1037, :category_id =&gt; 7301)
+    #
+    def self.archive(project_id, options = {})
+      find(:all, :params =&gt; options.merge(:project_id =&gt; project_id), :from =&gt; :archive)
+    end
+
+    def comments(options = {})
+      @comments ||= Comment.find(:all, :params =&gt; options.merge(:post_id =&gt; id))
+    end
+  end
+
+  # == Creating Comments for Multiple Resources
+  #
+  # Comments can be created for messages, milestones, and to-dos, identified
+  # by the &lt;tt&gt;post_id&lt;/tt&gt;, &lt;tt&gt;milestone_id&lt;/tt&gt;, and &lt;tt&gt;todo_item_id&lt;/tt&gt;
+  # params respectively.
+  #
+  # For example, to create a comment on the message with id #8675309:
+  #
+  #   c = Basecamp::Comment.new(:post_id =&gt; 8675309)
+  #   c.body = 'Great tune'
+  #   c.save # =&gt; true
+  #
+  # Similarly, to create a comment on a milestone:
+  #
+  #   c = Basecamp::Comment.new(:milestone_id =&gt; 8473647)
+  #   c.body = 'Is this done yet?'
+  #   c.save # =&gt; true
+  #
+  class Comment &lt; Resource
+    parent_resources :post, :milestone, :todo_item
+  end
+
+  class TodoList &lt; Resource
+    parent_resources :project
+
+    # Returns all lists for a project. If complete is true, only completed lists
+    # are returned. If complete is false, only uncompleted lists are returned.
+    def self.all(project_id, complete=nil)
+      filter = case complete
+        when nil   then &quot;all&quot;
+        when true  then &quot;finished&quot;
+        when false then &quot;pending&quot;
+        else raise ArgumentError, &quot;invalid value for `complete'&quot;
+      end
+
+      find(:all, :params =&gt; { :project_id =&gt; project_id, :filter =&gt; filter })
+    end
+
+    def todo_items(options={})
+      @todo_items ||= TodoItem.find(:all, :params =&gt; options.merge(:todo_list_id =&gt; id))
+    end
+  end
+
+  class TodoItem &lt; Resource
+    parent_resources :todo_list
+
+    def todo_list(options={})
+      @todo_list ||= TodoList.find(todo_list_id, options)
+    end
+
+    def time_entries(options={})
+      @time_entries ||= TimeEntry.find(:all, :params =&gt; options.merge(:todo_item_id =&gt; id))
+    end
+
+    def comments(options = {})
+      @comments ||= Comment.find(:all, :params =&gt; options.merge(:todo_item_id =&gt; id))
+    end
+
+    def complete!
+      put(:complete)
+    end
+
+    def uncomplete!
+      put(:uncomplete)
+    end
+  end
+
+  class TimeEntry &lt; Resource
+    parent_resources :project, :todo_item
+
+    def self.all(project_id, page=0)
+      find(:all, :params =&gt; { :project_id =&gt; project_id, :page =&gt; page })
+    end
+
+    def self.report(options={})
+      find(:all, :from =&gt; :report, :params =&gt; options)
+    end
+
+    def todo_item(options={})
+      @todo_item ||= todo_item_id &amp;&amp; TodoItem.find(todo_item_id, options)
+    end
+  end
+
+  class Attachment
+    attr_accessor :id, :filename, :content, :content_type
+
+    def self.create(filename, content)
+      returning new(filename, content) do |attachment|
+        attachment.save
+      end
+    end
+
+    def initialize(filename, content, content_type = 'application/octet-stream')
+      @filename, @content, @content_type = filename, content, content_type
+    end
+
+    def attributes
+      { :file =&gt; id, :original_filename =&gt; filename, :content_type =&gt; content_type }
+    end
+
+    def to_xml(options = {})
+      { :file =&gt; attributes }.to_xml(options)
+    end
+
+    def inspect
+      to_s
+    end
+
+    def save
+      response = Basecamp.connection.post('/upload', content, 'Content-Type' =&gt; content_type)
+
+      if response.code == '200'
+        self.id = Hash.from_xml(response.body)['upload']['id']
+        true
+      else
+        raise &quot;Could not save attachment: #{response.message} (#{response.code})&quot;
+      end
+    end
+  end
+
   class Record #:nodoc:
     attr_reader :type
 
     def initialize(type, hash)
-      @type = type
-      @hash = hash
+      @type, @hash = type, hash
     end
 
     def [](name)
       name = dashify(name)
+
       case @hash[name]
-        when Hash then 
-          @hash[name] = (@hash[name].keys.length == 1 &amp;&amp; Array === @hash[name].values.first) ?
-            @hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) } :
-            Record.new(name, @hash[name])
-        else @hash[name]
+      when Hash then 
+        @hash[name] = if (@hash[name].keys.length == 1 &amp;&amp; @hash[name].values.first.is_a?(Array))
+          @hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) }
+        else
+          Record.new(name, @hash[name])
+        end
+      else
+        @hash[name]
       end
     end
 
     def id
-      @hash[&quot;id&quot;]
+      @hash['id']
     end
 
     def attributes
@@ -79,28 +394,38 @@ class Basecamp
       end
   end
 
-  # A wrapper to represent a file that should be uploaded. This is used so that
-  # the form/multi-part encoder knows when to encode a field as a file, versus
-  # when to encode it as a simple field.
-  class FileUpload
-    attr_reader :filename, :content
-    
-    def initialize(filename, content)
-      @filename = filename
-      @content = content
+  attr_accessor :use_xml
+
+  class &lt;&lt; self
+    attr_reader :site, :user, :password, :use_ssl
+
+    def establish_connection!(site, user, password, use_ssl = false)
+      @site     = site
+      @user     = user
+      @password = password
+      @use_ssl  = use_ssl
+
+      Resource.user = user
+      Resource.password = password
+      Resource.site = (use_ssl ? &quot;https&quot; : &quot;http&quot;) + &quot;://&quot; + site
+
+      @connection = Connection.new(self)
     end
-  end
 
-  attr_accessor :use_xml
+    def connection
+      @connection || raise('No connection established')
+    end
+  end
 
-  # Connects
-  def initialize(url, user_name, password, use_ssl = false)
+  def initialize
     @use_xml = false
-    @user_name, @password = user_name, password
-    connect!(url, use_ssl)
   end
 
-  # Return the list of all accessible projects.
+  # ==========================================================================
+  # GENERAL
+  # ==========================================================================
+
+  # Return the list of all accessible projects
   def projects
     records &quot;project&quot;, &quot;/project/list&quot;
   end
@@ -115,6 +440,10 @@ class Basecamp
     records &quot;attachment-category&quot;, &quot;/projects/#{project_id}/attachment_categories&quot;
   end
 
+  # ==========================================================================
+  # CONTACT MANAGEMENT
+  # ==========================================================================
+
   # Return information for the company with the given id
   def company(id)
     record &quot;/contacts/company/#{id}&quot;
@@ -133,172 +462,9 @@ class Basecamp
     record &quot;/contacts/person/#{id}&quot;
   end
 
-  # Return information about the message(s) with the given id(s). The API
-  # limits you to requesting 25 messages at a time, so if you need to get more
-  # than that, you'll need to do it in multiple requests.
-  def message(*ids)
-    result = records(&quot;post&quot;, &quot;/msg/get/#{ids.join(&quot;,&quot;)}&quot;)
-    result.length == 1 ? result.first : result
-  end
-
-  # Returns a summary of all messages in the given project (and category, if
-  # specified). The summary is simply the title and category of the message,
-  # as well as the number of attachments (if any).
-  def message_list(project_id, category_id=nil)
-    url = &quot;/projects/#{project_id}/msg&quot;
-    url &lt;&lt; &quot;/cat/#{category_id}&quot; if category_id
-    url &lt;&lt; &quot;/archive&quot;
-    
-    records &quot;post&quot;, url
-  end
-
-  # Create a new message in the given project. The +message+ parameter should
-  # be a hash. The +email_to+ parameter must be an array of person-id's that
-  # should be notified of the post.
-  #
-  # If you want to add attachments to the message, the +attachments+ parameter
-  # should be an array of hashes, where each has has a :name key (optional),
-  # and a :file key (required). The :file key must refer to a Basecamp::FileUpload
-  # instance.
-  #
-  #   msg = session.post_message(158141,
-  #      { :title =&gt; &quot;Requirements&quot;,
-  #        :body =&gt; &quot;Here are the requirements documents you asked for.&quot;,
-  #        :category_id =&gt; 2301121 },
-  #      [john.id, martha.id],
-  #      [ { :name =&gt; &quot;Primary Requirements&quot;,
-  #          :file =&gt; Basecamp::FileUpload.new('primary.doc&quot;, File.read('primary.doc')) },
-  #        { :file =&gt; Basecamp::FileUpload.new('other.doc', File.read('other.doc')) } ])
-  def post_message(project_id, message, notify=[], attachments=[])
-    prepare_attachments(attachments)
-    record &quot;/projects/#{project_id}/msg/create&quot;,
-      :post =&gt; message,
-      :notify =&gt; notify,
-      :attachments =&gt; attachments
-  end
-
-  # Edit the message with the given id. The +message+ parameter should
-  # be a hash. The +email_to+ parameter must be an array of person-id's that
-  # should be notified of the post.
-  #
-  # The +attachments+ parameter, if used, should be the same as described for
-  # #post_message.
-  def update_message(id, message, notify=[], attachments=[])
-    prepare_attachments(attachments)
-    record &quot;/msg/update/#{id}&quot;,
-      :post =&gt; message,
-      :notify =&gt; notify,
-      :attachments =&gt; attachments
-  end
-
-  # Deletes the message with the given id, and returns it.
-  def delete_message(id)
-    record &quot;/msg/delete/#{id}&quot;
-  end
-
-  # Return a list of the comments for the specified message.
-  def comments(post_id)
-    records &quot;comment&quot;, &quot;/msg/comments/#{post_id}&quot;
-  end
-
-  # Retrieve a specific comment
-  def comment(id)
-    record &quot;/msg/comment/#{id}&quot;
-  end
-
-  # Add a new comment to a message. +comment+ must be a hash describing the
-  # comment. You can add attachments to the comment, too, by giving them in
-  # an array. See the #post_message method for a description of how to do that.
-  def create_comment(post_id, comment, attachments=[])
-    prepare_attachments(attachments)
-    record &quot;/msg/create_comment&quot;, :comment =&gt; comment.merge(:post_id =&gt; post_id),
-      :attachments =&gt; attachments
-  end
-
-  # Update the given comment. Attachments follow the same format as #post_message.
-  def update_comment(id, comment, attachments=[])
-    prepare_attachments(attachments)
-    record &quot;/msg/update_comment&quot;, :comment_id =&gt; id,
-      :comment =&gt; comment, :attachments =&gt; attachments
-  end
-
-  # Deletes (and returns) the given comment.
-  def delete_comment(id)
-    record &quot;/msg/delete_comment/#{id}&quot;
-  end
-
-  # =========================================================================
-  # TODO LISTS AND ITEMS
-  # =========================================================================
-
-  # Marks the given item completed.
-  def complete_item(id)
-    record &quot;/todos/complete_item/#{id}&quot;
-  end
-
-  # Marks the given item uncompleted.
-  def uncomplete_item(id)
-    record &quot;/todos/uncomplete_item/#{id}&quot;
-  end
-
-  # Creates a new to-do item.
-  def create_item(list_id, content, responsible_party=nil, notify=true)
-    record &quot;/todos/create_item/#{list_id}&quot;,
-      :content =&gt; content, :responsible_party =&gt; responsible_party,
-      :notify =&gt; notify
-  end
-
-  # Creates a new list using the given hash of list metadata.
-  def create_list(project_id, list)
-    record &quot;/projects/#{project_id}/todos/create_list&quot;, list
-  end
-
-  # Deletes the given item from it's parent list.
-  def delete_item(id)
-    record &quot;/todos/delete_item/#{id}&quot;
-  end
-
-  # Deletes the given list and all of its items.
-  def delete_list(id)
-    record &quot;/todos/delete_list/#{id}&quot;
-  end
-
-  # Retrieves the specified list, and all of its items.
-  def get_list(id)
-    record &quot;/todos/list/#{id}&quot;
-  end
-
-  # Return all lists for a project. If complete is true, only completed lists
-  # are returned. If complete is false, only uncompleted lists are returned.
-  def lists(project_id, complete=nil)
-    records &quot;todo-list&quot;, &quot;/projects/#{project_id}/todos/lists&quot;, :complete =&gt; complete
-  end
-
-  # Repositions an item to be at the given position in its list
-  def move_item(id, to)
-    record &quot;/todos/move_item/#{id}&quot;, :to =&gt; to
-  end
-
-  # Repositions a list to be at the given position in its project
-  def move_list(id, to)
-    record &quot;/todos/move_list/#{id}&quot;, :to =&gt; to
-  end
-
-  # Updates the given item
-  def update_item(id, content, responsible_party=nil, notify=true)
-    record &quot;/todos/update_item/#{id}&quot;,
-      :item =&gt; { :content =&gt; content }, :responsible_party =&gt; responsible_party,
-      :notify =&gt; notify
-  end
-
-  # Updates the given list's metadata
-  def update_list(id, list)
-    record &quot;/todos/update_list/#{id}&quot;, :list =&gt; list
-  end
-
-  # =========================================================================
+  # ==========================================================================
   # MILESTONES
-  # =========================================================================
+  # ==========================================================================
 
   # Complete the milestone with the given id
   def complete_milestone(id)
@@ -342,76 +508,46 @@ class Basecamp
       :move_upcoming_milestones_off_weekends =&gt; move_off_weekends
   end
 
-  # Make a raw web-service request to Basecamp. This will return a Hash of
-  # Arrays of the response, and may seem a little odd to the uninitiated.
-  def request(path, parameters = {}, second_try = false)
-    response = post(path, convert_body(parameters), &quot;Content-Type&quot; =&gt; content_type)
-
-    if response.code.to_i / 100 == 2
-      result = XmlSimple.xml_in(response.body, 'keeproot' =&gt; true,
-        'contentkey' =&gt; '__content__', 'forcecontent' =&gt; true)
-      typecast_value(result)
-    elsif response.code == &quot;302&quot; &amp;&amp; !second_try
-      connect!(@url, !@use_ssl)
-      request(path, parameters, true)
-    else
-      raise &quot;#{response.message} (#{response.code})&quot;
-    end
-  end
+  private
 
-  # A convenience method for wrapping the result of a query in a Record
-  # object. This assumes that the result is a singleton, not a collection.
-  def record(path, parameters={})
-    result = request(path, parameters)
-    (result &amp;&amp; !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
-  end
+    # Make a raw web-service request to Basecamp. This will return a Hash of
+    # Arrays of the response, and may seem a little odd to the uninitiated.
+    def request(path, parameters = {})
+      response = Basecamp.connection.post(path, convert_body(parameters), &quot;Content-Type&quot; =&gt; content_type)
 
-  # A convenience method for wrapping the result of a query in Record
-  # objects. This assumes that the result is a collection--any singleton
-  # result will be wrapped in an array.
-  def records(node, path, parameters={})
-    result = request(path, parameters).values.first or return []
-    result = result[node] or return []
-    result = [result] unless Array === result
-    result.map { |row| Record.new(node, row) }
-  end
+      if response.code.to_i / 100 == 2
+        result = XmlSimple.xml_in(response.body, 'keeproot' =&gt; true, 'contentkey' =&gt; '__content__', 'forcecontent' =&gt; true)
+        typecast_value(result)
+      else
+        raise &quot;#{response.message} (#{response.code})&quot;
+      end
+    end
 
-  private
+    # A convenience method for wrapping the result of a query in a Record
+    # object. This assumes that the result is a singleton, not a collection.
+    def record(path, parameters={})
+      result = request(path, parameters)
+      (result &amp;&amp; !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
+    end
 
-    def connect!(url, use_ssl)
-      @use_ssl = use_ssl
-      @url = url
-      @connection = Net::HTTP.new(url, use_ssl ? 443 : 80)
-      @connection.use_ssl = @use_ssl
-      @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
+    # A convenience method for wrapping the result of a query in Record
+    # objects. This assumes that the result is a collection--any singleton
+    # result will be wrapped in an array.
+    def records(node, path, parameters={})
+      result = request(path, parameters).values.first or return []
+      result = result[node] or return []
+      result = [result] unless Array === result
+      result.map { |row| Record.new(node, row) }
     end
 
     def convert_body(body)
-      body = use_xml ? body.to_xml : body.to_yaml
+      body = use_xml ? body.to_legacy_xml : body.to_yaml
     end
 
     def content_type
       use_xml ? &quot;application/xml&quot; : &quot;application/x-yaml&quot;
     end
 
-    def post(path, body, header={})
-      request = Net::HTTP::Post.new(path, header.merge('Accept' =&gt; 'application/xml'))
-      request.basic_auth(@user_name, @password)
-      @connection.request(request, body)
-    end
-
-    def store_file(contents)
-      response = post(&quot;/upload&quot;, contents, 'Content-Type' =&gt; 'application/octet-stream',
-        'Accept' =&gt; 'application/xml')
-
-      if response.code == &quot;200&quot;
-        result = XmlSimple.xml_in(response.body, 'keeproot' =&gt; true, 'forcearray' =&gt; false)
-        return result[&quot;upload&quot;][&quot;id&quot;]
-      else
-        raise &quot;Could not store file: #{response.message} (#{response.code})&quot;
-      end
-    end
-
     def typecast_value(value)
       case value
       when Hash
@@ -464,16 +600,6 @@ class Basecamp
             gsub(/&amp;apos;/, &quot;'&quot;).
             gsub(/&amp;amp;/, &quot;&amp;&quot;)
     end
-
-    def prepare_attachments(list)
-      (list || []).each do |data|
-        upload = data[:file]
-        id = store_file(upload.content)
-        data[:file] = { :file =&gt; id,
-                        :content_type =&gt; &quot;application/octet-stream&quot;,
-                        :original_filename =&gt; upload.filename }
-      end
-    end
 end
 
 # A minor hack to let Xml-Simple serialize symbolic keys in hashes
@@ -484,7 +610,7 @@ class Symbol
 end
 
 class Hash
-  def to_xml
+  def to_legacy_xml
     XmlSimple.xml_out({:request =&gt; self}, 'keeproot' =&gt; true, 'noattr' =&gt; true)
   end
-end
+end
\ No newline at end of file</diff>
      <filename>vendor/basecamp.rb</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>a36f29472c657ac46ff01f69914fd64b2f87dcf8</id>
    </parent>
  </parents>
  <author>
    <name>Luke Randall</name>
    <email>luke.randall@gmail.com</email>
  </author>
  <url>http://github.com/scoop/basecamp_notify/commit/fb90c4790016a99bc0f0c68d35c2d169d468350c</url>
  <id>fb90c4790016a99bc0f0c68d35c2d169d468350c</id>
  <committed-date>2008-12-18T07:58:16-08:00</committed-date>
  <authored-date>2008-09-11T06:25:39-07:00</authored-date>
  <message>Update to use Basecamp's new RESTful API

Signed-off-by: Patrick Lenz &lt;patrick@limited-overload.de&gt;</message>
  <tree>72b0edde4cece528276ff1fd3bb4d896c7607efb</tree>
  <committer>
    <name>Patrick Lenz</name>
    <email>patrick@limited-overload.de</email>
  </committer>
</commit>
