federails 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +182 -7
  3. data/Rakefile +5 -5
  4. data/app/controllers/federails/application_controller.rb +23 -0
  5. data/app/controllers/federails/client/activities_controller.rb +21 -0
  6. data/app/controllers/federails/client/actors_controller.rb +37 -0
  7. data/app/controllers/federails/client/followings_controller.rb +101 -0
  8. data/app/controllers/federails/server/activities_controller.rb +65 -0
  9. data/app/controllers/federails/server/actors_controller.rb +34 -0
  10. data/app/controllers/federails/server/followings_controller.rb +19 -0
  11. data/app/controllers/federails/server/nodeinfo_controller.rb +22 -0
  12. data/app/controllers/federails/server/server_controller.rb +17 -0
  13. data/app/controllers/federails/server/web_finger_controller.rb +38 -0
  14. data/app/helpers/federails/application_helper.rb +8 -0
  15. data/app/jobs/federails/notify_inbox_job.rb +12 -0
  16. data/app/mailers/federails/application_mailer.rb +2 -2
  17. data/app/models/concerns/federails/entity.rb +57 -0
  18. data/app/models/concerns/federails/has_uuid.rb +35 -0
  19. data/app/models/federails/activity.rb +35 -0
  20. data/app/models/federails/actor.rb +189 -0
  21. data/app/models/federails/following.rb +52 -0
  22. data/app/policies/federails/client/activity_policy.rb +6 -0
  23. data/app/policies/federails/client/actor_policy.rb +15 -0
  24. data/app/policies/federails/client/following_policy.rb +35 -0
  25. data/app/policies/federails/federails_policy.rb +59 -0
  26. data/app/policies/federails/server/activity_policy.rb +6 -0
  27. data/app/policies/federails/server/actor_policy.rb +23 -0
  28. data/app/policies/federails/server/following_policy.rb +6 -0
  29. data/app/views/federails/client/activities/_activity.html.erb +5 -0
  30. data/app/views/federails/client/activities/_activity.json.jbuilder +1 -0
  31. data/app/views/federails/client/activities/_index.json.jbuilder +1 -0
  32. data/app/views/federails/client/activities/feed.html.erb +4 -0
  33. data/app/views/federails/client/activities/feed.json.jbuilder +1 -0
  34. data/app/views/federails/client/activities/index.html.erb +5 -0
  35. data/app/views/federails/client/activities/index.json.jbuilder +1 -0
  36. data/app/views/federails/client/actors/_actor.json.jbuilder +14 -0
  37. data/app/views/federails/client/actors/_lookup_form.html.erb +5 -0
  38. data/app/views/federails/client/actors/index.html.erb +24 -0
  39. data/app/views/federails/client/actors/index.json.jbuilder +1 -0
  40. data/app/views/federails/client/actors/show.html.erb +100 -0
  41. data/app/views/federails/client/actors/show.json.jbuilder +1 -0
  42. data/app/views/federails/client/followings/_follow.html.erb +4 -0
  43. data/app/views/federails/client/followings/_follower.html.erb +7 -0
  44. data/app/views/federails/client/followings/_following.json.jbuilder +1 -0
  45. data/app/views/federails/client/followings/_form.html.erb +21 -0
  46. data/app/views/federails/client/followings/index.html.erb +29 -0
  47. data/app/views/federails/client/followings/index.json.jbuilder +1 -0
  48. data/app/views/federails/client/followings/show.html.erb +21 -0
  49. data/app/views/federails/client/followings/show.json.jbuilder +1 -0
  50. data/app/views/federails/server/activities/_activity.activitypub.jbuilder +14 -0
  51. data/app/views/federails/server/activities/outbox.activitypub.jbuilder +18 -0
  52. data/app/views/federails/server/activities/show.activitypub.jbuilder +1 -0
  53. data/app/views/federails/server/actors/_actor.activitypub.jbuilder +21 -0
  54. data/app/views/federails/server/actors/followers.activitypub.jbuilder +18 -0
  55. data/app/views/federails/server/actors/following.activitypub.jbuilder +18 -0
  56. data/app/views/federails/server/actors/show.activitypub.jbuilder +1 -0
  57. data/app/views/federails/server/followings/_following.activitypub.jbuilder +7 -0
  58. data/app/views/federails/server/followings/show.activitypub.jbuilder +1 -0
  59. data/app/views/federails/server/nodeinfo/index.nodeinfo.jbuilder +6 -0
  60. data/app/views/federails/server/nodeinfo/show.nodeinfo.jbuilder +19 -0
  61. data/app/views/federails/server/web_finger/find.jrd.jbuilder +24 -0
  62. data/app/views/federails/server/web_finger/host_meta.xrd.erb +5 -0
  63. data/config/initializers/mime_types.rb +21 -0
  64. data/config/routes.rb +43 -0
  65. data/db/migrate/20200712133150_create_federails_actors.rb +24 -0
  66. data/db/migrate/20200712143127_create_federails_followings.rb +14 -0
  67. data/db/migrate/20200712174938_create_federails_activities.rb +11 -0
  68. data/db/migrate/20240731145400_change_actor_entity_rel_to_polymorphic.rb +11 -0
  69. data/db/migrate/20241002094500_add_uuids.rb +13 -0
  70. data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
  71. data/lib/federails/configuration.rb +92 -0
  72. data/lib/federails/engine.rb +6 -0
  73. data/lib/federails/utils/host.rb +54 -0
  74. data/lib/federails/version.rb +1 -1
  75. data/lib/federails.rb +34 -3
  76. data/lib/fediverse/inbox.rb +71 -0
  77. data/lib/fediverse/notifier.rb +60 -0
  78. data/lib/fediverse/request.rb +38 -0
  79. data/lib/fediverse/signature.rb +49 -0
  80. data/lib/fediverse/webfinger.rb +117 -0
  81. data/lib/generators/federails/install/USAGE +9 -0
  82. data/lib/generators/federails/install/install_generator.rb +10 -0
  83. data/lib/generators/federails/install/templates/federails.rb +1 -0
  84. data/lib/generators/federails/install/templates/federails.yml +23 -0
  85. data/lib/tasks/factory_bot.rake +15 -0
  86. metadata +170 -10
  87. data/app/views/layouts/federails/application.html.erb +0 -15
@@ -0,0 +1,57 @@
1
+ module Federails
2
+ module Entity
3
+ extend ActiveSupport::Concern
4
+
5
+ included do # rubocop:todo Metrics/BlockLength
6
+ include ActiveSupport::Callbacks
7
+ define_callbacks :followed
8
+
9
+ # Define a method that will be called after the entity receives a follow request
10
+ # @param method [Symbol] The name of the method to call, or a block that will be called directly
11
+ # @example
12
+ # after_followed :accept_follow
13
+ def self.after_followed(method)
14
+ set_callback :followed, :after, method
15
+ end
16
+
17
+ has_one :actor, class_name: 'Federails::Actor', as: :entity, dependent: :destroy
18
+
19
+ after_create :create_actor
20
+
21
+ # Configures the mapping between entity and actor
22
+ # @param username_field [Symbol] The method or attribute name that returns the preferred username for ActivityPub
23
+ # @param name_field [Symbol] The method or attribute name that returns the preferred name for ActivityPub
24
+ # @param profile_url_method [Symbol] The route method name that will generate the profile URL for ActivityPub
25
+ # @param actor_type [String] The ActivityStreams Actor type for this entity; defaults to 'Person'
26
+ # @param include_in_user_count [boolean] Should this entity be included in the nodeinfo user count? Defaults to true
27
+ # @example
28
+ # acts_as_federails_actor username_field: :username, name_field: :display_name, profile_url_method: :url_for, actor_type: 'Person'
29
+ def self.acts_as_federails_actor(
30
+ username_field: Federails::Configuration.user_username_field,
31
+ name_field: Federails::Configuration.user_name_field,
32
+ profile_url_method: Federails.configuration.user_profile_url_method,
33
+ actor_type: 'Person',
34
+ include_in_user_count: true
35
+ )
36
+ Federails::Configuration.register_entity(
37
+ self,
38
+ username_field: username_field,
39
+ name_field: name_field,
40
+ profile_url_method: profile_url_method,
41
+ actor_type: actor_type,
42
+ include_in_user_count: include_in_user_count
43
+ )
44
+ end
45
+
46
+ # Automatically run default acts_as_federails_actor
47
+ # this can be optionally called again with different configuration in the entity
48
+ acts_as_federails_actor
49
+
50
+ private
51
+
52
+ def create_actor
53
+ Federails::Actor.create! entity: self
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ module Federails
2
+ module HasUuid
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_validation :generate_uuid
7
+ validates :uuid, presence: true, uniqueness: true
8
+
9
+ def self.find_param(param)
10
+ find_by!(uuid: param)
11
+ end
12
+ end
13
+
14
+ def to_param
15
+ uuid
16
+ end
17
+
18
+ # Override UUID accessor to provide lazy initialization of UUIDs for old data
19
+ def uuid
20
+ if self[:uuid].blank?
21
+ generate_uuid
22
+ save!
23
+ end
24
+ self[:uuid]
25
+ end
26
+
27
+ private
28
+
29
+ def generate_uuid
30
+ return if self[:uuid].present?
31
+
32
+ (self.uuid = SecureRandom.uuid) while self[:uuid].blank? || self.class.exists?(uuid: self[:uuid])
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ module Federails
2
+ class Activity < ApplicationRecord
3
+ include Federails::HasUuid
4
+
5
+ belongs_to :entity, polymorphic: true
6
+ belongs_to :actor
7
+
8
+ scope :feed_for, lambda { |actor|
9
+ actor_ids = []
10
+ Following.accepted.where(actor: actor).find_each do |following|
11
+ actor_ids << following.target_actor_id
12
+ end
13
+ where(actor_id: actor_ids)
14
+ }
15
+
16
+ after_create_commit :post_to_inboxes
17
+
18
+ def recipients
19
+ return [] unless actor.local?
20
+
21
+ case entity_type
22
+ when 'Federails::Following'
23
+ [(action == 'Accept' ? entity.actor : entity.target_actor)]
24
+ else
25
+ actor.followers
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def post_to_inboxes
32
+ NotifyInboxJob.perform_later(self)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,189 @@
1
+ require 'federails/utils/host'
2
+ require 'fediverse/webfinger'
3
+
4
+ module Federails
5
+ class Actor < ApplicationRecord # rubocop:disable Metrics/ClassLength
6
+ include Federails::HasUuid
7
+
8
+ validates :federated_url, presence: { unless: :entity }, uniqueness: { unless: :entity }
9
+ validates :username, presence: { unless: :entity }
10
+ validates :server, presence: { unless: :entity }
11
+ validates :inbox_url, presence: { unless: :entity }
12
+ validates :outbox_url, presence: { unless: :entity }
13
+ validates :followers_url, presence: { unless: :entity }
14
+ validates :followings_url, presence: { unless: :entity }
15
+ validates :profile_url, presence: { unless: :entity }
16
+ validates :entity_id, uniqueness: { scope: :entity_type }, if: :local?
17
+
18
+ belongs_to :entity, polymorphic: true, optional: true
19
+ # FIXME: Handle this with something like undelete
20
+ has_many :activities, dependent: :destroy
21
+ has_many :activities_as_entity, class_name: 'Federails::Activity', as: :entity, dependent: :destroy
22
+ has_many :following_followers, class_name: 'Federails::Following', foreign_key: :target_actor_id, dependent: :destroy, inverse_of: :target_actor
23
+ has_many :following_follows, class_name: 'Federails::Following', dependent: :destroy, inverse_of: :actor
24
+ has_many :followers, source: :actor, through: :following_followers
25
+ has_many :follows, source: :target_actor, through: :following_follows
26
+
27
+ scope :local, -> { where.not(entity: nil) }
28
+
29
+ def local?
30
+ entity.present?
31
+ end
32
+
33
+ def federated_url
34
+ local? ? Federails::Engine.routes.url_helpers.server_actor_url(self) : attributes['federated_url'].presence
35
+ end
36
+
37
+ def username
38
+ return attributes['username'] unless local?
39
+
40
+ entity.send(entity_configuration[:username_field]).to_s
41
+ end
42
+
43
+ def name
44
+ value = (entity.send(entity_configuration[:name_field]).to_s if local?)
45
+
46
+ value || attributes['name'] || username
47
+ end
48
+
49
+ def server
50
+ local? ? Utils::Host.localhost : attributes['server']
51
+ end
52
+
53
+ def inbox_url
54
+ local? ? Federails::Engine.routes.url_helpers.server_actor_inbox_url(self) : attributes['inbox_url']
55
+ end
56
+
57
+ def outbox_url
58
+ local? ? Federails::Engine.routes.url_helpers.server_actor_outbox_url(self) : attributes['outbox_url']
59
+ end
60
+
61
+ def followers_url
62
+ local? ? Federails::Engine.routes.url_helpers.followers_server_actor_url(self) : attributes['followers_url']
63
+ end
64
+
65
+ def followings_url
66
+ local? ? Federails::Engine.routes.url_helpers.following_server_actor_url(self) : attributes['followings_url']
67
+ end
68
+
69
+ def profile_url
70
+ return attributes['profile_url'].presence unless local?
71
+
72
+ method = entity_configuration[:profile_url_method]
73
+ return Federails::Engine.routes.url_helpers.server_actor_url self unless method
74
+
75
+ Rails.application.routes.url_helpers.send method, [entity]
76
+ end
77
+
78
+ def at_address
79
+ "#{username}@#{server}"
80
+ end
81
+
82
+ def short_at_address
83
+ local? ? "@#{username}" : at_address
84
+ end
85
+
86
+ def follows?(actor)
87
+ list = following_follows.where target_actor: actor
88
+ return list.first if list.count == 1
89
+
90
+ false
91
+ end
92
+
93
+ def entity_configuration
94
+ Federails::Configuration.entity_types[entity.class.name]
95
+ end
96
+
97
+ class << self
98
+ def find_by_account(account) # rubocop:todo Metrics/AbcSize
99
+ parts = Fediverse::Webfinger.split_account account
100
+
101
+ if Fediverse::Webfinger.local_user? parts
102
+ actor = nil
103
+ Federails::Configuration.entity_types.each_value do |entity|
104
+ actor ||= entity[:class].find_by(entity[:username_field] => parts[:username])&.actor
105
+ end
106
+ raise ActiveRecord::RecordNotFound if actor.nil?
107
+ else
108
+ actor = find_by username: parts[:username], server: parts[:domain]
109
+ actor ||= Fediverse::Webfinger.fetch_actor(parts[:username], parts[:domain])
110
+ end
111
+
112
+ actor
113
+ end
114
+
115
+ def find_by_federation_url(federated_url)
116
+ local_route = Utils::Host.local_route federated_url
117
+ return find_param(local_route[:id]) if local_route && local_route[:controller] == 'federails/server/actors' && local_route[:action] == 'show'
118
+
119
+ actor = find_by federated_url: federated_url
120
+ return actor if actor
121
+
122
+ Fediverse::Webfinger.fetch_actor_url(federated_url)
123
+ end
124
+
125
+ def find_or_create_by_account(account)
126
+ actor = find_by_account account
127
+ # Create/update distant actors
128
+ actor.save! unless actor.local?
129
+
130
+ actor
131
+ end
132
+
133
+ def find_or_create_by_federation_url(url)
134
+ actor = find_by_federation_url url
135
+ # Create/update distant actors
136
+ actor.save! unless actor.local?
137
+
138
+ actor
139
+ end
140
+
141
+ # Find or create actor from a given actor hash or actor id (actor's URL)
142
+ def find_or_create_by_object(object)
143
+ case object
144
+ when String
145
+ find_or_create_by_federation_url object
146
+ when Hash
147
+ find_or_create_by_federation_url object['id']
148
+ else
149
+ raise "Unsupported object type for actor (#{object.class})"
150
+ end
151
+ end
152
+ end
153
+
154
+ def public_key
155
+ ensure_key_pair_exists!
156
+ self[:public_key]
157
+ end
158
+
159
+ def private_key
160
+ ensure_key_pair_exists!
161
+ self[:private_key]
162
+ end
163
+
164
+ def key_id
165
+ "#{federated_url}#main-key"
166
+ end
167
+
168
+ private
169
+
170
+ def ensure_key_pair_exists!
171
+ return if self[:private_key].present? || !local?
172
+
173
+ update!(generate_key_pair)
174
+ end
175
+
176
+ def generate_key_pair
177
+ rsa_key = OpenSSL::PKey::RSA.new 2048
178
+ cipher = OpenSSL::Cipher.new('AES-128-CBC')
179
+ {
180
+ private_key: if Rails.application.credentials.secret_key_base
181
+ rsa_key.to_pem(cipher, Rails.application.credentials.secret_key_base)
182
+ else
183
+ rsa_key.to_pem
184
+ end,
185
+ public_key: rsa_key.public_key.to_pem,
186
+ }
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,52 @@
1
+ module Federails
2
+ class Following < ApplicationRecord
3
+ include Federails::HasUuid
4
+
5
+ enum status: { pending: 0, accepted: 1 }
6
+
7
+ validates :target_actor_id, uniqueness: { scope: [:actor_id, :target_actor_id] }
8
+
9
+ belongs_to :actor
10
+ belongs_to :target_actor, class_name: 'Federails::Actor'
11
+ # FIXME: Handle this with something like undelete
12
+ has_many :activities, as: :entity, dependent: :destroy
13
+
14
+ after_create :after_follow
15
+ after_create :create_activity
16
+ after_destroy :destroy_activity
17
+
18
+ scope :with_actor, ->(actor) { where(actor_id: actor.id).or(where(target_actor_id: actor.id)) }
19
+
20
+ def federated_url
21
+ attributes['federated_url'].presence || Federails::Engine.routes.url_helpers.server_actor_following_url(actor_id: actor_id, id: id)
22
+ end
23
+
24
+ def accept!
25
+ update! status: :accepted
26
+ Activity.create! actor: target_actor, action: 'Accept', entity: self
27
+ end
28
+
29
+ class << self
30
+ def new_from_account(account, actor:)
31
+ target_actor = Actor.find_or_create_by_account account
32
+ new actor: actor, target_actor: target_actor
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def after_follow
39
+ target_actor&.entity&.run_callbacks :followed, :after do
40
+ self
41
+ end
42
+ end
43
+
44
+ def create_activity
45
+ Activity.create! actor: actor, action: 'Create', entity: self
46
+ end
47
+
48
+ def destroy_activity
49
+ Activity.create! actor: actor, action: 'Undo', entity: self
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,6 @@
1
+ module Federails
2
+ module Client
3
+ class ActivityPolicy < Federails::FederailsPolicy
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ module Federails
2
+ module Client
3
+ class ActorPolicy < Federails::FederailsPolicy
4
+ def lookup?
5
+ true
6
+ end
7
+
8
+ class Scope < Scope
9
+ def resolve
10
+ scope.local
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module Federails
2
+ module Client
3
+ class FollowingPolicy < Federails::FederailsPolicy
4
+ def show?
5
+ in_following?
6
+ end
7
+
8
+ def destroy?
9
+ in_following?
10
+ end
11
+
12
+ def accept?
13
+ in_following? && @record.target_actor_id == @user.actor.id
14
+ end
15
+
16
+ def follow?
17
+ create?
18
+ end
19
+
20
+ class Scope < Scope
21
+ def resolve
22
+ scope.with_actor(@user.actor)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def in_following?
29
+ return false if @user.blank?
30
+
31
+ @record.actor_id == @user.actor.id || @record.target_actor_id == @user.actor.id
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ module Federails
2
+ class FederailsPolicy
3
+ attr_reader :user, :record
4
+
5
+ def initialize(user, record)
6
+ @user = user
7
+ @record = record
8
+ end
9
+
10
+ def index?
11
+ true
12
+ end
13
+
14
+ def show?
15
+ true
16
+ end
17
+
18
+ def create?
19
+ @user.present?
20
+ end
21
+
22
+ def new?
23
+ create?
24
+ end
25
+
26
+ def update?
27
+ owner?
28
+ end
29
+
30
+ def edit?
31
+ update?
32
+ end
33
+
34
+ def destroy?
35
+ owner?
36
+ end
37
+
38
+ class Scope
39
+ attr_reader :user, :scope
40
+
41
+ def initialize(user, scope)
42
+ @user = user
43
+ @scope = scope
44
+ end
45
+
46
+ def resolve
47
+ scope.all
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def owner?
54
+ return false unless @user
55
+
56
+ @record.actor_id == @user.actor.id
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,6 @@
1
+ module Federails
2
+ module Server
3
+ class ActivityPolicy < Federails::FederailsPolicy
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,23 @@
1
+ module Federails
2
+ module Server
3
+ class ActorPolicy < Federails::FederailsPolicy
4
+ def show?
5
+ @record.local?
6
+ end
7
+
8
+ def following?
9
+ true
10
+ end
11
+
12
+ def followers?
13
+ true
14
+ end
15
+
16
+ class Scope < Scope
17
+ def resolve
18
+ scope.local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ module Federails
2
+ module Server
3
+ class FollowingPolicy < Federails::FederailsPolicy
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ <div>
2
+ <b><%= activity.actor.name %></b>
3
+ <code><%= activity.action %></code>
4
+ <!-- < %= link_to activity.entity_type, activity.entity %>-->
5
+ </div>
@@ -0,0 +1 @@
1
+ json.extract! activity, :id, :entity_id, :entity_type, :action, :actor_id, :created_at
@@ -0,0 +1 @@
1
+ json.array! @activities, partial: 'federails/client/activities/activity', as: :activity
@@ -0,0 +1,4 @@
1
+ <h1>Your feed !</h1>
2
+ <% @activities.each do |activity| %>
3
+ <%= render 'activity', activity: activity %>
4
+ <% end %>
@@ -0,0 +1 @@
1
+ json.partial! 'federails/client/activities/index'
@@ -0,0 +1,5 @@
1
+ <h1>Listing activities</h1>
2
+
3
+ <%= @activities.each do |activity| %>
4
+ <%= render 'activity', activity: activity %>
5
+ <% end %>
@@ -0,0 +1 @@
1
+ json.partial! 'federails/client/activities/index'
@@ -0,0 +1,14 @@
1
+ json.extract! actor,
2
+ :id,
3
+ :name,
4
+ :federated_url,
5
+ :username,
6
+ :inbox_url,
7
+ :outbox_url,
8
+ :followers_url,
9
+ :followings_url,
10
+ :profile_url,
11
+ :at_address,
12
+ :user_id,
13
+ :created_at,
14
+ :updated_at
@@ -0,0 +1,5 @@
1
+ <%= form_tag federails.lookup_client_actors_url, method: :get do %>
2
+ <%= label_tag :account %>
3
+ <%= text_field_tag :account, nil, placeholder: 'user@domain.tld or username', required: true %>
4
+ <%= submit_tag 'Search' %>
5
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <h1>Listing actors</h1>
2
+
3
+ <table>
4
+ <thead>
5
+ <tr>
6
+ <th>Name</th>
7
+ <th>Username</th>
8
+ <th>Federation address</th>
9
+ <th>Local?</th>
10
+ <th></th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <% @actors.each do |actor| %>
15
+ <tr>
16
+ <td><%= actor.name %></td>
17
+ <td><%= actor.username %></td>
18
+ <td><%= actor.at_address %></td>
19
+ <td><%= actor.local? %></td>
20
+ <td><%= link_to 'Show', federails.client_actor_url(actor) %></td>
21
+ </tr>
22
+ <% end %>
23
+ </tbody>
24
+ </table>
@@ -0,0 +1 @@
1
+ json.array! @actors, partial: 'federails/client/actors/actor', as: :actor