diff --git a/app/controllers/well_known/node_info_controller.rb b/app/controllers/well_known/node_info_controller.rb new file mode 100644 index 000000000..3bc90cda7 --- /dev/null +++ b/app/controllers/well_known/node_info_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module WellKnown + class NodeInfoController < ActionController::Base + include RoutingHelper + + before_action { response.headers['Vary'] = 'Accept' } + + def index + render json: ActiveModelSerializers::SerializableResource.new({}, serializer: NodeDiscoverySerializer) + end + + def show + render json: ActiveModelSerializers::SerializableResource.new({}, serializer: NodeInfoSerializer) + end + end +end diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index cb5aca5d0..6de4f2b0f 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -14,6 +14,10 @@ class InstancePresenter to: Setting ) + def active_count(timespan: Time.zone.now - 1.month..Time.zone.now) + Status.select('distinct (account_id)').where(local: true, created_at: timespan).count + end + def contact_account Account.find_local(Setting.site_contact_username.gsub(/\A@/, '')) end diff --git a/app/serializers/node_discovery_serializer.rb b/app/serializers/node_discovery_serializer.rb new file mode 100644 index 000000000..dfb9e4a05 --- /dev/null +++ b/app/serializers/node_discovery_serializer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class NodeDiscoverySerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :links + + def links + [ + { + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: node_info_schema_url('2.0'), + }, + ] + end +end diff --git a/app/serializers/node_info_serializer.rb b/app/serializers/node_info_serializer.rb new file mode 100644 index 000000000..42853e4d1 --- /dev/null +++ b/app/serializers/node_info_serializer.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +class NodeInfoSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :version, :usage, :software, :services, + :protocols, :openRegistrations, :metaData + + def version + '2.0' + end + + def usage + { + users: { + total: instance_presenter.user_count, + activeHalfyear: instance_presenter.active_count(timespan: Time.zone.now - 6.months..Time.zone.now), + activeMonth: instance_presenter.active_count, + }, + localPosts: instance_presenter.status_count, + } + end + + def software + { + version: Mastodon::Version.to_s, + name: 'mastodon', + repository: Mastodon::Version.source_base_url, + } + end + + def services + { + outbound: [], + inbound: [], + } + end + + def protocols + %w(ostatus activitypub) + end + + def openRegistrations + Setting.open_registrations + end + + def metaData + { + nodeName: instance_presenter.site_title, + nodeDescription: instance_presenter.site_description, + nodeTerms: instance_presenter.site_terms, + siteContactEmail: instance_presenter.site_contact_email, + domain_count: instance_presenter.domain_count, + features: features, + invitesEnabled: Setting.min_invite_role != 'admin', + federation: federation, + } + end + + def features + %w(mastodon_api mastodon_api_streaming) + end + + def federation + domains = DomainBlock.all + feds = { + reject_media: [], + reject_reports: [], + } + domains.each do |domain| + feds[domain.severity] = [] unless feds.keys.include?(domain.severity) + feds[domain.severity] << domain.domain + feds[:reject_media] << domain.domain if domain.reject_media + feds[:reject_reports] << domain.domain if domain.reject_reports + end + feds + end + + private + + def instance_presenter + @instance_presenter ||= InstancePresenter.new + end +end diff --git a/config/routes.rb b/config/routes.rb index ded62981d..c455fb071 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,7 +19,9 @@ tokens: 'oauth/tokens' end - get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' } + get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' } + get '.well-known/nodeinfo', to: 'well_known/node_info#index', as: :node_info, defaults: { format: 'json' } + get 'nodeinfo/:version_number', to: 'well_known/node_info#show', as: :node_info_schema, defaults: { format: 'json' } get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger get '.well-known/change-password', to: redirect('/auth/edit') get 'manifest', to: 'manifests#show', defaults: { format: 'json' } diff --git a/spec/controllers/well_known/node_info_controller_spec.rb b/spec/controllers/well_known/node_info_controller_spec.rb new file mode 100644 index 000000000..e5cff763b --- /dev/null +++ b/spec/controllers/well_known/node_info_controller_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +describe WellKnown::NodeInfoController, type: :controller do + render_views + + describe 'GET #index' do + it 'returns json document pointing to node info' do + get :index + + json_response = JSON.parse(response.body) + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'application/json' + expect(json_response.keys.include?('links')).to be true + expect(json_response['links'][0]['rel']).to eq 'http://nodeinfo.diaspora.software/ns/schema/2.0' + expect(json_response['links'][0]['href'].include?('nodeinfo/2.0')).to be true + end + end + + describe 'GET #show' do + it 'returns json document pointing to node info' do + get :show, params: { version_number: 2 } + + json_response = JSON.parse(response.body) + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'application/json' + expect(json_response.keys.include?('version')).to be true + expect(json_response.keys.include?('usage')).to be true + expect(json_response.keys.include?('software')).to be true + expect(json_response.keys.include?('services')).to be true + expect(json_response.keys.include?('protocols')).to be true + expect(json_response.keys.include?('openRegistrations')).to be true + expect(json_response.keys.include?('usage')).to be true + expect(json_response.keys.include?('metaData')).to be true + end + end +end