├── .gitignore ├── lib ├── ostatus │ ├── version.rb │ ├── thread.rb │ ├── link.rb │ ├── activity.rb │ ├── entry.rb │ ├── author.rb │ ├── portable_contacts.rb │ ├── feed.rb │ └── salmon.rb └── ostatus.rb ├── Rakefile ├── spec ├── helper.rb ├── salmon_spec.rb ├── feed_spec.rb ├── activity_spec.rb ├── author_spec.rb ├── builder_spec.rb ├── entry_spec.rb └── portable_contacts_spec.rb ├── test ├── example_page.html ├── example_feed_link_without_href.atom ├── example_feed_empty_author.atom ├── example_feed_false_connected.atom ├── example_feed.atom └── mime_type_bug_feed.atom ├── .travis.yml ├── Gemfile ├── README.md └── ostatus.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | -------------------------------------------------------------------------------- /lib/ostatus/version.rb: -------------------------------------------------------------------------------- 1 | module OStatus 2 | VERSION = "1.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rake/testtask' 4 | 5 | Rake::TestTask.new do |t| 6 | t.pattern = "spec/*_spec.rb" 7 | end 8 | -------------------------------------------------------------------------------- /spec/helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | require 'turn/autorun' 3 | 4 | Turn.config do |c| 5 | c.natural = true 6 | end 7 | 8 | require "mocha/setup" 9 | -------------------------------------------------------------------------------- /test/example_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | script: bundle exec rake test 3 | rvm: 4 | - 1.9.2 5 | - 1.9.3 6 | - 2.0.0 7 | - ruby-head 8 | - rbx-19mode 9 | script: "bundle exec rake test" 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ostatus.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem "minitest" 8 | gem "rake" 9 | gem "ansi" 10 | gem "turn" 11 | gem "mocha" # stubs 12 | end 13 | -------------------------------------------------------------------------------- /lib/ostatus.rb: -------------------------------------------------------------------------------- 1 | require_relative 'ostatus/feed' 2 | require_relative 'ostatus/entry' 3 | require_relative 'ostatus/author' 4 | require_relative 'ostatus/activity' 5 | require_relative 'ostatus/portable_contacts' 6 | require_relative 'ostatus/salmon' 7 | require_relative 'ostatus/thread' 8 | require_relative 'ostatus/link' -------------------------------------------------------------------------------- /spec/salmon_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative '../lib/ostatus/salmon.rb' 3 | 4 | describe OStatus::Salmon do 5 | describe "Salmon.from_xml" do 6 | it "returns nil if source is empty string" do 7 | OStatus::Salmon.from_xml("").must_equal(nil) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OStatus 2 | ======= 3 | 4 | This gem implements the OStatus protocol data streams and the technologies that are related to it such as ActivityStreams, PortableContacts, and Salmon. 5 | 6 | What it does 7 | ------------ 8 | 9 | Right now, it simply parses Atom and gives the ability to parse the XML for each of the objects in the OStatus world. 10 | -------------------------------------------------------------------------------- /lib/ostatus/thread.rb: -------------------------------------------------------------------------------- 1 | require 'xml/libxml' 2 | require 'atom/xml/parser.rb' 3 | 4 | module OStatus 5 | 6 | # This will parse the Thread Atom extension 7 | class Thread 8 | include Atom::Xml::Parseable 9 | attribute :ref, :type, :source 10 | uri_attribute :href 11 | 12 | def initialize(o) 13 | case o 14 | when XML::Reader 15 | if current_node_is?(o, 'in-reply-to') 16 | parse(o, :once => true) 17 | else 18 | raise ArgumentError, "Thread created with node other than thr:in-reply-to: #{o.name}" 19 | end 20 | when Hash 21 | [:href, :ref, :type, :source].each do |attr| 22 | self.send("#{attr}=", o[attr]) 23 | end 24 | else 25 | raise ArgumentError, "Don't know how to handle #{o}" 26 | end 27 | end 28 | 29 | def length=(v) 30 | @length = v.to_i 31 | end 32 | 33 | def to_s 34 | self.href 35 | end 36 | 37 | def ==(o) 38 | o.respond_to?(:href) && o.href == self.href 39 | end 40 | 41 | def inspect 42 | "" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /ostatus.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "ostatus/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "ostatus" 7 | s.version = OStatus::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ['Hackers of the Severed Hand'] 10 | s.email = ['hotsh@xomb.org'] 11 | s.homepage = "http://github.com/hotsh/ostatus" 12 | s.summary = %q{Implementations of the OStatus data stream objects.} 13 | s.description = %q{This project is to be used to jumpstart OStatus related projects that implement the PubSubHubbub protocols by providing the common fundamentals of Atom parsing and OStatus object creation.} 14 | 15 | s.rubyforge_project = "ostatus" 16 | 17 | s.add_dependency "ratom", "~> 0.8.2" 18 | s.add_development_dependency "rspec", "~> 2.10.0" 19 | s.add_development_dependency "rake", "~> 0.9.2" 20 | 21 | s.files = `git ls-files`.split("\n") 22 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 23 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 24 | s.require_paths = ["lib"] 25 | end 26 | -------------------------------------------------------------------------------- /spec/feed_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative '../lib/ostatus/feed.rb' 3 | 4 | describe OStatus::Feed do 5 | describe "#initialize" do 6 | it "should detect a feed URI in an HTML page" do 7 | @feed = OStatus::Feed.from_url('test/example_page.html') 8 | @feed.url.must_equal 'test/example_feed.atom' 9 | end 10 | end 11 | 12 | describe "with example_feed.atom" do 13 | before(:each) do 14 | @feed = OStatus::Feed.from_url('test/example_feed.atom') 15 | end 16 | 17 | describe "#atom" do 18 | it "should return a String containing the atom information" do 19 | @feed.atom.start_with?(" true) 17 | else 18 | raise ArgumentError, "Link created with node other than atom:link: #{o.name}" 19 | end 20 | when Hash 21 | [:href, :rel, :type, :length, :hreflang, :title].each do |attr| 22 | self.send("#{attr}=", o[attr]) 23 | end 24 | else 25 | raise ArgumentError, "Don't know how to handle #{o}" 26 | end 27 | end 28 | 29 | remove_method :length= 30 | def length=(v) 31 | @length = v.to_i 32 | end 33 | 34 | def href 35 | @href || self.text 36 | end 37 | 38 | def to_s 39 | self.href 40 | end 41 | 42 | def ==(o) 43 | o.respond_to?(:href) && o.href == self.href 44 | end 45 | 46 | # This will fetch the URL referenced by the link. 47 | # 48 | # If the URL contains a valid feed, a Feed will be returned, otherwise, 49 | # the body of the response will be returned. 50 | # 51 | # TODO: Handle redirects. 52 | # 53 | def fetch(options = {}) 54 | begin 55 | Atom::Feed.load_feed(URI.parse(self.href), options) 56 | rescue ArgumentError 57 | Net::HTTP.get_response(URI.parse(self.href)).body 58 | end 59 | end 60 | 61 | def inspect 62 | "" 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/author_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative '../lib/ostatus/author.rb' 3 | 4 | describe OStatus::Author do 5 | before(:each) do 6 | feed = OStatus::Feed.from_url('test/example_feed.atom') 7 | @author = feed.author 8 | feed = OStatus::Feed.from_url('test/example_feed_empty_author.atom') 9 | @author_empty = feed.author 10 | end 11 | 12 | describe "#activity" do 13 | it "should return an Activity instance" do 14 | @author.activity.class.must_equal(OStatus::Activity) 15 | end 16 | 17 | it "should give an Activity instance that is relevant to the author subtree" do 18 | @author.activity.object_type.must_equal(:person) 19 | end 20 | end 21 | 22 | describe "#portable_contacts" do 23 | it "should return an PortableContacts instance" do 24 | @author.portable_contacts.class.must_equal(OStatus::PortableContacts) 25 | end 26 | 27 | it "should give an PortableContacts instance that is relevant to the author subtree" do 28 | @author.portable_contacts.preferred_username.must_equal('greenmanspirit') 29 | end 30 | end 31 | 32 | describe "#uri" do 33 | it "should give a String containing the content of the uri tag" do 34 | @author.uri.must_equal('http://identi.ca/user/141464') 35 | end 36 | 37 | it "should give nil when no uri is given" do 38 | @author_empty.uri.must_equal(nil) 39 | end 40 | end 41 | 42 | describe "#name" do 43 | it "should give a String containing the content of the name tag" do 44 | @author.name.must_equal('greenmanspirit') 45 | end 46 | 47 | it "should give nil when no name is given" do 48 | @author_empty.name.must_equal(nil) 49 | end 50 | end 51 | 52 | describe "email" do 53 | it "should give a String containing the content of the email tag" do 54 | @author.email.must_equal('foo@example.com') 55 | end 56 | 57 | it "should give nil when no email is given" do 58 | @author_empty.email.must_equal(nil) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/ostatus/activity.rb: -------------------------------------------------------------------------------- 1 | module OStatus 2 | ACTIVITY_NS = 'http://activitystrea.ms/spec/1.0/' 3 | 4 | # This class represents an Activity object for an OStatus entry. 5 | class Activity 6 | SCHEMA_ROOT = 'http://activitystrea.ms/schema/1.0/' 7 | 8 | def initialize(entry) 9 | @entry = entry 10 | end 11 | 12 | # Returns the object field or nil if it does not exist. 13 | def object 14 | if @entry.is_a? Hash 15 | @entry[:object] 16 | else 17 | @entry.activity_object 18 | end 19 | end 20 | 21 | # Returns the target field or nil if it does not exist. 22 | def target 23 | if @entry.is_a? Hash 24 | @entry[:object] 25 | else 26 | @entry.activity_target 27 | end 28 | end 29 | 30 | # Returns the verb field or nil if it does not exist. 31 | # :favorite, :follow, :like, :"make-friend", :join, :play, 32 | # :post, :save, :share, :tag, :update 33 | def verb 34 | if @entry.is_a? Hash 35 | @entry[:object] 36 | else 37 | obj = @entry.activity_verb 38 | if obj.nil? 39 | nil 40 | elsif obj.start_with?(SCHEMA_ROOT) 41 | obj[SCHEMA_ROOT.size..-1].intern unless obj.nil? 42 | else 43 | obj 44 | end 45 | end 46 | end 47 | 48 | def verb= value 49 | if @entry.is_a? Hash 50 | @entry[:object] = value 51 | else 52 | if [:favorite, :follow, :like, :"make-friend", :join, 53 | :play, :save, :share, :tag, :update].include? value 54 | value = "#{SCHEMA_ROOT}#{value}" 55 | end 56 | 57 | @entry.activity_verb = value 58 | end 59 | end 60 | 61 | # Returns the object-type field or nil if it does not exist. 62 | # :article, :audio, :bookmark, :comment, :file, :folder, :group, 63 | # :list, :note, :person, :photo, :"photo-album", :place, :playlist, 64 | # :product, :review, :service, :status, :video 65 | def object_type 66 | if @entry.is_a? Hash 67 | @entry[:object_type] 68 | else 69 | obj = @entry.activity_object_type 70 | if obj.nil? 71 | nil 72 | elsif obj.start_with?(SCHEMA_ROOT) 73 | obj[SCHEMA_ROOT.size..-1].intern unless obj.nil? 74 | else 75 | obj 76 | end 77 | end 78 | end 79 | 80 | # Returns a hash of all relevant fields. 81 | def info 82 | { 83 | :object => self.object, 84 | :target => self.target, 85 | :verb => self.verb, 86 | :object_type => self.object_type 87 | } 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/ostatus/entry.rb: -------------------------------------------------------------------------------- 1 | require_relative 'activity' 2 | require_relative 'author' 3 | require_relative 'thread' 4 | require_relative 'link' 5 | 6 | module OStatus 7 | THREAD_NS = 'http://purl.org/syndication/thread/1.0' 8 | 9 | # Holds information about an individual entry in the Feed. 10 | class Entry < Atom::Entry 11 | include Atom::SimpleExtensions 12 | 13 | add_extension_namespace :activity, ACTIVITY_NS 14 | element 'activity:object-type' 15 | element 'activity:object', :class => OStatus::Author 16 | element 'activity:verb' 17 | element 'activity:target' 18 | 19 | add_extension_namespace :thr, THREAD_NS 20 | element 'thr:in-reply-to', :class => OStatus::Thread 21 | 22 | # This is for backwards compatibility with some implementations of Activity 23 | # Streams. It should not be used, and in fact is obscured as it is not a 24 | # method in OStatus::Activity. 25 | element 'activity:actor', :class => OStatus::Author 26 | 27 | namespace Atom::NAMESPACE 28 | element :title, :id, :summary 29 | element :updated, :published, :class => DateTime, :content_only => true 30 | element :source, :class => Atom::Source 31 | elements :links, :class => OStatus::Link 32 | 33 | elements :categories, :class => Atom::Category 34 | element :content, :class => Atom::Content 35 | element :author, :class => OStatus::Author 36 | 37 | def activity 38 | Activity.new(self) 39 | end 40 | 41 | def activity= value 42 | if value.object_type 43 | self.activity_object_type = OStatus::Activity::SCHEMA_ROOT + value.object_type.to_s 44 | end 45 | self.activity_object = value.activity_object if value.object 46 | if value.verb 47 | self.activity_verb = OStatus::Activity::SCHEMA_ROOT + value.activity_verb.to_s 48 | end 49 | self.activity_target = value.activity_target if value.target 50 | end 51 | 52 | def url 53 | if links.alternate 54 | links.alternate.href 55 | elsif links.self 56 | links.self.href 57 | else 58 | links.map.each do |l| 59 | l.href 60 | end.compact.first 61 | end 62 | end 63 | 64 | def url= value 65 | links << Atom::Link.new(:rel => "alternate", :href => value) 66 | end 67 | 68 | def link 69 | links.group_by { |l| l.rel.intern } 70 | end 71 | 72 | def link= options 73 | links.clear << Atom::Link.new(options) 74 | end 75 | 76 | # Returns a Hash of all fields. 77 | def info 78 | { 79 | :activity => self.activity.info, 80 | :id => self.id, 81 | :title => self.title, 82 | :content => self.content, 83 | :link => self.link, 84 | :published => self.published, 85 | :updated => self.updated 86 | } 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/ostatus/author.rb: -------------------------------------------------------------------------------- 1 | require_relative 'activity' 2 | require_relative 'portable_contacts' 3 | 4 | module OStatus 5 | require 'atom' 6 | 7 | # Holds information about the author of the Feed. 8 | class Author < Atom::Person 9 | require 'date' 10 | 11 | include Atom::SimpleExtensions 12 | 13 | add_extension_namespace :activity, ACTIVITY_NS 14 | element 'activity:object-type' 15 | 16 | namespace Atom::NAMESPACE 17 | element :email 18 | element :uri 19 | 20 | elements :links, :class => Atom::Link 21 | 22 | add_extension_namespace :poco, POCO_NS 23 | element 'poco:id' 24 | element 'poco:displayName' 25 | element 'poco:nickname' 26 | element 'poco:updated', :class => DateTime, :content_only => true 27 | element 'poco:published', :class => DateTime, :content_only => true 28 | element 'poco:birthday', :class => Date, :content_only => true 29 | element 'poco:anniversary', :class => Date, :content_only => true 30 | element 'poco:gender' 31 | element 'poco:note' 32 | element 'poco:preferredUsername' 33 | element 'poco:connected' 34 | 35 | def initialize *args 36 | self.activity_object_type = "http://activitystrea.ms/schema/1.0/person" 37 | super(*args) 38 | end 39 | 40 | # unfortunately ratom doesn't handle elements with the same local name well. 41 | # this is a workaround for that. 42 | attr_writer :name, :poco_name 43 | 44 | def name 45 | @name or self[Atom::NAMESPACE, 'name'].first 46 | end 47 | 48 | def poco_name 49 | @poco_name or self[POCO_NS, 'name'].first 50 | end 51 | 52 | def to_xml(*args) 53 | x = super(*args) 54 | 55 | if self.name 56 | node = XML::Node.new('name') 57 | node << self.name 58 | x << node 59 | end 60 | 61 | if self.poco_name 62 | node = XML::Node.new('poco:name') 63 | node << self.poco_name 64 | x << node 65 | end 66 | 67 | x 68 | end 69 | 70 | # Gives an instance of an OStatus::Activity that parses the fields 71 | # having an activity prefix. 72 | def activity 73 | OStatus::Activity.new(self) 74 | end 75 | 76 | # Returns an instance of a PortableContacts that further describe the 77 | # author's contact information, if it exists. 78 | def portable_contacts 79 | PortableContacts.new(self) 80 | end 81 | 82 | def portable_contacts= poco 83 | [ 'id', 'name', 'nickname', 'updated', 'published', 'birthday', 84 | 'anniversary', 'gender', 'note', 'connected'].each do |p| 85 | v = poco.send(p) 86 | self.send("poco_#{p}=", v) if v 87 | end 88 | 89 | self.poco_displayName = poco.display_name if poco.display_name 90 | self.poco_preferredUsername = poco.preferred_username if poco.preferred_username 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/ostatus/portable_contacts.rb: -------------------------------------------------------------------------------- 1 | module OStatus 2 | POCO_NS = 'http://portablecontacts.net/spec/1.0' 3 | 4 | # Holds information about the extended contact information 5 | # in the Feed given in the Portable Contacts specification. 6 | class PortableContacts 7 | 8 | # Instantiates a OStatus::PortableContacts object from either 9 | # a given root that contains all tags as an ratom Person 10 | # or a Hash containing the properties. 11 | def initialize(parent) 12 | if parent.is_a? Hash 13 | @options = parent 14 | else 15 | @parent = parent 16 | end 17 | end 18 | 19 | def id; get_prop(:id); end 20 | def id= value; set_prop(:id, value); end 21 | 22 | def name; get_prop(:name); end 23 | def name= value; set_prop(:name, value); end 24 | 25 | def nickname; get_prop(:nickname); end 26 | def nickname= value; set_prop(:nickname, value); end 27 | 28 | def gender; get_prop(:gender); end 29 | def gender= value; set_prop(:gender, value); end 30 | 31 | def note; get_prop(:note); end 32 | def note= value; set_prop(:note, value); end 33 | 34 | def display_name; get_prop(:display_name, 'displayName'); end 35 | def display_name= value; set_prop(:display_name, value, 'displayName'); end 36 | 37 | def preferred_username 38 | get_prop(:preferred_username, 'preferredUsername') 39 | end 40 | 41 | def preferred_username= value 42 | set_prop(:preferred_username, value, 'preferredUsername') 43 | end 44 | 45 | def updated; get_datetime(:updated); end 46 | def published; get_datetime(:published); end 47 | 48 | def birthday; get_date(:birthday); end 49 | def anniversary; get_date(:anniversary); end 50 | 51 | # Returns a boolean that indicates that a bi-directional connection 52 | # has been established between the user and the contact, if it is 53 | # able to assert this. 54 | def connected 55 | return @options[:connected] unless @options.nil? 56 | str = @parent.poco_connected 57 | 58 | if str == "true" 59 | true 60 | elsif str == "false" 61 | false 62 | else 63 | nil 64 | end 65 | end 66 | 67 | private 68 | 69 | def get_prop name, xmlName = name 70 | @options ? @options[name] : @parent.send("poco_#{xmlName}") 71 | end 72 | 73 | def set_prop name, value, xmlName = name 74 | if @options 75 | @options[name] = value 76 | else 77 | @parent.send("poco_#{xmlName}=", value) 78 | end 79 | end 80 | 81 | def get_datetime x 82 | if @options 83 | dt = @options[x] 84 | DateTime.parse(dt) if dt 85 | else 86 | @parent.send("poco_#{x}") 87 | end 88 | end 89 | 90 | def get_date x 91 | if @options 92 | d = @options[x] 93 | Date.parse(d) if d 94 | else 95 | @parent.send("poco_#{x}") 96 | end 97 | end 98 | 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative '../lib/ostatus/feed.rb' 3 | 4 | describe 'XML builder' do 5 | before(:each) do 6 | @feed_url = 'http://example.org/feed' 7 | @poco_id = '68b329da9893e34099c7d8ad5cb9c940' 8 | @poco = OStatus::PortableContacts.new(:id => @poco_id, 9 | :display_name => 'Dean Venture', 10 | :preferred_username => 'dean') 11 | @author = OStatus::Author.new(:name => 'Dean Venture', 12 | :email => 'dean@venture.com', 13 | :uri => 'http://geocities.com/~dean', 14 | :portable_contacts => @poco) 15 | @feed = OStatus::Feed.from_data(@feed_url, 16 | :title => "Dean's Updates", 17 | :id => @feed_url, 18 | :author => @author, 19 | :entries => [], 20 | :links => { 21 | :hub => [{:href => 'http://example.org/hub'}] 22 | }) 23 | end 24 | 25 | it 'should generate the title' do 26 | @feed.atom.must_match("Dean's Updates") 27 | end 28 | 29 | it 'should generate the id' do 30 | @feed.atom.must_match("<id>#{@feed_url}") 31 | end 32 | 33 | it 'should generate a self link' do 34 | # depending on this attribute order is a really terrible idea, but oh well. 35 | @feed.atom.must_match("<link rel=\"self\" href=\"#{@feed_url}\"/>") 36 | end 37 | 38 | it 'should generate the hub link' do 39 | # depending on this attribute order is a really terrible idea, but oh well. 40 | @feed.atom.must_match('<link rel="hub" href="http://example.org/hub"/>') 41 | end 42 | 43 | describe 'when generating the author' do 44 | specify { @feed.atom.must_match('<name>Dean Venture') } 45 | specify { @feed.atom.must_match('<email>dean@venture.com') } 46 | specify { @feed.atom.must_match('<uri>http://geocities.com/~dean') } 47 | specify { @feed.atom.must_match("<poco:id>#{@poco_id}") } 48 | specify { @feed.atom.must_match('<poco:displayName>Dean Venture') } 49 | specify { @feed.atom.must_match('<poco:preferredUsername>dean') } 50 | end 51 | 52 | describe 'when generating a feed with entries' do 53 | before do 54 | @now = Time.now 55 | 56 | @feed.entries << OStatus::Entry.new( 57 | :title => 'atom powered robots are running amok lol', 58 | :content => 'atom powered robots are running amok lol', 59 | :updated => @now, 60 | :published => @now, 61 | :id => 'http://example.org/feed/1', 62 | :link => { :href => 'http://example.org/feed/1' } 63 | ) 64 | end 65 | 66 | specify { @feed.atom.must_match('<title>atom powered robots') } 67 | specify { @feed.atom.must_match('<content>atom powered robots') } 68 | specify { @feed.atom.must_match(/#{Regexp.escape("<updated>#{@now.iso8601}")}/) } 69 | specify { @feed.atom.must_match(/#{Regexp.escape("<published>#{@now.iso8601}")}/) } 70 | specify { @feed.atom.must_match('<id>http://example.org/feed/1') } 71 | specify { @feed.atom.must_match('<link href="http://example.org/feed/1"/>') } 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/entry_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative '../lib/ostatus/entry.rb' 3 | 4 | describe OStatus::Entry do 5 | before(:each) do 6 | @feed = OStatus::Feed.from_url('test/example_feed.atom') 7 | @entry = @feed.entries[0] 8 | @feed_link_without_href = OStatus::Feed.from_url('test/example_feed_link_without_href.atom') 9 | @entry_link_without_href = @feed_link_without_href.entries[0] 10 | end 11 | 12 | describe "#activity" do 13 | it "should return an Activity instance" do 14 | @entry.activity.class.must_equal(OStatus::Activity) 15 | end 16 | end 17 | 18 | describe "#title" do 19 | it "should give a String containing the content of the title tag" do 20 | @entry.title.must_equal("staples come out of the head tomorrow, oh yeah") 21 | end 22 | end 23 | 24 | describe "#content" do 25 | it "should give a String containing the content of the content tag" do 26 | @entry.content.must_equal("staples come out of the head tomorrow, oh yeah") 27 | end 28 | end 29 | 30 | describe "#updated" do 31 | it "should return a DateTime instance" do 32 | @entry.updated.instance_of?(DateTime).must_equal(true) 33 | end 34 | 35 | it "should return a DateTime representing the time given in the updated tag" do 36 | @entry.updated.strftime("%Y-%m-%dT%I:%M:%S%z").must_equal('2011-03-22T02:15:14+0000') 37 | end 38 | end 39 | 40 | describe "#published" do 41 | it "should return a DateTime instance" do 42 | @entry.published.instance_of?(DateTime).must_equal(true) 43 | end 44 | 45 | it "should return a DateTime representing the time given in the published tag" do 46 | @entry.published.strftime("%Y-%m-%dT%I:%M:%S%z").must_equal('2011-02-21T02:15:14+0000') 47 | end 48 | end 49 | 50 | describe "#id" do 51 | it "should return the id given in the id tag" do 52 | @entry.id.must_equal('http://identi.ca/notice/64991641') 53 | end 54 | end 55 | 56 | describe "#info" do 57 | it "should give a Hash" do 58 | @entry.info.instance_of?(Hash).must_equal(true) 59 | end 60 | 61 | it "should contain the id" do 62 | @entry.info[:id].must_equal('http://identi.ca/notice/64991641') 63 | end 64 | 65 | it "should contain the content" do 66 | @entry.info[:content].must_equal("staples come out of the head tomorrow, oh yeah") 67 | end 68 | 69 | it "should contain the title" do 70 | @entry.info[:title].must_equal("staples come out of the head tomorrow, oh yeah") 71 | end 72 | 73 | it "should contain a Hash for the link" do 74 | @entry.info[:link].class.must_equal(Hash) 75 | end 76 | 77 | it "should contain the published DateTime" do 78 | @entry.info[:published].class.must_equal(DateTime) 79 | @entry.info[:published].strftime("%Y-%m-%dT%I:%M:%S%z").must_equal('2011-02-21T02:15:14+0000') 80 | end 81 | 82 | it "should contain the updated DateTime" do 83 | @entry.info[:updated].class.must_equal(DateTime) 84 | @entry.info[:updated].strftime("%Y-%m-%dT%I:%M:%S%z").must_equal('2011-03-22T02:15:14+0000') 85 | end 86 | end 87 | 88 | describe "#links" do 89 | it "should use OStatus::Link elements" do 90 | @entry.links.first.class.must_equal(OStatus::Link) 91 | end 92 | end 93 | 94 | describe "#url" do 95 | it "should return the alternate link's href attribute" do 96 | @entry.url.must_equal("http://identi.ca/notice/64991641") 97 | end 98 | 99 | it "should return the alternate link's content if there's no href" do 100 | @entry_link_without_href.url.must_equal("http://identi.ca/notice/89057569") 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/portable_contacts_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative '../lib/ostatus/feed.rb' 3 | require_relative '../lib/ostatus/entry.rb' 4 | require_relative '../lib/ostatus/activity.rb' 5 | 6 | describe OStatus::PortableContacts do 7 | before(:each) do 8 | feed = OStatus::Feed.from_url('test/example_feed.atom') 9 | author = feed.author 10 | @poco = author.portable_contacts 11 | feed = OStatus::Feed.from_url('test/example_feed_false_connected.atom') 12 | author = feed.author 13 | @poco_false = author.portable_contacts 14 | feed = OStatus::Feed.from_url('test/example_feed_empty_author.atom') 15 | author = feed.author 16 | @poco_empty = author.portable_contacts 17 | end 18 | 19 | describe "#id" do 20 | it "should give a String containing the content of the title tag" do 21 | @poco.id.must_equal('foobar') 22 | end 23 | 24 | it "should give nil if the id tag does not exist" do 25 | @poco_empty.id.must_equal(nil) 26 | end 27 | end 28 | 29 | describe "#display_name" do 30 | it "should give a String containing the content of the displayName tag" do 31 | @poco.display_name.must_equal('Adam Hobaugh') 32 | end 33 | 34 | it "should give nil if the displayName tag does not exist" do 35 | @poco_empty.display_name.must_equal(nil) 36 | end 37 | end 38 | 39 | describe "#name" do 40 | it "should give a String containing the content of the name tag" do 41 | @poco.name.must_equal('barbaz') 42 | end 43 | 44 | it "should give nil if the name tag does not exist" do 45 | @poco_empty.name.must_equal(nil) 46 | end 47 | end 48 | 49 | describe "#nickname" do 50 | it "should give a String containing the content of the nickname tag" do 51 | @poco.nickname.must_equal('spaz') 52 | end 53 | 54 | it "should give nil if the nickname tag does not exist" do 55 | @poco_empty.nickname.must_equal(nil) 56 | end 57 | end 58 | 59 | describe "#published" do 60 | it "should give a DateTime instance" do 61 | @poco.published.class.must_equal(DateTime) 62 | end 63 | 64 | it "should give a DateTime containing the content of the published tag" do 65 | @poco.published.strftime("%Y-%m-%dT%I:%M:%S%z").must_equal('2012-02-21T02:15:14+0000') 66 | end 67 | 68 | it "should give nil if the published tag does not exist" do 69 | @poco_empty.published.must_equal(nil) 70 | end 71 | end 72 | 73 | describe "#updated" do 74 | it "should give a DateTime instance" do 75 | @poco.updated.class.must_equal(DateTime) 76 | end 77 | 78 | it "should give a DateTime containing the content of the updated tag" do 79 | @poco.updated.strftime("%Y-%m-%dT%I:%M:%S%z").must_equal('2013-02-21T02:15:14+0000') 80 | end 81 | 82 | it "should give nil if the updated tag does not exist" do 83 | @poco_empty.updated.must_equal(nil) 84 | end 85 | end 86 | 87 | describe "#birthday" do 88 | it "should give a Date instance" do 89 | @poco.birthday.class.must_equal(Date) 90 | end 91 | 92 | it "should give a Date containing the content of the birthday tag" do 93 | @poco.birthday.strftime("%Y-%m-%d").must_equal('2014-02-21') 94 | end 95 | 96 | it "should give nil if the birthday tag does not exist" do 97 | @poco_empty.birthday.must_equal(nil) 98 | end 99 | end 100 | 101 | describe "#anniversary" do 102 | it "should give a Date instance" do 103 | @poco.anniversary.class.must_equal(Date) 104 | end 105 | 106 | it "should give a Date containing the content of the anniversary tag" do 107 | @poco.anniversary.strftime("%Y-%m-%d").must_equal('2015-02-21') 108 | end 109 | 110 | it "should give nil if the anniversary tag does not exist" do 111 | @poco_empty.anniversary.must_equal(nil) 112 | end 113 | end 114 | 115 | describe "#gender" do 116 | it "should give a String containing the content of the gender tag" do 117 | @poco.gender.must_equal('male') 118 | end 119 | 120 | it "should give nil if the gender tag does not exist" do 121 | @poco_empty.gender.must_equal(nil) 122 | end 123 | end 124 | 125 | describe "#note" do 126 | it "should give a String containing the content of the note tag" do 127 | @poco.note.must_equal("foo\nbar") 128 | end 129 | 130 | it "should give nil if the note tag does not exist" do 131 | @poco_empty.note.must_equal(nil) 132 | end 133 | end 134 | 135 | describe "#preferred_username" do 136 | it "should give a String containing the content of the preferred_username tag" do 137 | @poco.preferred_username.must_equal("greenmanspirit") 138 | end 139 | 140 | it "should give nil if the preferred_username tag does not exist" do 141 | @poco_empty.preferred_username.must_equal(nil) 142 | end 143 | end 144 | 145 | describe "#connected" do 146 | it "should give a boolean true when the content of the connected tag is 'true'" do 147 | @poco.connected.must_equal(true) 148 | end 149 | 150 | it "should give a boolean false when the content of the connected tag is 'false'" do 151 | @poco_false.connected.must_equal(false) 152 | end 153 | 154 | it "should give nil if the connected tag does not exist" do 155 | @poco_empty.connected.must_equal(nil) 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/ostatus/feed.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'atom' 3 | 4 | require_relative 'entry' 5 | require_relative 'author' 6 | 7 | module OStatus 8 | 9 | # This class represents an OStatus Feed object. 10 | class Feed < Atom::Feed 11 | include Atom::SimpleExtensions 12 | 13 | namespace Atom::NAMESPACE 14 | 15 | add_extension_namespace :poco, POCO_NS 16 | add_extension_namespace :poco, ACTIVITY_NS 17 | element :id, :rights, :icon, :logo 18 | element :generator, :class => Atom::Generator 19 | element :title, :subtitle, :class => Atom::Content 20 | element :updated, :class => Time, :content_only => true 21 | elements :links, :class => Atom::Link 22 | elements :authors, :class => OStatus::Author 23 | elements :categories, :class => Atom::Category 24 | elements :entries, :class => OStatus::Entry 25 | 26 | attr_reader :url 27 | 28 | # Store in reverse order so that the -1 from .index "not found" 29 | # will sort properly 30 | MIME_ORDER = ['application/atom+xml', 'application/rss+xml', 31 | 'application/xml'].reverse 32 | 33 | def initialize(str, url, access_token, options) 34 | @str = str 35 | @url = url 36 | @access_token = access_token 37 | @options = options 38 | 39 | if str 40 | 41 | if str =~ /<html/ 42 | doc = LibXML::XML::HTMLParser.string(str).parse 43 | links = doc.find( 44 | "//*[contains(concat(' ',normalize-space(@rel),' '), 'alternate')]" 45 | ).map {|el| 46 | {:type => el.attributes['type'].to_s, 47 | :href => el.attributes['href'].to_s} 48 | }.sort {|a, b| 49 | MIME_ORDER.index(b[:type]) || -1 <=> 50 | MIME_ORDER.index(a[:type]) || -1 51 | } 52 | 53 | # Resolve relative links 54 | link = URI::parse(links.first[:href]) rescue URI.new 55 | 56 | unless link.host 57 | link.host = URI::parse(@url).host rescue nil 58 | end 59 | 60 | unless link.absolute? 61 | link.path = File::dirname(URI::parse(@url).path) \ 62 | + '/' + link.path rescue nil 63 | end 64 | 65 | @url = link.to_s 66 | @str = str = open(@url).read 67 | end 68 | 69 | super(XML::Reader.string(str)) 70 | else 71 | super(options) 72 | end 73 | end 74 | 75 | # Creates a new Feed instance given by the atom feed located at 'url' 76 | # and optionally using the OAuth::AccessToken given. 77 | def Feed.from_url(url, access_token = nil) 78 | if access_token.nil? 79 | # simply open the url 80 | str = open(url).read 81 | else 82 | # open the url through OAuth 83 | str = access_token.get(url).body 84 | end 85 | 86 | Feed.new(str, url, access_token, nil) 87 | end 88 | 89 | # Creates a new Feed instance that contains the information given by 90 | # the various instances of author and entries. 91 | def Feed.from_data(url, options) 92 | Feed.new(nil, url, nil, options) 93 | end 94 | 95 | def Feed.from_string(str) 96 | Feed.new(str, nil, nil, nil) 97 | end 98 | 99 | # Returns an array of Atom::Link instances for all link tags 100 | # that have a rel equal to that given by attribute. 101 | # 102 | # For example: 103 | # link(:hub).first.href -- Gets the first link tag with rel="hub" and 104 | # returns the contents of the href attribute. 105 | # 106 | def link(attribute) 107 | links.find_all { |l| l.rel == attribute.to_s } 108 | end 109 | 110 | def links=(given) 111 | self.links.clear 112 | given.each do |rel,links| 113 | links.each do |l| 114 | self.links << Atom::Link.new(l.merge({:rel => rel})) 115 | end 116 | end 117 | end 118 | 119 | # Returns an array of URLs for each hub link tag. 120 | def hubs 121 | link(:hub).map { |link| link.href } 122 | end 123 | 124 | # Returns the salmon URL from the link tag. 125 | def salmon 126 | link(:salmon).first.href 127 | end 128 | 129 | # This method will return a String containing the actual content of 130 | # the atom feed. It will make a network request (through OAuth if 131 | # an access token was given) to retrieve the document if necessary. 132 | def atom 133 | if @str != nil 134 | @str 135 | elsif @options == nil and @access_token == nil 136 | # simply open the url 137 | open(@url).read 138 | elsif @options == nil and @url != nil 139 | # open the url through OAuth 140 | @access_token.get(@url).body 141 | else 142 | self.links << Atom::Link.new(:rel => 'self', :href => @url) if @url 143 | self.links << Atom::Link.new(:rel => 'edit', :href => @url) if @url 144 | self.to_xml 145 | end 146 | end 147 | 148 | # Returns an OStatus::Author that will parse the author information 149 | # within the Feed. 150 | def author 151 | @options ? @options[:author] : self.authors.first 152 | end 153 | 154 | def author= author 155 | self.authors.clear << author 156 | end 157 | 158 | def hubs= hubs 159 | hubs.each do |hub| 160 | links << Atom::Link.new(:rel => 'hub', :href => hub) 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/ostatus/salmon.rb: -------------------------------------------------------------------------------- 1 | require 'xml' 2 | require 'atom' 3 | require 'digest/sha2' 4 | 5 | module OStatus 6 | class Salmon 7 | attr_accessor :entry 8 | 9 | # Create a Salmon instance for a particular OStatus::Entry 10 | def initialize entry, signature = nil, plaintext = nil 11 | @entry = entry 12 | @signature = signature 13 | @plaintext = plaintext 14 | end 15 | 16 | # Creates an entry for following a particular Author. 17 | def Salmon.from_follow(user_author, followed_author) 18 | entry = OStatus::Entry.new( 19 | :author => user_author, 20 | :title => "Now following #{followed_author.name}", 21 | :content => Atom::Content::Html.new("Now following #{followed_author.name}") 22 | ) 23 | 24 | entry.activity.verb = :follow 25 | entry.activity_object = followed_author 26 | 27 | OStatus::Salmon.new(entry) 28 | end 29 | 30 | # Creates an entry for unfollowing a particular Author. 31 | def Salmon.from_unfollow(user_author, followed_author) 32 | entry = OStatus::Entry.new( 33 | :author => user_author, 34 | :title => "Stopped following #{followed_author.name}", 35 | :content => Atom::Content::Html.new("Stopped following #{followed_author.name}") 36 | ) 37 | 38 | entry.activity_verb = "http://ostatus.org/schema/1.0/unfollow" 39 | entry.activity_object = followed_author 40 | 41 | OStatus::Salmon.new(entry) 42 | end 43 | 44 | # Will pull a OStatus::Entry from a magic envelope described by the xml. 45 | def Salmon.from_xml source 46 | if source.is_a?(String) 47 | if source.length == 0 48 | return nil 49 | end 50 | 51 | source = XML::Document.string(source, 52 | :options => XML::Parser::Options::NOENT) 53 | end 54 | 55 | # Retrieve the envelope 56 | envelope = source.find('/me:env', 57 | 'me:http://salmon-protocol.org/ns/magic-env').first 58 | 59 | if envelope.nil? 60 | return nil 61 | end 62 | 63 | data = envelope.find('me:data', 64 | 'me:http://salmon-protocol.org/ns/magic-env').first 65 | if data.nil? 66 | return nil 67 | end 68 | 69 | data_type = data.attributes["type"] 70 | if data_type.nil? 71 | data_type = 'application/atom+xml' 72 | armored_data_type = '' 73 | else 74 | armored_data_type = Base64::urlsafe_encode64(data_type) 75 | end 76 | 77 | encoding = envelope.find('me:encoding', 78 | 'me:http://salmon-protocol.org/ns/magic-env').first 79 | 80 | algorithm = envelope.find( 81 | 'me:alg', 82 | 'me:http://salmon-protocol.org/ns/magic-env').first 83 | 84 | signature = source.find('me:sig', 85 | 'me:http://salmon-protocol.org/ns/magic-env').first 86 | 87 | # Parse fields 88 | 89 | if signature.nil? 90 | # Well, if we cannot verify, we don't accept 91 | return nil 92 | else 93 | # XXX: Handle key_id attribute 94 | signature = signature.content 95 | signature = Base64::urlsafe_decode64(signature) 96 | end 97 | 98 | if encoding.nil? 99 | # When the encoding is omitted, use base64url 100 | # Cite: Magic Envelope Draft Spec Section 3.3 101 | armored_encoding = '' 102 | encoding = 'base64url' 103 | else 104 | armored_encoding = Base64::urlsafe_encode64(encoding.content) 105 | encoding = encoding.content.downcase 106 | end 107 | 108 | if algorithm.nil? 109 | # When algorithm is omitted, use 'RSA-SHA256' 110 | # Cite: Magic Envelope Draft Spec Section 3.3 111 | armored_algorithm = '' 112 | algorithm = 'rsa-sha256' 113 | else 114 | armored_algorithm = Base64::urlsafe_encode64(algorithm.content) 115 | algorithm = algorithm.content.downcase 116 | end 117 | 118 | # Retrieve and decode data payload 119 | 120 | data = data.content 121 | armored_data = data 122 | 123 | case encoding 124 | when 'base64url' 125 | data = Base64::urlsafe_decode64(data) 126 | else 127 | # Unsupported data encoding 128 | return nil 129 | end 130 | 131 | # Signature plaintext 132 | plaintext = "#{armored_data}.#{armored_data_type}.#{armored_encoding}.#{armored_algorithm}" 133 | 134 | # Interpret data payload 135 | payload = XML::Reader.string(data) 136 | Salmon.new OStatus::Entry.new(payload), signature, plaintext 137 | end 138 | 139 | # Generate the xml for this Salmon notice and sign with the given private 140 | # key. 141 | def to_xml key 142 | # Generate magic envelope 143 | magic_envelope = XML::Document.new 144 | 145 | magic_envelope.root = XML::Node.new 'env' 146 | 147 | me_ns = XML::Namespace.new(magic_envelope.root, 148 | 'me', 'http://salmon-protocol.org/ns/magic-env') 149 | 150 | magic_envelope.root.namespaces.namespace = me_ns 151 | 152 | # Armored Data <me:data> 153 | data = @entry.to_xml 154 | @plaintext = data 155 | data_armored = Base64::urlsafe_encode64(data) 156 | elem = XML::Node.new 'data', data_armored, me_ns 157 | elem.attributes['type'] = 'application/atom+xml' 158 | data_type_armored = 'YXBwbGljYXRpb24vYXRvbSt4bWw=' 159 | magic_envelope.root << elem 160 | 161 | # Encoding <me:encoding> 162 | magic_envelope.root << XML::Node.new('encoding', 'base64url', me_ns) 163 | encoding_armored = 'YmFzZTY0dXJs' 164 | 165 | # Signing Algorithm <me:alg> 166 | magic_envelope.root << XML::Node.new('alg', 'RSA-SHA256', me_ns) 167 | algorithm_armored = 'UlNBLVNIQTI1Ng==' 168 | 169 | # Signature <me:sig> 170 | plaintext = "#{data_armored}.#{data_type_armored}.#{encoding_armored}.#{algorithm_armored}" 171 | 172 | # Assign @signature to the signature generated from the plaintext 173 | sign(plaintext, key) 174 | 175 | signature_armored = Base64::urlsafe_encode64(@signature) 176 | magic_envelope.root << XML::Node.new('sig', signature_armored, me_ns) 177 | 178 | magic_envelope.to_s :indent => true, :encoding => XML::Encoding::UTF_8 179 | end 180 | 181 | # Return the EMSA string for this Salmon instance given the size of the 182 | # public key modulus. 183 | def signature modulus_byte_length 184 | plaintext = Digest::SHA2.new(256).digest(@plaintext) 185 | 186 | prefix = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20" 187 | padding_count = modulus_byte_length - prefix.bytes.count - plaintext.bytes.count - 3 188 | 189 | padding = "" 190 | padding_count.times do 191 | padding = padding + "\xff" 192 | end 193 | 194 | "\x00\x01#{padding}\x00#{prefix}#{plaintext}" 195 | end 196 | 197 | def sign message, key 198 | @plaintext = message 199 | 200 | modulus_byte_count = key.private_key.modulus.size 201 | 202 | @signature = signature(modulus_byte_count) 203 | @signature = key.decrypt(@signature) 204 | end 205 | 206 | # Use RSA to verify the signature 207 | # key - RSA::KeyPair with the public key to use 208 | def verified? key 209 | # RSA encryption is needed to compare the signatures 210 | 211 | # Get signature to check 212 | emsa = self.signature key.public_key.modulus.size 213 | 214 | # Get signature in payload 215 | emsa_signature = key.encrypt(@signature) 216 | 217 | # RSA gem drops leading 0s since it does math upon an Integer 218 | # As a workaround, I check for what I expect the second byte to be (\x01) 219 | # This workaround will also handle seeing a \x00 first if the RSA gem is 220 | # fixed. 221 | if emsa_signature.getbyte(0) == 1 222 | emsa_signature = "\x00#{emsa_signature}" 223 | end 224 | 225 | # Does the signature match? 226 | # Return the result. 227 | emsa_signature == emsa 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /test/example_feed_link_without_href.atom: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/"> 3 | <generator uri="http://status.net" version="1.0.1">StatusNet</generator> 4 | <id>http://identi.ca/api/statuses/user_timeline/55937.atom?max_id=89098925</id> 5 | <title>reality timeline 6 | Updates from reality on Identi.ca! 7 | http://avatar3.status.net/i/identica/55937-96-20111008001238.jpeg 8 | 2012-02-01T02:37:57+00:00 9 | 10 | http://activitystrea.ms/schema/1.0/person 11 | http://identi.ca/user/55937 12 | reality 13 | 14 | 15 | 16 | 17 | 18 | 52.41548 -4.08292 19 | reality 20 | Luke Slater 21 | Into the acoustic degree; a little bleak and edgy 22 | 23 | Aberystwyth, Wales 24 | 25 | 26 | homepage 27 | http://nc.no.de/ 28 | true 29 | 30 | 31 | 32 | 33 | 34 | 35 | http://activitystrea.ms/schema/1.0/person 36 | http://identi.ca/user/55937 37 | Luke Slater 38 | 39 | 40 | 41 | 42 | 43 | 52.41548 -4.08292 44 | reality 45 | Luke Slater 46 | Into the acoustic degree; a little bleak and edgy 47 | 48 | Aberystwyth, Wales 49 | 50 | 51 | homepage 52 | http://nc.no.de/ 53 | true 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | http://identi.ca/notice/89057569 70 | RT @psquid I think I’ll move to using char[][], just to annoy @speeddefrost. 71 | RT @<span class="vcard"><a href="http://micro.fragdev.com/psquid" class="url" title="Psychedelic Squid"><span class="fn nickname mention">psquid</span></a></span> I think I’ll move to using char[][], just to annoy @<span class="vcard"><a href="http://identi.ca/user/36662" class="url" title="speeddefrost"><span class="fn nickname mention">speeddefrost</span></a></span>. 72 | http://identi.ca/notice/89057569 73 | http://activitystrea.ms/schema/1.0/share 74 | 2012-01-23T03:04:39+00:00 75 | 2012-01-23T03:04:39+00:00 76 | 77 | http://activitystrea.ms/schema/1.0/activity 78 | http://micro.fragdev.com/notice/116469 79 | I think I’ll move to using char[][], just to annoy @speeddefrost. 80 | I think I&#8217;ll move to using char[][], just to annoy &#64;<span class="vcard"><a href="http://identi.ca/speeddefrost" class="url" title="speeddefrost"><span class="fn nickname mention">speeddefrost</span></a></span>. 81 | http://micro.fragdev.com/notice/116469 82 | http://activitystrea.ms/schema/1.0/post 83 | 2012-01-23T03:02:26+00:00 84 | 2012-01-23T03:02:26+00:00 85 | 86 | http://activitystrea.ms/schema/1.0/person 87 | http://micro.fragdev.com/user/425 88 | psquid 89 | 90 | 91 | 92 | 93 | 94 | -47.15 -126.71666 95 | psquid 96 | psquid 97 | Unhinge the daydream door; delve deep. 98 | 99 | R'lyeh 100 | 101 | 102 | homepage 103 | http://psquid.net 104 | true 105 | 106 | 107 | 108 | 109 | http://activitystrea.ms/schema/1.0/note 110 | http://micro.fragdev.com/notice/116469 111 | I think I’ll move to using char[][], just to annoy @speeddefrost. 112 | I think I&#8217;ll move to using char[][], just to annoy &#64;<span class="vcard"><a href="http://identi.ca/speeddefrost" class="url" title="speeddefrost"><span class="fn nickname mention">speeddefrost</span></a></span>. 113 | 114 | 115 | 116 | 117 | 118 | 119 | http://micro.fragdev.com/api/statuses/user_timeline/425.atom 120 | psquid 121 | 122 | 123 | http://avatar3.status.net/i/identica/549245-original-20120131093453.jpeg 124 | 2012-02-01T02:27:07+00:00 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /test/example_feed_empty_author.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | StatusNet 4 | http://identi.ca/api/statuses/user_timeline/141464.atom 5 | greenmanspirit timeline 6 | Updates from greenmanspirit on Identi.ca! 7 | http://avatar.identi.ca/141464-96-20100607212940.jpeg 8 | 2011-03-12T22:10:24+00:00 9 | 10 | http://activitystrea.ms/schema/1.0/person 11 | 12 | 13 | 14 | 15 | 16 | 0 0 17 | 18 | 19 | 20 | 21 | http://activitystrea.ms/schema/1.0/person 22 | http://identi.ca/user/141464 23 | Adam Hobaugh 24 | 25 | 26 | 27 | 28 | 29 | 0 0 30 | greenmanspirit 31 | Adam Hobaugh 32 | 33 | homepage 34 | http://adamhobaugh.com 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | http://activitystrea.ms/schema/1.0/note 50 | Foobar 51 | Barbaz 52 | http://identi.ca/notice/64991641 53 | staples come out of the head tomorrow, oh yeah 54 | staples come out of the head tomorrow, oh yeah 55 | 56 | http://activitystrea.ms/schema/1.0/post 57 | 2011-02-21T02:15:14+00:00 58 | 2011-03-22T02:15:14+00:00 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | http://identi.ca/notice/64985575 67 | getting back into applying for jobs now that I have my debt back under control from unemployment 68 | getting back into applying for jobs now that I have my debt back under control from unemployment 69 | 70 | 2011-02-21T00:41:24+00:00 71 | 2011-03-22T00:41:24+00:00 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | http://activitystrea.ms/schema/1.0/note 80 | http://identi.ca/notice/61452377 81 | It's times like this I wish I had sleepytime tea 82 | It's times like this I wish I had sleepytime tea 83 | 84 | http://activitystrea.ms/schema/1.0/post 85 | 2011-01-03T05:11:50+00:00 86 | 2011-01-03T05:11:50+00:00 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | http://activitystrea.ms/schema/1.0/note 95 | http://identi.ca/notice/61441098 96 | My latest accomplishment - http://ravel.me/greenmanspirit/ro20b 97 | My latest accomplishment - <a href="http://ravel.me/greenmanspirit/ro20b" title="http://www.ravelry.com/projects/greenmanspirit/reversible-strands-for-men-and-women-too" rel="nofollow external">http://ravel.me/greenmanspirit/ro20b</a> 98 | 99 | http://activitystrea.ms/schema/1.0/post 100 | 2011-01-02T22:49:44+00:00 101 | 2011-01-02T22:49:44+00:00 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | http://activitystrea.ms/schema/1.0/note 110 | http://identi.ca/notice/60190679 111 | First field trip today, and its to the mall 112 | First field trip today, and its to the mall 113 | 114 | http://activitystrea.ms/schema/1.0/post 115 | 2010-12-10T18:50:20+00:00 116 | 2010-12-10T18:50:20+00:00 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | http://activitystrea.ms/schema/1.0/note 125 | http://identi.ca/notice/59612332 126 | bacon makes everything better 127 | bacon makes everything better 128 | 129 | http://activitystrea.ms/schema/1.0/post 130 | 2010-12-02T02:33:38+00:00 131 | 2010-12-02T02:33:38+00:00 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | http://activitystrea.ms/schema/1.0/note 140 | http://identi.ca/notice/57207676 141 | I am now a professional driver, will post my cdl once I'm not to lazy to scratch off personal info 142 | I am now a professional driver, will post my cdl once I'm not to lazy to scratch off personal info 143 | 144 | http://activitystrea.ms/schema/1.0/post 145 | 2010-10-23T00:53:05+00:00 146 | 2010-10-23T00:53:05+00:00 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | http://activitystrea.ms/schema/1.0/note 155 | http://identi.ca/notice/56394008 156 | Has anyone ever used a pedometer to guide a workout plan, I have one laying around but need a workout plan to go with it and if its worth it 157 | Has anyone ever used a pedometer to guide a workout plan, I have one laying around but need a workout plan to go with it and if its worth it 158 | 159 | http://activitystrea.ms/schema/1.0/post 160 | 2010-10-16T20:29:50+00:00 161 | 2010-10-16T20:29:50+00:00 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | http://activitystrea.ms/schema/1.0/note 170 | http://identi.ca/notice/56176750 171 | Stargate Universe is by far my favorite show, its what Voyager should have been 172 | Stargate Universe is by far my favorite show, its what Voyager should have been 173 | 174 | http://activitystrea.ms/schema/1.0/post 175 | 2010-10-14T23:24:48+00:00 176 | 2010-10-14T23:24:48+00:00 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | http://activitystrea.ms/schema/1.0/note 185 | http://identi.ca/notice/54276991 186 | Anyone have 500 I can borrow to cover till my bus job pays, I will to pay $50 in interest, payments 45 for 11, 55 month 12, will pay notary 187 | Anyone have 500 I can borrow to cover till my bus job pays, I will to pay $50 in interest, payments 45 for 11, 55 month 12, will pay notary 188 | 189 | http://activitystrea.ms/schema/1.0/post 190 | 2010-10-05T15:48:37+00:00 191 | 2010-10-05T15:48:37+00:00 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | http://activitystrea.ms/schema/1.0/note 200 | http://identi.ca/notice/54003337 201 | First day of bus driver training 202 | First day of bus driver training 203 | 204 | http://activitystrea.ms/schema/1.0/post 205 | 2010-10-04T11:33:04+00:00 206 | 2010-10-04T11:33:04+00:00 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | http://activitystrea.ms/schema/1.0/note 215 | http://identi.ca/notice/53268824 216 | my car is 300 pounds lighter, just returned all my asbestos stuff! 217 | my car is 300 pounds lighter, just returned all my asbestos stuff! 218 | 219 | http://activitystrea.ms/schema/1.0/post 220 | 2010-09-30T18:47:35+00:00 221 | 2010-09-30T18:47:35+00:00 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | http://activitystrea.ms/schema/1.0/note 230 | http://identi.ca/notice/53053891 231 | Got the job as a school bus driver, I start cdl class next week 232 | Got the job as a school bus driver, I start cdl class next week 233 | 234 | http://activitystrea.ms/schema/1.0/post 235 | 2010-09-29T16:34:59+00:00 236 | 2010-09-29T16:34:59+00:00 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | http://activitystrea.ms/schema/1.0/note 245 | http://identi.ca/notice/52674737 246 | My bro is selling a nook and kindle for a friend, much fun shall be had by me 247 | My bro is selling a nook and kindle for a friend, much fun shall be had by me 248 | 249 | http://activitystrea.ms/schema/1.0/post 250 | 2010-09-27T16:04:44+00:00 251 | 2010-09-27T16:04:44+00:00 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | http://activitystrea.ms/schema/1.0/note 260 | http://identi.ca/notice/52400602 261 | Made mozzarella today, woot, next step, Swiss or Cheddar? Hmm 262 | Made mozzarella today, woot, next step, Swiss or Cheddar? Hmm 263 | 264 | http://activitystrea.ms/schema/1.0/post 265 | 2010-09-25T23:54:10+00:00 266 | 2010-09-25T23:54:10+00:00 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | http://activitystrea.ms/schema/1.0/note 275 | http://identi.ca/notice/52201233 276 | RT @candrews #Facebook went down yesterday, taking 1,000s of sites down with it. Is this single point of failure really worth it? http:/ ... 277 | RT @<span class="vcard"><a href="http://identi.ca/user/12287" class="url" title="Craig Andrews"><span class="fn nickname">candrews</span></a></span> #<span class="tag"><a href="http://identi.ca/tag/facebook" rel="tag">Facebook</a></span> went down yesterday, taking 1,000s of sites down with it. Is this single point of failure really worth it? http:/ ... 278 | 279 | http://activitystrea.ms/schema/1.0/post 280 | 2010-09-24T18:40:10+00:00 281 | 2010-09-24T18:40:10+00:00 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | http://activitystrea.ms/schema/1.0/note 292 | http://identi.ca/notice/52198476 293 | anyone with an iphone want to trade a 15 dollar gift card for 15 cash, I got this as a gift and it is useless to me 294 | anyone with an iphone want to trade a 15 dollar gift card for 15 cash, I got this as a gift and it is useless to me 295 | 296 | http://activitystrea.ms/schema/1.0/post 297 | 2010-09-24T18:20:10+00:00 298 | 2010-09-24T18:20:10+00:00 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | http://activitystrea.ms/schema/1.0/note 307 | http://identi.ca/notice/52078750 308 | everytime the compressor for my air conditioner shuts off, my dog jumps thinking someone is at the door 309 | everytime the compressor for my air conditioner shuts off, my dog jumps thinking someone is at the door 310 | 311 | http://activitystrea.ms/schema/1.0/post 312 | 2010-09-24T02:40:22+00:00 313 | 2010-09-24T02:40:22+00:00 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | http://activitystrea.ms/schema/1.0/note 322 | http://identi.ca/notice/51893773 323 | 5.5 hours without power 324 | 5.5 hours without power 325 | 326 | http://activitystrea.ms/schema/1.0/post 327 | 2010-09-23T02:15:02+00:00 328 | 2010-09-23T02:15:02+00:00 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | -------------------------------------------------------------------------------- /test/example_feed_false_connected.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | StatusNet 4 | http://identi.ca/api/statuses/user_timeline/141464.atom 5 | greenmanspirit timeline 6 | Updates from greenmanspirit on Identi.ca! 7 | http://avatar.identi.ca/141464-96-20100607212940.jpeg 8 | 2011-03-12T22:10:24+00:00 9 | 10 | http://activitystrea.ms/schema/1.0/person 11 | http://identi.ca/user/141464 12 | greenmanspirit 13 | foo@example.com 14 | 15 | 16 | 17 | 18 | 19 | 0 0 20 | greenmanspirit 21 | Adam Hobaugh 22 | foobar 23 | barbaz 24 | spaz 25 | 2012-02-21T02:15:14+00:00 26 | 2013-02-21T02:15:14+00:00 27 | 2014-02-21 28 | 2015-02-21 29 | male 30 | foo 31 | bar 32 | -08:00 33 | false 34 | 35 | homepage 36 | http://adamhobaugh.com 37 | true 38 | 39 | 40 | 41 | 42 | 43 | 44 | http://activitystrea.ms/schema/1.0/person 45 | http://identi.ca/user/141464 46 | Adam Hobaugh 47 | 48 | 49 | 50 | 51 | 52 | 0 0 53 | greenmanspirit 54 | Adam Hobaugh 55 | 56 | homepage 57 | http://adamhobaugh.com 58 | true 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | http://activitystrea.ms/schema/1.0/note 73 | Foobar 74 | Barbaz 75 | http://identi.ca/notice/64991641 76 | staples come out of the head tomorrow, oh yeah 77 | staples come out of the head tomorrow, oh yeah 78 | 79 | http://activitystrea.ms/schema/1.0/post 80 | 2011-02-21T02:15:14+00:00 81 | 2011-03-22T02:15:14+00:00 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | http://identi.ca/notice/64985575 90 | getting back into applying for jobs now that I have my debt back under control from unemployment 91 | getting back into applying for jobs now that I have my debt back under control from unemployment 92 | 93 | 2011-02-21T00:41:24+00:00 94 | 2011-03-22T00:41:24+00:00 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | http://activitystrea.ms/schema/1.0/note 103 | http://identi.ca/notice/61452377 104 | It's times like this I wish I had sleepytime tea 105 | It's times like this I wish I had sleepytime tea 106 | 107 | http://activitystrea.ms/schema/1.0/post 108 | 2011-01-03T05:11:50+00:00 109 | 2011-01-03T05:11:50+00:00 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | http://activitystrea.ms/schema/1.0/note 118 | http://identi.ca/notice/61441098 119 | My latest accomplishment - http://ravel.me/greenmanspirit/ro20b 120 | My latest accomplishment - <a href="http://ravel.me/greenmanspirit/ro20b" title="http://www.ravelry.com/projects/greenmanspirit/reversible-strands-for-men-and-women-too" rel="nofollow external">http://ravel.me/greenmanspirit/ro20b</a> 121 | 122 | http://activitystrea.ms/schema/1.0/post 123 | 2011-01-02T22:49:44+00:00 124 | 2011-01-02T22:49:44+00:00 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | http://activitystrea.ms/schema/1.0/note 133 | http://identi.ca/notice/60190679 134 | First field trip today, and its to the mall 135 | First field trip today, and its to the mall 136 | 137 | http://activitystrea.ms/schema/1.0/post 138 | 2010-12-10T18:50:20+00:00 139 | 2010-12-10T18:50:20+00:00 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | http://activitystrea.ms/schema/1.0/note 148 | http://identi.ca/notice/59612332 149 | bacon makes everything better 150 | bacon makes everything better 151 | 152 | http://activitystrea.ms/schema/1.0/post 153 | 2010-12-02T02:33:38+00:00 154 | 2010-12-02T02:33:38+00:00 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | http://activitystrea.ms/schema/1.0/note 163 | http://identi.ca/notice/57207676 164 | I am now a professional driver, will post my cdl once I'm not to lazy to scratch off personal info 165 | I am now a professional driver, will post my cdl once I'm not to lazy to scratch off personal info 166 | 167 | http://activitystrea.ms/schema/1.0/post 168 | 2010-10-23T00:53:05+00:00 169 | 2010-10-23T00:53:05+00:00 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | http://activitystrea.ms/schema/1.0/note 178 | http://identi.ca/notice/56394008 179 | Has anyone ever used a pedometer to guide a workout plan, I have one laying around but need a workout plan to go with it and if its worth it 180 | Has anyone ever used a pedometer to guide a workout plan, I have one laying around but need a workout plan to go with it and if its worth it 181 | 182 | http://activitystrea.ms/schema/1.0/post 183 | 2010-10-16T20:29:50+00:00 184 | 2010-10-16T20:29:50+00:00 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | http://activitystrea.ms/schema/1.0/note 193 | http://identi.ca/notice/56176750 194 | Stargate Universe is by far my favorite show, its what Voyager should have been 195 | Stargate Universe is by far my favorite show, its what Voyager should have been 196 | 197 | http://activitystrea.ms/schema/1.0/post 198 | 2010-10-14T23:24:48+00:00 199 | 2010-10-14T23:24:48+00:00 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | http://activitystrea.ms/schema/1.0/note 208 | http://identi.ca/notice/54276991 209 | Anyone have 500 I can borrow to cover till my bus job pays, I will to pay $50 in interest, payments 45 for 11, 55 month 12, will pay notary 210 | Anyone have 500 I can borrow to cover till my bus job pays, I will to pay $50 in interest, payments 45 for 11, 55 month 12, will pay notary 211 | 212 | http://activitystrea.ms/schema/1.0/post 213 | 2010-10-05T15:48:37+00:00 214 | 2010-10-05T15:48:37+00:00 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | http://activitystrea.ms/schema/1.0/note 223 | http://identi.ca/notice/54003337 224 | First day of bus driver training 225 | First day of bus driver training 226 | 227 | http://activitystrea.ms/schema/1.0/post 228 | 2010-10-04T11:33:04+00:00 229 | 2010-10-04T11:33:04+00:00 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | http://activitystrea.ms/schema/1.0/note 238 | http://identi.ca/notice/53268824 239 | my car is 300 pounds lighter, just returned all my asbestos stuff! 240 | my car is 300 pounds lighter, just returned all my asbestos stuff! 241 | 242 | http://activitystrea.ms/schema/1.0/post 243 | 2010-09-30T18:47:35+00:00 244 | 2010-09-30T18:47:35+00:00 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | http://activitystrea.ms/schema/1.0/note 253 | http://identi.ca/notice/53053891 254 | Got the job as a school bus driver, I start cdl class next week 255 | Got the job as a school bus driver, I start cdl class next week 256 | 257 | http://activitystrea.ms/schema/1.0/post 258 | 2010-09-29T16:34:59+00:00 259 | 2010-09-29T16:34:59+00:00 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | http://activitystrea.ms/schema/1.0/note 268 | http://identi.ca/notice/52674737 269 | My bro is selling a nook and kindle for a friend, much fun shall be had by me 270 | My bro is selling a nook and kindle for a friend, much fun shall be had by me 271 | 272 | http://activitystrea.ms/schema/1.0/post 273 | 2010-09-27T16:04:44+00:00 274 | 2010-09-27T16:04:44+00:00 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | http://activitystrea.ms/schema/1.0/note 283 | http://identi.ca/notice/52400602 284 | Made mozzarella today, woot, next step, Swiss or Cheddar? Hmm 285 | Made mozzarella today, woot, next step, Swiss or Cheddar? Hmm 286 | 287 | http://activitystrea.ms/schema/1.0/post 288 | 2010-09-25T23:54:10+00:00 289 | 2010-09-25T23:54:10+00:00 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | http://activitystrea.ms/schema/1.0/note 298 | http://identi.ca/notice/52201233 299 | RT @candrews #Facebook went down yesterday, taking 1,000s of sites down with it. Is this single point of failure really worth it? http:/ ... 300 | RT @<span class="vcard"><a href="http://identi.ca/user/12287" class="url" title="Craig Andrews"><span class="fn nickname">candrews</span></a></span> #<span class="tag"><a href="http://identi.ca/tag/facebook" rel="tag">Facebook</a></span> went down yesterday, taking 1,000s of sites down with it. Is this single point of failure really worth it? http:/ ... 301 | 302 | http://activitystrea.ms/schema/1.0/post 303 | 2010-09-24T18:40:10+00:00 304 | 2010-09-24T18:40:10+00:00 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | http://activitystrea.ms/schema/1.0/note 315 | http://identi.ca/notice/52198476 316 | anyone with an iphone want to trade a 15 dollar gift card for 15 cash, I got this as a gift and it is useless to me 317 | anyone with an iphone want to trade a 15 dollar gift card for 15 cash, I got this as a gift and it is useless to me 318 | 319 | http://activitystrea.ms/schema/1.0/post 320 | 2010-09-24T18:20:10+00:00 321 | 2010-09-24T18:20:10+00:00 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | http://activitystrea.ms/schema/1.0/note 330 | http://identi.ca/notice/52078750 331 | everytime the compressor for my air conditioner shuts off, my dog jumps thinking someone is at the door 332 | everytime the compressor for my air conditioner shuts off, my dog jumps thinking someone is at the door 333 | 334 | http://activitystrea.ms/schema/1.0/post 335 | 2010-09-24T02:40:22+00:00 336 | 2010-09-24T02:40:22+00:00 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | http://activitystrea.ms/schema/1.0/note 345 | http://identi.ca/notice/51893773 346 | 5.5 hours without power 347 | 5.5 hours without power 348 | 349 | http://activitystrea.ms/schema/1.0/post 350 | 2010-09-23T02:15:02+00:00 351 | 2010-09-23T02:15:02+00:00 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | -------------------------------------------------------------------------------- /test/example_feed.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | StatusNet 4 | http://identi.ca/api/statuses/user_timeline/141464.atom 5 | greenmanspirit timeline 6 | Updates from greenmanspirit on Identi.ca! 7 | http://avatar.identi.ca/141464-96-20100607212940.jpeg 8 | 2011-03-12T22:10:24+00:00 9 | 10 | http://activitystrea.ms/schema/1.0/person 11 | http://identi.ca/user/141464 12 | greenmanspirit 13 | foo@example.com 14 | 15 | 16 | 17 | 18 | 19 | 0 0 20 | greenmanspirit 21 | Adam Hobaugh 22 | foobar 23 | barbaz 24 | spaz 25 | 2012-02-21T02:15:14+00:00 26 | 2013-02-21T02:15:14+00:00 27 | 2014-02-21 28 | 2015-02-21 29 | male 30 | foo 31 | bar 32 | -08:00 33 | true 34 | 35 | homepage 36 | http://adamhobaugh.com 37 | true 38 | 39 | 40 | 41 | 42 | 43 | 44 | http://activitystrea.ms/schema/1.0/person 45 | http://identi.ca/user/141464 46 | Adam Hobaugh 47 | 48 | 49 | 50 | 51 | 52 | 0 0 53 | greenmanspirit 54 | Adam Hobaugh 55 | 56 | homepage 57 | http://adamhobaugh.com 58 | true 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | http://activitystrea.ms/schema/1.0/note 74 | 75 | http://activitystrea.ms/schema/1.0/person 76 | http://identi.ca/user/141464 77 | greenmanspirit 78 | foo@example.com 79 | 80 | 81 | 82 | 83 | 84 | 0 0 85 | greenmanspirit 86 | Adam Hobaugh 87 | foobar 88 | barbaz 89 | spaz 90 | 2012-02-21T02:15:14+00:00 91 | 2013-02-21T02:15:14+00:00 92 | 2014-02-21 93 | 2015-02-21 94 | male 95 | foo 96 | bar 97 | -08:00 98 | true 99 | 100 | homepage 101 | http://adamhobaugh.com 102 | true 103 | 104 | 105 | 106 | 107 | 108 | Barbaz 109 | http://identi.ca/notice/64991641 110 | staples come out of the head tomorrow, oh yeah 111 | staples come out of the head tomorrow, oh yeah 112 | 113 | http://activitystrea.ms/schema/1.0/post 114 | 2011-02-21T02:15:14+00:00 115 | 2011-03-22T02:15:14+00:00 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | http://identi.ca/notice/64985575 124 | getting back into applying for jobs now that I have my debt back under control from unemployment 125 | getting back into applying for jobs now that I have my debt back under control from unemployment 126 | 127 | 2011-02-21T00:41:24+00:00 128 | 2011-03-22T00:41:24+00:00 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | http://activitystrea.ms/schema/1.0/note 137 | http://identi.ca/notice/61452377 138 | It's times like this I wish I had sleepytime tea 139 | It's times like this I wish I had sleepytime tea 140 | 141 | http://activitystrea.ms/schema/1.0/post 142 | 2011-01-03T05:11:50+00:00 143 | 2011-01-03T05:11:50+00:00 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | http://activitystrea.ms/schema/1.0/note 152 | http://identi.ca/notice/61441098 153 | My latest accomplishment - http://ravel.me/greenmanspirit/ro20b 154 | My latest accomplishment - <a href="http://ravel.me/greenmanspirit/ro20b" title="http://www.ravelry.com/projects/greenmanspirit/reversible-strands-for-men-and-women-too" rel="nofollow external">http://ravel.me/greenmanspirit/ro20b</a> 155 | 156 | http://activitystrea.ms/schema/1.0/post 157 | 2011-01-02T22:49:44+00:00 158 | 2011-01-02T22:49:44+00:00 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | http://activitystrea.ms/schema/1.0/note 167 | http://identi.ca/notice/60190679 168 | First field trip today, and its to the mall 169 | First field trip today, and its to the mall 170 | 171 | http://activitystrea.ms/schema/1.0/post 172 | 2010-12-10T18:50:20+00:00 173 | 2010-12-10T18:50:20+00:00 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | http://activitystrea.ms/schema/1.0/note 182 | http://identi.ca/notice/59612332 183 | bacon makes everything better 184 | bacon makes everything better 185 | 186 | http://activitystrea.ms/schema/1.0/post 187 | 2010-12-02T02:33:38+00:00 188 | 2010-12-02T02:33:38+00:00 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | http://activitystrea.ms/schema/1.0/note 197 | http://identi.ca/notice/57207676 198 | I am now a professional driver, will post my cdl once I'm not to lazy to scratch off personal info 199 | I am now a professional driver, will post my cdl once I'm not to lazy to scratch off personal info 200 | 201 | http://activitystrea.ms/schema/1.0/post 202 | 2010-10-23T00:53:05+00:00 203 | 2010-10-23T00:53:05+00:00 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | http://activitystrea.ms/schema/1.0/note 212 | http://identi.ca/notice/56394008 213 | Has anyone ever used a pedometer to guide a workout plan, I have one laying around but need a workout plan to go with it and if its worth it 214 | Has anyone ever used a pedometer to guide a workout plan, I have one laying around but need a workout plan to go with it and if its worth it 215 | 216 | http://activitystrea.ms/schema/1.0/post 217 | 2010-10-16T20:29:50+00:00 218 | 2010-10-16T20:29:50+00:00 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | http://activitystrea.ms/schema/1.0/note 227 | http://identi.ca/notice/56176750 228 | Stargate Universe is by far my favorite show, its what Voyager should have been 229 | Stargate Universe is by far my favorite show, its what Voyager should have been 230 | 231 | http://activitystrea.ms/schema/1.0/post 232 | 2010-10-14T23:24:48+00:00 233 | 2010-10-14T23:24:48+00:00 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | http://activitystrea.ms/schema/1.0/note 242 | http://identi.ca/notice/54276991 243 | Anyone have 500 I can borrow to cover till my bus job pays, I will to pay $50 in interest, payments 45 for 11, 55 month 12, will pay notary 244 | Anyone have 500 I can borrow to cover till my bus job pays, I will to pay $50 in interest, payments 45 for 11, 55 month 12, will pay notary 245 | 246 | http://activitystrea.ms/schema/1.0/post 247 | 2010-10-05T15:48:37+00:00 248 | 2010-10-05T15:48:37+00:00 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | http://activitystrea.ms/schema/1.0/note 257 | http://identi.ca/notice/54003337 258 | First day of bus driver training 259 | First day of bus driver training 260 | 261 | http://activitystrea.ms/schema/1.0/post 262 | 2010-10-04T11:33:04+00:00 263 | 2010-10-04T11:33:04+00:00 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | http://activitystrea.ms/schema/1.0/note 272 | http://identi.ca/notice/53268824 273 | my car is 300 pounds lighter, just returned all my asbestos stuff! 274 | my car is 300 pounds lighter, just returned all my asbestos stuff! 275 | 276 | http://activitystrea.ms/schema/1.0/post 277 | 2010-09-30T18:47:35+00:00 278 | 2010-09-30T18:47:35+00:00 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | http://activitystrea.ms/schema/1.0/note 287 | http://identi.ca/notice/53053891 288 | Got the job as a school bus driver, I start cdl class next week 289 | Got the job as a school bus driver, I start cdl class next week 290 | 291 | http://activitystrea.ms/schema/1.0/post 292 | 2010-09-29T16:34:59+00:00 293 | 2010-09-29T16:34:59+00:00 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | http://activitystrea.ms/schema/1.0/note 302 | http://identi.ca/notice/52674737 303 | My bro is selling a nook and kindle for a friend, much fun shall be had by me 304 | My bro is selling a nook and kindle for a friend, much fun shall be had by me 305 | 306 | http://activitystrea.ms/schema/1.0/post 307 | 2010-09-27T16:04:44+00:00 308 | 2010-09-27T16:04:44+00:00 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | http://activitystrea.ms/schema/1.0/note 317 | http://identi.ca/notice/52400602 318 | Made mozzarella today, woot, next step, Swiss or Cheddar? Hmm 319 | Made mozzarella today, woot, next step, Swiss or Cheddar? Hmm 320 | 321 | http://activitystrea.ms/schema/1.0/post 322 | 2010-09-25T23:54:10+00:00 323 | 2010-09-25T23:54:10+00:00 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | http://activitystrea.ms/schema/1.0/note 332 | http://identi.ca/notice/52201233 333 | RT @candrews #Facebook went down yesterday, taking 1,000s of sites down with it. Is this single point of failure really worth it? http:/ ... 334 | RT @<span class="vcard"><a href="http://identi.ca/user/12287" class="url" title="Craig Andrews"><span class="fn nickname">candrews</span></a></span> #<span class="tag"><a href="http://identi.ca/tag/facebook" rel="tag">Facebook</a></span> went down yesterday, taking 1,000s of sites down with it. Is this single point of failure really worth it? http:/ ... 335 | 336 | http://activitystrea.ms/schema/1.0/post 337 | 2010-09-24T18:40:10+00:00 338 | 2010-09-24T18:40:10+00:00 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | http://activitystrea.ms/schema/1.0/note 349 | http://identi.ca/notice/52198476 350 | anyone with an iphone want to trade a 15 dollar gift card for 15 cash, I got this as a gift and it is useless to me 351 | anyone with an iphone want to trade a 15 dollar gift card for 15 cash, I got this as a gift and it is useless to me 352 | 353 | http://activitystrea.ms/schema/1.0/post 354 | 2010-09-24T18:20:10+00:00 355 | 2010-09-24T18:20:10+00:00 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | http://activitystrea.ms/schema/1.0/note 364 | http://identi.ca/notice/52078750 365 | everytime the compressor for my air conditioner shuts off, my dog jumps thinking someone is at the door 366 | everytime the compressor for my air conditioner shuts off, my dog jumps thinking someone is at the door 367 | 368 | http://activitystrea.ms/schema/1.0/post 369 | 2010-09-24T02:40:22+00:00 370 | 2010-09-24T02:40:22+00:00 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | http://activitystrea.ms/schema/1.0/note 379 | http://identi.ca/notice/51893773 380 | 5.5 hours without power 381 | 5.5 hours without power 382 | 383 | http://activitystrea.ms/schema/1.0/post 384 | 2010-09-23T02:15:02+00:00 385 | 2010-09-23T02:15:02+00:00 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | -------------------------------------------------------------------------------- /test/mime_type_bug_feed.atom: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | otakukuma - Identi.ca 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 |
64 |
65 |
66 |
67 | 793 |
794 |
795 |
796 | 834 |
835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | --------------------------------------------------------------------------------