diff --git a/docs/_entities/event.md b/docs/_entities/event.md
new file mode 100644
index 0000000..3328267
--- /dev/null
+++ b/docs/_entities/event.md
@@ -0,0 +1,78 @@
+---
+title: Event
+---
+
+This entity represents an event.
+
+See also: [EventParticipation][event_participation]
+
+## Properties
+
+| Property | Type (Length) | Description |
+| --------- | ---------------------------- | --------------------------------------------- |
+| `author` | [diaspora\* ID][diaspora-id] | The diaspora\* ID of the author of the event. |
+| `guid` | [GUID][guid] | The GUID of the event. |
+| `summary` | [String][string] (255) | The summary of the event. |
+| `start` | [Timestamp][timestamp] | The start time of the event (in UTC). |
+
+## Optional Properties
+
+| Property | Type (Length) | Description |
+| ------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `end` | [Timestamp][timestamp] | The end time of the event (in UTC). If missing it is an open-end or a single `all_day` event. |
+| `all_day` | [Boolean][boolean] | `true` if it is an all day event. Time/timezone is ignored. `false` by default. |
+| `timezone` | [Timezone][timezone] | If the event is fixed to a specific timezone, this can be set. The `start`/`end` timestamps are then displayed in this timezone. This is useful for local events. If missing or empty the timestamps are displayed in the timezone of the user. |
+| `description` | [Markdown][markdown] (65535) | Description of the event. |
+| `location` | [Location][location] | Location of the event. |
+
+## Examples
+
+### With start, end and timezone
+
+~~~xml
+
+ alice@example.org
+ bb8371f0b1c901342ebd55853a9b5d75
+ Cool event
+ 2016-12-27T12:00:00Z
+ 2016-12-27T13:00:00Z
+ false
+ Europe/Berlin
+ You need to see this!
+
+ Vienna, Austria
+ 48.208174
+ 16.373819
+
+
+~~~
+
+### All day event
+
+~~~xml
+
+ alice@example.org
+ bb8371f0b1c901342ebd55853a9b5d75
+ Cool event
+ 2016-12-27T00:00:00Z
+
+ true
+
+ You need to see this!
+
+ Vienna, Austria
+ 48.208174
+ 16.373819
+
+
+~~~
+
+[event_participation]: {{ site.baseurl }}/entities/event_participation.html
+[diaspora-id]: {{ site.baseurl }}/federation/types.html#diaspora-id
+[guid]: {{ site.baseurl }}/federation/types.html#guid
+[string]: {{ site.baseurl }}/federation/types.html#string
+[timestamp]: {{ site.baseurl }}/federation/types.html#timestamp
+[markdown]: {{ site.baseurl }}/federation/types.html#markdown
+[boolean]: {{ site.baseurl }}/federation/types.html#boolean
+[timezone]: {{ site.baseurl }}/federation/types.html#timezone
+[location]: {{ site.baseurl }}/entities/location.html
diff --git a/docs/_entities/event_participation.md b/docs/_entities/event_participation.md
new file mode 100644
index 0000000..177d715
--- /dev/null
+++ b/docs/_entities/event_participation.md
@@ -0,0 +1,54 @@
+---
+title: EventParticipation
+---
+
+This entity represents a participation in an [Event][event].
+
+See also: [Relayable][relayable]
+
+## Properties
+
+| Property | Type | Description |
+| ------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| `author` | [diaspora\* ID][diaspora-id] | The diaspora\* ID of the author of the event participation. |
+| `guid` | [GUID][guid] | The GUID of the event participation. |
+| `parent_guid` | [GUID][guid] | The GUID of the [Event][event]. |
+| `status` | [String][string] | The participation status, lowercase string as defined in [RFC 5545, Section 3.2.12][status] (`accepted`, `declined` or `tentative`). |
+| `author_signature` | [Signature][signature] | The signature from the author of the event participation. |
+| `parent_author_signature` | [Signature][signature] | The signature from the author of the [Event][event]. |
+
+## Examples
+
+### From author
+
+~~~xml
+
+ alice@example.org
+ 92f26ff0b1cb01342ebd55853a9b5d75
+ bb8371f0b1c901342ebd55853a9b5d75
+ accepted
+ dT6KbT7kp0bE+s3//ZErxO1wvVIqtD0lY67i81+dO43B4D2m5kjCdzW240eWt/jZmcHIsdxXf4WHNdrb6ZDnamA8I1FUVnLjHA9xexBITQsSLXrcV88UdammSmmOxl1Ac4VUXqFpdavm6a7/MwOJ7+JHP8TbUO9siN+hMfgUbtY=
+
+
+~~~
+
+### From parent author
+
+~~~xml
+
+ alice@example.org
+ 92f26ff0b1cb01342ebd55853a9b5d75
+ bb8371f0b1c901342ebd55853a9b5d75
+ accepted
+ dT6KbT7kp0bE+s3//ZErxO1wvVIqtD0lY67i81+dO43B4D2m5kjCdzW240eWt/jZmcHIsdxXf4WHNdrb6ZDnamA8I1FUVnLjHA9xexBITQsSLXrcV88UdammSmmOxl1Ac4VUXqFpdavm6a7/MwOJ7+JHP8TbUO9siN+hMfgUbtY=
+ gWasNPpSnMcKBIMWyzfoVO6sr8eRYkhUqy3PIkkh53n/ki+DM9mnh3ayotI0+6un9aq1N3XkS7Vn05ZD3+nHVby6i21XkYgPnbD8pWYuBBj7VGPyahT70BUs/vSvY8KX8V3wYfsPsaiAgJsAFg2UHYdY3r4/oWdIIbBZc21O3zk=
+
+~~~
+
+[diaspora-id]: {{ site.baseurl }}/federation/types.html#diaspora-id
+[guid]: {{ site.baseurl }}/federation/types.html#guid
+[string]: {{ site.baseurl }}/federation/types.html#string
+[status]: https://tools.ietf.org/html/rfc5545#section-3.2.12
+[signature]: {{ site.baseurl }}/federation/types.html#signature
+[event]: {{ site.baseurl }}/entities/event.html
+[relayable]: {{ site.baseurl }}/federation/relayable.html
diff --git a/docs/_entities/status_message.md b/docs/_entities/status_message.md
index c6715e9..1302f4f 100644
--- a/docs/_entities/status_message.md
+++ b/docs/_entities/status_message.md
@@ -22,6 +22,7 @@ This entity represents a reshare of a status message. It inherits from [Post][po
| `location` | [Location][location] | The Location information of the status message. |
| `photo` | [Photo][photo]s | The attached Photos of the status message, the `status_message_guid` and the `author` need to match the status message. |
| `poll` | [Poll][poll] | The attached Poll of the status message. |
+| `event` | [Event][event] | The attached Event of the status message. |
## Examples
@@ -183,3 +184,4 @@ This entity represents a reshare of a status message. It inherits from [Post][po
[location]: {{ site.baseurl }}/entities/location.html
[photo]: {{ site.baseurl }}/entities/photo.html
[poll]: {{ site.baseurl }}/entities/poll.html
+[event]: {{ site.baseurl }}/entities/event.html
diff --git a/docs/federation/types.md b/docs/federation/types.md
index aa09c5d..4d1dcd5 100644
--- a/docs/federation/types.md
+++ b/docs/federation/types.md
@@ -85,6 +85,12 @@ An [ISO 8601][iso8601] date.
Example: `2016-02-19`
+## Timezone
+
+A timezone in the form `Area/Location` as used in the [Time Zone Database][tz].
+
+Example: `Europe/Berlin`
+
## Signature
Signature with the private RSA key using the RSA-SHA256 algorithm and base64-encoded.
@@ -98,3 +104,4 @@ Example:
[entities]: {{ site.baseurl }}/entities/
[commonmark]: http://spec.commonmark.org/
[iso8601]: https://www.w3.org/TR/NOTE-datetime
+[tz]: https://www.iana.org/time-zones
diff --git a/lib/diaspora_federation/entities.rb b/lib/diaspora_federation/entities.rb
index 343e502..6760c56 100644
--- a/lib/diaspora_federation/entities.rb
+++ b/lib/diaspora_federation/entities.rb
@@ -28,6 +28,10 @@ require "diaspora_federation/entities/poll"
require "diaspora_federation/entities/poll_participation"
require "diaspora_federation/entities/location"
+
+require "diaspora_federation/entities/event"
+require "diaspora_federation/entities/event_participation"
+
require "diaspora_federation/entities/photo"
require "diaspora_federation/entities/status_message"
require "diaspora_federation/entities/reshare"
diff --git a/lib/diaspora_federation/entities/event.rb b/lib/diaspora_federation/entities/event.rb
new file mode 100644
index 0000000..4115b8c
--- /dev/null
+++ b/lib/diaspora_federation/entities/event.rb
@@ -0,0 +1,55 @@
+module DiasporaFederation
+ module Entities
+ # This entity represents an event and it is federated as a part of a status message.
+ #
+ # @see Validators::EventValidator
+ class Event < Entity
+ # @!attribute [r] author
+ # The diaspora* ID of the person who created the event
+ # @see Person#author
+ # @return [String] author diaspora* ID
+ property :author, :string
+
+ # @!attribute [r] guid
+ # A random string of at least 16 chars
+ # @see Validation::Rule::Guid
+ # @return [String] guid
+ property :guid, :string
+
+ # @!attribute [r] summary
+ # The summary of the event
+ # @return [String] event summary
+ property :summary, :string
+
+ # @!attribute [r] description
+ # Description of the event
+ # @return [String] event description
+ property :description, :string, default: nil
+
+ # @!attribute [r] start
+ # The start time of the event
+ # @return [String] event start
+ property :start, :timestamp
+
+ # @!attribute [r] end
+ # The end time of the event
+ # @return [String] event end
+ property :end, :timestamp, default: nil
+
+ # @!attribute [r] all_day
+ # Points if the event is an all day event
+ # @return [Boolean] is it an all day event
+ property :all_day, :boolean, default: false
+
+ # @!attribute [r] timezone
+ # Timezone to which the event is fixed to
+ # @return [String] timezone
+ property :timezone, :string, default: nil
+
+ # @!attribute [r] location
+ # Location of the event
+ # @return [Entities::Location] location
+ entity :location, Entities::Location, default: nil
+ end
+ end
+end
diff --git a/lib/diaspora_federation/entities/event_participation.rb b/lib/diaspora_federation/entities/event_participation.rb
new file mode 100644
index 0000000..4c2dc22
--- /dev/null
+++ b/lib/diaspora_federation/entities/event_participation.rb
@@ -0,0 +1,27 @@
+module DiasporaFederation
+ module Entities
+ # This entity represents a participation in an event, i.e. it is issued when a user responds to en event.
+ #
+ # @see Validators::EventParticipationValidator
+ class EventParticipation < Entity
+ # Old signature order
+ # @deprecated
+ LEGACY_SIGNATURE_ORDER = %i(author guid parent_guid status).freeze
+
+ # The {EventParticipation} parent is an {Event}
+ PARENT_TYPE = "Event".freeze
+
+ include Relayable
+
+ # Redefine the author property without +diaspora_handle+ +xml_name+
+ # @deprecated Can be removed after XMLs are generated with new names
+ property :author, :string
+
+ # @!attribute [r] status
+ # The participation status of the user
+ # "accepted", "declined" or "tentative"
+ # @return [String] event participation status
+ property :status, :string
+ end
+ end
+end
diff --git a/lib/diaspora_federation/entities/status_message.rb b/lib/diaspora_federation/entities/status_message.rb
index 555b8b5..7b9494d 100644
--- a/lib/diaspora_federation/entities/status_message.rb
+++ b/lib/diaspora_federation/entities/status_message.rb
@@ -26,6 +26,11 @@ module DiasporaFederation
# @return [Entities::Poll] poll
entity :poll, Entities::Poll, default: nil
+ # @!attribute [r] event
+ # Optional event attached to the status message
+ # @return [Entities::Event] event
+ entity :event, Entities::Event, default: nil
+
# @!attribute [r] public
# Shows whether the status message is visible to everyone or only to some aspects
# @return [Boolean] is it public
diff --git a/lib/diaspora_federation/test/factories.rb b/lib/diaspora_federation/test/factories.rb
index 4fad247..fbfa20d 100644
--- a/lib/diaspora_federation/test/factories.rb
+++ b/lib/diaspora_federation/test/factories.rb
@@ -200,6 +200,24 @@ module DiasporaFederation
poll_answer_guid { generate(:guid) }
end
+ factory :event_entity, class: DiasporaFederation::Entities::Event do
+ author { generate(:diaspora_id) }
+ guid
+ summary "Cool event"
+ description "You need to see this!"
+ start { Time.now.utc.change(min: 0).change(sec: 0).change(usec: 0) - 1.hour }
+ add_attribute(:end) { Time.now.utc.change(min: 0).change(sec: 0).change(usec: 0) + 1.hour }
+ all_day false
+ timezone "Europe/Berlin"
+ end
+
+ factory :event_participation_entity,
+ class: DiasporaFederation::Entities::EventParticipation, parent: :relayable_entity do
+ author { generate(:diaspora_id) }
+ guid
+ status "accepted"
+ end
+
factory :related_entity, class: DiasporaFederation::Entities::RelatedEntity do
author { generate(:diaspora_id) }
local true
diff --git a/lib/diaspora_federation/validators.rb b/lib/diaspora_federation/validators.rb
index e9c54f5..6fea7e4 100644
--- a/lib/diaspora_federation/validators.rb
+++ b/lib/diaspora_federation/validators.rb
@@ -44,6 +44,8 @@ require "diaspora_federation/validators/account_deletion_validator"
require "diaspora_federation/validators/comment_validator"
require "diaspora_federation/validators/contact_validator"
require "diaspora_federation/validators/conversation_validator"
+require "diaspora_federation/validators/event_participation_validator"
+require "diaspora_federation/validators/event_validator"
require "diaspora_federation/validators/h_card_validator"
require "diaspora_federation/validators/like_validator"
require "diaspora_federation/validators/location_validator"
diff --git a/lib/diaspora_federation/validators/event_participation_validator.rb b/lib/diaspora_federation/validators/event_participation_validator.rb
new file mode 100644
index 0000000..f7588a4
--- /dev/null
+++ b/lib/diaspora_federation/validators/event_participation_validator.rb
@@ -0,0 +1,12 @@
+module DiasporaFederation
+ module Validators
+ # This validates a {Entities::EventParticipation}.
+ class EventParticipationValidator < Validation::Validator
+ include Validation
+
+ include RelayableValidator
+
+ rule :status, regular_expression: {regex: /\A(accepted|declined|tentative)\z/}
+ end
+ end
+end
diff --git a/lib/diaspora_federation/validators/event_validator.rb b/lib/diaspora_federation/validators/event_validator.rb
new file mode 100644
index 0000000..f6a20c8
--- /dev/null
+++ b/lib/diaspora_federation/validators/event_validator.rb
@@ -0,0 +1,21 @@
+module DiasporaFederation
+ module Validators
+ # This validates a {Entities::Event}.
+ class EventValidator < Validation::Validator
+ include Validation
+
+ rule :author, %i(not_empty diaspora_id)
+
+ rule :guid, :guid
+
+ rule :summary, [:not_empty, length: {maximum: 255}]
+ rule :description, length: {maximum: 65_535}
+
+ rule :start, :not_nil
+
+ rule :all_day, :boolean
+
+ rule :timezone, regular_expression: {regex: %r{\A[A-Za-z_-]{,14}(/[A-Za-z_-]{,14}){1,2}\z}}
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index 3dc7213..b85508f 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -39,6 +39,12 @@ FactoryGirl.define do
after(:create, &:save)
end
+ factory :event, class: Entity do
+ entity_type "Event"
+ author { FactoryGirl.build(:person) }
+ after(:create, &:save)
+ end
+
factory :conversation, class: Entity do
entity_type "Conversation"
author { FactoryGirl.build(:person) }
diff --git a/spec/lib/diaspora_federation/entities/event_participation_spec.rb b/spec/lib/diaspora_federation/entities/event_participation_spec.rb
new file mode 100644
index 0000000..d743614
--- /dev/null
+++ b/spec/lib/diaspora_federation/entities/event_participation_spec.rb
@@ -0,0 +1,35 @@
+module DiasporaFederation
+ describe Entities::EventParticipation do
+ let(:parent) { FactoryGirl.create(:event, author: bob) }
+ let(:parent_entity) { FactoryGirl.build(:related_entity, author: bob.diaspora_id) }
+ let(:data) {
+ add_signatures(
+ FactoryGirl.build(
+ :event_participation_entity,
+ author: alice.diaspora_id,
+ parent_guid: parent.guid,
+ parent: parent_entity
+ )
+ )
+ }
+
+ let(:xml) { <<-XML }
+
+ #{data[:author]}
+ #{data[:guid]}
+ #{parent.guid}
+ #{data[:status]}
+ #{data[:author_signature]}
+ #{data[:parent_author_signature]}
+
+XML
+
+ let(:string) { "EventParticipation:#{data[:guid]}:#{parent.guid}" }
+
+ it_behaves_like "an Entity subclass"
+
+ it_behaves_like "an XML Entity"
+
+ it_behaves_like "a relayable Entity"
+ end
+end
diff --git a/spec/lib/diaspora_federation/entities/event_spec.rb b/spec/lib/diaspora_federation/entities/event_spec.rb
new file mode 100644
index 0000000..0507ae2
--- /dev/null
+++ b/spec/lib/diaspora_federation/entities/event_spec.rb
@@ -0,0 +1,52 @@
+module DiasporaFederation
+ describe Entities::Event do
+ let(:location) { FactoryGirl.build(:location_entity) }
+ let(:data) {
+ FactoryGirl.attributes_for(:event_entity).merge(author: alice.diaspora_id, location: location)
+ }
+
+ let(:xml) { <<-XML }
+
+ #{data[:author]}
+ #{data[:guid]}
+ #{data[:summary]}
+ #{data[:description]}
+ #{data[:start].utc.iso8601}
+ #{data[:end].utc.iso8601}
+ #{data[:all_day]}
+ #{data[:timezone]}
+
+ #{location.address}
+ #{location.lat}
+ #{location.lng}
+
+
+XML
+
+ let(:string) { "Event:#{data[:guid]}" }
+
+ it_behaves_like "an Entity subclass"
+
+ it_behaves_like "an XML Entity"
+
+ context "default values" do
+ it "uses default values" do
+ minimal_xml = <<-XML
+
+ #{data[:author]}
+ #{data[:guid]}
+ #{data[:summary]}
+ #{data[:start].utc.iso8601}
+
+XML
+
+ parsed_instance = DiasporaFederation::Salmon::XmlPayload.unpack(Nokogiri::XML::Document.parse(minimal_xml).root)
+ expect(parsed_instance.end).to be_nil
+ expect(parsed_instance.all_day).to be_falsey
+ expect(parsed_instance.timezone).to be_nil
+ expect(parsed_instance.description).to be_nil
+ expect(parsed_instance.location).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/diaspora_federation/entities/status_message_spec.rb b/spec/lib/diaspora_federation/entities/status_message_spec.rb
index 25b7473..1f0ca4a 100644
--- a/spec/lib/diaspora_federation/entities/status_message_spec.rb
+++ b/spec/lib/diaspora_federation/entities/status_message_spec.rb
@@ -9,6 +9,7 @@ module DiasporaFederation
photos: [photo1, photo2],
location: location,
poll: nil,
+ event: nil,
provider_display_name: "something"
)
}
diff --git a/spec/lib/diaspora_federation/validators/event_participation_validator_spec.rb b/spec/lib/diaspora_federation/validators/event_participation_validator_spec.rb
new file mode 100644
index 0000000..3296d86
--- /dev/null
+++ b/spec/lib/diaspora_federation/validators/event_participation_validator_spec.rb
@@ -0,0 +1,17 @@
+module DiasporaFederation
+ describe Validators::EventParticipationValidator do
+ let(:entity) { :event_participation_entity }
+
+ it_behaves_like "a common validator"
+
+ it_behaves_like "a relayable validator"
+
+ describe "#status" do
+ it_behaves_like "a property with a value validation/restriction" do
+ let(:property) { :status }
+ let(:wrong_values) { ["", "yes", "foobar"] }
+ let(:correct_values) { %w(accepted declined tentative) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/diaspora_federation/validators/event_validator_spec.rb b/spec/lib/diaspora_federation/validators/event_validator_spec.rb
new file mode 100644
index 0000000..1bd3864
--- /dev/null
+++ b/spec/lib/diaspora_federation/validators/event_validator_spec.rb
@@ -0,0 +1,62 @@
+module DiasporaFederation
+ describe Validators::EventValidator do
+ let(:entity) { :event_entity }
+
+ it_behaves_like "a common validator"
+
+ it_behaves_like "a diaspora* ID validator" do
+ let(:property) { :author }
+ let(:mandatory) { true }
+ end
+
+ it_behaves_like "a guid validator" do
+ let(:property) { :guid }
+ end
+
+ describe "#summary" do
+ it_behaves_like "a property with a value validation/restriction" do
+ let(:property) { :summary }
+ let(:wrong_values) { ["a" * 256, nil, ""] }
+ let(:correct_values) { ["a" * 255] }
+ end
+ end
+
+ describe "#description" do
+ it_behaves_like "a property with a value validation/restriction" do
+ let(:property) { :description }
+ let(:wrong_values) { ["a" * 65_536] }
+ let(:correct_values) { ["a" * 65_535, nil, ""] }
+ end
+ end
+
+ describe "#start" do
+ it_behaves_like "a property with a value validation/restriction" do
+ let(:property) { :start }
+ let(:wrong_values) { [nil] }
+ let(:correct_values) { [Time.now.utc] }
+ end
+ end
+
+ describe "#end" do
+ it_behaves_like "a property with a value validation/restriction" do
+ let(:property) { :end }
+ let(:wrong_values) { [] }
+ let(:correct_values) { [nil, Time.now.utc] }
+ end
+ end
+
+ describe "#all_day" do
+ it_behaves_like "a boolean validator" do
+ let(:property) { :all_day }
+ end
+ end
+
+ describe "#timezone" do
+ it_behaves_like "a property with a value validation/restriction" do
+ let(:property) { :timezone }
+ let(:wrong_values) { ["foobar"] }
+ let(:correct_values) { [nil, "Europe/Berlin", "America/Argentina/ComodRivadavia"] }
+ end
+ end
+ end
+end