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. --- config/routes.rb | 3 +++ 1 file changed, 3 insertions(+) (limited to 'config') diff --git a/config/routes.rb b/config/routes.rb index 870d8afd4..f6e2dce5c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -127,6 +127,7 @@ Rails.application.routes.draw do resources :media, only: [:create] resources :apps, only: [:create] resources :blocks, only: [:index] + resources :mutes, only: [:index] resources :favourites, only: [:index] resources :reports, only: [:index, :create] @@ -160,6 +161,8 @@ Rails.application.routes.draw do post :unfollow post :block post :unblock + post :mute + post :unmute end 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 'config') 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 caf5b8e9757679b93b9a34b0c55f43cb47910201 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 4 Mar 2017 22:17:10 +0100 Subject: Fix #431 - convert gif to webm during upload. Web UI treats them like it did before. In the API, attachments now can be either image, video or gifv. Gifv is to be treated like images in terms of behaviour, but are videos by file type. --- .../javascripts/components/actions/accounts.jsx | 7 +- .../components/extended_video_player.jsx | 21 ++ .../components/components/media_gallery.jsx | 232 ++++++++++++++------- .../javascripts/components/components/status.jsx | 4 +- .../components/components/video_player.jsx | 4 +- .../features/status/components/detailed_status.jsx | 2 +- .../features/ui/containers/modal_container.jsx | 22 +- app/assets/stylesheets/stream_entries.scss | 31 ++- app/models/media_attachment.rb | 69 +++--- app/views/api/v1/media/create.rabl | 4 +- .../stream_entries/_detailed_status.html.haml | 6 +- app/views/stream_entries/_media.html.haml | 4 + app/views/stream_entries/_simple_status.html.haml | 15 +- config/application.rb | 5 +- ...20170304202101_add_type_to_media_attachments.rb | 12 ++ db/schema.rb | 3 +- lib/paperclip/gif_transcoder.rb | 21 ++ 17 files changed, 325 insertions(+), 137 deletions(-) create mode 100644 app/assets/javascripts/components/components/extended_video_player.jsx create mode 100644 app/views/stream_entries/_media.html.haml create mode 100644 db/migrate/20170304202101_add_type_to_media_attachments.rb create mode 100644 lib/paperclip/gif_transcoder.rb (limited to 'config') diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index 8af0b15d8..05fa8e68d 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -75,11 +75,16 @@ export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; export function fetchAccount(id) { return (dispatch, getState) => { + dispatch(fetchRelationships([id])); + + if (getState().getIn(['accounts', id], null) !== null) { + return; + } + dispatch(fetchAccountRequest(id)); api(getState).get(`/api/v1/accounts/${id}`).then(response => { dispatch(fetchAccountSuccess(response.data)); - dispatch(fetchRelationships([id])); }).catch(error => { dispatch(fetchAccountFail(id, error)); }); diff --git a/app/assets/javascripts/components/components/extended_video_player.jsx b/app/assets/javascripts/components/components/extended_video_player.jsx new file mode 100644 index 000000000..66e5dee16 --- /dev/null +++ b/app/assets/javascripts/components/components/extended_video_player.jsx @@ -0,0 +1,21 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; + +const ExtendedVideoPlayer = React.createClass({ + + propTypes: { + src: React.PropTypes.string.isRequired + }, + + mixins: [PureRenderMixin], + + render () { + return ( +
+
+ ); + }, + +}); + +export default ExtendedVideoPlayer; diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx index b0e397e80..cd2394023 100644 --- a/app/assets/javascripts/components/components/media_gallery.jsx +++ b/app/assets/javascripts/components/components/media_gallery.jsx @@ -43,6 +43,141 @@ const spoilerButtonStyle = { zIndex: '100' }; +const itemStyle = { + boxSizing: 'border-box', + position: 'relative', + float: 'left', + border: 'none', + display: 'block' +}; + +const thumbStyle = { + display: 'block', + width: '100%', + height: '100%', + textDecoration: 'none', + backgroundSize: 'cover', + cursor: 'zoom-in' +}; + +const gifvThumbStyle = { + position: 'relative', + zIndex: '1', + width: '100%', + height: '100%', + objectFit: 'cover', + top: '50%', + transform: 'translateY(-50%)', + cursor: 'zoom-in' +}; + +const Item = React.createClass({ + + propTypes: { + attachment: ImmutablePropTypes.map.isRequired, + index: React.PropTypes.number.isRequired, + size: React.PropTypes.number.isRequired, + onClick: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleClick (e) { + const { index, onClick } = this.props; + + if (e.button === 0) { + e.preventDefault(); + onClick(index); + } + + e.stopPropagation(); + }, + + render () { + const { attachment, index, size } = this.props; + + let width = 50; + let height = 100; + let top = 'auto'; + let left = 'auto'; + let bottom = 'auto'; + let right = 'auto'; + + if (size === 1) { + width = 100; + } + + if (size === 4 || (size === 3 && index > 0)) { + height = 50; + } + + if (size === 2) { + if (index === 0) { + right = '2px'; + } else { + left = '2px'; + } + } else if (size === 3) { + if (index === 0) { + right = '2px'; + } else if (index > 0) { + left = '2px'; + } + + if (index === 1) { + bottom = '2px'; + } else if (index > 1) { + top = '2px'; + } + } else if (size === 4) { + if (index === 0 || index === 2) { + right = '2px'; + } + + if (index === 1 || index === 3) { + left = '2px'; + } + + if (index < 2) { + bottom = '2px'; + } else { + top = '2px'; + } + } + + let thumbnail = ''; + + if (attachment.get('type') === 'image') { + thumbnail = ( + + ); + } else if (attachment.get('type') === 'gifv') { + thumbnail = ( +
- -
- ); - }); + children = media.take(4).map((attachment, i) => ); } return (
-
+
+ {children}
); diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index 110d26c6d..fb49db069 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -74,8 +74,8 @@ const Status = React.createClass({ } if (status.get('media_attachments').size > 0 && !this.props.muted) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ; + if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { + media = ; } else { media = ; } diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index 5c2cef21a..df09da912 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -175,7 +175,7 @@ const VideoPlayer = React.createClass({ ); } else { return ( -
+
{spoilerButton} @@ -197,7 +197,7 @@ const VideoPlayer = React.createClass({
{spoilerButton} {muteButton} -
); } diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index caa46ff3c..623872953 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -38,7 +38,7 @@ const DetailedStatus = React.createClass({ let applicationLink = ''; if (status.get('media_attachments').size > 0) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { media = ; } else { media = ; diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx index d8301b20f..e3c4281b9 100644 --- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -9,6 +9,7 @@ import ImageLoader from 'react-imageloader'; import LoadingIndicator from '../../../components/loading_indicator'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import ExtendedVideoPlayer from '../../../components/extended_video_player'; const mapStateToProps = state => ({ media: state.getIn(['modal', 'media']), @@ -131,27 +132,34 @@ const Modal = React.createClass({ return null; } - const url = media.get(index).get('url'); + const attachment = media.get(index); + const url = attachment.get('url'); - let leftNav, rightNav; + let leftNav, rightNav, content; - leftNav = rightNav = ''; + leftNav = rightNav = content = ''; if (media.size > 1) { leftNav =
; rightNav =
; } - return ( - - {leftNav} - + if (attachment.get('type') === 'image') { + content = ( + ); + } else if (attachment.get('type') === 'gifv') { + content = ; + } + return ( + + {leftNav} + {content} {rightNav} ); diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss index 3b2e88f6d..b9a9a1da3 100644 --- a/app/assets/stylesheets/stream_entries.scss +++ b/app/assets/stylesheets/stream_entries.scss @@ -104,8 +104,12 @@ overflow: hidden; width: 100%; box-sizing: border-box; - height: 110px; - display: flex; + position: relative; + + .status__attachments__inner { + display: flex; + height: 214px; + } } } @@ -184,8 +188,12 @@ overflow: hidden; width: 100%; box-sizing: border-box; - height: 300px; - display: flex; + position: relative; + + .status__attachments__inner { + display: flex; + height: 360px; + } } .video-player { @@ -231,11 +239,19 @@ text-decoration: none; cursor: zoom-in; } + + video { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; + top: 50%; + transform: translateY(-50%); + } } .video-item { - max-width: 196px; - a { cursor: pointer; } @@ -258,6 +274,9 @@ width: 100%; height: 100%; cursor: pointer; + position: absolute; + top: 0; + left: 0; display: flex; align-items: center; justify-content: center; diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 6925f9b0d..620a92dbc 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -1,15 +1,32 @@ # frozen_string_literal: true class MediaAttachment < ApplicationRecord + self.inheritance_column = nil + + enum type: [:image, :gifv, :video] + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze + IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze + VIDEO_STYLES = { + small: { + convert_options: { + output: { + vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + }, + }, + format: 'png', + time: 0, + }, + }.freeze + belongs_to :account, inverse_of: :media_attachments belongs_to :status, inverse_of: :media_attachments has_attached_file :file, - styles: -> (f) { file_styles f }, - processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] }, + styles: ->(f) { file_styles f }, + processors: ->(f) { file_processors f }, convert_options: { all: '-quality 90 -strip' } validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES validates_attachment_size :file, less_than: 8.megabytes @@ -27,45 +44,45 @@ class MediaAttachment < ApplicationRecord self.file = URI.parse(url) end - def image? - IMAGE_MIME_TYPES.include? file_content_type - end - - def video? - VIDEO_MIME_TYPES.include? file_content_type - end - - def type - image? ? 'image' : 'video' - end - def to_param shortcode end before_create :set_shortcode + before_post_process :set_type class << self private def file_styles(f) - if f.instance.image? + if f.instance.file_content_type == 'image/gif' { - original: '1280x1280>', - small: '400x400>', - } - else - { - small: { + small: IMAGE_STYLES[:small], + original: { + format: 'webm', convert_options: { output: { - vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + 'c:v' => 'libvpx', + 'crf' => 6, + 'b:v' => '500K', }, }, - format: 'png', - time: 1, }, } + elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type + IMAGE_STYLES + else + VIDEO_STYLES + end + end + + def file_processors(f) + if f.file_content_type == 'image/gif' + [:gif_transcoder] + elsif VIDEO_MIME_TYPES.include? f.file_content_type + [:transcoder] + else + [:thumbnail] end end end @@ -80,4 +97,8 @@ class MediaAttachment < ApplicationRecord break if MediaAttachment.find_by(shortcode: shortcode).nil? end end + + def set_type + self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image + end end diff --git a/app/views/api/v1/media/create.rabl b/app/views/api/v1/media/create.rabl index 0b42e6e3d..916217cbd 100644 --- a/app/views/api/v1/media/create.rabl +++ b/app/views/api/v1/media/create.rabl @@ -1,5 +1,5 @@ object @media attribute :id, :type -node(:url) { |media| full_asset_url(media.file.url( :original)) } -node(:preview_url) { |media| full_asset_url(media.file.url( :small)) } +node(:url) { |media| full_asset_url(media.file.url(:original)) } +node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } node(:text_url) { |media| medium_url(media) } diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 6c1c1ce84..8c0456b1f 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -22,9 +22,9 @@ .detailed-status__attachments - if status.sensitive? = render partial: 'stream_entries/content_spoiler' - - status.media_attachments.each do |media| - .media-item - = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" + .status__attachments__inner + - status.media_attachments.each do |media| + = render partial: 'stream_entries/media', locals: { media: media } %div.detailed-status__meta %data.dt-published{ value: status.created_at.to_time.iso8601 } diff --git a/app/views/stream_entries/_media.html.haml b/app/views/stream_entries/_media.html.haml new file mode 100644 index 000000000..cd7faa700 --- /dev/null +++ b/app/views/stream_entries/_media.html.haml @@ -0,0 +1,4 @@ +.media-item + = link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : "", target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do + - unless media.image? + %video{ src: media.file.url(:original), autoplay: true, loop: true }/ diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index 52ad39220..cb2c976ce 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -22,11 +22,12 @@ - if status.sensitive? = render partial: 'stream_entries/content_spoiler' - if status.media_attachments.first.video? - .video-item - = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do - .video-item__play - = fa_icon('play') + .status__attachments__inner + .video-item + = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do + .video-item__play + = fa_icon('play') - else - - status.media_attachments.each do |media| - .media-item - = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" + .status__attachments__inner + - status.media_attachments.each do |media| + = render partial: 'stream_entries/media', locals: { media: media } diff --git a/config/application.rb b/config/application.rb index 1ea65619c..30ed608c5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,12 +2,13 @@ require_relative 'boot' require 'rails/all' -require_relative '../app/lib/exceptions' - # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) +require_relative '../app/lib/exceptions' +require_relative '../lib/paperclip/gif_transcoder' + Dotenv::Railtie.load module Mastodon diff --git a/db/migrate/20170304202101_add_type_to_media_attachments.rb b/db/migrate/20170304202101_add_type_to_media_attachments.rb new file mode 100644 index 000000000..514079958 --- /dev/null +++ b/db/migrate/20170304202101_add_type_to_media_attachments.rb @@ -0,0 +1,12 @@ +class AddTypeToMediaAttachments < ActiveRecord::Migration[5.0] + def up + add_column :media_attachments, :type, :integer, default: 0, null: false + + MediaAttachment.where(file_content_type: MediaAttachment::IMAGE_MIME_TYPES).update_all(type: MediaAttachment.types[:image]) + MediaAttachment.where(file_content_type: MediaAttachment::VIDEO_MIME_TYPES).update_all(type: MediaAttachment.types[:video]) + end + + def down + remove_column :media_attachments, :type + end +end diff --git a/db/schema.rb b/db/schema.rb index 8cc3bd8e3..4ec85ef2b 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: 20170303212857) do +ActiveRecord::Schema.define(version: 20170304202101) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,6 +98,7 @@ ActiveRecord::Schema.define(version: 20170303212857) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "shortcode" + t.integer "type", default: 0, null: false t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true, using: :btree t.index ["status_id"], name: "index_media_attachments_on_status_id", using: :btree end diff --git a/lib/paperclip/gif_transcoder.rb b/lib/paperclip/gif_transcoder.rb new file mode 100644 index 000000000..33d2c4a01 --- /dev/null +++ b/lib/paperclip/gif_transcoder.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Paperclip + # This transcoder is only to be used for the MediaAttachment model + # to convert animated gifs to webm + class GifTranscoder < Paperclip::Processor + def make + num_frames = identify('-format %n :file', file: file.path).to_i + + return file unless options[:style] == :original && num_frames > 1 + + final_file = Paperclip::Transcoder.make(file, options, attachment) + + attachment.instance.file_file_name = 'media.webm' + attachment.instance.file_content_type = 'video/webm' + attachment.instance.type = MediaAttachment.types[:gifv] + + final_file + end + 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 'config') 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 85fce04d1b4353c3645c15967d222cdcdb488dfd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 22:55:24 +0100 Subject: Detect videos with no sound, handle them like gifv --- app/models/media_attachment.rb | 2 +- config/application.rb | 1 + lib/paperclip/video_transcoder.rb | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 lib/paperclip/video_transcoder.rb (limited to 'config') diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 620a92dbc..d560bd673 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -80,7 +80,7 @@ class MediaAttachment < ApplicationRecord if f.file_content_type == 'image/gif' [:gif_transcoder] elsif VIDEO_MIME_TYPES.include? f.file_content_type - [:transcoder] + [:video_transcoder] else [:thumbnail] end diff --git a/config/application.rb b/config/application.rb index 30ed608c5..cb009b24c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,6 +8,7 @@ Bundler.require(*Rails.groups) require_relative '../app/lib/exceptions' require_relative '../lib/paperclip/gif_transcoder' +require_relative '../lib/paperclip/video_transcoder' Dotenv::Railtie.load diff --git a/lib/paperclip/video_transcoder.rb b/lib/paperclip/video_transcoder.rb new file mode 100644 index 000000000..c3504c17c --- /dev/null +++ b/lib/paperclip/video_transcoder.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Paperclip + # This transcoder is only to be used for the MediaAttachment model + # to check when uploaded videos are actually gifv's + class VideoTranscoder < Paperclip::Processor + def make + meta = ::Av.cli.identify(@file.path) + attachment.instance.type = MediaAttachment.types[:gifv] unless meta[:audio_encode] + + Paperclip::Transcoder.make(file, options, attachment) + end + end +end -- cgit From ebc01bf0f61e58648ea7bfd4c915b4f373761e1d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 23:03:49 +0100 Subject: Make the paperclip filename interpolator smarter about the :original style If an :original gets converted into another format, it would get saved as original_filename *anyway*, so generating the extension is pointless and yields bad results for when you change the style definition later. This way, old gifs will still have correct URLs --- config/initializers/paperclip.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'config') diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 71a7b514e..580a3196e 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -2,6 +2,11 @@ Paperclip.options[:read_timeout] = 60 +Paperclip.interpolates :filename do |attachment, style| + return attachment.original_filename if style == :original + [basename(attachment, style), extension(attachment, style)].delete_if(&:empty?).join('.') +end + if ENV['S3_ENABLED'] == 'true' Aws.eager_autoload!(services: %w(S3)) -- 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 'config') 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 'config') 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