Mastodonを読む/新規トゥートの投稿
をテンプレートにして作成
[
トップ
] [
新規
|
一覧
|
単語検索
|
最終更新
|
ヘルプ
]
開始行:
[[Mastodonを読む]]
#contents
*はじめに [#j10cefd2]
前回までで「/」にアクセスしたとき(ログイン済みとする)に...
*クライアントサイドの処理 [#o1c8247e]
**app/assets/javascripts/components/features/compose/inde...
PCの場合にしろ、スマホの場合にしろ、トゥートを行うための...
&ref(compose.jpg);
renderメソッドについては何度も見ているので省略します。
**app/assets/javascripts/components/features/compose/cont...
コンポーネントとActionをつなぐ記述を確認します。ConposeFo...
#code{{
import { connect } from 'react-redux';
import ComposeForm from '../components/compose_form';
import {
changeCompose,
submitCompose,
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
changeComposeSpoilerText,
insertEmojiCompose
} from '../../../actions/compose';
const mapDispatchToProps = (dispatch) => ({
onChange (text) {
dispatch(changeCompose(text));
},
onSubmit () {
dispatch(submitCompose());
},
他のイベントハンドラ
});
export default connect(mapStateToProps, mapDispatchToProp...
}}
ところで、jsxは全く知らないのですが、「export default」と...
**app/assets.javascripts/components/actions/compose.jsx [...
というわけで、submitCompose。ちょっと長めですがやってるこ...
#code{{
export function submitCompose() {
return function (dispatch, getState) {
const status = emojione.shortnameToUnicode(getState()...
if (!status || !status.length) {
return;
}
dispatch(submitComposeRequest());
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_re...
media_ids: getState().getIn(['compose', 'media_atta...
sensitive: getState().getIn(['compose', 'sensitive'...
spoiler_text: getState().getIn(['compose', 'spoiler...
visibility: getState().getIn(['compose', 'privacy'])
}, {
headers: {
'Idempotency-Key': getState().getIn(['compose', '...
}
}).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get...
dispatch(updateTimeline('home', { ...response.data ...
if (response.data.in_reply_to_id === null && respon...
if (getState().getIn(['timelines', 'community', '...
dispatch(updateTimeline('community', { ...respo...
}
if (getState().getIn(['timelines', 'public', 'loa...
dispatch(updateTimeline('public', { ...response...
}
}
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
};
};
}}
「/api/v1/statuses」に入力をPOSTして、成功したら各タイム...
*サーバサイドの処理 [#y45db5e6]
ここまででクライアントサイドでどういう処理が行われている...
**app/controllers/api/v1/statuses_controller.rb [#dff20b9a]
「/api/v1/statuses」にアクセスされたときに呼び出されるの...
#code(Ruby){{
def create
@status = PostStatusService.new.call(current_user.acc...
status_params[:s...
status_params[:i...
media_ids: statu...
sensitive: statu...
spoiler_text: st...
visibility: stat...
application: doo...
idempotency: req...
render :show
end
}}
Serviceって使ったことないけど昔からあるんですかね?ともか...
**app/services/post_status_service.rb [#e33fe559]
#code(Ruby){{
class PostStatusService < BaseService
# Post a text status update, fetch and notify remote us...
# @param [Account] account Account from which to post
# @param [String] text Message
# @param [Status] in_reply_to Optional status to reply to
# @param [Hash] options
# @option [Boolean] :sensitive
# @option [String] :visibility
# @option [String] :spoiler_text
# @option [Enumerable] :media_ids Optional array of med...
# @option [Doorkeeper::Application] :application
# @option [String] :idempotency Optional idempotency key
# @return [Status]
def call(account, text, in_reply_to = nil, options = {})
if options[:idempotency].present?
existing_id = redis.get("idempotency:status:#{accou...
return Status.find(existing_id) if existing_id
end
media = validate_media!(options[:media_ids])
status = nil
ApplicationRecord.transaction do
status = account.statuses.create!(text: text,
thread: in_reply_...
sensitive: option...
spoiler_text: opt...
visibility: optio...
language: detect_...
application: opti...
attach_media(status, media)
end
process_mentions_service.call(status)
process_hashtags_service.call(status)
LinkCrawlWorker.perform_async(status.id) unless statu...
DistributionWorker.perform_async(status.id)
Pubsubhubbub::DistributionWorker.perform_async(status...
if options[:idempotency].present?
redis.setex("idempotency:status:#{account.id}:#{opt...
end
status
end
}}
送信されたトゥート(Status)の保存自体は普通のRailsでのモ...
**app/services/process_mentions_service.rb [#c3447622]
まず、process_mentions_serviceです。その名の通り、メンシ...
#code(Ruby){{
class ProcessMentionsService < BaseService
include StreamEntryRenderer
# Scan status for mentions and fetch remote mentioned u...
# local mention pointers, send Salmon notifications to ...
# remote users
# @param [Status] status
def call(status)
return unless status.local?
status.text.scan(Account::MENTION_RE).each do |match|
username, domain = match.first.split('@')
mentioned_account = Account.find_remote(username, d...
if mentioned_account.nil? && !domain.nil?
begin
mentioned_account = follow_remote_account_servi...
rescue Goldfinger::Error, HTTP::Error
mentioned_account = nil
end
end
next if mentioned_account.nil?
mentioned_account.mentions.where(status: status).fi...
end
status.mentions.includes(:account).each do |mention|
mentioned_account = mention.account
if mentioned_account.local?
NotifyService.new.call(mentioned_account, mention)
else
NotificationWorker.perform_async(stream_entry_to_...
end
end
end
}}
リモートユーザの場合はリモートユーザがいるインスタンスに...
**app/workers/distribution_worker.rb [#k3add90a]
次にいくつかWorkerを実行しています。このWorkerは[[Sidekiq...
DistributionWorkerでは投稿したトゥートをフォロワーなどに...
#code(Ruby){{
class DistributionWorker < ApplicationWorker
include Sidekiq::Worker
def perform(status_id)
FanOutOnWriteService.new.call(Status.find(status_id))
rescue ActiveRecord::RecordNotFound
info("Couldn't find the status")
end
end
}}
app/services/fan_out_on_write_service.rb
#code(Ruby){{
class FanOutOnWriteService < BaseService
# Push a status into home and mentions feeds
# @param [Status] status
def call(status)
raise Mastodon::RaceConditionError if status.visibili...
deliver_to_self(status) if status.account.local?
if status.direct_visibility?
deliver_to_mentioned_followers(status)
else
deliver_to_followers(status)
end
return if status.account.silenced? || !status.public_...
render_anonymous_payload(status)
deliver_to_hashtags(status)
return if status.reply? && status.in_reply_to_account...
deliver_to_public(status)
end
}}
**app/workers/pubsubhubbub/distribution_worker.rb [#v654e...
最後にPubsubhubbubの方のDistributionWorkerです。
#code(Ruby){{
class Pubsubhubbub::DistributionWorker
include Sidekiq::Worker
sidekiq_options queue: 'push'
def perform(stream_entry_id)
stream_entry = StreamEntry.find(stream_entry_id)
return if stream_entry.status&.direct_visibility?
account = stream_entry.account
payload = AtomSerializer.render(AtomSerializer.new.fe...
domains = account.followers_domains
Subscription.where(account: account).active.select('i...
next unless domains.include?(Addressable::URI.parse...
Pubsubhubbub::DeliveryWorker.perform_async(subscrip...
end
rescue ActiveRecord::RecordNotFound
true
end
end
}}
これにより連合を組んでいるインスタンスにトゥートが送信さ...
*おわりに [#y42a185d]
今回は新規トゥート送信時のクライアントサイド、サーバサイ...
一方、サーバサイドではServiceを使用し処理をモジュール化、...
終了行:
[[Mastodonを読む]]
#contents
*はじめに [#j10cefd2]
前回までで「/」にアクセスしたとき(ログイン済みとする)に...
*クライアントサイドの処理 [#o1c8247e]
**app/assets/javascripts/components/features/compose/inde...
PCの場合にしろ、スマホの場合にしろ、トゥートを行うための...
&ref(compose.jpg);
renderメソッドについては何度も見ているので省略します。
**app/assets/javascripts/components/features/compose/cont...
コンポーネントとActionをつなぐ記述を確認します。ConposeFo...
#code{{
import { connect } from 'react-redux';
import ComposeForm from '../components/compose_form';
import {
changeCompose,
submitCompose,
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
changeComposeSpoilerText,
insertEmojiCompose
} from '../../../actions/compose';
const mapDispatchToProps = (dispatch) => ({
onChange (text) {
dispatch(changeCompose(text));
},
onSubmit () {
dispatch(submitCompose());
},
他のイベントハンドラ
});
export default connect(mapStateToProps, mapDispatchToProp...
}}
ところで、jsxは全く知らないのですが、「export default」と...
**app/assets.javascripts/components/actions/compose.jsx [...
というわけで、submitCompose。ちょっと長めですがやってるこ...
#code{{
export function submitCompose() {
return function (dispatch, getState) {
const status = emojione.shortnameToUnicode(getState()...
if (!status || !status.length) {
return;
}
dispatch(submitComposeRequest());
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_re...
media_ids: getState().getIn(['compose', 'media_atta...
sensitive: getState().getIn(['compose', 'sensitive'...
spoiler_text: getState().getIn(['compose', 'spoiler...
visibility: getState().getIn(['compose', 'privacy'])
}, {
headers: {
'Idempotency-Key': getState().getIn(['compose', '...
}
}).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get...
dispatch(updateTimeline('home', { ...response.data ...
if (response.data.in_reply_to_id === null && respon...
if (getState().getIn(['timelines', 'community', '...
dispatch(updateTimeline('community', { ...respo...
}
if (getState().getIn(['timelines', 'public', 'loa...
dispatch(updateTimeline('public', { ...response...
}
}
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
};
};
}}
「/api/v1/statuses」に入力をPOSTして、成功したら各タイム...
*サーバサイドの処理 [#y45db5e6]
ここまででクライアントサイドでどういう処理が行われている...
**app/controllers/api/v1/statuses_controller.rb [#dff20b9a]
「/api/v1/statuses」にアクセスされたときに呼び出されるの...
#code(Ruby){{
def create
@status = PostStatusService.new.call(current_user.acc...
status_params[:s...
status_params[:i...
media_ids: statu...
sensitive: statu...
spoiler_text: st...
visibility: stat...
application: doo...
idempotency: req...
render :show
end
}}
Serviceって使ったことないけど昔からあるんですかね?ともか...
**app/services/post_status_service.rb [#e33fe559]
#code(Ruby){{
class PostStatusService < BaseService
# Post a text status update, fetch and notify remote us...
# @param [Account] account Account from which to post
# @param [String] text Message
# @param [Status] in_reply_to Optional status to reply to
# @param [Hash] options
# @option [Boolean] :sensitive
# @option [String] :visibility
# @option [String] :spoiler_text
# @option [Enumerable] :media_ids Optional array of med...
# @option [Doorkeeper::Application] :application
# @option [String] :idempotency Optional idempotency key
# @return [Status]
def call(account, text, in_reply_to = nil, options = {})
if options[:idempotency].present?
existing_id = redis.get("idempotency:status:#{accou...
return Status.find(existing_id) if existing_id
end
media = validate_media!(options[:media_ids])
status = nil
ApplicationRecord.transaction do
status = account.statuses.create!(text: text,
thread: in_reply_...
sensitive: option...
spoiler_text: opt...
visibility: optio...
language: detect_...
application: opti...
attach_media(status, media)
end
process_mentions_service.call(status)
process_hashtags_service.call(status)
LinkCrawlWorker.perform_async(status.id) unless statu...
DistributionWorker.perform_async(status.id)
Pubsubhubbub::DistributionWorker.perform_async(status...
if options[:idempotency].present?
redis.setex("idempotency:status:#{account.id}:#{opt...
end
status
end
}}
送信されたトゥート(Status)の保存自体は普通のRailsでのモ...
**app/services/process_mentions_service.rb [#c3447622]
まず、process_mentions_serviceです。その名の通り、メンシ...
#code(Ruby){{
class ProcessMentionsService < BaseService
include StreamEntryRenderer
# Scan status for mentions and fetch remote mentioned u...
# local mention pointers, send Salmon notifications to ...
# remote users
# @param [Status] status
def call(status)
return unless status.local?
status.text.scan(Account::MENTION_RE).each do |match|
username, domain = match.first.split('@')
mentioned_account = Account.find_remote(username, d...
if mentioned_account.nil? && !domain.nil?
begin
mentioned_account = follow_remote_account_servi...
rescue Goldfinger::Error, HTTP::Error
mentioned_account = nil
end
end
next if mentioned_account.nil?
mentioned_account.mentions.where(status: status).fi...
end
status.mentions.includes(:account).each do |mention|
mentioned_account = mention.account
if mentioned_account.local?
NotifyService.new.call(mentioned_account, mention)
else
NotificationWorker.perform_async(stream_entry_to_...
end
end
end
}}
リモートユーザの場合はリモートユーザがいるインスタンスに...
**app/workers/distribution_worker.rb [#k3add90a]
次にいくつかWorkerを実行しています。このWorkerは[[Sidekiq...
DistributionWorkerでは投稿したトゥートをフォロワーなどに...
#code(Ruby){{
class DistributionWorker < ApplicationWorker
include Sidekiq::Worker
def perform(status_id)
FanOutOnWriteService.new.call(Status.find(status_id))
rescue ActiveRecord::RecordNotFound
info("Couldn't find the status")
end
end
}}
app/services/fan_out_on_write_service.rb
#code(Ruby){{
class FanOutOnWriteService < BaseService
# Push a status into home and mentions feeds
# @param [Status] status
def call(status)
raise Mastodon::RaceConditionError if status.visibili...
deliver_to_self(status) if status.account.local?
if status.direct_visibility?
deliver_to_mentioned_followers(status)
else
deliver_to_followers(status)
end
return if status.account.silenced? || !status.public_...
render_anonymous_payload(status)
deliver_to_hashtags(status)
return if status.reply? && status.in_reply_to_account...
deliver_to_public(status)
end
}}
**app/workers/pubsubhubbub/distribution_worker.rb [#v654e...
最後にPubsubhubbubの方のDistributionWorkerです。
#code(Ruby){{
class Pubsubhubbub::DistributionWorker
include Sidekiq::Worker
sidekiq_options queue: 'push'
def perform(stream_entry_id)
stream_entry = StreamEntry.find(stream_entry_id)
return if stream_entry.status&.direct_visibility?
account = stream_entry.account
payload = AtomSerializer.render(AtomSerializer.new.fe...
domains = account.followers_domains
Subscription.where(account: account).active.select('i...
next unless domains.include?(Addressable::URI.parse...
Pubsubhubbub::DeliveryWorker.perform_async(subscrip...
end
rescue ActiveRecord::RecordNotFound
true
end
end
}}
これにより連合を組んでいるインスタンスにトゥートが送信さ...
*おわりに [#y42a185d]
今回は新規トゥート送信時のクライアントサイド、サーバサイ...
一方、サーバサイドではServiceを使用し処理をモジュール化、...
ページ名: