Add digest e-mails
This commit is contained in:
		
							parent
							
								
									f5457cc3d2
								
							
						
					
					
						commit
						6b81d10030
					
				
					 25 changed files with 117 additions and 32 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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?
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@
 | 
			
		|||
 | 
			
		||||
class Setting < RailsSettings::Base
 | 
			
		||||
  source Rails.root.join('config/settings.yml')
 | 
			
		||||
  namespace Rails.env
 | 
			
		||||
 | 
			
		||||
  def to_param
 | 
			
		||||
    var
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<%= yield %>
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<%= t('application_mailer.signature', instance: Rails.configuration.x.local_domain) %>
 | 
			
		||||
<%= t('application_mailer.settings', link: settings_preferences_url) %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}") %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										15
									
								
								app/views/notification_mailer/digest.text.erb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/views/notification_mailer/digest.text.erb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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 %>
 | 
			
		||||
| 
						 | 
				
			
			@ -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 } %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}") %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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") %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 } %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 } %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										14
									
								
								app/workers/digest_mailer_worker.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/workers/digest_mailer_worker.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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: "*"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ defaults: &defaults
 | 
			
		|||
    favourite: false
 | 
			
		||||
    mention: false
 | 
			
		||||
    follow_request: true
 | 
			
		||||
    digest: true
 | 
			
		||||
  interactions:
 | 
			
		||||
    must_be_follower: false
 | 
			
		||||
    must_be_following: false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue