From 442fdbfc5309f46c23a073829e5fe16d10c7c6ca Mon Sep 17 00:00:00 2001 From: Kit Redgrave Date: Sun, 5 Feb 2017 19:51:56 -0600 Subject: Mute button progress so far. WIP, doesn't entirely work correctly. --- app/controllers/api/v1/accounts_controller.rb | 19 +++++++++++++++++-- app/controllers/api/v1/mutes_controller.rb | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/mutes_controller.rb (limited to 'app/controllers') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 94dba1d03..d691ac987 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::AccountsController < ApiController - before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock] - before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock] + before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute] before_action :require_user!, except: [:show, :following, :followers, :statuses] before_action :set_account, except: [:verify_credentials, :suggestions, :search] @@ -86,10 +86,17 @@ class Api::V1::AccountsController < ApiController @followed_by = { @account.id => false } @blocking = { @account.id => true } @requested = { @account.id => false } + @muting = { @account.id => current_user.account.muting?(@account.id) } render action: :relationship end + def mute + MuteService.new.call(current_user.account, @account) + set_relationship + render action: :relationship + end + def unfollow UnfollowService.new.call(current_user.account, @account) set_relationship @@ -102,6 +109,12 @@ class Api::V1::AccountsController < ApiController render action: :relationship end + def unmute + UnmuteService.new.call(current_user.account, @account) + set_relationship + render action: :relationship + end + def relationships ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i] @@ -109,6 +122,7 @@ class Api::V1::AccountsController < ApiController @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) + @muting = Account.muting_map(ids, current_user.account_id) @requested = Account.requested_map(ids, current_user.account_id) end @@ -130,6 +144,7 @@ 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) + @muting = Account.muting_map([@account.id], current_user.account_id) @requested = Account.requested_map([@account.id], current_user.account_id) end end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb new file mode 100644 index 000000000..42a9ed412 --- /dev/null +++ b/app/controllers/api/v1/mutes_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Api::V1::MutesController < ApiController + before_action -> { doorkeeper_authorize! :follow } + before_action :require_user! + + respond_to :json + + def index + results = Mute.where(account: current_account).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h + @accounts = results.map { |f| accounts[f.target_account_id] } + + set_account_counters_maps(@accounts) + + next_path = api_v1_mutes_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + prev_path = api_v1_mutes_url(since_id: results.first.id) unless results.empty? + + set_pagination_headers(next_path, prev_path) + end +end -- cgit From 6b81d100306259cd17b38d3f0f9dec0f0fb5b5d9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 3 Mar 2017 23:45:48 +0100 Subject: Add digest e-mails --- app/controllers/api_controller.rb | 1 + app/controllers/settings/preferences_controller.rb | 3 ++- app/lib/formatter.rb | 5 +++++ app/mailers/notification_mailer.rb | 13 +++++++++++++ app/models/setting.rb | 1 - app/models/user.rb | 7 ++++--- app/views/layouts/mailer.text.erb | 2 +- app/views/notification_mailer/_status.text.erb | 4 ++-- app/views/notification_mailer/digest.text.erb | 15 +++++++++++++++ app/views/notification_mailer/favourite.text.erb | 4 ++-- app/views/notification_mailer/follow.text.erb | 4 ++-- app/views/notification_mailer/follow_request.text.erb | 4 ++-- app/views/notification_mailer/mention.text.erb | 4 ++-- app/views/notification_mailer/reblog.text.erb | 4 ++-- app/views/settings/preferences/show.html.haml | 1 + app/workers/digest_mailer_worker.rb | 14 ++++++++++++++ config/application.rb | 7 ------- config/environments/production.rb | 7 +++++++ config/locales/en.yml | 11 +++++++++++ config/locales/simple_form.en.yml | 1 + config/settings.yml | 1 + .../20170303212857_add_last_emailed_at_to_users.rb | 5 +++++ db/schema.rb | 3 ++- lib/tasks/mastodon.rake | 11 ++++++++++- spec/mailers/previews/notification_mailer_preview.rb | 17 ++++++++++++----- 25 files changed, 117 insertions(+), 32 deletions(-) create mode 100644 app/views/notification_mailer/digest.text.erb create mode 100644 app/workers/digest_mailer_worker.rb create mode 100644 db/migrate/20170303212857_add_last_emailed_at_to_users.rb (limited to 'app/controllers') diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index c2002cb79..db16f82e5 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -79,6 +79,7 @@ class ApiController < ApplicationController def require_user! current_resource_owner + set_user_activity rescue ActiveRecord::RecordNotFound render json: { error: 'This method requires an authenticated user' }, status: 422 end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index b7479bf8c..60400e465 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -14,6 +14,7 @@ class Settings::PreferencesController < ApplicationController reblog: user_params[:notification_emails][:reblog] == '1', favourite: user_params[:notification_emails][:favourite] == '1', mention: user_params[:notification_emails][:mention] == '1', + digest: user_params[:notification_emails][:digest] == '1', } current_user.settings['interactions'] = { @@ -33,6 +34,6 @@ class Settings::PreferencesController < ApplicationController private def user_params - params.require(:user).permit(:locale, :setting_default_privacy, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention], interactions: [:must_be_follower, :must_be_following]) + params.require(:user).permit(:locale, :setting_default_privacy, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following]) end end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index e353c3504..b58952ae0 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -29,6 +29,11 @@ class Formatter sanitize(html, tags: %w(a br p span), attributes: %w(href rel class)) end + def plaintext(status) + return status.text if status.local? + strip_tags(status.text) + end + def simplified_format(account) return reformat(account.note) unless account.local? diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index a1b084682..bf4c16e43 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -49,4 +49,17 @@ class NotificationMailer < ApplicationMailer mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) end end + + def digest(recipient, opts = {}) + @me = recipient + @since = opts[:since] || @me.user.last_emailed_at || @me.user.current_sign_in_at + @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since) + @follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count + + return if @notifications.empty? + + I18n.with_locale(@me.user.locale || I18n.default_locale) do + mail to: @me.user.email, subject: I18n.t('notification_mailer.digest.subject', count: @notifications.size) + end + end end diff --git a/app/models/setting.rb b/app/models/setting.rb index 3796253d4..31e1ee198 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -2,7 +2,6 @@ class Setting < RailsSettings::Base source Rails.root.join('config/settings.yml') - namespace Rails.env def to_param var diff --git a/app/models/user.rb b/app/models/user.rb index 08aac2679..bf2916d90 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,9 +14,10 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), unless: 'locale.nil?' validates :email, email: true - scope :prolific, -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') } - scope :recent, -> { order('id desc') } - scope :admins, -> { where(admin: true) } + scope :prolific, -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') } + scope :recent, -> { order('id desc') } + scope :admins, -> { where(admin: true) } + scope :confirmed, -> { where.not(confirmed_at: nil) } def send_devise_notification(notification, *args) devise_mailer.send(notification, self, *args).deliver_later diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb index ae52173b5..21bf444c3 100644 --- a/app/views/layouts/mailer.text.erb +++ b/app/views/layouts/mailer.text.erb @@ -1,5 +1,5 @@ <%= yield %> - --- <%= t('application_mailer.signature', instance: Rails.configuration.x.local_domain) %> +<%= t('application_mailer.settings', link: settings_preferences_url) %> diff --git a/app/views/notification_mailer/_status.text.erb b/app/views/notification_mailer/_status.text.erb index b089a7b73..85a0136b7 100644 --- a/app/views/notification_mailer/_status.text.erb +++ b/app/views/notification_mailer/_status.text.erb @@ -1,3 +1,3 @@ -<%= strip_tags(@status.content) %> +<%= raw Formatter.instance.plaintext(status) %> -<%= web_url("statuses/#{@status.id}") %> +<%= raw t('application_mailer.view')%> <%= web_url("statuses/#{status.id}") %> diff --git a/app/views/notification_mailer/digest.text.erb b/app/views/notification_mailer/digest.text.erb new file mode 100644 index 000000000..95aed6793 --- /dev/null +++ b/app/views/notification_mailer/digest.text.erb @@ -0,0 +1,15 @@ +<%= display_name(@me) %>, + +<%= raw t('notification_mailer.digest.body', since: @since, instance: root_url) %> +<% @notifications.each do |notification| %> + +* <%= raw t('notification_mailer.digest.mention', name: notification.from_account.acct) %> + + <%= raw Formatter.instance.plaintext(notification.target_status) %> + + <%= raw t('application_mailer.view')%> <%= web_url("statuses/#{notification.target_status.id}") %> +<% end %> +<% if @follows_since > 0 %> + +<%= raw t('notification_mailer.digest.new_followers_summary', count: @follows_since) %> +<% end %> diff --git a/app/views/notification_mailer/favourite.text.erb b/app/views/notification_mailer/favourite.text.erb index b2e1e3e9e..99852592f 100644 --- a/app/views/notification_mailer/favourite.text.erb +++ b/app/views/notification_mailer/favourite.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.favourite.body', name: @account.acct) %> +<%= raw t('notification_mailer.favourite.body', name: @account.acct) %> -<%= render partial: 'status' %> +<%= render partial: 'status', locals: { status: @status } %> diff --git a/app/views/notification_mailer/follow.text.erb b/app/views/notification_mailer/follow.text.erb index 4b2ec142c..af41a3080 100644 --- a/app/views/notification_mailer/follow.text.erb +++ b/app/views/notification_mailer/follow.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.follow.body', name: @account.acct) %> +<%= raw t('notification_mailer.follow.body', name: @account.acct) %> -<%= web_url("accounts/#{@account.id}") %> +<%= raw t('application_mailer.view')%> <%= web_url("accounts/#{@account.id}") %> diff --git a/app/views/notification_mailer/follow_request.text.erb b/app/views/notification_mailer/follow_request.text.erb index c0d38ec67..49087a575 100644 --- a/app/views/notification_mailer/follow_request.text.erb +++ b/app/views/notification_mailer/follow_request.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.follow_request.body', name: @account.acct) %> +<%= raw t('notification_mailer.follow_request.body', name: @account.acct) %> -<%= web_url("follow_requests") %> +<%= raw t('application_mailer.view')%> <%= web_url("follow_requests") %> diff --git a/app/views/notification_mailer/mention.text.erb b/app/views/notification_mailer/mention.text.erb index 31a294bb9..c0d4be1d8 100644 --- a/app/views/notification_mailer/mention.text.erb +++ b/app/views/notification_mailer/mention.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.mention.body', name: @status.account.acct) %> +<%= raw t('notification_mailer.mention.body', name: @status.account.acct) %> -<%= render partial: 'status' %> +<%= render partial: 'status', locals: { status: @status } %> diff --git a/app/views/notification_mailer/reblog.text.erb b/app/views/notification_mailer/reblog.text.erb index 7af8052ca..c32b48650 100644 --- a/app/views/notification_mailer/reblog.text.erb +++ b/app/views/notification_mailer/reblog.text.erb @@ -1,5 +1,5 @@ <%= display_name(@me) %>, -<%= t('notification_mailer.reblog.body', name: @account.acct) %> +<%= raw t('notification_mailer.reblog.body', name: @account.acct) %> -<%= render partial: 'status' %> +<%= render partial: 'status', locals: { status: @status } %> diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index aee0540d2..a17279b1e 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -16,6 +16,7 @@ = ff.input :reblog, as: :boolean, wrapper: :with_label = ff.input :favourite, as: :boolean, wrapper: :with_label = ff.input :mention, as: :boolean, wrapper: :with_label + = ff.input :digest, as: :boolean, wrapper: :with_label = f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff| = ff.input :must_be_follower, as: :boolean, wrapper: :with_label diff --git a/app/workers/digest_mailer_worker.rb b/app/workers/digest_mailer_worker.rb new file mode 100644 index 000000000..dedb21e4e --- /dev/null +++ b/app/workers/digest_mailer_worker.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class DigestMailerWorker + include Sidekiq::Worker + + sidekiq_options queue: 'mailers' + + def perform(user_id) + user = User.find(user_id) + return unless user.settings.notification_emails['digest'] + NotificationMailer.digest(user.account).deliver_now! + user.touch(:last_emailed_at) + end +end diff --git a/config/application.rb b/config/application.rb index 8da5ade3c..1ea65619c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -49,12 +49,5 @@ module Mastodon Doorkeeper::AuthorizedApplicationsController.layout 'admin' Doorkeeper::Application.send :include, ApplicationExtension end - - config.action_dispatch.default_headers = { - 'Server' => 'Mastodon', - 'X-Frame-Options' => 'DENY', - 'X-Content-Type-Options' => 'nosniff', - 'X-XSS-Protection' => '1; mode=block', - } end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 67ff63914..dc5dd4afd 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -109,4 +109,11 @@ Rails.application.configure do config.to_prepare do StatsD.backend = StatsD::Instrument::Backends::NullBackend.new if ENV['STATSD_ADDR'].blank? end + + config.action_dispatch.default_headers = { + 'Server' => 'Mastodon', + 'X-Frame-Options' => 'DENY', + 'X-Content-Type-Options' => 'nosniff', + 'X-XSS-Protection' => '1; mode=block', + } end diff --git a/config/locales/en.yml b/config/locales/en.yml index 6da30acda..f11a689e4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,6 +29,8 @@ en: unfollow: Unfollow application_mailer: signature: Mastodon notifications from %{instance} + settings: 'Change e-mail preferences: %{link}' + view: 'View:' applications: invalid_url: The provided URL is invalid auth: @@ -83,6 +85,15 @@ en: reblog: body: 'Your status was boosted by %{name}:' subject: "%{name} boosted your status" + digest: + subject: + one: "1 new notification since your last visit 🐘" + other: "%{count} new notifications since your last visit 🐘" + body: 'Here is a brief summary of what you missed on %{instance} since your last visit on %{since}:' + mention: "%{name} mentioned you in:" + new_followers_summary: + one: You have acquired one new follower! Yay! + other: You have gotten %{count} new followers! Amazing! pagination: next: Next prev: Prev diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 4d1758f82..170af01cf 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -34,6 +34,7 @@ en: follow_request: Send e-mail when someone requests to follow you mention: Send e-mail when someone mentions you reblog: Send e-mail when someone reblogs your status + digest: Send digest e-mails 'no': 'No' required: mark: "*" diff --git a/config/settings.yml b/config/settings.yml index 71ce12e63..6ae9217a4 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -11,6 +11,7 @@ defaults: &defaults favourite: false mention: false follow_request: true + digest: true interactions: must_be_follower: false must_be_following: false diff --git a/db/migrate/20170303212857_add_last_emailed_at_to_users.rb b/db/migrate/20170303212857_add_last_emailed_at_to_users.rb new file mode 100644 index 000000000..9ae3da4fb --- /dev/null +++ b/db/migrate/20170303212857_add_last_emailed_at_to_users.rb @@ -0,0 +1,5 @@ +class AddLastEmailedAtToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :last_emailed_at, :datetime, null: true, default: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index c2d88ac13..8cc3bd8e3 100644 --- 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: 20170301222600) do +ActiveRecord::Schema.define(version: 20170303212857) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -283,6 +283,7 @@ ActiveRecord::Schema.define(version: 20170301222600) do t.string "encrypted_otp_secret_salt" t.integer "consumed_timestep" t.boolean "otp_required_for_login" + t.datetime "last_emailed_at" t.index ["account_id"], name: "index_users_on_account_id", using: :btree t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree t.index ["email"], name: "index_users_on_email", unique: true, using: :btree diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 8482d4124..bb10410b5 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -43,7 +43,7 @@ namespace :mastodon do namespace :feeds do desc 'Clear timelines of inactive users' task clear: :environment do - User.where('current_sign_in_at < ?', 14.days.ago).find_each do |user| + User.confirmed.where('current_sign_in_at < ?', 14.days.ago).find_each do |user| Redis.current.del(FeedManager.instance.key(:home, user.account_id)) end end @@ -53,4 +53,13 @@ namespace :mastodon do Redis.current.keys('feed:*').each { |key| Redis.current.del(key) } end end + + namespace :emails do + desc 'Send out digest e-mails' + task digest: :environment do + User.confirmed.joins(:account).where(accounts: { silenced: false, suspended: false }).where('current_sign_in_at < ?', 20.days.ago).find_each do |user| + DigestMailerWorker.perform_async(user.id) + end + end + end end diff --git a/spec/mailers/previews/notification_mailer_preview.rb b/spec/mailers/previews/notification_mailer_preview.rb index 8fc8d0d34..a08a80d17 100644 --- a/spec/mailers/previews/notification_mailer_preview.rb +++ b/spec/mailers/previews/notification_mailer_preview.rb @@ -1,24 +1,31 @@ # Preview all emails at http://localhost:3000/rails/mailers/notification_mailer class NotificationMailerPreview < ActionMailer::Preview - # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/mention def mention - # NotificationMailer.mention + m = Mention.last + NotificationMailer.mention(m.account, Notification.find_by(activity: m)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/follow def follow - # NotificationMailer.follow + f = Follow.last + NotificationMailer.follow(f.target_account, Notification.find_by(activity: f)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/favourite def favourite - # NotificationMailer.favourite + f = Favourite.last + NotificationMailer.favourite(f.status.account, Notification.find_by(activity: f)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/reblog def reblog - # NotificationMailer.reblog + r = Status.where.not(reblog_of_id: nil).first + NotificationMailer.reblog(r.reblog.account, Notification.find_by(activity: r)) end + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/digest + def digest + NotificationMailer.digest(Account.first, since: 90.days.ago) + end end -- cgit From 5f4e402204c9da289d1a6b5e3902bf2c9cfe61c1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 17:27:17 +0100 Subject: Improved /api/v1/accounts/:id/statuses with new params: only_media, exclude_replies Redirect /:username to /users/:username Redirect /:username/:id to /users/:username/updates/:id Updated API documentation and sponsors --- app/controllers/api/v1/accounts_controller.rb | 17 ++------------- app/models/status.rb | 9 +++++--- config/routes.rb | 4 +++- docs/Contributing-to-Mastodon/Sponsors.md | 2 +- docs/Using-the-API/API.md | 30 +++++++++++++++++++++++++-- 5 files changed, 40 insertions(+), 22 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index d691ac987..c666651b4 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -47,6 +47,8 @@ class Api::V1::AccountsController < ApiController def statuses @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) + @statuses = @statuses.where(id: MediaAttachment.where(account: @account).where.not(status_id: nil).reorder('').select('distinct status_id')) if params[:only_media] + @statuses = @statuses.without_replies if params[:exclude_replies] @statuses = cache_collection(@statuses, Status) set_maps(@statuses) @@ -58,21 +60,6 @@ class Api::V1::AccountsController < ApiController set_pagination_headers(next_path, prev_path) end - def media_statuses - media_ids = MediaAttachment.where(account: @account).where.not(status_id: nil).reorder('').select('distinct status_id') - @statuses = @account.statuses.where(id: media_ids).permitted_for(@account, current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - @statuses = cache_collection(@statuses, Status) - - set_maps(@statuses) - set_counters_maps(@statuses) - - next_path = media_statuses_api_v1_account_url(max_id: @statuses.last.id) unless @statuses.empty? - prev_path = media_statuses_api_v1_account_url(since_id: @statuses.first.id) unless @statuses.empty? - - set_pagination_headers(next_path, prev_path) - render action: :statuses - end - def follow FollowService.new.call(current_user.account, @account.acct) set_relationship diff --git a/app/models/status.rb b/app/models/status.rb index e5e740360..663ac1e34 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -37,6 +37,9 @@ class Status < ApplicationRecord scope :remote, -> { where.not(uri: nil) } scope :local, -> { where(uri: nil) } + scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } + scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } + cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account def reply? @@ -109,8 +112,8 @@ class Status < ApplicationRecord def as_public_timeline(account = nil, local_only = false) query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .where(visibility: :public) - .where('(statuses.reply = false OR statuses.in_reply_to_account_id = statuses.account_id)') - .where('statuses.reblog_of_id IS NULL') + .without_replies + .without_reblogs query = query.where('accounts.domain IS NULL') if local_only @@ -121,7 +124,7 @@ class Status < ApplicationRecord query = tag.statuses .joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .where(visibility: :public) - .where('statuses.reblog_of_id IS NULL') + .without_reblogs query = query.where('accounts.domain IS NULL') if local_only diff --git a/config/routes.rb b/config/routes.rb index f6e2dce5c..a0c76f829 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -153,7 +153,6 @@ Rails.application.routes.draw do member do get :statuses - get 'statuses/media', to: 'accounts#media_statuses', as: :media_statuses get :followers get :following @@ -180,5 +179,8 @@ Rails.application.routes.draw do root 'home#index' + get '/:username', to: redirect('/users/%{username}') + get '/:username/:id', to: redirect('/users/%{username}/updates/%{id}') + match '*unmatched_route', via: :all, to: 'application#raise_not_found' end diff --git a/docs/Contributing-to-Mastodon/Sponsors.md b/docs/Contributing-to-Mastodon/Sponsors.md index 9cdd5b03d..475791684 100644 --- a/docs/Contributing-to-Mastodon/Sponsors.md +++ b/docs/Contributing-to-Mastodon/Sponsors.md @@ -32,7 +32,7 @@ These people make the development of Mastodon possible through [Patreon](https:/ - [C418](https://mastodon.social/users/C418) - [halcy](https://icosahedron.website/users/halcy) - [Extropic](https://gnusocial.no/extropic) -- TBD +- [Pat Monaghan](http://iwrite.software/) - TBD - TBD - TBD diff --git a/docs/Using-the-API/API.md b/docs/Using-the-API/API.md index 07c1b25a9..af7858286 100644 --- a/docs/Using-the-API/API.md +++ b/docs/Using-the-API/API.md @@ -75,6 +75,10 @@ Query parameters: - `max_id` (optional): Skip statuses younger than ID (e.g. navigate backwards in time) - `since_id` (optional): Skip statuses older than ID (e.g. check for updates) +Query parameters for public and tag timelines only: + +- `local` (optional): Only return statuses originating from this instance + ### Notifications **GET /api/v1/notifications** @@ -115,7 +119,14 @@ Returns authenticated user's account. **GET /api/v1/accounts/:id/statuses** -Returns statuses by user. Same options as timeline are permitted. +Returns statuses by user. + +Query parameters: + +- `max_id` (optional): Skip statuses younger than ID (e.g. navigate backwards in time) +- `since_id` (optional): Skip statuses older than ID (e.g. check for updates) +- `only_media` (optional): Only return statuses that have media attachments +- `exclude_replies` (optional): Skip statuses that reply to other statuses **GET /api/v1/accounts/:id/following** @@ -127,7 +138,7 @@ Returns users the given user is followed by. **GET /api/v1/accounts/relationships** -Returns relationships (`following`, `followed_by`, `blocking`) of the current user to a list of given accounts. +Returns relationships (`following`, `followed_by`, `blocking`, `muting`, `requested`) of the current user to a list of given accounts. Query parameters: @@ -146,6 +157,14 @@ Query parameters: Returns accounts blocked by authenticated user. +**GET /api/v1/mutes** + +Returns accounts muted by authenticated user. + +**GET /api/v1/follow_requests** + +Returns accounts that want to follow the authenticated user but are waiting for approval. + **GET /api/v1/favourites** Returns statuses favourited by authenticated user. @@ -207,6 +226,13 @@ Returns the updated relationship to the user. Returns the updated relationship to the user. +# Muting and unmuting users + +**POST /api/v1/accounts/:id/mute** +**POST /api/v1/accounts/:id/unmute** + +Returns the updated relationship to the user. + ### OAuth apps **POST /api/v1/apps** -- cgit From 8d93f0ca563322764ee4b5395598af542eb678e7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 23:43:58 +0100 Subject: Increase max bitrate of converted webms, slightly optimized counter queries (Because postgres can tell that count(*) needs no extra checks, but counting a specific column requires them) --- app/controllers/api_controller.rb | 10 +++++----- app/models/media_attachment.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index db16f82e5..c43011754 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -102,14 +102,14 @@ class ApiController < ApplicationController def set_counters_maps(statuses) # rubocop:disable Style/AccessorMethodName status_ids = statuses.compact.map { |s| s.reblog? ? s.reblog_of_id : s.id }.uniq - @favourites_counts_map = Favourite.select('status_id, COUNT(id) AS favourites_count').group('status_id').where(status_id: status_ids).map { |f| [f.status_id, f.favourites_count] }.to_h - @reblogs_counts_map = Status.select('statuses.id, COUNT(reblogs.id) AS reblogs_count').joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.id = reblogs.reblog_of_id').where(id: status_ids).group('statuses.id').map { |r| [r.id, r.reblogs_count] }.to_h + @favourites_counts_map = Favourite.select('status_id, COUNT(*) AS favourites_count').group('status_id').where(status_id: status_ids).map { |f| [f.status_id, f.favourites_count] }.to_h + @reblogs_counts_map = Status.select('statuses.id, COUNT(*) AS reblogs_count').joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.id = reblogs.reblog_of_id').where(id: status_ids).group('statuses.id').map { |r| [r.id, r.reblogs_count] }.to_h end def set_account_counters_maps(accounts) # rubocop:disable Style/AccessorMethodName account_ids = accounts.compact.map(&:id).uniq - @followers_counts_map = Follow.unscoped.select('target_account_id, COUNT(account_id) AS followers_count').group('target_account_id').where(target_account_id: account_ids).map { |f| [f.target_account_id, f.followers_count] }.to_h - @following_counts_map = Follow.unscoped.select('account_id, COUNT(target_account_id) AS following_count').group('account_id').where(account_id: account_ids).map { |f| [f.account_id, f.following_count] }.to_h - @statuses_counts_map = Status.unscoped.select('account_id, COUNT(id) AS statuses_count').group('account_id').where(account_id: account_ids).map { |s| [s.account_id, s.statuses_count] }.to_h + @followers_counts_map = Follow.unscoped.select('target_account_id, COUNT(*) AS followers_count').group('target_account_id').where(target_account_id: account_ids).map { |f| [f.target_account_id, f.followers_count] }.to_h + @following_counts_map = Follow.unscoped.select('account_id, COUNT(*) AS following_count').group('account_id').where(account_id: account_ids).map { |f| [f.account_id, f.following_count] }.to_h + @statuses_counts_map = Status.unscoped.select('account_id, COUNT(*) AS statuses_count').group('account_id').where(account_id: account_ids).map { |s| [s.account_id, s.statuses_count] }.to_h end end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 30a76f3ec..5241d9515 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -64,7 +64,7 @@ class MediaAttachment < ApplicationRecord output: { 'c:v' => 'libvpx', 'crf' => 4, - 'b:v' => '500K', + 'b:v' => '1300K', 'vsync' => 'cfr', }, }, -- cgit From d6cb4bbe99f16b40c5962130d19ca3336124af1b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 6 Mar 2017 01:50:35 +0100 Subject: Performance improvement for profiles --- app/controllers/api/v1/accounts_controller.rb | 1 + app/models/media_attachment.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'app/controllers') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index c666651b4..9c84e0a1b 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -53,6 +53,7 @@ class Api::V1::AccountsController < ApiController set_maps(@statuses) set_counters_maps(@statuses) + set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) next_path = statuses_api_v1_account_url(max_id: @statuses.last.id) unless @statuses.empty? prev_path = statuses_api_v1_account_url(since_id: @statuses.first.id) unless @statuses.empty? diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 1090f1b39..818190214 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -67,7 +67,8 @@ class MediaAttachment < ApplicationRecord 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'', 'vsync' => 'cfr', 'b:v' => '1300K', - 'crf' => 4, + 'maxrate' => '500K', + 'crf' => 6, }, }, }, -- cgit From 1fb3e8988b017155a9c23f19afa162b58e11590d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 6 Mar 2017 02:25:41 +0100 Subject: Revert earlier fix due to new bug reports --- app/controllers/api_controller.rb | 10 +++++----- config/initializers/rabl_init.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index c43011754..db16f82e5 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -102,14 +102,14 @@ class ApiController < ApplicationController def set_counters_maps(statuses) # rubocop:disable Style/AccessorMethodName status_ids = statuses.compact.map { |s| s.reblog? ? s.reblog_of_id : s.id }.uniq - @favourites_counts_map = Favourite.select('status_id, COUNT(*) AS favourites_count').group('status_id').where(status_id: status_ids).map { |f| [f.status_id, f.favourites_count] }.to_h - @reblogs_counts_map = Status.select('statuses.id, COUNT(*) AS reblogs_count').joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.id = reblogs.reblog_of_id').where(id: status_ids).group('statuses.id').map { |r| [r.id, r.reblogs_count] }.to_h + @favourites_counts_map = Favourite.select('status_id, COUNT(id) AS favourites_count').group('status_id').where(status_id: status_ids).map { |f| [f.status_id, f.favourites_count] }.to_h + @reblogs_counts_map = Status.select('statuses.id, COUNT(reblogs.id) AS reblogs_count').joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.id = reblogs.reblog_of_id').where(id: status_ids).group('statuses.id').map { |r| [r.id, r.reblogs_count] }.to_h end def set_account_counters_maps(accounts) # rubocop:disable Style/AccessorMethodName account_ids = accounts.compact.map(&:id).uniq - @followers_counts_map = Follow.unscoped.select('target_account_id, COUNT(*) AS followers_count').group('target_account_id').where(target_account_id: account_ids).map { |f| [f.target_account_id, f.followers_count] }.to_h - @following_counts_map = Follow.unscoped.select('account_id, COUNT(*) AS following_count').group('account_id').where(account_id: account_ids).map { |f| [f.account_id, f.following_count] }.to_h - @statuses_counts_map = Status.unscoped.select('account_id, COUNT(*) AS statuses_count').group('account_id').where(account_id: account_ids).map { |s| [s.account_id, s.statuses_count] }.to_h + @followers_counts_map = Follow.unscoped.select('target_account_id, COUNT(account_id) AS followers_count').group('target_account_id').where(target_account_id: account_ids).map { |f| [f.target_account_id, f.followers_count] }.to_h + @following_counts_map = Follow.unscoped.select('account_id, COUNT(target_account_id) AS following_count').group('account_id').where(account_id: account_ids).map { |f| [f.account_id, f.following_count] }.to_h + @statuses_counts_map = Status.unscoped.select('account_id, COUNT(id) AS statuses_count').group('account_id').where(account_id: account_ids).map { |s| [s.account_id, s.statuses_count] }.to_h end end diff --git a/config/initializers/rabl_init.rb b/config/initializers/rabl_init.rb index 325bf0c78..f7be0c607 100644 --- a/config/initializers/rabl_init.rb +++ b/config/initializers/rabl_init.rb @@ -1,6 +1,6 @@ Rabl.configure do |config| config.cache_all_output = false - config.cache_sources = !!Rails.env.production? + config.cache_sources = Rails.env.production? config.include_json_root = false config.view_paths = [Rails.root.join('app/views')] end -- cgit From 02349b32696d6559ed64dbe4f401892d5fa5ddf7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 14 Mar 2017 15:59:21 +0100 Subject: Obfuscate filenames better, double rate limits --- app/controllers/concerns/obfuscate_filename.rb | 6 +++++- config/initializers/rack-attack.rb | 4 ++-- docs/Using-the-API/Push-notifications.md | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/concerns/obfuscate_filename.rb b/app/controllers/concerns/obfuscate_filename.rb index dde7ce8c6..9c896fb09 100644 --- a/app/controllers/concerns/obfuscate_filename.rb +++ b/app/controllers/concerns/obfuscate_filename.rb @@ -13,6 +13,10 @@ module ObfuscateFilename file = params.dig(*path) return if file.nil? - file.original_filename = 'media' + File.extname(file.original_filename) + file.original_filename = secure_token + File.extname(file.original_filename) + end + + def secure_token(length = 16) + SecureRandom.hex(length / 2) end end diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb index 3f0ee1d7a..70f7846d1 100644 --- a/config/initializers/rack-attack.rb +++ b/config/initializers/rack-attack.rb @@ -1,6 +1,6 @@ class Rack::Attack # Rate limits for the API - throttle('api', limit: 150, period: 5.minutes) do |req| + throttle('api', limit: 300, period: 5.minutes) do |req| req.ip if req.path.match(/\A\/api\/v/) end @@ -11,7 +11,7 @@ class Rack::Attack headers = { 'X-RateLimit-Limit' => match_data[:limit].to_s, 'X-RateLimit-Remaining' => '0', - 'X-RateLimit-Reset' => (now + (match_data[:period] - now.to_i % match_data[:period])).iso8601(6) + 'X-RateLimit-Reset' => (now + (match_data[:period] - now.to_i % match_data[:period])).iso8601(6), } [429, headers, [{ error: 'Throttled' }.to_json]] diff --git a/docs/Using-the-API/Push-notifications.md b/docs/Using-the-API/Push-notifications.md index d98c8833a..fc373e723 100644 --- a/docs/Using-the-API/Push-notifications.md +++ b/docs/Using-the-API/Push-notifications.md @@ -1,4 +1,4 @@ Push notifications ================== -**Note: This push notification design turned out to not be fully operational on the side of Firebase. A different approach is in consideration** +See for an example of how to create push notifications for a mobile app. It involves using the Mastodon streaming API on behalf of the app's users, as a sort of proxy. -- cgit