Mastodonを読む/リモートフォローの流れ
をテンプレートにして作成
[
トップ
] [
新規
|
一覧
|
単語検索
|
最終更新
|
ヘルプ
]
開始行:
[[Mastodonを読む]]
#contents
*はじめに [#x21a0437]
前回はフォローについて見てきました。Mastodonの場合、別イ...
*リモートフォローのやり方 [#pd234e56]
別インスタンスにいるユーザをどう見つけるかですが、今回は...
&ref(account-page.jpg);
「リモートフォロー」ボタンをクリックすると、以下のように...
&ref(remote-follow.jpg);
*app/controllers/remote_follow_controller.rb [#jf8ca318]
routes.rbを確認すると以下のようになっています。
#code(Ruby){{
resources :accounts, path: 'users', only: [:show], para...
get :remote_follow, to: 'remote_follow#new'
post :remote_follow, to: 'remote_follow#create'
}}
remote_followはRemoteFollowControllerで処理されるようです...
newは置いといてcreateメソッド。
#code(Ruby){{
def create
@remote_follow = RemoteFollow.new(resource_params)
if @remote_follow.valid?
resource = Goldfinger.finger("acct:#{@remo...
redirect_url_link = resource&.link('http://ostatus....
if redirect_url_link.nil? || redirect_url_link.temp...
@remote_follow.errors.add(:acct, I18n.t('remote_f...
render(:new) && return
end
session[:remote_follow] = @remote_follow.acct
redirect_to Addressable::Template.new(redirect_url_...
else
render :new
end
rescue Goldfinger::Error
@remote_follow.errors.add(:acct, I18n.t('remote_follo...
render :new
end
}}
「&.」という書き方はRuby2.3からできるようになったようで、...
Goldfingerは前回も確認したようにWebFingerを実装するgemで...
#code{{
{
"subject":"acct:junjis0203@mastodon.dev",
"aliases":[
"http://mastodon.dev/@junjis0203",
"http://mastodon.dev/users/junjis0203"
],
"links":[
{"rel":"http://webfinger.net/rel/profile-page","type"...
{"rel":"http://schemas.google.com/g/2010#updates-from...
{"rel":"self","type":"application/activity+json","hre...
{"rel":"salmon","href":"http://mastodon.dev/api/salmo...
{"rel":"magic-public-key","href":"data:application/ma...
{"rel":"http://ostatus.org/schema/1.0/subscribe","tem...
]
}
}}
createメソッドでは「http://ostatus.org/schema/1.0/subscri...
開発環境のままだとわかりにくいので、自分がmstdn.jp(イン...
**app/models/remote_follow.rb [#b71ac29f]
RemoteFollowは一見、ActiveRecordを継承したクラスに見えま...
#code(Ruby){{
class RemoteFollow
include ActiveModel::Validations
attr_accessor :acct
validates :acct, presence: true
def initialize(attrs = {})
@acct = attrs[:acct].strip unless attrs[:acct].nil?
end
end
}}
ActiveModel::Validationsをincludeすればvalid?とかerrorsが...
*app/controllers/authorize_follows_controller.rb [#z1c193...
ここから、処理が行われるインスタンスが変わります。具体的...
まずはshowメソッド。いろいろやっています。
#code(Ruby){{
def show
@account = located_account || render(:error)
end
def located_account
if acct_param_is_url?
account_from_remote_fetch
else
account_from_remote_follow
end
end
def acct_param_is_url?
parsed_uri.path && %w[http https].include?(parsed_uri...
end
def parsed_uri
Addressable::URI.parse(acct_without_prefix).normalize
end
def acct_without_prefix
acct_params.gsub(/\Aacct:/, '')
end
def acct_params
params.fetch(:acct, '')
end
def account_from_remote_follow
FollowRemoteAccountService.new.call(acct_without_pref...
end
}}
acctパラメータで渡されているのは「acct:admin@mastodon.dev...
**app/services/follow_remote_account_service.rb [#e41416bb]
FollowRemoteAccountServiceは前回も出てきました。前回は概...
#code(Ruby){{
# Find or create a local account for a remote user.
# When creating, look up the user's webfinger and fetch...
# important information from their feed
# @param [String] uri User URI in the form of username@...
# @return [Account]
def call(uri, redirected = nil)
username, domain = uri.split('@')
return Account.find_local(username) if TagManager.ins...
account = Account.find_remote(username, domain)
return account unless account_needs_webfinger_update?...
}}
まずここまで。uriで渡されたのはリモートフォローしようとし...
#code(Ruby){{
Rails.logger.debug "Looking up webfinger for #{uri}"
data = Goldfinger.finger("acct:#{uri}")
raise Goldfinger::Error, 'Missing resource links' if ...
data.link('salmon').nil? ||
data.link('http://webfinger.net/rel/profile-page')....
data.link('magic-public-key').nil?
# Disallow account hijacking
confirmed_username, confirmed_domain = data.subject.g...
unless confirmed_username.casecmp(username).zero? && ...
return call("#{confirmed_username}@#{confirmed_doma...
raise Goldfinger::Error, 'Requested and returned ac...
end
return Account.find_local(confirmed_username) if TagM...
confirmed_account = Account.find_remote(confirmed_use...
if confirmed_account.nil?
Rails.logger.debug "Creating new remote account for...
domain_block = DomainBlock.find_by(domain: domain)
account = Account.new(username: confirmed_username,...
account.suspended = true if domain_block && domai...
account.silenced = true if domain_block && domai...
account.private_key = nil
else
account = confirmed_account
end
}}
WebFingerでフォロー対象のアカウントの情報を取得しています...
:1回目のWebFinger|リモートフォロー対象がいるインスタンスB...
:2回目のWebFinger|入力されたアカウントがいるインスタンスA...
と相互にWebFingerが使用されています。で、取得した情報を使...
#code(Ruby){{
account.last_webfingered_at = Time.now.utc
account.remote_url = data.link('http://schemas.googl...
account.salmon_url = data.link('salmon').href
account.url = data.link('http://webfinger.net...
account.public_key = magic_key_to_pem(data.link('mag...
body, xml = get_feed(account.remote_url)
hubs = get_hubs(xml)
account.uri = get_account_uri(xml)
account.hub_url = hubs.first.attribute('href').value
account.save!
get_profile(body, account)
account
end
}}
残り。各種URLが設定されています。
**createメソッド [#e1f7309b]
さて、というわけでフォロー対象のインスタンスから情報を取...
そして、フォローボタンが押されると今度は実際のフォロー処...
#code(Ruby){{
def create
@account = follow_attempt.try(:target_account)
if @account.nil?
render :error
else
redirect_to web_url("accounts/#{@account.id}")
end
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermi...
render :error
end
private
def follow_attempt
FollowService.new.call(current_account, acct_without_...
end
}}
FollowServiceは前回も出てきました。
#code(Ruby){{
class FollowService < BaseService
include StreamEntryRenderer
# Follow a remote user, notify remote user about the fo...
# @param [Account] source_account From which to follow
# @param [String] uri User URI to follow in the form of...
def call(source_account, uri)
target_account = FollowRemoteAccountService.new.call(...
raise ActiveRecord::RecordNotFound if target_account....
raise Mastodon::NotPermittedError if target_account....
if target_account.locked?
request_follow(source_account, target_account)
else
direct_follow(source_account, target_account)
end
end
}}
さらにdirect_followメソッド。
#code(Ruby){{
def direct_follow(source_account, target_account)
follow = source_account.follow!(target_account)
if target_account.local?
NotifyService.new.call(target_account, follow)
else
Pubsubhubbub::SubscribeWorker.perform_async(target_...
NotificationWorker.perform_async(build_follow_xml(f...
AfterRemoteFollowWorker.perform_async(follow.id)
end
MergeWorker.perform_async(target_account.id, source_a...
follow
end
}}
前回はさらっと流しましたが今回はNotificationWorkerの先を...
なお、build_follow_xmlメソッドでは以下のようなXMLができる...
#code{{
<?xml version="1.0"?>
<entry xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
xmlns:poco="http://portablecontacts.net/spec/1.0"
xmlns:media="http://purl.org/syndication/atommedia"
xmlns:ostatus="http://ostatus.org/schema/1.0"
xmlns:mastodon="http://mastodon.social/schema/1.0">
<id>tag:localhost:3000,2017-05-17:objectId=3:objectType...
<title>admin started following junjis0203</title>
<content type="html">admin started following junjis0203...
<author>
<id>http://localhost:3000/users/admin</id>
<activity:object-type>http://activitystrea.ms/schema/...
<uri>http://localhost:3000/users/admin</uri>
<name>admin</name>
<email>admin@localhost:3000</email>
<link rel="alternate" type="text/html" href="http://l...
<link rel="avatar" type="" media:width="120" media:he...
<link rel="header" type="" media:width="700" media:he...
<poco:preferredUsername>admin</poco:preferredUsername>
<mastodon:scope>public</mastodon:scope>
</author>
<activity:object-type>http://activitystrea.ms/schema/1....
<activity:verb>http://activitystrea.ms/schema/1.0/follo...
<activity:object>
<id>http://localhost:3000/users/junjis0203</id>
<activity:object-type>http://activitystrea.ms/schema/...
<uri>http://localhost:3000/users/junjis0203</uri>
<name>junjis0203</name>
<email>junjis0203@localhost:3000</email>
<link rel="alternate" type="text/html" href="http://l...
<link rel="avatar" type="" media:width="120" media:he...
<link rel="header" type="" media:width="700" media:he...
<poco:preferredUsername>junjis0203</poco:preferredUse...
<mastodon:scope>public</mastodon:scope>
</activity:object>
</entry>
}}
上記のXMLは画面でadmin→junjis0203のフォローを行った後、ra...
a = Account.find_local('admin')
f = a.active_relationships.first
puts AtomSerializer.render(AtomSerializer.new.follow_sal...
**app/works/notification_worker.rb [#gff24b21]
NotificationWorker。
#code(Ruby){{
class NotificationWorker
include Sidekiq::Worker
sidekiq_options queue: 'push', retry: 5
def perform(xml, source_account_id, target_account_id)
SendInteractionService.new.call(xml, Account.find(sou...
end
end
}}
SendInteractionService呼んでるだけです。
#code(Ruby){{
class SendInteractionService < BaseService
# Send an Atom representation of an interaction to a re...
# @param [String] Entry XML
# @param [Account] source_account
# @param [Account] target_account
def call(xml, source_account, target_account)
envelope = salmon.pack(xml, source_account.keypair)
salmon.post(target_account.salmon_url, envelope)
end
private
def salmon
@salmon ||= OStatus2::Salmon.new
end
end
}}
こちらもSalmonにポストしてるだけ。ですが、ここでまたイン...
*app/controllers/api/salmon_controller.rb [#uee7afca]
というわけで「/api/salmon/:id」の処理。updateメソッドが呼...
#code(Ruby){{
class Api::SalmonController < ApiController
before_action :set_account
respond_to :txt
def update
payload = request.body.read
if !payload.nil? && verify?(payload)
SalmonWorker.perform_async(@account.id, payload.for...
head 201
else
head 202
end
end
}}
SalmonWorker。ProcessInteractionService呼んでるだけです。
#code(Ruby){{
class SalmonWorker
include Sidekiq::Worker
sidekiq_options backtrace: true
def perform(account_id, body)
ProcessInteractionService.new.call(body, Account.find...
rescue Nokogiri::XML::XPath::SyntaxError, ActiveRecord:...
true
end
end
}}
ProcessInteractionService。
#code(Ruby){{
class ProcessInteractionService < BaseService
include AuthorExtractor
# Record locally the remote interaction with our user
# @param [String] envelope Salmon envelope
# @param [Account] target_account Account the Salmon wa...
def call(envelope, target_account)
body = salmon.unpack(envelope)
xml = Nokogiri::XML(body)
xml.encoding = 'utf-8'
account = author_from_xml(xml.at_xpath('/xmlns:entry'...
return if account.nil? || account.suspended?
if salmon.verify(envelope, account.keypair)
RemoteProfileUpdateWorker.perform_async(account.id,...
case verb(xml)
when :follow
follow!(account, target_account) unless target_ac...
}}
author_from_xmlはAuthorExtractor(app/services/concerns/a...
verbメソッドで<activity:verb>に書かれている動作を取得、そ...
つまり、インスタンスA(リモートフォローした人がいるインス...
*おわりに [#e97721a1]
今回はリモートフォローについて見てきました。リモートフォ...
+リモートフォロー対象がいるインスタンス(インスタンスB)...
+対象をフォローするアカウントがいるインスタンス(インスタ...
+さらに情報同期のためにインスタンスAからインスタンスBに情...
とインスタンスをまたいで情報のやり取りが行われていました...
終了行:
[[Mastodonを読む]]
#contents
*はじめに [#x21a0437]
前回はフォローについて見てきました。Mastodonの場合、別イ...
*リモートフォローのやり方 [#pd234e56]
別インスタンスにいるユーザをどう見つけるかですが、今回は...
&ref(account-page.jpg);
「リモートフォロー」ボタンをクリックすると、以下のように...
&ref(remote-follow.jpg);
*app/controllers/remote_follow_controller.rb [#jf8ca318]
routes.rbを確認すると以下のようになっています。
#code(Ruby){{
resources :accounts, path: 'users', only: [:show], para...
get :remote_follow, to: 'remote_follow#new'
post :remote_follow, to: 'remote_follow#create'
}}
remote_followはRemoteFollowControllerで処理されるようです...
newは置いといてcreateメソッド。
#code(Ruby){{
def create
@remote_follow = RemoteFollow.new(resource_params)
if @remote_follow.valid?
resource = Goldfinger.finger("acct:#{@remo...
redirect_url_link = resource&.link('http://ostatus....
if redirect_url_link.nil? || redirect_url_link.temp...
@remote_follow.errors.add(:acct, I18n.t('remote_f...
render(:new) && return
end
session[:remote_follow] = @remote_follow.acct
redirect_to Addressable::Template.new(redirect_url_...
else
render :new
end
rescue Goldfinger::Error
@remote_follow.errors.add(:acct, I18n.t('remote_follo...
render :new
end
}}
「&.」という書き方はRuby2.3からできるようになったようで、...
Goldfingerは前回も確認したようにWebFingerを実装するgemで...
#code{{
{
"subject":"acct:junjis0203@mastodon.dev",
"aliases":[
"http://mastodon.dev/@junjis0203",
"http://mastodon.dev/users/junjis0203"
],
"links":[
{"rel":"http://webfinger.net/rel/profile-page","type"...
{"rel":"http://schemas.google.com/g/2010#updates-from...
{"rel":"self","type":"application/activity+json","hre...
{"rel":"salmon","href":"http://mastodon.dev/api/salmo...
{"rel":"magic-public-key","href":"data:application/ma...
{"rel":"http://ostatus.org/schema/1.0/subscribe","tem...
]
}
}}
createメソッドでは「http://ostatus.org/schema/1.0/subscri...
開発環境のままだとわかりにくいので、自分がmstdn.jp(イン...
**app/models/remote_follow.rb [#b71ac29f]
RemoteFollowは一見、ActiveRecordを継承したクラスに見えま...
#code(Ruby){{
class RemoteFollow
include ActiveModel::Validations
attr_accessor :acct
validates :acct, presence: true
def initialize(attrs = {})
@acct = attrs[:acct].strip unless attrs[:acct].nil?
end
end
}}
ActiveModel::Validationsをincludeすればvalid?とかerrorsが...
*app/controllers/authorize_follows_controller.rb [#z1c193...
ここから、処理が行われるインスタンスが変わります。具体的...
まずはshowメソッド。いろいろやっています。
#code(Ruby){{
def show
@account = located_account || render(:error)
end
def located_account
if acct_param_is_url?
account_from_remote_fetch
else
account_from_remote_follow
end
end
def acct_param_is_url?
parsed_uri.path && %w[http https].include?(parsed_uri...
end
def parsed_uri
Addressable::URI.parse(acct_without_prefix).normalize
end
def acct_without_prefix
acct_params.gsub(/\Aacct:/, '')
end
def acct_params
params.fetch(:acct, '')
end
def account_from_remote_follow
FollowRemoteAccountService.new.call(acct_without_pref...
end
}}
acctパラメータで渡されているのは「acct:admin@mastodon.dev...
**app/services/follow_remote_account_service.rb [#e41416bb]
FollowRemoteAccountServiceは前回も出てきました。前回は概...
#code(Ruby){{
# Find or create a local account for a remote user.
# When creating, look up the user's webfinger and fetch...
# important information from their feed
# @param [String] uri User URI in the form of username@...
# @return [Account]
def call(uri, redirected = nil)
username, domain = uri.split('@')
return Account.find_local(username) if TagManager.ins...
account = Account.find_remote(username, domain)
return account unless account_needs_webfinger_update?...
}}
まずここまで。uriで渡されたのはリモートフォローしようとし...
#code(Ruby){{
Rails.logger.debug "Looking up webfinger for #{uri}"
data = Goldfinger.finger("acct:#{uri}")
raise Goldfinger::Error, 'Missing resource links' if ...
data.link('salmon').nil? ||
data.link('http://webfinger.net/rel/profile-page')....
data.link('magic-public-key').nil?
# Disallow account hijacking
confirmed_username, confirmed_domain = data.subject.g...
unless confirmed_username.casecmp(username).zero? && ...
return call("#{confirmed_username}@#{confirmed_doma...
raise Goldfinger::Error, 'Requested and returned ac...
end
return Account.find_local(confirmed_username) if TagM...
confirmed_account = Account.find_remote(confirmed_use...
if confirmed_account.nil?
Rails.logger.debug "Creating new remote account for...
domain_block = DomainBlock.find_by(domain: domain)
account = Account.new(username: confirmed_username,...
account.suspended = true if domain_block && domai...
account.silenced = true if domain_block && domai...
account.private_key = nil
else
account = confirmed_account
end
}}
WebFingerでフォロー対象のアカウントの情報を取得しています...
:1回目のWebFinger|リモートフォロー対象がいるインスタンスB...
:2回目のWebFinger|入力されたアカウントがいるインスタンスA...
と相互にWebFingerが使用されています。で、取得した情報を使...
#code(Ruby){{
account.last_webfingered_at = Time.now.utc
account.remote_url = data.link('http://schemas.googl...
account.salmon_url = data.link('salmon').href
account.url = data.link('http://webfinger.net...
account.public_key = magic_key_to_pem(data.link('mag...
body, xml = get_feed(account.remote_url)
hubs = get_hubs(xml)
account.uri = get_account_uri(xml)
account.hub_url = hubs.first.attribute('href').value
account.save!
get_profile(body, account)
account
end
}}
残り。各種URLが設定されています。
**createメソッド [#e1f7309b]
さて、というわけでフォロー対象のインスタンスから情報を取...
そして、フォローボタンが押されると今度は実際のフォロー処...
#code(Ruby){{
def create
@account = follow_attempt.try(:target_account)
if @account.nil?
render :error
else
redirect_to web_url("accounts/#{@account.id}")
end
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermi...
render :error
end
private
def follow_attempt
FollowService.new.call(current_account, acct_without_...
end
}}
FollowServiceは前回も出てきました。
#code(Ruby){{
class FollowService < BaseService
include StreamEntryRenderer
# Follow a remote user, notify remote user about the fo...
# @param [Account] source_account From which to follow
# @param [String] uri User URI to follow in the form of...
def call(source_account, uri)
target_account = FollowRemoteAccountService.new.call(...
raise ActiveRecord::RecordNotFound if target_account....
raise Mastodon::NotPermittedError if target_account....
if target_account.locked?
request_follow(source_account, target_account)
else
direct_follow(source_account, target_account)
end
end
}}
さらにdirect_followメソッド。
#code(Ruby){{
def direct_follow(source_account, target_account)
follow = source_account.follow!(target_account)
if target_account.local?
NotifyService.new.call(target_account, follow)
else
Pubsubhubbub::SubscribeWorker.perform_async(target_...
NotificationWorker.perform_async(build_follow_xml(f...
AfterRemoteFollowWorker.perform_async(follow.id)
end
MergeWorker.perform_async(target_account.id, source_a...
follow
end
}}
前回はさらっと流しましたが今回はNotificationWorkerの先を...
なお、build_follow_xmlメソッドでは以下のようなXMLができる...
#code{{
<?xml version="1.0"?>
<entry xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
xmlns:poco="http://portablecontacts.net/spec/1.0"
xmlns:media="http://purl.org/syndication/atommedia"
xmlns:ostatus="http://ostatus.org/schema/1.0"
xmlns:mastodon="http://mastodon.social/schema/1.0">
<id>tag:localhost:3000,2017-05-17:objectId=3:objectType...
<title>admin started following junjis0203</title>
<content type="html">admin started following junjis0203...
<author>
<id>http://localhost:3000/users/admin</id>
<activity:object-type>http://activitystrea.ms/schema/...
<uri>http://localhost:3000/users/admin</uri>
<name>admin</name>
<email>admin@localhost:3000</email>
<link rel="alternate" type="text/html" href="http://l...
<link rel="avatar" type="" media:width="120" media:he...
<link rel="header" type="" media:width="700" media:he...
<poco:preferredUsername>admin</poco:preferredUsername>
<mastodon:scope>public</mastodon:scope>
</author>
<activity:object-type>http://activitystrea.ms/schema/1....
<activity:verb>http://activitystrea.ms/schema/1.0/follo...
<activity:object>
<id>http://localhost:3000/users/junjis0203</id>
<activity:object-type>http://activitystrea.ms/schema/...
<uri>http://localhost:3000/users/junjis0203</uri>
<name>junjis0203</name>
<email>junjis0203@localhost:3000</email>
<link rel="alternate" type="text/html" href="http://l...
<link rel="avatar" type="" media:width="120" media:he...
<link rel="header" type="" media:width="700" media:he...
<poco:preferredUsername>junjis0203</poco:preferredUse...
<mastodon:scope>public</mastodon:scope>
</activity:object>
</entry>
}}
上記のXMLは画面でadmin→junjis0203のフォローを行った後、ra...
a = Account.find_local('admin')
f = a.active_relationships.first
puts AtomSerializer.render(AtomSerializer.new.follow_sal...
**app/works/notification_worker.rb [#gff24b21]
NotificationWorker。
#code(Ruby){{
class NotificationWorker
include Sidekiq::Worker
sidekiq_options queue: 'push', retry: 5
def perform(xml, source_account_id, target_account_id)
SendInteractionService.new.call(xml, Account.find(sou...
end
end
}}
SendInteractionService呼んでるだけです。
#code(Ruby){{
class SendInteractionService < BaseService
# Send an Atom representation of an interaction to a re...
# @param [String] Entry XML
# @param [Account] source_account
# @param [Account] target_account
def call(xml, source_account, target_account)
envelope = salmon.pack(xml, source_account.keypair)
salmon.post(target_account.salmon_url, envelope)
end
private
def salmon
@salmon ||= OStatus2::Salmon.new
end
end
}}
こちらもSalmonにポストしてるだけ。ですが、ここでまたイン...
*app/controllers/api/salmon_controller.rb [#uee7afca]
というわけで「/api/salmon/:id」の処理。updateメソッドが呼...
#code(Ruby){{
class Api::SalmonController < ApiController
before_action :set_account
respond_to :txt
def update
payload = request.body.read
if !payload.nil? && verify?(payload)
SalmonWorker.perform_async(@account.id, payload.for...
head 201
else
head 202
end
end
}}
SalmonWorker。ProcessInteractionService呼んでるだけです。
#code(Ruby){{
class SalmonWorker
include Sidekiq::Worker
sidekiq_options backtrace: true
def perform(account_id, body)
ProcessInteractionService.new.call(body, Account.find...
rescue Nokogiri::XML::XPath::SyntaxError, ActiveRecord:...
true
end
end
}}
ProcessInteractionService。
#code(Ruby){{
class ProcessInteractionService < BaseService
include AuthorExtractor
# Record locally the remote interaction with our user
# @param [String] envelope Salmon envelope
# @param [Account] target_account Account the Salmon wa...
def call(envelope, target_account)
body = salmon.unpack(envelope)
xml = Nokogiri::XML(body)
xml.encoding = 'utf-8'
account = author_from_xml(xml.at_xpath('/xmlns:entry'...
return if account.nil? || account.suspended?
if salmon.verify(envelope, account.keypair)
RemoteProfileUpdateWorker.perform_async(account.id,...
case verb(xml)
when :follow
follow!(account, target_account) unless target_ac...
}}
author_from_xmlはAuthorExtractor(app/services/concerns/a...
verbメソッドで<activity:verb>に書かれている動作を取得、そ...
つまり、インスタンスA(リモートフォローした人がいるインス...
*おわりに [#e97721a1]
今回はリモートフォローについて見てきました。リモートフォ...
+リモートフォロー対象がいるインスタンス(インスタンスB)...
+対象をフォローするアカウントがいるインスタンス(インスタ...
+さらに情報同期のためにインスタンスAからインスタンスBに情...
とインスタンスをまたいで情報のやり取りが行われていました...
ページ名: