+
);
@@ -57,7 +56,6 @@ const leftNavStyle = {
background: 'rgba(0, 0, 0, 0.5)',
padding: '30px 15px',
cursor: 'pointer',
- color: '#fff',
fontSize: '24px',
top: '0',
left: '-61px',
@@ -72,7 +70,6 @@ const rightNavStyle = {
background: 'rgba(0, 0, 0, 0.5)',
padding: '30px 15px',
cursor: 'pointer',
- color: '#fff',
fontSize: '24px',
top: '0',
right: '-61px',
@@ -143,11 +140,11 @@ const Modal = React.createClass({
leftNav = rightNav = '';
if (hasLeft) {
- leftNav =
;
+ leftNav =
;
}
if (hasRight) {
- rightNav =
;
+ rightNav =
;
}
return (
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index c4c876e30e4..c590f703897 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -256,6 +256,35 @@ button:focus {
}
}
+.compact-header {
+ h1 {
+ font-size: 24px;
+ line-height: 28px;
+ color: $color3;
+ overflow: hidden;
+ font-weight: 500;
+ margin-bottom: 20px;
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
+
+ small {
+ font-weight: 400;
+ color: $color2;
+ }
+
+ img {
+ display: inline-block;
+ margin-bottom: -5px;
+ margin-right: 15px;
+ width: 36px;
+ height: 36px;
+ }
+ }
+}
+
@import 'forms';
@import 'accounts';
@import 'stream_entries';
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 6bb683f17aa..f0948b0f39a 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -34,6 +34,7 @@
.column-icon {
color: $color3;
+ background: lighten($color1, 4%);
&:hover {
color: lighten($color3, 7%);
@@ -187,7 +188,7 @@
a.status__content__spoiler-link {
display: inline-block;
border-radius: 2px;
- color: lighten($color1, 6%);
+ color: lighten($color1, 8%);
font-weight: 500;
font-size: 11px;
padding: 0px 6px;
@@ -200,7 +201,7 @@ a.status__content__spoiler-link {
padding-left: 68px;
position: relative;
min-height: 48px;
- border-bottom: 1px solid lighten($color1, 6%);
+ border-bottom: 1px solid lighten($color1, 8%);
cursor: default;
.status__relative-time {
@@ -226,6 +227,8 @@ a.status__content__spoiler-link {
}
.detailed-status {
+ background: lighten($color1, 4%);
+
.status__content {
font-size: 19px;
line-height: 24px;
@@ -237,12 +240,19 @@ a.status__content__spoiler-link {
}
}
+.detailed-status__meta {
+ margin-top: 15px;
+ color: lighten($color1, 26%);
+ font-size: 14px;
+ line-height: 18px;
+}
+
.detailed-status__action-bar {
background: lighten($color1, 4%);
display: flex;
flex-direction: row;
- border-top: 1px solid lighten($color1, 6%);
- border-bottom: 1px solid lighten($color1, 6%);
+ border-top: 1px solid lighten($color1, 8%);
+ border-bottom: 1px solid lighten($color1, 8%);
padding: 10px 0;
}
@@ -257,7 +267,7 @@ a.status__content__spoiler-link {
.account {
padding: 10px;
- border-bottom: 1px solid lighten($color1, 6%);
+ border-bottom: 1px solid lighten($color1, 8%);
.account__display-name {
flex: 1 1 auto;
@@ -298,6 +308,7 @@ a.status__content__spoiler-link {
word-wrap: break-word;
font-weight: 400;
overflow: hidden;
+ color: $color3;
p {
margin-bottom: 20px;
@@ -325,8 +336,8 @@ a.status__content__spoiler-link {
}
.account__action-bar {
- border-top: 1px solid lighten($color1, 6%);
- border-bottom: 1px solid lighten($color1, 6%);
+ border-top: 1px solid lighten($color1, 8%);
+ border-bottom: 1px solid lighten($color1, 8%);
line-height: 36px;
overflow: hidden;
flex: 0 0 auto;
@@ -337,7 +348,7 @@ a.status__content__spoiler-link {
text-decoration: none;
overflow: hidden;
width: 80px;
- border-left: 1px solid lighten($color1, 6%);
+ border-left: 1px solid lighten($color1, 8%);
padding: 10px 5px;
& > span {
@@ -412,8 +423,9 @@ a.status__content__spoiler-link {
opacity: 0.5;
}
- .status__content__spoiler-link {
+ a.status__content__spoiler-link {
background: lighten($color1, 26%);
+ color: lighten($color1, 4%);
&:hover {
background: lighten($color1, 29%);
@@ -422,6 +434,20 @@ a.status__content__spoiler-link {
}
}
+.notification__message {
+ margin-left: 68px;
+ padding: 8px 0;
+ padding-bottom: 0;
+ cursor: default;
+ color: $color3;
+ font-size: 15px;
+ position: relative;
+
+ .fa {
+ color: $color4;
+ }
+}
+
.notification__display-name {
color: inherit;
text-decoration: none;
@@ -646,7 +672,7 @@ a.status__content__spoiler-link {
.tabs-bar {
display: flex;
- background: lighten($color1, 6%);
+ background: lighten($color1, 8%);
flex: 0 0 auto;
overflow-y: auto;
}
@@ -660,7 +686,7 @@ a.status__content__spoiler-link {
text-align: center;
font-size:12px;
font-weight: 500;
- border-bottom: 2px solid lighten($color1, 6%);
+ border-bottom: 2px solid lighten($color1, 8%);
&.active {
border-bottom: 2px solid $color4;
@@ -850,7 +876,8 @@ a.status__content__spoiler-link {
}
.column-link {
- background: lighten($color1, 6%);
+ background: lighten($color1, 8%);
+ color: $color5;
&:hover {
background: lighten($color1, 11%);
@@ -883,6 +910,7 @@ a.status__content__spoiler-link {
.autosuggest-textarea__textarea {
height: 100px;
+ background: $color5;
}
.autosuggest-textarea__suggestions {
@@ -968,11 +996,40 @@ button.active i.fa-retweet {
}
.status-card {
+ display: flex;
+ cursor: pointer;
+ font-size: 14px;
+ border: 1px solid lighten($color1, 8%);
+ border-radius: 4px;
+ color: lighten($color1, 26%);
+ margin-top: 14px;
+ text-decoration: none;
+ overflow: hidden;
+
&:hover {
- background: lighten($color1, 6%);
+ background: lighten($color1, 8%);
}
}
+.status-card__title {
+ display: block;
+ font-weight: 500;
+ margin-bottom: 5px;
+ color: $color3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-card__description {
+ color: $color3;
+}
+
+.status-card__image {
+ flex: 0 0 100px;
+ background: lighten($color1, 8%);
+}
+
.load-more {
display: block;
color: lighten($color1, 26%);
@@ -981,7 +1038,7 @@ button.active i.fa-retweet {
text-decoration: none;
&:hover {
- background: lighten($color1, 6%);
+ background: lighten($color1, 8%);
}
}
@@ -1020,3 +1077,53 @@ button.active i.fa-retweet {
font-size: 14px;
margin: 0;
}
+
+.loading-indicator {
+ color: $color2;
+}
+
+.collapsable-collapsed {
+ color: $color3;
+ background: lighten($color1, 4%);
+}
+
+.collapsable {
+ color: $color5;
+ background: lighten($color1, 8%);
+}
+
+.media-spoiler {
+ background: $color8;
+ color: $color5;
+}
+
+.modal-container--preloader {
+ background: lighten($color1, 8%);
+}
+
+.account--panel {
+ background: lighten($color1, 4%);
+ border-top: 1px solid lighten($color1, 8%);
+ border-bottom: 1px solid lighten($color1, 8%);
+}
+
+.column-settings--outer {
+ background: lighten($color1, 8%);
+}
+
+.column-settings--section {
+ color: $color3;
+}
+
+.modal-container--nav {
+ color: $color5;
+}
+
+.account--follows-info {
+ color: $color5;
+}
+
+.setting-toggle {
+ color: $color3;
+}
+
diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss
index 595275527ef..d427c1466f6 100644
--- a/app/assets/stylesheets/stream_entries.scss
+++ b/app/assets/stylesheets/stream_entries.scss
@@ -5,24 +5,24 @@
.entry {
background: lighten($color2, 8%);
- &, .detailed-status.light {
+ .detailed-status.light, .status.light {
border-bottom: 1px solid $color2;
}
&:last-child {
- &, .detailed-status.light {
+ &, .detailed-status.light, .status.light {
border-bottom: 0;
border-radius: 0 0 4px 4px;
}
}
&:first-child {
- &, .detailed-status.light {
+ &, .detailed-status.light, .status.light {
border-radius: 4px 4px 0 0;
}
&:last-child {
- &, .detailed-status.light {
+ &, .detailed-status.light, .status.light {
border-radius: 4px;
}
}
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
index a30e97e715d..740083735f5 100644
--- a/app/controllers/api/v1/follow_requests_controller.rb
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -18,12 +18,12 @@ class Api::V1::FollowRequestsController < ApiController
end
def authorize
- FollowRequest.find_by!(account_id: params[:id], target_account: current_account).authorize!
+ AuthorizeFollowService.new.call(Account.find(params[:id]), current_account)
render_empty
end
def reject
- FollowRequest.find_by!(account_id: params[:id], target_account: current_account).reject!
+ RejectFollowService.new.call(Account.find(params[:id]), current_account)
render_empty
end
end
diff --git a/app/controllers/concerns/obfuscate_filename.rb b/app/controllers/concerns/obfuscate_filename.rb
index 9f12cb7e97a..dde7ce8c686 100644
--- a/app/controllers/concerns/obfuscate_filename.rb
+++ b/app/controllers/concerns/obfuscate_filename.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module ObfuscateFilename
extend ActiveSupport::Concern
diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb
index fb8f0976c32..5d20f8c2d65 100644
--- a/app/helpers/atom_builder_helper.rb
+++ b/app/helpers/atom_builder_helper.rb
@@ -143,6 +143,10 @@ module AtomBuilderHelper
xml.link(:rel => 'mentioned', :href => TagManager::COLLECTIONS[:public], 'ostatus:object-type' => TagManager::TYPES[:collection])
end
+ def privacy_scope(xml, level)
+ xml['mastodon'].scope(level)
+ end
+
def include_author(xml, account)
object_type xml, :person
uri xml, TagManager.instance.uri_for(account)
@@ -152,6 +156,7 @@ module AtomBuilderHelper
link_alternate xml, TagManager.instance.url_for(account)
link_avatar xml, account
portable_contact xml, account
+ privacy_scope xml, account.locked? ? :private : :public
end
def rich_content(xml, activity)
@@ -216,6 +221,7 @@ module AtomBuilderHelper
end
category(xml, 'nsfw') if stream_entry.target.sensitive?
+ privacy_scope(xml, stream_entry.target.visibility)
end
end
end
@@ -237,6 +243,7 @@ module AtomBuilderHelper
end
category(xml, 'nsfw') if stream_entry.activity.sensitive?
+ privacy_scope(xml, stream_entry.activity.visibility)
end
private
@@ -249,6 +256,7 @@ module AtomBuilderHelper
'xmlns:poco' => TagManager::POCO_XMLNS,
'xmlns:media' => TagManager::MEDIA_XMLNS,
'xmlns:ostatus' => TagManager::OS_XMLNS,
+ 'xmlns:mastodon' => TagManager::MTDN_XMLNS,
}, &block)
end
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index f0928a9451b..623a1af0352 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -107,7 +107,6 @@ class FeedManager
should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked
should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked
should_filter ||= (status.account.silenced? && !receiver.following?(status.account)) # of if the account is silenced and I'm not following them
- should_filter ||= (status.private_visibility? && !receiver.following?(status.account)) # or if the mentioned account is not permitted to see the private status
if status.reply? && !status.in_reply_to_account_id.nil? # or it's a reply
should_filter ||= receiver.blocking?(status.in_reply_to_account) # to a user I blocked
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index 2508eea97d6..9fef70fda6f 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -7,15 +7,18 @@ class TagManager
include RoutingHelper
VERBS = {
- post: 'http://activitystrea.ms/schema/1.0/post',
- share: 'http://activitystrea.ms/schema/1.0/share',
- favorite: 'http://activitystrea.ms/schema/1.0/favorite',
- unfavorite: 'http://activitystrea.ms/schema/1.0/unfavorite',
- delete: 'http://activitystrea.ms/schema/1.0/delete',
- follow: 'http://activitystrea.ms/schema/1.0/follow',
- unfollow: 'http://ostatus.org/schema/1.0/unfollow',
- block: 'http://mastodon.social/schema/1.0/block',
- unblock: 'http://mastodon.social/schema/1.0/unblock',
+ post: 'http://activitystrea.ms/schema/1.0/post',
+ share: 'http://activitystrea.ms/schema/1.0/share',
+ favorite: 'http://activitystrea.ms/schema/1.0/favorite',
+ unfavorite: 'http://activitystrea.ms/schema/1.0/unfavorite',
+ delete: 'http://activitystrea.ms/schema/1.0/delete',
+ follow: 'http://activitystrea.ms/schema/1.0/follow',
+ request_friend: 'http://activitystrea.ms/schema/1.0/request-friend',
+ authorize: 'http://activitystrea.ms/schema/1.0/authorize',
+ reject: 'http://activitystrea.ms/schema/1.0/reject',
+ unfollow: 'http://ostatus.org/schema/1.0/unfollow',
+ block: 'http://mastodon.social/schema/1.0/block',
+ unblock: 'http://mastodon.social/schema/1.0/unblock',
}.freeze
TYPES = {
@@ -38,6 +41,7 @@ class TagManager
POCO_XMLNS = 'http://portablecontacts.net/spec/1.0'
DFRN_XMLNS = 'http://purl.org/macgirvin/dfrn/1.0'
OS_XMLNS = 'http://ostatus.org/schema/1.0'
+ MTDN_XMLNS = 'http://mastodon.social/schema/1.0'
def unique_tag(date, id, type)
"tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}"
diff --git a/app/models/account.rb b/app/models/account.rb
index c2a41c4c638..ed5c461974a 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -95,6 +95,10 @@ class Account < ApplicationRecord
follow_requests.where(target_account: other_account).exists?
end
+ def followers_domains
+ followers.reorder('').select('DISTINCT accounts.domain').map(&:domain)
+ end
+
def local?
domain.nil?
end
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index 3f3616dce0e..cd8e2098c2f 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -12,11 +12,11 @@ class Favourite < ApplicationRecord
validates :status_id, uniqueness: { scope: :account_id }
def verb
- :favorite
+ destroyed? ? :unfavorite : :favorite
end
def title
- "#{account.acct} favourited a status by #{status.account.acct}"
+ destroyed? ? "#{account.acct} no longer favourites a status by #{status.account.acct}" : "#{account.acct} favourited a status by #{status.account.acct}"
end
delegate :object_type, to: :target
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 936ad0691b9..989c2c2a23b 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -2,6 +2,7 @@
class FollowRequest < ApplicationRecord
include Paginable
+ include Streamable
belongs_to :account
belongs_to :target_account, class_name: 'Account'
@@ -12,12 +13,47 @@ class FollowRequest < ApplicationRecord
validates :account_id, uniqueness: { scope: :target_account_id }
def authorize!
+ @verb = :authorize
+
account.follow!(target_account)
MergeWorker.perform_async(target_account.id, account.id)
+
destroy!
end
def reject!
+ @verb = :reject
destroy!
end
+
+ def verb
+ destroyed? ? (@verb || :delete) : :request_friend
+ end
+
+ def target
+ target_account
+ end
+
+ def object_type
+ :person
+ end
+
+ def hidden?
+ true
+ end
+
+ def title
+ if destroyed?
+ case @verb
+ when :authorize
+ "#{target_account.acct} authorized #{account.acct}'s request to follow"
+ when :reject
+ "#{target_account.acct} rejected #{account.acct}'s request to follow"
+ else
+ "#{account.acct} withdrew the request to follow #{target_account.acct}"
+ end
+ else
+ "#{account.acct} requested to follow #{target_account.acct}"
+ end
+ end
end
diff --git a/app/models/status.rb b/app/models/status.rb
index d2be7230844..93594ec8f48 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -76,7 +76,11 @@ class Status < ApplicationRecord
end
def permitted?(other_account = nil)
- private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : other_account.nil? || !account.blocking?(other_account)
+ if private_visibility?
+ (account.id == other_account&.id || other_account&.following?(account) || mentions.include?(other_account))
+ else
+ other_account.nil? || !account.blocking?(other_account)
+ end
end
def ancestors(account = nil)
@@ -153,6 +157,10 @@ class Status < ApplicationRecord
where('1 = 1')
elsif !account.nil? && target_account.blocking?(account)
where('1 = 0')
+ elsif !account.nil?
+ joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id')
+ .where('mentions.account_id = ?', account.id)
+ .where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:private])
else
where.not(visibility: :private)
end
diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb
index fcc691befdd..e0b85be1589 100644
--- a/app/models/stream_entry.rb
+++ b/app/models/stream_entry.rb
@@ -30,7 +30,7 @@ class StreamEntry < ApplicationRecord
end
def targeted?
- [:follow, :unfollow, :block, :unblock, :share, :favorite].include? verb
+ [:follow, :request_friend, :authorize, :unfollow, :block, :unblock, :share, :favorite].include? verb
end
def target
diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb
new file mode 100644
index 00000000000..1590d843385
--- /dev/null
+++ b/app/services/authorize_follow_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AuthorizeFollowService < BaseService
+ include StreamEntryRenderer
+
+ def call(source_account, target_account)
+ follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
+ follow_request.authorize!
+ NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local?
+ end
+end
diff --git a/app/services/block_service.rb b/app/services/block_service.rb
index e04b6cc392f..095d2a8eb6e 100644
--- a/app/services/block_service.rb
+++ b/app/services/block_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class BlockService < BaseService
+ include StreamEntryRenderer
+
def call(account, target_account)
return if account.id == target_account.id
@@ -10,6 +12,6 @@ class BlockService < BaseService
block = account.block!(target_account)
BlockWorker.perform_async(account.id, target_account.id)
- NotificationWorker.perform_async(block.stream_entry.id, target_account.id) unless target_account.local?
+ NotificationWorker.perform_async(stream_entry_to_xml(block.stream_entry), account.id, target_account.id) unless target_account.local?
end
end
diff --git a/app/services/concerns/stream_entry_renderer.rb b/app/services/concerns/stream_entry_renderer.rb
new file mode 100644
index 00000000000..a4255daead0
--- /dev/null
+++ b/app/services/concerns/stream_entry_renderer.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module StreamEntryRenderer
+ def stream_entry_to_xml(stream_entry)
+ renderer = StreamEntriesController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
+ renderer.render(:show, assigns: { stream_entry: stream_entry }, formats: [:atom])
+ end
+end
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index d5fbd29e9c2..ce1722b7719 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class FavouriteService < BaseService
+ include StreamEntryRenderer
+
# Favourite a status and notify remote user
# @param [Account] account
# @param [Status] status
@@ -15,7 +17,7 @@ class FavouriteService < BaseService
if status.local?
NotifyService.new.call(favourite.status.account, favourite)
else
- NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
+ NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id)
end
favourite
diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb
index 3c3694a6505..baefa3a8614 100644
--- a/app/services/fetch_remote_account_service.rb
+++ b/app/services/fetch_remote_account_service.rb
@@ -22,7 +22,9 @@ class FetchRemoteAccountService < BaseService
Rails.logger.debug "Going to webfinger #{username}@#{domain}"
- return FollowRemoteAccountService.new.call("#{username}@#{domain}")
+ account = FollowRemoteAccountService.new.call("#{username}@#{domain}")
+ UpdateRemoteProfileService.new.call(xml, account) unless account.nil?
+ account
rescue TypeError
Rails.logger.debug "Unparseable URL given: #{url}"
nil
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 9f34cb6ac5e..ac0392d16b2 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class FollowService < BaseService
+ include StreamEntryRenderer
+
# Follow a remote user, notify remote user about the follow
# @param [Account] source_account From which to follow
# @param [String] uri User URI to follow in the form of username@domain
@@ -20,10 +22,14 @@ class FollowService < BaseService
private
def request_follow(source_account, target_account)
- return unless target_account.local?
-
follow_request = FollowRequest.create!(account: source_account, target_account: target_account)
- NotifyService.new.call(target_account, follow_request)
+
+ if target_account.local?
+ NotifyService.new.call(target_account, follow_request)
+ else
+ NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), source_account.id, target_account.id)
+ AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
+ end
follow_request
end
@@ -34,8 +40,9 @@ class FollowService < BaseService
if target_account.local?
NotifyService.new.call(target_account, follow)
else
- subscribe_service.call(target_account)
- NotificationWorker.perform_async(follow.stream_entry.id, target_account.id)
+ subscribe_service.call(target_account) unless target_account.subscribed?
+ NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id)
+ AfterRemoteFollowWorker.perform_async(follow.id)
end
MergeWorker.perform_async(target_account.id, source_account.id)
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index c411e3e82dd..f0a62aa14e8 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -106,7 +106,8 @@ class ProcessFeedService < BaseService
text: content(entry),
spoiler_text: content_warning(entry),
created_at: published(entry),
- reply: thread?(entry)
+ reply: thread?(entry),
+ visibility: visibility_scope(entry)
)
if thread?(entry)
@@ -144,15 +145,9 @@ class ProcessFeedService < BaseService
def mentions_from_xml(parent, xml)
processed_account_ids = []
- public_visibility = false
xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link|
- if link['ostatus:object-type'] == TagManager::TYPES[:collection] && link['href'] == TagManager::COLLECTIONS[:public]
- public_visibility = true
- next
- elsif link['ostatus:object-type'] == TagManager::TYPES[:group]
- next
- end
+ next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type']
url = Addressable::URI.parse(link['href'])
@@ -172,9 +167,6 @@ class ProcessFeedService < BaseService
# So we can skip duplicate mentions
processed_account_ids << mentioned_account.id
end
-
- parent.visibility = public_visibility ? :public : :unlisted
- parent.save!
end
def hashtags_from_xml(parent, xml)
@@ -230,6 +222,10 @@ class ProcessFeedService < BaseService
xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || ''
end
+ def visibility_scope(xml = @xml)
+ xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public
+ end
+
def published(xml = @xml)
xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content
end
diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb
index 5f91e312718..8420ca35193 100644
--- a/app/services/process_interaction_service.rb
+++ b/app/services/process_interaction_service.rb
@@ -29,6 +29,12 @@ class ProcessInteractionService < BaseService
case verb(xml)
when :follow
follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account)
+ when :request_friend
+ follow_request!(account, target_account) unless !target_account.locked? || target_account.blocking?(account)
+ when :authorize
+ authorize_follow_request!(account, target_account)
+ when :reject
+ reject_follow_request!(account, target_account)
when :unfollow
unfollow!(account, target_account)
when :favorite
@@ -72,6 +78,22 @@ class ProcessInteractionService < BaseService
NotifyService.new.call(target_account, follow)
end
+ def follow_request!(account, target_account)
+ follow_request = FollowRequest.create!(account: account, target_account: target_account)
+ NotifyService.new.call(target_account, follow_request)
+ end
+
+ def authorize_follow_request!(account, target_account)
+ follow_request = FollowRequest.find_by(account: target_account, target_account: account)
+ follow_request&.authorize!
+ SubscribeService.new.call(account) unless account.subscribed?
+ end
+
+ def reject_follow_request!(account, target_account)
+ follow_request = FollowRequest.find_by(account: target_account, target_account: account)
+ follow_request&.reject!
+ end
+
def unfollow!(account, target_account)
account.unfollow!(target_account)
end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index 72568e702da..d3d3af8af2e 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProcessMentionsService < BaseService
+ include StreamEntryRenderer
+
# Scan status for mentions and fetch remote mentioned users, create
# local mention pointers, send Salmon notifications to mentioned
# remote users
@@ -28,12 +30,10 @@ class ProcessMentionsService < BaseService
status.mentions.each do |mention|
mentioned_account = mention.account
- next if status.private_visibility? && (!mentioned_account.following?(status.account) || !mentioned_account.local?)
-
if mentioned_account.local?
NotifyService.new.call(mentioned_account, mention)
else
- NotificationWorker.perform_async(status.stream_entry.id, mentioned_account.id)
+ NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
end
end
end
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 4ea0dbf6c42..7a52f041fc5 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ReblogService < BaseService
+ include StreamEntryRenderer
+
# Reblog a status and notify its remote author
# @param [Account] account Account to reblog from
# @param [Status] reblogged_status Status to be reblogged
@@ -18,15 +20,9 @@ class ReblogService < BaseService
if reblogged_status.local?
NotifyService.new.call(reblog.reblog.account, reblog)
else
- NotificationWorker.perform_async(reblog.stream_entry.id, reblog.reblog.account_id)
+ NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), account.id, reblog.reblog.account_id)
end
reblog
end
-
- private
-
- def send_interaction_service
- @send_interaction_service ||= SendInteractionService.new
- end
end
diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb
new file mode 100644
index 00000000000..0c568b981f2
--- /dev/null
+++ b/app/services/reject_follow_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class RejectFollowService < BaseService
+ include StreamEntryRenderer
+
+ def call(source_account, target_account)
+ follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
+ follow_request.reject!
+ NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local?
+ end
+end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 48e8dd3b8f2..b1a646b1453 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class RemoveStatusService < BaseService
+ include StreamEntryRenderer
+
def call(status)
remove_from_self(status) if status.account.local?
remove_from_followers(status)
@@ -43,7 +45,7 @@ class RemoveStatusService < BaseService
def send_delete_salmon(account, status)
return unless status.local?
- NotificationWorker.perform_async(status.stream_entry.id, account.id)
+ NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, account.id)
end
def remove_reblogs(status)
diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb
index 05a1e77e39d..99113eecaf2 100644
--- a/app/services/send_interaction_service.rb
+++ b/app/services/send_interaction_service.rb
@@ -2,27 +2,16 @@
class SendInteractionService < BaseService
# Send an Atom representation of an interaction to a remote Salmon endpoint
- # @param [StreamEntry] stream_entry
+ # @param [String] Entry XML
+ # @param [Account] source_account
# @param [Account] target_account
- def call(stream_entry, target_account)
- envelope = salmon.pack(entry_xml(stream_entry), stream_entry.account.keypair)
+ def call(xml, source_account, target_account)
+ envelope = salmon.pack(xml, source_account.keypair)
salmon.post(target_account.salmon_url, envelope)
end
private
- def entry_xml(stream_entry)
- Nokogiri::XML::Builder.new do |xml|
- entry(xml, true) do
- author(xml) do
- include_author xml, stream_entry.account
- end
-
- include_entry xml, stream_entry
- end
- end.to_xml
- end
-
def salmon
@salmon ||= OStatus2::Salmon.new
end
diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb
index f389364f910..84b1050c153 100644
--- a/app/services/unblock_service.rb
+++ b/app/services/unblock_service.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
class UnblockService < BaseService
+ include StreamEntryRenderer
+
def call(account, target_account)
return unless account.blocking?(target_account)
unblock = account.unblock!(target_account)
- NotificationWorker.perform_async(unblock.stream_entry.id, target_account.id) unless target_account.local?
+ NotificationWorker.perform_async(stream_entry_to_xml(unblock.stream_entry), account.id, target_account.id) unless target_account.local?
end
end
diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb
index de6e84e7d46..04293ee08b9 100644
--- a/app/services/unfavourite_service.rb
+++ b/app/services/unfavourite_service.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
class UnfavouriteService < BaseService
+ include StreamEntryRenderer
+
def call(account, status)
favourite = Favourite.find_by!(account: account, status: status)
favourite.destroy!
unless status.local?
- NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
+ NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id)
end
favourite
diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb
index f469793c1af..178da4da313 100644
--- a/app/services/unfollow_service.rb
+++ b/app/services/unfollow_service.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
class UnfollowService < BaseService
+ include StreamEntryRenderer
+
# Unfollow and notify the remote user
# @param [Account] source_account Where to unfollow from
# @param [Account] target_account Which to unfollow
def call(source_account, target_account)
follow = source_account.unfollow!(target_account)
- NotificationWorker.perform_async(follow.stream_entry.id, target_account.id) unless target_account.local?
+ NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id) unless target_account.local?
UnmergeWorker.perform_async(target_account.id, source_account.id)
end
end
diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb
index ad9c56540da..dc315db197b 100644
--- a/app/services/update_remote_profile_service.rb
+++ b/app/services/update_remote_profile_service.rb
@@ -10,6 +10,7 @@ class UpdateRemoteProfileService < BaseService
unless author_xml.nil?
account.display_name = author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).nil?
account.note = author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).nil?
+ account.locked = author_xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content == 'private'
unless account.suspended? || DomainBlock.find_by(domain: account.domain)&.reject_media?
account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'].blank?
diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml
index 88bfe3d610c..022a3a9e48e 100644
--- a/app/views/about/index.html.haml
+++ b/app/views/about/index.html.haml
@@ -32,6 +32,7 @@
= link_to t('about.learn_more'), about_more_path
= link_to t('about.terms'), terms_path
= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
+ = link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
= link_to t('about.get_started'), new_user_registration_path, class: 'button webapp-btn'
= link_to t('auth.login'), new_user_session_path, class: 'button webapp-btn'
diff --git a/app/views/admin/settings/index.html.haml b/app/views/admin/settings/index.html.haml
index 5b482213bac..1429dbd9ee2 100644
--- a/app/views/admin/settings/index.html.haml
+++ b/app/views/admin/settings/index.html.haml
@@ -15,6 +15,10 @@
%td= best_in_place @settings['site_contact_username'], :value, url: admin_setting_path(@settings['site_contact_username']), place_holder: 'Enter a username'
%tr
%td= best_in_place @settings['site_contact_email'], :value, url: admin_setting_path(@settings['site_contact_email']), place_holder: 'Enter a public e-mail address'
+ %tr
+ %td
+ %strong Site title
+ %td= best_in_place @settings['site_title'], :value, url: admin_setting_path(@settings['site_title'])
%tr
%td
%strong Site description
@@ -33,4 +37,4 @@
Displayed on extended information page
%br/
You can use HTML tags
- %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description'])
\ No newline at end of file
+ %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description'])
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index e122e1c55fe..7eae6982bd0 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -13,7 +13,7 @@
%title
= "#{yield(:page_title)} - " if content_for?(:page_title)
- Mastodon
+ = Setting.site_title
= stylesheet_link_tag 'application', media: 'all'
= csrf_meta_tags
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 412ec4fa5da..32a50e15862 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -1,10 +1,18 @@
+- content_for :page_title do
+ = "##{@tag.name}"
+
+.compact-header
+ %h1<
+ = link_to 'Mastodon', root_path
+ %small= "##{@tag.name}"
+
- if @statuses.empty?
.accounts-grid
= render partial: 'accounts/nothing_here'
- else
.activity-stream.h-feed
- = render partial: 'stream_entries/status', collection: @statuses, as: :status, cached: true
+ = render partial: 'stream_entries/status', collection: @statuses, as: :status
-.pagination
- - if @statuses.size == 20
+- if @statuses.size == 20
+ .pagination
= link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb
new file mode 100644
index 00000000000..ad94d276940
--- /dev/null
+++ b/app/workers/after_remote_follow_request_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AfterRemoteFollowRequestWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: 5
+
+ def perform(follow_request_id)
+ follow_request = FollowRequest.find(follow_request_id)
+ updated_account = FetchRemoteAccountService.new.call(follow_request.target_account.remote_url)
+
+ return if updated_account.locked?
+
+ follow_request.destroy
+ FollowService.new.call(follow_request.account, updated_account.acct)
+ end
+end
diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb
new file mode 100644
index 00000000000..496aaf73eb9
--- /dev/null
+++ b/app/workers/after_remote_follow_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AfterRemoteFollowWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: 5
+
+ def perform(follow_id)
+ follow = Follow.find(follow_id)
+ updated_account = FetchRemoteAccountService.new.call(follow.target_account.remote_url)
+
+ return unless updated_account.locked?
+
+ follow.destroy
+ FollowService.new.call(follow.account, updated_account.acct)
+ end
+end
diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb
index e4c38d3844b..1a2faefd8e0 100644
--- a/app/workers/notification_worker.rb
+++ b/app/workers/notification_worker.rb
@@ -5,7 +5,7 @@ class NotificationWorker
sidekiq_options retry: 5
- def perform(stream_entry_id, target_account_id)
- SendInteractionService.new.call(StreamEntry.find(stream_entry_id), Account.find(target_account_id))
+ def perform(xml, source_account_id, target_account_id)
+ SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id))
end
end
diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb
index d5437bf6b51..4576dc4a249 100644
--- a/app/workers/pubsubhubbub/distribution_worker.rb
+++ b/app/workers/pubsubhubbub/distribution_worker.rb
@@ -8,13 +8,18 @@ class Pubsubhubbub::DistributionWorker
def perform(stream_entry_id)
stream_entry = StreamEntry.find(stream_entry_id)
- return if stream_entry.hidden?
+ # Most hidden stream entries should not be PuSHed,
+ # but statuses need to be distributed to trusted
+ # followers even when they are hidden
+ return if stream_entry.hidden? && stream_entry.activity_type != 'Status'
account = stream_entry.account
renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
payload = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
+ domains = account.followers_domains
- Subscription.where(account: account).active.select('id').find_each do |subscription|
+ Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
+ next unless domains.include?(Addressable::URI.parse(subscription.callback_url).host)
Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
end
rescue ActiveRecord::RecordNotFound
diff --git a/app/workers/push_notification_worker.rb b/app/workers/push_notification_worker.rb
deleted file mode 100644
index a61d0e349ce..00000000000
--- a/app/workers/push_notification_worker.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class PushNotificationWorker
- include Sidekiq::Worker
-
- def perform(notification_id)
- SendPushNotificationService.new.call(Notification.find(notification_id))
- rescue ActiveRecord::RecordNotFound
- true
- end
-end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index b1b1e7995dc..c6c7c236e7e 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -10,6 +10,7 @@ en:
get_started: Get started
learn_more: Learn more
links: Links
+ other_instances: Other instances
source_code: Source code
status_count_after: statuses
status_count_before: Who authored
diff --git a/config/settings.yml b/config/settings.yml
index a78bd067d53..71ce12e632e 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -1,5 +1,6 @@
# config/app.yml for rails-settings-cached
defaults: &defaults
+ site_title: 'Mastodon'
site_description: ''
site_extended_description: ''
site_contact_username: ''
diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md
index 2f15df083d9..c4e22876902 100644
--- a/docs/Using-Mastodon/List-of-Mastodon-instances.md
+++ b/docs/Using-Mastodon/List-of-Mastodon-instances.md
@@ -1,12 +1,18 @@
-List of Mastodon instances
+List of Known Mastodon instances
==========================
-* [mastodon.social](https://mastodon.social)
-* [social.tchncs.de](https://social.tchncs.de)
-* [on.vu](https://on.vu)
-* [animalliberation.social](https://animalliberation.social)
-* [socially.constructed.space](https://socially.constructed.space)
-* [epiktistes.com](https://epiktistes.com)
-* [toot.zone](https://toot.zone)
+| Name | Theme/Notes, if applicable | Open Registrations |
+| -------------|-------------|---|
+| [mastodon.social](https://mastodon.social) |Flagship, quick updates|Yes|
+| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|
+| [social.tchncs.de](https://social.tchncs.de)|N/A|Yes|
+| [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|
+| [socially.constructed.space](https://socially.constructed.space) |Single user|No|
+| [epiktistes.com](https://epiktistes.com) |N/A|Yes|
+| [toot.zone](https://toot.zone) |N/A|Yes|
+| [on.vu](https://on.vu) | Appears defunct|No|
+| [gay.crime.team](https://gay.crime.team) |N/A|Yes(?)|
+| [gnusocial.me](https://gnusocial.me) |Yes, it's a mastodon instance now|Yes|
-Let me know if you start running one so I can add it to the list!
+
+Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request).
diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb
index 97d69ab7bff..cc4958ab598 100644
--- a/spec/controllers/api/v1/follows_controller_spec.rb
+++ b/spec/controllers/api/v1/follows_controller_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
before do
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
+ stub_request(:head, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(:status => 405, :body => "", :headers => {})
stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
diff --git a/spec/helpers/atom_builder_helper_spec.rb b/spec/helpers/atom_builder_helper_spec.rb
index 3d3bd56a19c..0aca58ee7eb 100644
--- a/spec/helpers/atom_builder_helper_spec.rb
+++ b/spec/helpers/atom_builder_helper_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe AtomBuilderHelper, type: :helper do
describe '#feed' do
it 'creates a feed' do
- expect(used_in_builder { |xml| helper.feed(xml) }).to match '
'
+ expect(used_in_builder { |xml| helper.feed(xml) }).to match '
'
end
end