mirror of
https://github.com/havenweb/haven.git
synced 2025-07-17 08:54:21 +02:00
Microsub
This commit is contained in:
parent
c2054e21a8
commit
b3979cb8fe
@ -1,6 +1,49 @@
|
|||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
|
class JsonError < StandardError
|
||||||
|
attr_accessor :error, :error_description, :status, :options
|
||||||
|
def initialize(error, error_description, status, options={})
|
||||||
|
@error = error
|
||||||
|
@error_description = error_description
|
||||||
|
@status = status
|
||||||
|
@options = options
|
||||||
|
end
|
||||||
|
def to_json
|
||||||
|
j = { error: @error, error_description: @error_description}
|
||||||
|
options.each {|k,v| j[k]=v}
|
||||||
|
return j
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
|
rescue_from JsonError, with: :json_error_handler
|
||||||
|
|
||||||
|
def json_error_handler(json_error)
|
||||||
|
render json: json_error.to_json, status: json_error.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_auth_token
|
||||||
|
token = nil
|
||||||
|
if params[:access_token]
|
||||||
|
token = IndieAuthToken.find_by(access_token: params[:access_token])
|
||||||
|
elsif request.headers["Authorization"]
|
||||||
|
t = request.headers["Authorization"]
|
||||||
|
if t.start_with? "Bearer "
|
||||||
|
t2 = t.split("Bearer ",2).last
|
||||||
|
token = IndieAuthToken.find_by(access_token: t2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if token.nil?
|
||||||
|
raise JsonError.new("unauthorized", "This action requires an authentication token", 401)
|
||||||
|
end
|
||||||
|
@token = token
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_scope(scope)
|
||||||
|
unless @token.scope.split(" ").include? scope
|
||||||
|
raise JsonError.new("insufficient_scope","The provided authentication token does not include the scope: '#{scope}' which is required for this action", 403, scope: scope)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.get_settings
|
def self.get_settings
|
||||||
return Setting.first || Setting.new
|
return Setting.first || Setting.new
|
||||||
|
@ -75,9 +75,9 @@ class FeedsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@feed = Feed.find(params[:id])
|
@feed = current_user.feeds.find(params[:id])
|
||||||
feed_name = @feed.name
|
|
||||||
if ( (!@feed.nil?) and (current_user == @feed.user) )
|
if ( (!@feed.nil?) and (current_user == @feed.user) )
|
||||||
|
feed_name = @feed.name
|
||||||
@feed.destroy!
|
@feed.destroy!
|
||||||
flash[:notice] = "You have removed #{feed_name} from your feeds"
|
flash[:notice] = "You have removed #{feed_name} from your feeds"
|
||||||
else
|
else
|
||||||
|
@ -1,26 +1,11 @@
|
|||||||
class MicropubController < ApplicationController
|
class MicropubController < ApplicationController
|
||||||
class MicropubError < StandardError
|
|
||||||
attr_accessor :error, :error_description, :status, :options
|
|
||||||
def initialize(error, error_description, status, options={})
|
|
||||||
@error = error
|
|
||||||
@error_description = error_description
|
|
||||||
@status = status
|
|
||||||
@options = options
|
|
||||||
end
|
|
||||||
def to_json
|
|
||||||
j = { error: @error, error_description: @error_description}
|
|
||||||
options.each {|k,v| j[k]=v}
|
|
||||||
return j
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from MicropubError, with: :micropub_error_handler
|
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
before_action :set_auth_token
|
before_action :set_auth_token
|
||||||
|
|
||||||
def query
|
def query
|
||||||
unless params[:q]
|
unless params[:q]
|
||||||
raise MicropubError.new("invalid_request","This action requires a query to be specified with the 'q' parameter",400)
|
raise JsonError.new("invalid_request","This action requires a query to be specified with the 'q' parameter",400)
|
||||||
end
|
end
|
||||||
if params[:q] == "config"
|
if params[:q] == "config"
|
||||||
render json: {
|
render json: {
|
||||||
@ -47,16 +32,17 @@ class MicropubController < ApplicationController
|
|||||||
}, status: 200
|
}, status: 200
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
raise MicropubError.new("invalid_request","Unrecognized 'q' parameter: #{params[:q]}",400)
|
raise JsonError.new("invalid_request","Unrecognized 'q' parameter: #{params[:q]}",400)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# micropub overloads posts to a single endpoint for create/update/delete
|
# micropub overloads POSTs to a single endpoint for create/update/delete
|
||||||
# so this method handles all three
|
# so this method handles all three
|
||||||
def create
|
def create
|
||||||
validate_scope("create")
|
validate_scope("create")
|
||||||
if (params[:action]) and (params[:action] == "update")
|
raw_action = request.request_parameters["action"]
|
||||||
|
if (raw_action) and (raw_action == "update")
|
||||||
validate_scope("update")
|
validate_scope("update")
|
||||||
if params[:url]
|
if params[:url]
|
||||||
post = post_from_url(params[:url])
|
post = post_from_url(params[:url])
|
||||||
@ -73,60 +59,60 @@ class MicropubController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if params[:add]
|
if params[:add]
|
||||||
raise MicropubError.new(
|
raise JsonError.new(
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
"None of the poperties this server supports (content, published) allow multiple values",
|
"None of the poperties this server supports (content, published) allow multiple values",
|
||||||
400)
|
400)
|
||||||
end
|
end
|
||||||
if params[:delete]
|
if params[:delete]
|
||||||
raise MicropubError.new(
|
raise JsonError.new(
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
"All properties are required (content, published), none can be deleted",
|
"All properties are required (content, published), none can be deleted",
|
||||||
400)
|
400)
|
||||||
end
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
raise MicropubError.new(
|
raise JsonError.new(
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
"#{e.class.name}: #{e.message}",
|
"#{e.class.name}: #{e.message}",
|
||||||
400)
|
400)
|
||||||
end ## end begin/rescue
|
end ## end begin/rescue
|
||||||
else # no url param
|
else # no url param
|
||||||
raise MiropubError.new(
|
raise JsonError.new(
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
"Request includes an action of 'update' but is missing the URL of the post to update",
|
"Request includes an action of 'update' but is missing the URL of the post to update",
|
||||||
400)
|
400)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
elsif (params[:action]) and (params[:action] == "delete")
|
elsif (raw_action) and (raw_action == "delete")
|
||||||
validate_scope("delete")
|
validate_scope("delete")
|
||||||
if params[:url]
|
if params[:url]
|
||||||
post = post_from_url(params[:url])
|
post = post_from_url(params[:url])
|
||||||
begin
|
begin
|
||||||
post.destroy!
|
post.destroy!
|
||||||
rescue => e
|
rescue => e
|
||||||
raise MicropubError.new(
|
raise JsonError.new(
|
||||||
"server_error",
|
"server_error",
|
||||||
"#{e.class.name}: #{e.message}",
|
"#{e.class.name}: #{e.message}",
|
||||||
500)
|
500)
|
||||||
end
|
end
|
||||||
head 204 # no content from successful deletion
|
head 204 # no content from successful deletion
|
||||||
else
|
else
|
||||||
raise MicropubError.new("invalid_request",
|
raise JsonError.new("invalid_request",
|
||||||
"Request includes an action of 'delete' but is missing the URL of the post to delete",
|
"Request includes an action of 'delete' but is missing the URL of the post to delete",
|
||||||
400)
|
400)
|
||||||
end
|
end
|
||||||
elsif (params[:action] and params[:action]=="create") or (!params[:action]) # no action should be a create
|
elsif (raw_action and raw_action=="create") or (raw_action.nil?) # no action should be a create
|
||||||
@post = post_from_params(params)
|
@post = post_from_params(params)
|
||||||
@post.author = @token.user
|
@post.author = @token.user
|
||||||
@post.save!
|
@post.save!
|
||||||
response.set_header("Location", url_for(@post))
|
response.set_header("Location", url_for(@post))
|
||||||
head 201 #created
|
head 201 #created
|
||||||
elsif (params[:action]) # unknown action
|
elsif (raw_action) # unknown action
|
||||||
raise MicropubError.new("invalid_request",
|
raise JsonError.new("invalid_request",
|
||||||
"Request includes an unrecognized action of '#{params[:action]}', only 'update' and 'delete' are recognized",
|
"Request includes an unrecognized action of '#{raw_action}', only 'update' and 'delete' are recognized",
|
||||||
400, request_json: params)
|
400, request_json: params)
|
||||||
else
|
else
|
||||||
raise MicropubError.new("invalid_request",
|
raise JsonError.new("invalid_request",
|
||||||
"Unable to parse request, you shouldn't really be able to get to this error",
|
"Unable to parse request, you shouldn't really be able to get to this error",
|
||||||
400, request_json: params)
|
400, request_json: params)
|
||||||
end
|
end
|
||||||
@ -135,48 +121,20 @@ class MicropubController < ApplicationController
|
|||||||
def media
|
def media
|
||||||
validate_scope("media")
|
validate_scope("media")
|
||||||
if params[:file].nil?
|
if params[:file].nil?
|
||||||
raise MicropubError.new("invalid_request",
|
raise JsonError.new("invalid_request",
|
||||||
"Request to media endpoint requires a object named 'file'",
|
"Request to media endpoint requires a object named 'file'",
|
||||||
400, request_json: params)
|
400, request_json: params)
|
||||||
end
|
end
|
||||||
image = Image.new
|
image = Image.new
|
||||||
image.blob.attach params[:file]
|
image.blob.attach params[:file]
|
||||||
image.save
|
image.save
|
||||||
|
location = request.base_url + "/images/raw/#{image.id}/#{image.blob.filename.to_s}"
|
||||||
location = "/images/raw/#{image.id}/#{image.blob.filename.to_s}"
|
|
||||||
response.set_header("Location", location)
|
response.set_header("Location", location)
|
||||||
head 201 #created
|
head 201 #created
|
||||||
end
|
end
|
||||||
|
|
||||||
def micropub_error_handler(micropub_error)
|
|
||||||
render json: micropub_error.to_json, status: micropub_error.status
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_auth_token
|
|
||||||
token = nil
|
|
||||||
if params[:access_token]
|
|
||||||
token = IndieAuthToken.find_by(access_token: params[:access_token])
|
|
||||||
elsif request.headers["Authorization"]
|
|
||||||
t = request.headers["Authorization"]
|
|
||||||
if t.start_with? "Bearer "
|
|
||||||
t2 = t.split("Bearer ",2).last
|
|
||||||
token = IndieAuthToken.find_by(access_token: t2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if token.nil?
|
|
||||||
raise MicropubError.new("unauthorized", "This action requires an authentication token", 401)
|
|
||||||
end
|
|
||||||
@token = token
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_scope(scope)
|
|
||||||
unless @token.scope.split(" ").include? scope
|
|
||||||
raise MicropubError.new("insufficient_scope","The provided authentication token does not include the scope: '#{scope}' which is required for this action", 403, scope: scope)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# may return 404 directly if post not found
|
# may return 404 directly if post not found
|
||||||
def post_from_url(params_url)
|
def post_from_url(params_url)
|
||||||
post = nil
|
post = nil
|
||||||
@ -193,7 +151,7 @@ class MicropubController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if post.nil?
|
if post.nil?
|
||||||
raise MicrpubError.new("not found","No post found at url: #{params_url}",404)
|
raise JsonError.new("not found","No post found at url: #{params_url}",404)
|
||||||
end
|
end
|
||||||
return post
|
return post
|
||||||
end
|
end
|
||||||
@ -201,7 +159,7 @@ class MicropubController < ApplicationController
|
|||||||
# supported params: name, content, published
|
# supported params: name, content, published
|
||||||
def post_from_params(params)
|
def post_from_params(params)
|
||||||
unless params[:h] and params[:h]=="entry"
|
unless params[:h] and params[:h]=="entry"
|
||||||
raise MicropubError.new("invalid_request","Only h-entry types supported by this server",400)
|
raise JsonError.new("invalid_request","Only h-entry types supported by this server",400)
|
||||||
end
|
end
|
||||||
content = ""
|
content = ""
|
||||||
if params[:content]
|
if params[:content]
|
||||||
@ -212,7 +170,7 @@ class MicropubController < ApplicationController
|
|||||||
elsif params[:name]
|
elsif params[:name]
|
||||||
content = "# #{params[:name]}"
|
content = "# #{params[:name]}"
|
||||||
else
|
else
|
||||||
raise MicropubError.new("invalid_request","New h-entry must have content or a name (or both)",400)
|
raise JsonError.new("invalid_request","New h-entry must have content or a name (or both)",400)
|
||||||
end
|
end
|
||||||
datetime = DateTime.now
|
datetime = DateTime.now
|
||||||
if params[:published]
|
if params[:published]
|
||||||
|
156
app/controllers/microsub_controller.rb
Normal file
156
app/controllers/microsub_controller.rb
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
class MicrosubController < ApplicationController
|
||||||
|
skip_before_action :verify_authenticity_token
|
||||||
|
before_action :set_auth_token
|
||||||
|
|
||||||
|
def get
|
||||||
|
# rails overwrites params[:action] with the controller action name
|
||||||
|
# we can use this syntax to fetch the original `action`
|
||||||
|
raw_action = request.query_parameters["action"]
|
||||||
|
if raw_action
|
||||||
|
if raw_action=="timeline"
|
||||||
|
get_timeline(params)
|
||||||
|
elsif raw_action=="follow"
|
||||||
|
get_subscriptions(params)
|
||||||
|
elsif raw_action=="channels"
|
||||||
|
list_channels
|
||||||
|
else
|
||||||
|
raise JsonError.new("invalid_request", "This request has an unknown action of #{raw_action}", 400)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise JsonError.new("invalid_request","Microsub requests must have an action parameter", 400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
raw_action = request.request_parameters["action"]
|
||||||
|
if raw_action
|
||||||
|
if raw_action == "follow"
|
||||||
|
subscribe_to_feed(params)
|
||||||
|
elsif raw_action == "unfollow"
|
||||||
|
unsubscribe_from_feed(params)
|
||||||
|
else
|
||||||
|
raise JsonError.new("invalid_request", "This request has an unknown action of #{params[:action]}", 400)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise JsonError.new("invalid_request","Microsub requests must have an action parameter", 400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get_timeline(params)
|
||||||
|
validate_scope("read")
|
||||||
|
@paging = nil
|
||||||
|
if params[:before] and params[:after]
|
||||||
|
@entries = @token.user.feed_entries
|
||||||
|
.where('sort_date < ?', params[:before])
|
||||||
|
.where('sort_date > ?', params[:after])
|
||||||
|
.order(sort_date: :desc).limit(20)
|
||||||
|
elsif params[:before]
|
||||||
|
@entries = @token.user.feed_entries
|
||||||
|
.where('sort_date < ?', params[:before])
|
||||||
|
.order(sort_date: :desc).limit(20)
|
||||||
|
elsif params[:after]
|
||||||
|
@entries = @token.user.feed_entries
|
||||||
|
.where('sort_date > ?', params[:after])
|
||||||
|
.order(sort_date: :desc).limit(20)
|
||||||
|
else
|
||||||
|
@entries = @token.user.feed_entries
|
||||||
|
.order(sort_date: :desc).limit(20)
|
||||||
|
end
|
||||||
|
unless @entries.count < 20
|
||||||
|
@paging = {
|
||||||
|
before: @entries.last.sort_date.iso8601,
|
||||||
|
after: @entries.first.sort_date.iso8601
|
||||||
|
}
|
||||||
|
end
|
||||||
|
items = @entries.map{|e|
|
||||||
|
{
|
||||||
|
type: "entry",
|
||||||
|
published: e.published.iso8601,
|
||||||
|
url: e.link,
|
||||||
|
uid: e.guid,
|
||||||
|
name: e.title,
|
||||||
|
content: {html: e.content},
|
||||||
|
author: {type: 'card', name: e.feed.name, url: e.feed.url},
|
||||||
|
_id: e.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = {}
|
||||||
|
result[:items] = items
|
||||||
|
result[:paging] = @paging unless @paging.nil?
|
||||||
|
render json: result, status: 200
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_subscriptions(params)
|
||||||
|
validate_scope("read")
|
||||||
|
items = []
|
||||||
|
if params[:channel] and params[:channel] == "default"
|
||||||
|
@token.user.feeds.each do |feed|
|
||||||
|
items << {
|
||||||
|
type: "feed",
|
||||||
|
name: feed.name,
|
||||||
|
url: feed.url
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
render json: {items: items}, status: 200
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_channels
|
||||||
|
validate_scope("read")
|
||||||
|
items = []
|
||||||
|
render json: {
|
||||||
|
channels: [
|
||||||
|
{uid: "notifications", name: "Notifications"},
|
||||||
|
{uid: "default", name: "Default"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscribe_to_feed(params)
|
||||||
|
validate_scope("follow")
|
||||||
|
if params[:channel] and params[:channel] == "default"
|
||||||
|
feed_url = params[:url]
|
||||||
|
raise JsonError.new("invalid_request","A URL is required for this action", 400) if feed_url.nil?
|
||||||
|
## duplicate code as feed_controller#add_feed
|
||||||
|
feed_url_host = URI(feed_url).host
|
||||||
|
request_host = URI(request.base_url).host
|
||||||
|
matching_feed = Feed.find_by(url: feed_url, user: current_user)
|
||||||
|
if (feed_url_host == request_host)
|
||||||
|
raise JsonError.new("invalid_request","You cannot subscribe to yourself", 400)
|
||||||
|
elsif matching_feed.nil?
|
||||||
|
feed = @token.user.feeds.create!(url: feed_url)
|
||||||
|
UpdateFeedJob.perform_now(feed)
|
||||||
|
if feed.feed_invalid?
|
||||||
|
raise JsonError.new("invalid_request","Error adding #{feed_url} to your feeds", 400)
|
||||||
|
else
|
||||||
|
render json: {type: "feed", url: feed_url}, status: 201
|
||||||
|
end
|
||||||
|
else # feed already exists
|
||||||
|
render json: {type: "feed", url: feed_url}, status: 200
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise JsonError.new("invalid_request","This server only allows subscribing to the 'default' channel", 400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribe_from_feed(params)
|
||||||
|
validate_scope("follow")
|
||||||
|
if params[:channel] and params[:channel] == "default"
|
||||||
|
if params[:url]
|
||||||
|
feed = @token.user.feeds.find_by(url: params[:url])
|
||||||
|
if feed.nil?
|
||||||
|
raise JsonError.new("not_found", "No feed found with URL #{params[:url]}", 404)
|
||||||
|
else
|
||||||
|
feed.destroy!
|
||||||
|
head 204
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise JsonError.new("invalid_request", "A URL is required for this action", 400)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise JsonError.new("invalid_request", "This server only allows managing subscriptions on the 'default' channel", 400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -13,6 +13,7 @@
|
|||||||
<link rel="authorization_endpoint" href="<%=indie_authorization_endpoint_url %>" />
|
<link rel="authorization_endpoint" href="<%=indie_authorization_endpoint_url %>" />
|
||||||
<link rel="token_endpoint" href="<%= indie_token_endpoint_url %>" />
|
<link rel="token_endpoint" href="<%= indie_token_endpoint_url %>" />
|
||||||
<link rel="micropub" href="<%= micropub_url %>" />
|
<link rel="micropub" href="<%= micropub_url %>" />
|
||||||
|
<link rel="microsub" href="<%= microsub_url %>" />
|
||||||
|
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
@ -12,6 +12,9 @@ Rails.application.routes.draw do
|
|||||||
post 'micropub', to: 'micropub#create'
|
post 'micropub', to: 'micropub#create'
|
||||||
post 'micropub_media', to: 'micropub#media', as: 'micropub_media'
|
post 'micropub_media', to: 'micropub#media', as: 'micropub_media'
|
||||||
|
|
||||||
|
get 'microsub', to: 'microsub#get', as: "microsub"
|
||||||
|
post 'microsub', to: 'microsub#create'
|
||||||
|
|
||||||
resources :feeds, only: [:index, :create, :destroy]
|
resources :feeds, only: [:index, :create, :destroy]
|
||||||
get 'read', to: 'feeds#read'
|
get 'read', to: 'feeds#read'
|
||||||
get 'read/:id', to: 'feeds#read_feed', as: 'read_feed'
|
get 'read/:id', to: 'feeds#read_feed', as: 'read_feed'
|
||||||
|
@ -5,8 +5,8 @@ class IndieAuthScopes
|
|||||||
"profile" => "see your display name",
|
"profile" => "see your display name",
|
||||||
"email" => "see your email address",
|
"email" => "see your email address",
|
||||||
## Microsub
|
## Microsub
|
||||||
# "read" => "fetch your Haven Reader feed",
|
"read" => "fetch your Haven Reader feed",
|
||||||
# "follow" => "subscribe to other sites in your Haven Reader",
|
"follow" => "subscribe to other sites in your Haven Reader",
|
||||||
# "mute" => "silence a site in your Haven Reader",
|
# "mute" => "silence a site in your Haven Reader",
|
||||||
# "block" => "silence a site in your Haven Reader",
|
# "block" => "silence a site in your Haven Reader",
|
||||||
#"channels",
|
#"channels",
|
||||||
|
108
test/integration/microsub_test.rb
Normal file
108
test/integration/microsub_test.rb
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class IndieAuthTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
|
test "can subscribe to feeds with microsub" do
|
||||||
|
token = create_washington_auth_token("profile email read follow")
|
||||||
|
|
||||||
|
# fetch (empty) feed list
|
||||||
|
get '/microsub', params: {
|
||||||
|
access_token: token,
|
||||||
|
action: "follow",
|
||||||
|
channel: "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
response_json = JSON.parse(response.body)
|
||||||
|
assert response_json.keys.include? "items"
|
||||||
|
assert_equal response_json["items"], []
|
||||||
|
|
||||||
|
# subscribe to a feed
|
||||||
|
feed_url = "https://havenweb.org/feed.xml"
|
||||||
|
post '/microsub', params: {
|
||||||
|
access_token: token,
|
||||||
|
action: "follow",
|
||||||
|
channel: "default",
|
||||||
|
url: feed_url
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :created
|
||||||
|
response_json = JSON.parse(response.body)
|
||||||
|
assert_equal response_json["type"], "feed"
|
||||||
|
assert_equal response_json["url"], feed_url
|
||||||
|
|
||||||
|
# fetch (populated) feed list
|
||||||
|
get '/microsub', params: {
|
||||||
|
access_token: token,
|
||||||
|
action: "follow",
|
||||||
|
channel: "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
response_json = JSON.parse(response.body)
|
||||||
|
assert response_json.keys.include? "items"
|
||||||
|
assert_equal response_json["items"].size, 1
|
||||||
|
|
||||||
|
# fetch timeline
|
||||||
|
get '/microsub', params: {
|
||||||
|
access_token: token,
|
||||||
|
action: "timeline",
|
||||||
|
channel: "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
response_json = JSON.parse(response.body)
|
||||||
|
assert response_json["items"].size > 0
|
||||||
|
assert_equal response_json["items"].first["type"], "entry"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# context: scope, state, code_verifier, client_id, redirect_uri
|
||||||
|
def create_washington_auth_request(scope)
|
||||||
|
post user_session_path, params: {user: {
|
||||||
|
email: users(:washington).email,
|
||||||
|
password: "georgepass"
|
||||||
|
}}
|
||||||
|
|
||||||
|
context = {}
|
||||||
|
context["scope"] = scope
|
||||||
|
context["state"] = SecureRandom.urlsafe_base64(10)
|
||||||
|
context["code_verifier"] = SecureRandom.urlsafe_base64(10)
|
||||||
|
context["client_id"] = "http://localhost:12345"
|
||||||
|
context["redirect_uri"] = "http://localhost:12345/redirect"
|
||||||
|
|
||||||
|
approval_params = {}
|
||||||
|
scope.split(" ").each {|s| approval_params[s] = 1}
|
||||||
|
approval_params["code_challenge"] =
|
||||||
|
Base64.urlsafe_encode64(
|
||||||
|
Digest::SHA256.digest(
|
||||||
|
context["code_verifier"])).chomp("=")
|
||||||
|
approval_params["commit"] = "Approve"
|
||||||
|
["state", "client_id", "redirect_uri"].each do |p|
|
||||||
|
approval_params[p] = context[p]
|
||||||
|
end
|
||||||
|
|
||||||
|
post indie_auth_approval_path, params: approval_params
|
||||||
|
return context
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_washington_auth_token(scope)
|
||||||
|
context = create_washington_auth_request(scope)
|
||||||
|
assert_response :redirect
|
||||||
|
redirected_url = URI.parse(response.headers['Location'])
|
||||||
|
query_parameters = CGI.parse(redirected_url.query)
|
||||||
|
# use the auth request to fetch a token
|
||||||
|
post indie_token_endpoint_path, params: {
|
||||||
|
"grant_type" => "authorization_code",
|
||||||
|
"code" => query_parameters["code"].first,
|
||||||
|
"client_id" => context["client_id"],
|
||||||
|
"redirect_uri" => context["redirect_uri"],
|
||||||
|
"code_verifier" => context["code_verifier"]
|
||||||
|
}
|
||||||
|
assert_response :success
|
||||||
|
response_json = JSON.parse(response.body)
|
||||||
|
assert response_json.keys.include?("access_token")
|
||||||
|
return response_json["access_token"]
|
||||||
|
end
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user