Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add: #8 サークル投稿の転送 #294

Merged
merged 30 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ef01e91
Add: `conversations`テーブルに`ancestor_status`プロパティ
kmycode Nov 15, 2023
9105d88
Fix test
kmycode Nov 15, 2023
be18712
Fix test more
kmycode Nov 15, 2023
84a5870
Add: `limited_visibility`に`Reply`を追加、`context`のURI
kmycode Nov 16, 2023
7edb06c
Add: 外部からの`context`受信処理
kmycode Nov 16, 2023
2ca4d3d
Fix test
kmycode Nov 16, 2023
c60095d
Merge branch 'kb_development' into kbtopic-8-circle-reply
kmycode Nov 16, 2023
52a69be
Add: 公開範囲「返信」
kmycode Nov 17, 2023
fee76c9
Fix test
kmycode Nov 17, 2023
e38267b
Fix: 返信に返信以外の公開範囲を設定できない問題
kmycode Nov 17, 2023
d3182a7
Add: ローカル投稿時にメンション追加・他サーバーへの転送
kmycode Nov 20, 2023
1d9ff2a
Fix test
kmycode Nov 20, 2023
ba02bbe
Fix test
kmycode Nov 20, 2023
6b1452b
Test: ローカルスレッドへの返信投稿の転送
kmycode Nov 20, 2023
71ead7e
Test: 未知のアカウントからのメンション
kmycode Nov 21, 2023
e588a14
Add: 編集・削除の連合に対応
kmycode Nov 23, 2023
aec726b
Remove: 重複テスト
kmycode Nov 23, 2023
320f080
Fix: 改善
kmycode Nov 24, 2023
104da54
Add: 編集削除の転送処理・返信なのにsilentなメンションでの通知
kmycode Nov 27, 2023
00ee8d1
Fix: リプライが第三者に届かない問題
kmycode Nov 28, 2023
fd0ab63
Merge branch 'kb_development' into kbtopic-8-circle-reply
kmycode Nov 28, 2023
d6c211b
Add: `always_sign_unsafe`
kmycode Nov 29, 2023
f70673f
Add: Subject
kmycode Nov 29, 2023
3e14583
Merge branch 'kbtopic-8-circle-reply' of https://github.com/kmycode/m…
kmycode Nov 29, 2023
3580135
Merge branch 'kb_development' into kbtopic-8-circle-reply
kmycode Nov 29, 2023
d2d8e2f
Remove space
kmycode Nov 29, 2023
300ab3f
Fix: 他人のスレッドの送信先一覧を非表示
kmycode Nov 29, 2023
75c7bc1
Merge branch 'kbtopic-8-circle-reply' of https://github.com/kmycode/m…
kmycode Nov 29, 2023
91988aa
Fix: おかしいコード
kmycode Nov 29, 2023
9977a39
Merge branch 'kb_development' into kbtopic-8-circle-reply
kmycode Nov 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/controllers/activitypub/contexts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class ActivityPub::ContextsController < ActivityPub::BaseController
include SignatureVerification

vary_by -> { 'Signature' if authorized_fetch_mode? }

before_action :set_context

def show
expires_in 3.minutes, public: true
render json: @context,
serializer: ActivityPub::ContextSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end

private

def set_context
@context = Conversation.find(params[:id])
end
end
10 changes: 0 additions & 10 deletions app/javascript/mastodon/components/compacted_status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,6 @@ export const defaultMediaVisibility = (status) => {
};

const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});

Expand Down
10 changes: 0 additions & 10 deletions app/javascript/mastodon/components/status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,6 @@ export const defaultMediaVisibility = (status) => {
};

const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});

Expand Down
11 changes: 11 additions & 0 deletions app/javascript/mastodon/components/visibility_icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ReactComponent as LoginIcon } from '@material-symbols/svg-600/outlined/
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/no_encryption.svg';
import { ReactComponent as PublicIcon } from '@material-symbols/svg-600/outlined/public.svg';
import { ReactComponent as ReplyIcon } from '@material-symbols/svg-600/outlined/reply.svg';
import { ReactComponent as LimitedIcon } from '@material-symbols/svg-600/outlined/shield.svg';
import { ReactComponent as PersonalIcon } from '@material-symbols/svg-600/outlined/sticky_note.svg';

Expand All @@ -23,6 +24,7 @@ type Visibility =
| 'mutual'
| 'circle'
| 'personal'
| 'reply'
| 'limited';

const messages = defineMessages({
Expand All @@ -49,6 +51,10 @@ const messages = defineMessages({
id: 'privacy.circle.short',
defaultMessage: 'Circle members only',
},
reply_short: {
id: 'privacy.reply.short',
defaultMessage: 'Reply',
},
personal_short: {
id: 'privacy.personal.short',
defaultMessage: 'Yourself only',
Expand Down Expand Up @@ -105,6 +111,11 @@ export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
iconComponent: CircleIcon,
text: intl.formatMessage(messages.circle_short),
},
reply: {
icon: 'reply',
iconComponent: ReplyIcon,
text: intl.formatMessage(messages.reply_short),
},
personal: {
icon: 'sticky-note-o',
iconComponent: PersonalIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ReactComponent as LoginIcon } from '@material-symbols/svg-600/outlined/
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/no_encryption.svg';
import { ReactComponent as PublicIcon } from '@material-symbols/svg-600/outlined/public.svg';
import { ReactComponent as ReplyIcon } from '@material-symbols/svg-600/outlined/reply.svg';
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';

Expand All @@ -38,6 +39,8 @@ const messages = defineMessages({
mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Mutual follows only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle' },
circle_long: { id: 'privacy.circle.long', defaultMessage: 'Circle members only' },
reply_short: { id: 'privacy.reply.short', defaultMessage: 'Reply' },
reply_long: { id: 'privacy.reply.long', defaultMessage: 'Reply to limited post' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
Expand Down Expand Up @@ -166,6 +169,7 @@ class PrivacyDropdown extends PureComponent {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool,
replyToLimited: PropTypes.bool,
container: PropTypes.func,
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
Expand Down Expand Up @@ -280,10 +284,22 @@ class PrivacyDropdown extends PureComponent {
};

render () {
const { value, container, disabled, intl } = this.props;
const { value, container, disabled, intl, replyToLimited } = this.props;
const { open, placement } = this.state;

const valueOption = this.options.find(item => item.value === value) || this.options[0];
if (replyToLimited) {
if (!this.selectableOptions.some((op) => op.value === 'reply')) {
this.selectableOptions.unshift(
{ icon: 'reply', iconComponent: ReplyIcon, value: 'reply', text: intl.formatMessage(messages.reply_short), meta: intl.formatMessage(messages.reply_long) },
);
}
} else {
if (this.selectableOptions.some((op) => op.value === 'reply')) {
this.selectableOptions = this.selectableOptions.filter((op) => op.value !== 'reply');
}
}

const valueOption = this.selectableOptions.find(item => item.value === value) || this.selectableOptions[0];

return (
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import PrivacyDropdown from '../components/privacy_dropdown';

const mapStateToProps = state => ({
value: state.getIn(['compose', 'privacy']),
replyToLimited: state.getIn(['compose', 'reply_to_limited']),
});

const mapDispatchToProps = dispatch => ({
Expand Down
21 changes: 19 additions & 2 deletions app/javascript/mastodon/reducers/compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const initialState = ImmutableMap({
caretPosition: null,
preselectDate: null,
in_reply_to: null,
reply_to_limited: false,
is_composing: false,
is_submitting: false,
is_changing_upload: false,
Expand Down Expand Up @@ -114,6 +115,10 @@ const initialPoll = ImmutableMap({
});

function statusToTextMentions(state, status) {
if (status.get('visibility_ex') === 'limited') {
return '';
}

let set = ImmutableOrderedSet([]);

if (status.getIn(['account', 'id']) !== me) {
Expand Down Expand Up @@ -144,6 +149,7 @@ function clearAll(state) {
if (!state.get('in_reply_to')) {
map.set('posted_on_this_session', true);
}
map.set('reply_to_limited', false);
map.set('limited_scope', null);
map.set('id', null);
map.set('in_reply_to', null);
Expand Down Expand Up @@ -293,6 +299,10 @@ const insertReference = (state, url, attributeType) => {
};

const privacyPreference = (a, b) => {
if (a === 'limited') {
return 'reply';
}
kmycode marked this conversation as resolved.
Show resolved Hide resolved

const order = ['public', 'public_unlisted', 'unlisted', 'login', 'private', 'direct'];
return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
};
Expand Down Expand Up @@ -411,6 +421,7 @@ export default function compose(state = initialState, action) {
map.set('id', null);
map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status));
map.set('reply_to_limited', action.status.get('visibility_ex') === 'limited');
map.set('privacy', privacyPreference(action.status.get('visibility_ex'), state.get('default_privacy')));
map.set('limited_scope', null);
map.set('searchability', privacyPreference(action.status.get('searchability'), state.get('default_searchability')));
Expand Down Expand Up @@ -521,7 +532,11 @@ export default function compose(state = initialState, action) {
return state.set('tagHistory', fromJS(action.tags));
case TIMELINE_DELETE:
if (action.id === state.get('in_reply_to')) {
return state.set('in_reply_to', null);
if (state.get('privacy') === 'reply') {
return state.set('in_reply_to', null).set('privacy', 'circle');
} else {
return state.set('in_reply_to', null);
}
} else if (action.id === state.get('id')) {
return state.set('id', null);
} else {
Expand Down Expand Up @@ -549,6 +564,7 @@ export default function compose(state = initialState, action) {
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
map.set('in_reply_to', action.status.get('in_reply_to_id'));
map.set('privacy', action.status.get('visibility_ex'));
map.set('reply_to_limited', action.status.get('limited_scope') === 'reply');
map.set('limited_scope', null);
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
map.set('focusDate', new Date());
Expand Down Expand Up @@ -583,8 +599,9 @@ export default function compose(state = initialState, action) {
if (action.status.get('visibility_ex') !== 'limited') {
map.set('privacy', action.status.get('visibility_ex'));
} else {
map.set('privacy', action.status.get('limited_scope') === 'mutual' ? 'mutual' : 'circle');
map.set('privacy', action.status.get('limited_scope') || 'circle');
}
map.set('reply_to_limited', action.status.get('limited_scope') === 'reply');
map.set('limited_scope', action.status.get('limited_scope'));
map.set('media_attachments', action.status.get('media_attachments'));
map.set('focusDate', new Date());
Expand Down
41 changes: 40 additions & 1 deletion app/lib/activitypub/activity/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def process_status

resolve_thread(@status)
fetch_replies(@status)
process_conversation! if @status.limited_visibility?
process_references!
distribute
forward_for_reply
Expand Down Expand Up @@ -132,7 +133,7 @@ def process_status_params
limited_scope: @status_parser.limited_scope,
searchability: @status_parser.searchability,
thread: replied_to_status,
conversation: conversation_from_uri(@object['conversation']),
conversation: conversation_from_activity,
media_attachment_ids: process_attachments.take(MediaAttachment::ACTIVITYPUB_STATUS_ATTACHMENT_MAX).map(&:id),
poll: process_poll,
}
Expand Down Expand Up @@ -184,6 +185,10 @@ def process_audience
@silenced_account_ids = @mentions.map(&:account_id) - accounts_in_audience.map(&:id)
end

def account_representative
accounts_in_audience.detect(&:local?) || Account.representative
end

def postprocess_audience_and_deliver
return if @status.mentions.find_by(account_id: @options[:delivered_to_account_id])

Expand Down Expand Up @@ -373,6 +378,10 @@ def fetch_replies(status)
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri, { 'request_id' => @options[:request_id] }) unless uri.nil?
end

def conversation_from_activity
conversation_from_context(@object['context']) || conversation_from_uri(@object['conversation'])
end

def conversation_from_uri(uri)
return nil if uri.nil?
return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
Expand All @@ -384,6 +393,26 @@ def conversation_from_uri(uri)
end
end

def conversation_from_context(uri)
return nil if uri.nil?
return Conversation.find_by(id: ActivityPub::TagManager.instance.uri_to_local_id(uri)) if ActivityPub::TagManager.instance.local_uri?(uri)

begin
conversation = Conversation.find_or_create_by!(uri: uri)

json = fetch_resource_without_id_validation(uri, account_representative)
return conversation if json.nil? || json['type'] != 'Group'
return conversation if json['inbox'].blank? || json['inbox'] == conversation.inbox_url

conversation.update!(inbox_url: json['inbox'])
conversation
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
retry
rescue Mastodon::UnexpectedResponseError
Conversation.find_or_create_by!(uri: uri)
end
end

def replied_to_status
return @replied_to_status if defined?(@replied_to_status)

Expand Down Expand Up @@ -483,6 +512,16 @@ def forward_for_reply
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
end

def process_conversation!
return unless @status.conversation.present? && @status.conversation.local?

ProcessConversationService.new.call(@status)

return if @json['signature'].blank?

ActivityPub::ForwardConversationWorker.perform_async(Oj.dump(@json), @status.id)
end

def increment_voters_count!
poll = replied_to_status.preloadable_poll

Expand Down
7 changes: 7 additions & 0 deletions app/lib/activitypub/activity/delete.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ def delete_note
return if @status.nil?

forwarder.forward! if forwarder.forwardable?
forward_for_conversation
delete_now!
end
end

def forward_for_conversation
return unless @status.conversation.present? && @status.conversation.local? && @json['signature'].present?

ActivityPub::ForwardConversationWorker.perform_async(Oj.dump(@json), @status.id, true)
end

def delete_friend
friend = FriendDomain.find_by(domain: @account.domain)
friend&.destroy
Expand Down
8 changes: 8 additions & 0 deletions app/lib/activitypub/activity/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,13 @@ def update_status
return if @status.nil?

ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])

forward_for_conversation
end

def forward_for_conversation
return unless @status.conversation.present? && @status.conversation.local? && @json['signature'].present?

ActivityPub::ForwardConversationWorker.perform_async(Oj.dump(@json), @status.id, true)
end
end
2 changes: 2 additions & 0 deletions app/lib/activitypub/parser/status_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ def limited_scope
:mutual
when 'Circle'
:circle
when 'Reply'
:reply
else
:none
end
Expand Down
Loading
Loading