Skip to content

Commit

Permalink
activity pub documented
Browse files Browse the repository at this point in the history
  • Loading branch information
michelson committed Sep 12, 2023
1 parent 3784c26 commit 472cf01
Show file tree
Hide file tree
Showing 17 changed files with 201 additions and 145 deletions.
12 changes: 12 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,15 @@ Style/StringLiteralsInInterpolation:

Layout/LineLength:
Max: 190

Metrics/BlockLength:
Max: 60

Metrics/ParameterLists:
Max: 8

Metrics/MethodLength:
Max: 13

Metrics/BlockLength:
Max: 80
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ gem "pry"

gem "webmock"

gem "sinatra"
gem "puma"
gem "sinatra"

gem "rake", "~> 13.0"

Expand Down
4 changes: 2 additions & 2 deletions activitypub.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ Gem::Specification.new do |spec|

spec.summary = "A Ruby library for the ActivityPub protocol."
spec.description = "A comprehensive library for building and parsing ActivityPub content in Ruby."
spec.homepage = "https://github.com/rauversion/activitypub_ruby"
spec.homepage = "https://github.com/rauversion/activitypub"
spec.license = "MIT"

spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
spec.required_ruby_version = Gem::Requirement.new(">= 2.4")

# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"

Expand Down
12 changes: 7 additions & 5 deletions app.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
require 'sinatra'
# frozen_string_literal: true

require "sinatra"
require_relative "lib/activitypub"

# For simplicity, we'll hardcode actor_id and generate a keypair on startup
actor_id = "https://example.com/actors/1"
keypair = ActivityPub::Signature.generate_keypair
outbox = ActivityPub::Outbox.new(actor_id: actor_id, private_key: keypair[:private])

post '/actors/1/outbox' do
post "/actors/1/outbox" do
target_inbox_url = params[:inbox_url] # expect the client to provide target inbox URL
activity_data = params[:activity_data] # and the activity data

response = outbox.send_activity(target_inbox_url, activity_data)
[response.code.to_i, response.body]
end

post '/actors/1/inbox' do
post "/actors/1/inbox" do
# This is where other actors would send activities to our actor
incoming_activity = JSON.parse(params['incoming_activity']) # JSON.parse(request.body.read)
incoming_activity = JSON.parse(params["incoming_activity"]) # JSON.parse(request.body.read)

# For demonstration purposes, we'll just print the activity to the console
puts "Received activity: #{incoming_activity}"
Expand All @@ -25,7 +27,7 @@
[200, "Activity received"]
end

get '/' do
get "/" do
%(
"ActivityPub Sinatra Example"
<h2>Send an Activity to Outbox</h2>
Expand Down
4 changes: 2 additions & 2 deletions lib/activitypub.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# frozen_string_literal: true

require "json"
require 'time'
require 'base64'
require "time"
require "base64"
require_relative "activitypub/version"
require_relative "activitypub/activity"
require_relative "activitypub/object"
Expand Down
14 changes: 10 additions & 4 deletions lib/activitypub/activity.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# frozen_string_literal: true

module ActivityPub
# The Activity class represents a specific action or event in the ActivityPub protocol.
# It encapsulates details of the action being taken and any associated metadata.
# This class provides methods for creating, validating, and processing activities
# in accordance with the ActivityPub standard.
class Activity
# The type of the activity, e.g., 'Create', 'Like', 'Follow', etc.
attr_accessor :type
Expand Down Expand Up @@ -26,7 +32,7 @@ def initialize(type:, actor:, object:, **other_attributes)
# Convert the activity into a hash representation.
def to_h
{
'@context': 'https://www.w3.org/ns/activitystreams',
'@context': "https://www.w3.org/ns/activitystreams",
type: @type,
actor: @actor,
object: @object
Expand All @@ -43,9 +49,9 @@ def to_json(*args)
# Load an activity from a hash.
def self.from_h(hash)
new(
type: hash['type'],
actor: hash['actor'],
object: hash['object']
type: hash["type"],
actor: hash["actor"],
object: hash["object"]
# Handle any other attributes as needed.
# ...
)
Expand Down
27 changes: 17 additions & 10 deletions lib/activitypub/actor.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

# The ActivityPub::Actor represents individual and
# collective entities which perform activities.
# Commonly used actor types include Person, Organization,
Expand All @@ -7,10 +9,15 @@
# Here's a basic representation for the ActivityPub::Actor:

module ActivityPub
# The Actor class represents participants in the ActivityPub network.
# An Actor can be an individual user, a group, a service, or any other entity
# that performs actions on the network. This class handles details related to
# actor identity, public keys, endpoints, and other metadata essential for
# secure and accurate communication within the ActivityPub protocol.
class Actor
attr_accessor :id, :type, :name, :preferred_username, :inbox, :outbox, :followers, :following

def initialize(id:, type: 'Person', name:, preferred_username:, inbox:, outbox:, followers: nil, following: nil)
def initialize(id:, type: "Person", name:, preferred_username:, inbox:, outbox:, followers: nil, following: nil)
@id = id
@type = type
@name = name
Expand All @@ -23,7 +30,7 @@ def initialize(id:, type: 'Person', name:, preferred_username:, inbox:, outbox:,

def to_h
{
'@context': 'https://www.w3.org/ns/activitystreams',
'@context': "https://www.w3.org/ns/activitystreams",
id: @id,
type: @type,
name: @name,
Expand All @@ -41,14 +48,14 @@ def to_json(*args)

def self.from_h(hash)
new(
id: hash['id'],
type: hash['type'] || 'Person',
name: hash['name'],
preferred_username: hash['preferredUsername'],
inbox: hash['inbox'],
outbox: hash['outbox'],
followers: hash['followers'],
following: hash['following']
id: hash["id"],
type: hash["type"] || "Person",
name: hash["name"],
preferred_username: hash["preferredUsername"],
inbox: hash["inbox"],
outbox: hash["outbox"],
followers: hash["followers"],
following: hash["following"]
)
end
end
Expand Down
15 changes: 9 additions & 6 deletions lib/activitypub/inbox.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
require 'net/http'
require 'json'
# frozen_string_literal: true

require "net/http"
require "json"

module ActivityPub
# The Inbox class manages the reception of activities from other actors.
# It is responsible for validating incoming activities, verifying signatures,
# and processing or storing them as needed in the context of the ActivityPub protocol.
class Inbox
attr_reader :actor_id

Expand All @@ -17,9 +22,7 @@ def accept_activity(activity_data, headers)
signature = headers["Signature"]

# Verify the activity's signature
unless ActivityPub::Signature.verify?(activity_data, signature, public_key)
raise "Invalid signature"
end
raise "Invalid signature" unless ActivityPub::Signature.verify?(activity_data, signature, public_key)

# Process activity
process_activity(activity_data)
Expand All @@ -35,7 +38,7 @@ def fetch_actor_public_key(actor_id)

# This assumes the public key is stored under a 'publicKey' key in the actor's profile.
# This structure may vary based on the implementation details of the server hosting the actor's profile.
profile['publicKey']['publicKeyPem']
profile["publicKey"]["publicKeyPem"]
end

def process_activity(activity_data)
Expand Down
35 changes: 19 additions & 16 deletions lib/activitypub/object.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# frozen_string_literal: true

module ActivityPub
# The Object class represents the primary data structure of the ActivityPub protocol.
# It provides a foundational element from which specific ActivityPub types (like "Note" or "Event") derive.
# This class facilitates the creation, validation, and manipulation of ActivityPub objects.
class Object
attr_accessor :id, :type, :published, :updated, :content, :attributed_to

def initialize(id:, type: 'Object', published: nil, updated: nil, content: nil, attributed_to: nil)
def initialize(id:, type: "Object", published: nil, updated: nil, content: nil, attributed_to: nil)
@id = id
@type = type
@published = published || Time.now.utc.iso8601
Expand All @@ -13,7 +18,7 @@ def initialize(id:, type: 'Object', published: nil, updated: nil, content: nil,

def to_h
{
'@context': 'https://www.w3.org/ns/activitystreams',
'@context': "https://www.w3.org/ns/activitystreams",
id: @id,
type: @type,
published: @published,
Expand All @@ -29,22 +34,20 @@ def to_json(*args)

def self.from_h(hash)
new(
id: hash['id'],
type: hash['type'] || 'Object',
published: hash['published'],
updated: hash['updated'],
content: hash['content'],
attributed_to: hash['attributedTo']
id: hash["id"],
type: hash["type"] || "Object",
published: hash["published"],
updated: hash["updated"],
content: hash["content"],
attributed_to: hash["attributedTo"]
)
end
end
end

=begin
obj = ActivityPub::Object.new(id: 'https://example.com/objects/1', content: 'Hello world!')
puts obj.to_json
loaded_obj = ActivityPub::Object.from_h(JSON.parse(obj.to_json))
puts loaded_obj.content # => "Hello world!"
=end
# obj = ActivityPub::Object.new(id: 'https://example.com/objects/1', content: 'Hello world!')
# puts obj.to_json
#
# loaded_obj = ActivityPub::Object.from_h(JSON.parse(obj.to_json))
# puts loaded_obj.content # => "Hello world!"
#
10 changes: 8 additions & 2 deletions lib/activitypub/outbox.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
require 'net/http'
# frozen_string_literal: true

require "net/http"

module ActivityPub
# The Outbox class is responsible for sending activities to other actors.
# It wraps the process of preparing an activity, signing it, and then delivering it to
# the recipient's inbox. This ensures that messages sent via ActivityPub are
# authenticated and securely delivered.
class Outbox
attr_reader :actor_id, :private_key

Expand Down Expand Up @@ -28,7 +34,7 @@ def send_activity(target_inbox_url, activity_data)
def post_activity(inbox_url, activity_data, signature)
uri = URI(inbox_url)
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/ld+json', 'Signature' => signature })
request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/ld+json", "Signature" => signature })
request.body = activity_data
http.request(request)
end
Expand Down
9 changes: 7 additions & 2 deletions lib/activitypub/signature.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
require 'openssl'
# frozen_string_literal: true

require "openssl"

# Digital signatures are essential in federated networks
# like ActivityPub to ensure data integrity and authenticity.
# A commonly used algorithm for generating these signatures
# in the ActivityPub community is the RSA-SHA256 algorithm.

module ActivityPub
# This class handles the generation and verification of ActivityPub signatures.
# It ensures that outgoing messages are authenticated and incoming messages
# are verified against a known public key.
class Signature
# Generates a new RSA private and public key pair
def self.generate_keypair
Expand All @@ -17,7 +22,7 @@ def self.generate_keypair
def self.sign(data, private_key_pem)
private_key = OpenSSL::PKey::RSA.new(private_key_pem)
signature = private_key.sign(OpenSSL::Digest::SHA256.new, data)
Base64.encode64(signature).gsub("\n", '')
Base64.encode64(signature).gsub("\n", "")
end

# Verifies the signature with the provided public key
Expand Down
38 changes: 20 additions & 18 deletions spec/activity_spec.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
# frozen_string_literal: true

RSpec.describe ActivityPub::Activity do
describe '#initialize' do
subject { described_class.new(type: 'Like', actor: 'https://example.com/users/alice', object: 'https://example.com/posts/1') }
describe "#initialize" do
subject { described_class.new(type: "Like", actor: "https://example.com/users/alice", object: "https://example.com/posts/1") }

it 'initializes with given attributes' do
expect(subject.type).to eq('Like')
expect(subject.actor).to eq('https://example.com/users/alice')
expect(subject.object).to eq('https://example.com/posts/1')
it "initializes with given attributes" do
expect(subject.type).to eq("Like")
expect(subject.actor).to eq("https://example.com/users/alice")
expect(subject.object).to eq("https://example.com/posts/1")
end
end

describe '#to_json' do
subject { described_class.new(type: 'Like', actor: 'https://example.com/users/alice', object: 'https://example.com/posts/1') }
describe "#to_json" do
subject { described_class.new(type: "Like", actor: "https://example.com/users/alice", object: "https://example.com/posts/1") }

it 'converts the activity to a JSON representation' do
it "converts the activity to a JSON representation" do
json_output = subject.to_json
parsed_output = JSON.parse(json_output)

expect(parsed_output['type']).to eq('Like')
expect(parsed_output['actor']).to eq('https://example.com/users/alice')
expect(parsed_output['object']).to eq('https://example.com/posts/1')
expect(parsed_output["type"]).to eq("Like")
expect(parsed_output["actor"]).to eq("https://example.com/users/alice")
expect(parsed_output["object"]).to eq("https://example.com/posts/1")
end
end

describe '.from_h' do
let(:activity_hash) { { 'type' => 'Like', 'actor' => 'https://example.com/users/alice', 'object' => 'https://example.com/posts/1' } }
describe ".from_h" do
let(:activity_hash) { { "type" => "Like", "actor" => "https://example.com/users/alice", "object" => "https://example.com/posts/1" } }

it 'loads an activity from a hash' do
it "loads an activity from a hash" do
activity = described_class.from_h(activity_hash)

expect(activity.type).to eq('Like')
expect(activity.actor).to eq('https://example.com/users/alice')
expect(activity.object).to eq('https://example.com/posts/1')
expect(activity.type).to eq("Like")
expect(activity.actor).to eq("https://example.com/users/alice")
expect(activity.object).to eq("https://example.com/posts/1")
end
end
end
Loading

0 comments on commit 472cf01

Please sign in to comment.