logo

mastofe

My custom branche(s) on git.pleroma.social/pleroma/mastofe
commit: b891a81008d2cf595cb37432a8e1f36606db16d6
parent: 2d2154ba75279186b064c887452b7d6ee70b8ba2
Author: Eugen Rochko <eugen@zeonfederated.com>
Date:   Thu, 22 Dec 2016 23:03:57 +0100

Follow call on locked account creates follow request instead
Reflect "requested" relationship in API and UI
Reflect inability of private posts to be reblogged in the UI
Disable Webfinger for locked accounts

Diffstat:

Mapp/assets/javascripts/components/components/icon_button.jsx17++++++++++-------
Mapp/assets/javascripts/components/components/status_action_bar.jsx2+-
Mapp/assets/javascripts/components/features/account/components/header.jsx19++++++++++++++-----
Mapp/assets/javascripts/components/features/status/components/action_bar.jsx2+-
Mapp/assets/stylesheets/components.scss3++-
Mapp/assets/stylesheets/forms.scss10++++++++++
Mapp/controllers/api/v1/accounts_controller.rb3+++
Mapp/controllers/stream_entries_controller.rb4+++-
Mapp/controllers/xrd_controller.rb2+-
Mapp/lib/feed_manager.rb10++++++++++
Mapp/models/account.rb6++++++
Aapp/models/follow_request.rb19+++++++++++++++++++
Mapp/models/status.rb2+-
Mapp/services/follow_service.rb29+++++++++++++++--------------
Mapp/services/reblog_service.rb2+-
Mapp/views/api/v1/accounts/relationship.rabl1+
Mapp/views/api/v1/accounts/show.rabl6+++---
Mapp/views/settings/profiles/show.html.haml12+++++++-----
Mconfig/initializers/simple_form.rb8+++-----
Mconfig/locales/simple_form.en.yml4++++
Adb/migrate/20161222204147_create_follow_requests.rb12++++++++++++
Mdb/schema.rb10+++++++++-
Aspec/fabricators/follow_request_fabricator.rb3+++
Aspec/models/follow_request_spec.rb6++++++
24 files changed, 145 insertions(+), 47 deletions(-)

diff --git a/app/assets/javascripts/components/components/icon_button.jsx b/app/assets/javascripts/components/components/icon_button.jsx @@ -5,17 +5,19 @@ const IconButton = React.createClass({ propTypes: { title: React.PropTypes.string.isRequired, icon: React.PropTypes.string.isRequired, - onClick: React.PropTypes.func.isRequired, + onClick: React.PropTypes.func, size: React.PropTypes.number, active: React.PropTypes.bool, style: React.PropTypes.object, - activeStyle: React.PropTypes.object + activeStyle: React.PropTypes.object, + disabled: React.PropTypes.bool }, getDefaultProps () { return { size: 18, - active: false + active: false, + disabled: false }; }, @@ -23,8 +25,10 @@ const IconButton = React.createClass({ handleClick (e) { e.preventDefault(); - this.props.onClick(); - e.stopPropagation(); + + if (!this.props.disabled) { + this.props.onClick(); + } }, render () { @@ -37,7 +41,6 @@ const IconButton = React.createClass({ width: `${this.props.size * 1.28571429}px`, height: `${this.props.size}px`, lineHeight: `${this.props.size}px`, - cursor: 'pointer', ...this.props.style }; @@ -46,7 +49,7 @@ const IconButton = React.createClass({ } return ( - <button aria-label={this.props.title} title={this.props.title} className={`icon-button ${this.props.active ? 'active' : ''}`} onClick={this.handleClick} style={style}> + <button aria-label={this.props.title} title={this.props.title} className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''}`} onClick={this.handleClick} style={style}> <i className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> </button> ); diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -76,7 +76,7 @@ const StatusActionBar = React.createClass({ return ( <div style={{ marginTop: '10px', overflow: 'hidden' }}> <div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div> - <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> + <div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> <div style={{ width: '18px', height: '18px', float: 'left' }}> diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx @@ -8,6 +8,7 @@ import IconButton from '../../../components/icon_button'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' } }); const Header = React.createClass({ @@ -36,11 +37,19 @@ const Header = React.createClass({ } if (me !== account.get('id')) { - actionBtn = ( - <div style={{ position: 'absolute', top: '10px', left: '20px' }}> - <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> - </div> - ); + if (account.getIn(['relationship', 'requested'])) { + actionBtn = ( + <div style={{ position: 'absolute', top: '10px', left: '20px' }}> + <IconButton size={26} disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} /> + </div> + ); + } else { + actionBtn = ( + <div style={{ position: 'absolute', top: '10px', left: '20px' }}> + <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> + </div> + ); + } } const content = { __html: emojify(account.get('note')) }; diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx @@ -60,7 +60,7 @@ const ActionBar = React.createClass({ return ( <div style={{ background: '#2f3441', display: 'flex', flexDirection: 'row', borderTop: '1px solid #363c4b', borderBottom: '1px solid #363c4b', padding: '10px 0' }}> <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div> - <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> + <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> <div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} /></div> </div> diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss @@ -44,13 +44,14 @@ color: #616b86; border: none; background: transparent; + cursor: pointer; &:hover { color: #717b98; } &.disabled { - color: #535b72; + color: #454b5e; cursor: default; } diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss @@ -14,6 +14,12 @@ code { margin-bottom: 15px; } + .hint { + display: block; + color: rgba(255, 255, 255, 0.8); + font-size: 12px; + } + .input.file, .input.select { padding: 15px 0; margin-bottom: 0; @@ -59,6 +65,10 @@ code { top: 1px; margin: 0; } + + .hint { + padding-left: 25px; + } } input[type=text], input[type=email], input[type=password], textarea { diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb @@ -84,10 +84,12 @@ class Api::V1::AccountsController < ApiController def relationships ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i] + @accounts = Account.where(id: ids).select('id') @following = Account.following_map(ids, current_user.account_id) @followed_by = Account.followed_by_map(ids, current_user.account_id) @blocking = Account.blocking_map(ids, current_user.account_id) + @requested = Account.requested_map(ids, current_user.account_id) end def search @@ -109,5 +111,6 @@ class Api::V1::AccountsController < ApiController @following = Account.following_map([@account.id], current_user.account_id) @followed_by = Account.followed_by_map([@account.id], current_user.account_id) @blocking = Account.blocking_map([@account.id], current_user.account_id) + @requested = Account.requested_map([@account.id], current_user.account_id) end end diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb @@ -43,8 +43,10 @@ class StreamEntriesController < ApplicationController end def set_stream_entry - @stream_entry = @account.stream_entries.where(hidden: false).find(params[:id]) + @stream_entry = @account.stream_entries.find(params[:id]) @type = @stream_entry.activity_type.downcase + + raise ActiveRecord::RecordNotFound if @stream_entry.hidden? && (@stream_entry.activity_type != 'Status' || (@stream_entry.activity_type == 'Status' && !@stream_entry.activity.permitted?(current_account))) end def check_account_suspension diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb @@ -13,7 +13,7 @@ class XrdController < ApplicationController end def webfinger - @account = Account.find_local!(username_from_resource) + @account = Account.where(locked: false).find_local!(username_from_resource) @canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}" @magic_key = pem_to_magic_key(@account.keypair.public_key) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb @@ -39,6 +39,16 @@ class FeedManager redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}") end + def merge_into_timeline(from_account, into_account) + timeline_key = key(:home, into_account.id) + + from_account.statuses.limit(MAX_ITEMS).each do |status| + redis.zadd(timeline_key, status.id, status.id) + end + + trim(:home, into_account.id) + end + def inline_render(target_account, template, object) rabl_scope = Class.new do include RoutingHelper diff --git a/app/models/account.rb b/app/models/account.rb @@ -34,6 +34,8 @@ class Account < ApplicationRecord has_many :notifications, inverse_of: :account, dependent: :destroy # Follow relations + has_many :follow_requests, dependent: :destroy + has_many :active_relationships, class_name: 'Follow', foreign_key: 'account_id', dependent: :destroy has_many :passive_relationships, class_name: 'Follow', foreign_key: 'target_account_id', dependent: :destroy @@ -179,6 +181,10 @@ class Account < ApplicationRecord def blocking_map(target_account_ids, account_id) Block.where(target_account_id: target_account_ids).where(account_id: account_id).map { |b| [b.target_account_id, true] }.to_h end + + def requested_map(target_account_ids, account_id) + FollowRequest.where(target_account_id: target_account_ids).where(account_id: account_id).map { |r| [r.target_account_id, true] }.to_h + end end before_create do diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class FollowRequest < ApplicationRecord + belongs_to :account + belongs_to :target_account, class_name: 'Account' + + validates :account, :target_account, presence: true + validates :account_id, uniqueness: { scope: :target_account_id } + + def authorize! + account.follow!(target_account) + FeedManager.instance.merge_into_timeline(target_account, account) + destroy! + end + + def reject! + destroy! + end +end diff --git a/app/models/status.rb b/app/models/status.rb @@ -170,7 +170,7 @@ class Status < ApplicationRecord text.strip! self.reblog = reblog.reblog if reblog? && reblog.reblog? self.in_reply_to_account_id = thread.account_id if reply? - self.visibility = :public if visibility.nil? + self.visibility = (account.locked? ? :private : :public) if visibility.nil? end private diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb @@ -10,6 +10,20 @@ class FollowService < BaseService raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? raise Mastodon::NotPermitted if target_account.blocking?(source_account) + if target_account.locked? + request_follow(source_account, target_account) + else + direct_follow(source_account, target_account) + end + end + + private + + def request_follow(source_account, target_account) + FollowRequest.create!(account: source_account, target_account: target_account) + end + + def direct_follow(source_account, target_account) follow = source_account.follow!(target_account) if target_account.local? @@ -19,25 +33,12 @@ class FollowService < BaseService NotificationWorker.perform_async(follow.stream_entry.id, target_account.id) end - merge_into_timeline(target_account, source_account) - + FeedManager.instance.merge_into_timeline(target_account, source_account) Pubsubhubbub::DistributionWorker.perform_async(follow.stream_entry.id) follow end - private - - def merge_into_timeline(from_account, into_account) - timeline_key = FeedManager.instance.key(:home, into_account.id) - - from_account.statuses.find_each do |status| - redis.zadd(timeline_key, status.id, status.id) - end - - FeedManager.instance.trim(:home, into_account.id) - end - def redis Redis.current end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb @@ -6,7 +6,7 @@ class ReblogService < BaseService # @param [Status] reblogged_status Status to be reblogged # @return [Status] def call(account, reblogged_status) - raise ActiveRecord::RecordInvalid if reblogged_status.private_visibility? + raise Mastodon::NotPermitted if reblogged_status.private_visibility? reblog = account.statuses.create!(reblog: reblogged_status, text: '') diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl @@ -4,3 +4,4 @@ attribute :id node(:following) { |account| @following[account.id] || false } node(:followed_by) { |account| @followed_by[account.id] || false } node(:blocking) { |account| @blocking[account.id] || false } +node(:requested) { |account| @requested[account.id] || false } diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl @@ -1,11 +1,11 @@ object @account -attributes :id, :username, :acct, :display_name +attributes :id, :username, :acct, :display_name, :locked node(:note) { |account| Formatter.instance.simplified_format(account) } node(:url) { |account| TagManager.instance.url_for(account) } -node(:avatar) { |account| full_asset_url(account.avatar.url( :original)) } -node(:header) { |account| full_asset_url(account.header.url( :original)) } +node(:avatar) { |account| full_asset_url(account.avatar.url(:original)) } +node(:header) { |account| full_asset_url(account.header.url(:original)) } node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : (account.try(:followers_count) || account.followers.count) } node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : (account.try(:following_count) || account.following.count) } node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : (account.try(:statuses_count) || account.statuses.count) } diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml @@ -4,11 +4,13 @@ = simple_form_for @account, url: settings_profile_path, html: { method: :put } do |f| = render 'shared/error_messages', object: @account - = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name') - = f.input :note, placeholder: t('simple_form.labels.defaults.note') - = f.input :avatar, wrapper: :with_label - = f.input :header, wrapper: :with_label - = f.input :locked, as: :boolean, wrapper: :with_label + .fields-group + = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name') + = f.input :note, placeholder: t('simple_form.labels.defaults.note') + = f.input :avatar, wrapper: :with_label + = f.input :header, wrapper: :with_label + + = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb @@ -5,8 +5,7 @@ SimpleForm.setup do |config| # wrapper, change the order or even add your own to the # stack. The options given below are used to wrap the # whole input. - config.wrappers :default, class: :input, - hint_class: :field_with_hint, error_class: :field_with_errors do |b| + config.wrappers :default, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b| ## Extensions enabled by default # Any of these extensions can be disabled for a # given input by passing: `f.input EXTENSION_NAME => false`. @@ -51,12 +50,11 @@ SimpleForm.setup do |config| # b.use :full_error, wrap_with: { tag: :span, class: :error } end - config.wrappers :with_label, class: :input, - hint_class: :field_with_hint, error_class: :field_with_errors do |b| + config.wrappers :with_label, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b| b.use :html5 + b.use :label_input b.use :hint, wrap_with: { tag: :span, class: :hint } b.use :error, wrap_with: { tag: :span, class: :error } - b.use :label_input end # The default wrapper to be used by the FormBuilder. diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml @@ -15,6 +15,7 @@ en: note: Bio password: Password username: Username + locked: Make account private interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow @@ -23,6 +24,9 @@ en: follow: Send e-mail when someone follows you mention: Send e-mail when someone mentions you reblog: Send e-mail when someone reblogs your status + hints: + defaults: + locked: Requires you to approve followers, defaults post privacy to followers-only and disables federation 'no': 'No' required: mark: "*" diff --git a/db/migrate/20161222204147_create_follow_requests.rb b/db/migrate/20161222204147_create_follow_requests.rb @@ -0,0 +1,12 @@ +class CreateFollowRequests < ActiveRecord::Migration[5.0] + def change + create_table :follow_requests do |t| + t.integer :account_id, null: false + t.integer :target_account_id, null: false + + t.timestamps null: false + end + + add_index :follow_requests, [:account_id, :target_account_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161222201034) do +ActiveRecord::Schema.define(version: 20161222204147) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -69,6 +69,14 @@ ActiveRecord::Schema.define(version: 20161222201034) do t.index ["account_id", "status_id"], name: "index_favourites_on_account_id_and_status_id", unique: true, using: :btree end + create_table "follow_requests", force: :cascade do |t| + t.integer "account_id", null: false + t.integer "target_account_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true, using: :btree + end + create_table "follows", force: :cascade do |t| t.integer "account_id", null: false t.integer "target_account_id", null: false diff --git a/spec/fabricators/follow_request_fabricator.rb b/spec/fabricators/follow_request_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:follow_request) do + +end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' + +RSpec.describe FollowRequest, type: :model do + describe '#authorize!' + describe '#reject!' +end