Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit a731684e91e4240aa496d45aaf7c9e309a0ec9ae @jhollinger committed
13 LICENSE
@@ -0,0 +1,13 @@
+ Copyright 2011 Jordan Hollinger
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
50 README.rdoc
@@ -0,0 +1,50 @@
+= Etherpad Lite Client
+
+etherpad-lite is a Ruby client for Etherpad Lite's HTTP JSON API. Etherpad Lite is a collaborative editor provided by the Etherpad Foundation. http://etherpad.org
+
+Initial inspriation was drawn from the fine PHP and Python client libraries. However, this is a much higher-level
+abstration of Etherpad Lite's API, and looks quite different from its progenitors. It should feel quite "Rubyish" to Rubyists; hopefully that's a good thing.
+
+== Installation
+
+ gem install etherpad-lite
+
+== Reading and writing Pads
+
+ require 'etherpad-lite'
+
+ # Connect to an Etherpad Lite instance
+ eth = EtherpadLite.connect('http://etherpad-lite.example.com', 'their api key')
+
+ # Get a Pad (or create one if it doesn't exist)
+ pad = eth.pad('my first etherpad lite pad')
+
+ puts pad.text
+ => "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http://j.mp/ep-lite\n\n\n"
+
+ # This will instantly write the changes to the Etherpad server
+ pad.text = "What hath God wrought?"
+
+ # And this will pull the latest changes back down
+ puts pad.text
+ => "What hath God wrought?"
+
+ # There are now 2 revisions!
+ puts pad.revison_numbers.size
+ => 2
+
+ # Revisions start at 0
+ puts pad.revison_numbers.last
+ => 1
+
+ # Iterate through each revision
+ pad.revisions.each do |pad_rev|
+ puts pad_rev.rev
+ puts pad_rev.text
+ end
+
+== Contributing
+
+This is still in the early stages, and the API isn't stable. Authors and Sessions are still missing, as is HTTPS support, tests, and more.
+
+Contributions are welcome, particularly if they make things better. Please be sure you changes work in both Ruby 1.8.7 and Ruby 1.9.2.
13 TODO.txt
@@ -0,0 +1,13 @@
+Make sure Instances, Pads and Groups are working properly and have sane APIs.
+
+Make sure it's working under Ruby 1.8.7
+
+Test JSONP support
+
+Add support for SSL/HTTPS
+
+Add support for Authors
+
+Add support for Sessions
+
+Add RSpect tests
BIN etherpad-lite-0.0.1.gem
Binary file not shown.
16 etherpad-lite.gemspec
@@ -0,0 +1,16 @@
+Gem::Specification.new do |spec|
+ spec.name = 'etherpad-lite'
+ spec.version = '0.0.1'
+ spec.summary = "A Ruby client library for Etherpad Lite"
+ spec.description = "etherpad-lite is a Ruby interface to Etherpad Lite's HTTP JSON API"
+ spec.authors = ['Jordan Hollinger']
+ spec.date = '2011-08-28'
+ spec.email = 'jordan@jordanhollinger.com'
+ spec.homepage = 'http://github.com/jhollinger/etherpad-lite'
+
+ spec.require_paths = ['lib']
+ spec.extra_rdoc_files = %w{README.rdoc}
+ spec.files = [Dir.glob('lib/**/*'), Dir.glob('spec/**/*'), 'README.rdoc', 'LICENSE', 'TODO.txt'].flatten
+
+ spec.specification_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION if spec.respond_to? :specification_version
+end
4 lib/etherpad-lite.rb
@@ -0,0 +1,4 @@
+require 'etherpad-lite/padded'
+require 'etherpad-lite/instance'
+require 'etherpad-lite/pad'
+require 'etherpad-lite/group'
80 lib/etherpad-lite/group.rb
@@ -0,0 +1,80 @@
+module EtherpadLite
+ # A Group of Pads
+ class Group
+ include Padded
+
+ GROUP_ID_REGEX = /^g\.[^\$]+/
+ METHOD_CREATE = 'createGroup'
+ METHOD_MAPP = 'createGroupIfNotExistsFor'
+ METHOD_DELETE = 'deleteGroup'
+ METHOD_PADS = 'listPads'
+
+ attr_reader :id, :instance, :mapper
+
+ # Creates a new Group. Optionally, you may pass the :mapper option your third party system's group id.
+ # This will allow you to find your Group again later using the same identifier as your foreign system.
+ # If you pass the mapper option, the method behaves like "create group for <mapper> if it doesn't already exist".
+ #
+ # Options:
+ # mapper => your foreign group id
+ def self.create(instance, options={})
+ id = options[:mapper] \
+ ? instance.call(METHOD_MAPP, :groupMapper => options[:mapper])[:groupID] \
+ : instance.call(METHOD_CREATE)[:groupID]
+ new instance, id, options
+ end
+
+ # Instantiates a Group object (presumed to already exist.)
+ #
+ # Options:
+ # mapper => the foreign id it's mapped to
+ def initialize(instance, id, options={})
+ @instance = instance
+ @id = id
+ @mapper = options[:mapper]
+ end
+
+ # Returns the Pad with the given id, creating it if it doesn't already exist.
+ # This requires an HTTP request, so if you *know* the Pad already exists, use Instance#get_pad instead.
+ def pad(id)
+ super groupify_pad_id(id), :group => self
+ end
+
+ # Returns the Pad with the given id (presumed to already exist).
+ # Use this instead of Instance#pad when you *know* the Pad already exists; it will save an HTTP request.
+ def get_pad(id)
+ super groupify_pad_id(id), :group => self
+ end
+
+ # Creates and returns a Pad with the given id.
+ #
+ # Options:
+ # text => 'initial Pad text'
+ def create_pad(id, options={})
+ options[:groupID] = @id
+ super id, options
+ end
+
+ # Returns an array of all the Pads in this Group.
+ def pads
+ pad_ids.map { |id| Pad.new(@instance, id, :group => self) }
+ end
+
+ # Returns an array of all the Pad ids in this Group.
+ def pad_ids
+ instance.call(METHOD_PADS, :groupID => @id)[:padIDs].keys
+ end
+
+ # Deletes the Group.
+ def delete
+ instance.call(METHOD_DELETE, :groupID => @id)
+ end
+
+ private
+
+ # Prepend the group_id to the pad name
+ def groupify_pad_id(pad_id)
+ "#{@id}$#{pad_id}"
+ end
+ end
+end
103 lib/etherpad-lite/instance.rb
@@ -0,0 +1,103 @@
+require 'uri'
+require 'net/http'
+require 'json'
+
+module EtherpadLite
+ # Returns an EtherpadLite::Instance object.
+ #
+ # eth = EtherpadLite.connect('https://etherpad.example.com', 'sdkjghJG73ksja8')
+ def self.connect(url, api_key, options={})
+ Instance.new url, api_key, options
+ end
+
+ # A EtherpadLite::Instance object represents an installation or connection to a Etherpad Lite instance.
+ #
+ # eth = EtherpadLite::Instance('http://etherpad.example.com', 'sdkjghJG73ksja8')
+ # eth.secure?
+ # => false
+ class Instance
+ include Padded
+
+ API_ROOT = 'api'
+ API_VERSION = 1
+ CODE_OK = 0;
+ CODE_INVALID_PARAMETERS = 1
+ CODE_INTERNAL_ERROR = 2
+ CODE_INVALID_METHOD = 3
+ CODE_INVALID_API_KEY = 4
+
+ attr_reader :api_key
+ attr_reader :uri
+
+ # Instantiate a new Etherpad Lite Instance. The url should include the protocol (i.e. http or https).
+ # If you are connecting to EtherpadLite on a different domain, you should usually use jsonp.
+ #
+ # Options:
+ # jsonp => true|false (default false)
+ def initialize(url, api_key, options={})
+ @uri = URI.parse(url)
+ raise ArgumentError, "#{url} is not a valid url" unless @uri.host and @uri.port
+ @api_key = api_key
+ @jsonp = options[:jsonp] if options.has_key? :jsonp
+ end
+
+ # Pad, Group, etc. all use this to send the HTTP API requests. The method is a URI under /api/VERSION/, and the options are URL parameters.
+ def call(method, options={})
+ # Parse options
+ options = {:apikey => api_key}.merge(options)
+ options[:jsonp] = '?' if @jsonp == true
+ params = options.map { |a| a.join('=') }.join('&').gsub(/\s/, '%20') # Surely Net::HTTP can do a better job of this...
+ # Make request
+ get = Net::HTTP::Get.new("/#{API_ROOT}/#{API_VERSION}/#{method}?#{params}")
+ resp = Net::HTTP::start(@uri.host, @uri.port) { |http| http.request(get) }
+ parse resp.body
+ end
+
+ # Returns a Group with the given id (it is presumed to already exist).
+ def group(id)
+ Group.new self, id
+ end
+
+ # Returns, creating if necessary, a Group for your given mapper (foreign system's group id).
+ def group_mapped_to(mapper)
+ create_group(:mapper => mapper)
+ end
+
+ # Creates a new Group. Optionally, you may pass the :mapper option your third party system's group id.
+ # This will allow you to find your Group again later using the same identifier as your foreign system.
+ #
+ # Options:
+ # mapper => your foreign group id
+ def create_group(options={})
+ Group.create self, options
+ end
+
+ # Returns true if the connection to the Etherpad Lite instance is using SSL/HTTPS.
+ def secure?
+ @uri.port == 443
+ end
+
+ private
+
+ def instance
+ self
+ end
+
+ # Parses the JSON response from the server, returning the data object as a Hash with symbolized keys.
+ # If the API response contains an error code, an exception is raised.
+ def parse(response)
+ response = JSON.parse(response, :symbolize_names => true)
+ case response[:code]
+ when CODE_OK then response[:data]
+ when CODE_INVALID_PARAMETERS
+ raise ArgumentError, response[:message]
+ when CODE_INVALID_API_KEY
+ raise ArgumentError, response[:message]
+ when CODE_INVALID_METHOD
+ raise ArgumentError, response[:message]
+ else
+ raise StandardError, "An unknown error ocurrced while handling the response: #{response.to_s}"
+ end
+ end
+ end
+end
146 lib/etherpad-lite/pad.rb
@@ -0,0 +1,146 @@
+module EtherpadLite
+ # An Etherpad Lite Pad
+ class Pad
+ METHOD_CREATE = 'createPad'
+ METHOD_CREATE_IN_GROUP = 'createGroupPad'
+ METHOD_READ = 'getText'
+ METHOD_WRITE = 'setText'
+ METHOD_DELETE = 'deletePad'
+ METHOD_NUM_REVISIONS = 'getRevisionsCount'
+ METHOD_READ_ONLY_ID = 'getReadOnlyID'
+ METHOD_GET_PUBLIC = 'getPublicStatus'
+ METHOD_SET_PUBLIC = 'setPublicStatus'
+ METHOD_PASSWORD_PROTECTED = 'isPasswordProtected'
+ METHOD_SET_PASSWORD = 'setPassword'
+
+ attr_reader :id, :instance, :rev
+
+ # Creates and returns a new Pad.
+ #
+ # Options:
+ # text => 'initial Pad text'
+ # groupID => group id of Group new Pad should belong to
+ def self.create(instance, id, options={})
+ if options[:groupID]
+ method = METHOD_CREATE_IN_GROUP
+ options[:padName] = id
+ group = Group.new instance, options[:groupID]
+ else
+ method = METHOD_CREATE
+ options[:padID] = id
+ group = nil
+ end
+ instance.call(method, options)
+ new instance, id, :group => group
+ end
+
+ # Returns the Pad for the given id, creating it if necessary.
+ # This is a "wastefull" method, in the sense that it will always try to create a Pad, requiring an HTTP call.
+ def self.get_or_create(instance, id, options={})
+ create(instance, id, options) rescue new(instance, id, options)
+ end
+
+ # Instantiate a Pad. It is presumed to already exist (via Pad.create).
+ #
+ # Options:
+ # group
+ # rev
+ def initialize(instance, id, options={})
+ @instance = instance
+ @id = id.to_s
+ @group = options[:group]
+ @rev = options[:rev]
+ end
+
+ # Returns the name of the Pad. For a normal pad, this is the same as it's id. But for a Group Pad,
+ # this strips away the group id part of the pad id.
+ def name
+ @id.sub(Group::GROUP_ID_REGEX, '').sub(/^\$/, '')
+ end
+
+ # Returns the group_id of this Pad, if any.
+ def group_id
+ unless @group_id
+ match = Group::GROUP_ID_REGEX.match(@id)
+ @group_id = match ? match[0] : nil
+ end
+ @group_id
+ end
+
+ # Returns this Pad's group, if any
+ def group
+ return nil unless group_id
+ @group ||= Group.new(@instance, group_id)
+ end
+
+ # Returns the Pad's text. Unless you specified a :rev when instantiating the Pad, or specify one here, this will return the latest revision.
+ #
+ # Options:
+ # rev => revision_number
+ def text(options={})
+ options[:padID] = @id
+ options[:rev] = @rev unless @rev.nil? or options.has_key? :rev
+ @instance.call(METHOD_READ, options)[:text]
+ end
+
+ # Writes txt to the Pad. There is no 'save' method; it is written immediately.
+ def text=(txt)
+ @instance.call(METHOD_WRITE, :padID => @id, :text => txt)
+ end
+
+ # Returns a Range of all this Pad's revision numbers
+ def revision_numbers
+ max = @instance.call(METHOD_NUM_REVISIONS, :padID => @id)[:revisions]
+ (0..max).to_a
+ end
+
+ # Returns an array of Pad objects, each with an increasing revision of the text.
+ def revisions
+ revision_numbers.map { |n| Pad.new(@instance, @id, :rev => n) }
+ end
+
+ # Returns the Pad's read-only id. This is cached.
+ def read_only_id
+ @read_only_id ||= @instance.call(METHOD_READ_ONLY_ID, :padID => @id)[:readOnlyID]
+ end
+
+ # Returns true if this is a public Pad (opposite of private).
+ # This only applies to Pads which belong to a group.
+ def public?
+ @instance.call(METHOD_GET_PUBLIC, :padID => @id)[:publicStatus]
+ end
+
+ # Set the pad's public status to true or false (opposite of private=)
+ def public=(status)
+ @instance.call(METHOD_SET_PUBLIC, :padID => @id, :publicStatus => status)
+ end
+
+ # Returns true if this is a private Pad (opposite of public)
+ # This only applies to Pads which belong to a group.
+ def private?
+ not public?
+ end
+
+ # Set the pad's private status to true or false (opposite of public=)
+ def private=(status)
+ public = !status
+ end
+
+ # Returns true if this Pad has a password, false if not.
+ # This only applies to Pads which belong to a group.
+ def password?
+ @instance.call(METHOD_PASSWORD_PROTECTED, :padID => @id)[:passwordProtection]
+ end
+
+ # Sets the Pad's password.
+ # This only applies to Pads which belong to a group.
+ def password=(new_password)
+ @instance.call(METHOD_SET_PASSWORD, :padID => @id, :password => new_password)
+ end
+
+ # Deletes the Pad
+ def delete
+ @instance.call(METHOD_DELETE, :padID => @id)
+ end
+ end
+end
25 lib/etherpad-lite/padded.rb
@@ -0,0 +1,25 @@
+module EtherpadLite
+ # Methods for dealing with pads belonging to something. Both Instance and Group include this, as they each have pads.
+ # This will work with any object which has an instance method, returning an EtherpadLite Instance object.
+ module Padded
+ # Returns the Pad with the given id, creating it if it doesn't already exist.
+ # This requires an HTTP request, so if you *know* the Pad already exists, use Instance#get_pad instead.
+ def pad(id, options={})
+ Pad.get_or_create instance, id, options
+ end
+
+ # Returns the Pad with the given id (presumed to already exist).
+ # Use this instead of Instance#pad when you *know* the Pad already exists; it will save an HTTP request.
+ def get_pad(id, options={})
+ Pad.new instance, id, options
+ end
+
+ # Creates and returns a Pad with the given id.
+ #
+ # Options:
+ # text => 'initial Pad text'
+ def create_pad(id, options={})
+ Pad.create instance, id, options
+ end
+ end
+end

0 comments on commit a731684

Please sign in to comment.
Something went wrong with that request. Please try again.