federails 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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