Import feature for following/blocking lists (addresses #62, #177, #201, #454)

master
Eugen Rochko 8 years ago
parent 03fb6c16ec
commit e8875c6046
  1. 12
      app/controllers/api/v1/timelines_controller.rb
  2. 34
      app/controllers/settings/imports_controller.rb
  3. 14
      app/models/import.rb
  4. 9
      app/views/layouts/admin.html.haml
  5. 11
      app/views/settings/imports/show.html.haml
  6. 54
      app/workers/import_worker.rb
  7. 8
      config/locales/en.yml
  8. 4
      config/locales/simple_form.en.yml
  9. 1
      config/navigation.rb
  10. 1
      config/routes.rb
  11. 11
      db/migrate/20170330163835_create_imports.rb
  12. 11
      db/migrate/20170330164118_add_attachment_data_to_imports.rb
  13. 14
      db/schema.rb
  14. 2
      spec/fabricators/import_fabricator.rb
  15. 5
      spec/models/import_spec.rb

@ -11,8 +11,8 @@ class Api::V1::TimelinesController < ApiController
@statuses = cache_collection(@statuses) @statuses = cache_collection(@statuses)
set_maps(@statuses) set_maps(@statuses)
set_counters_maps(@statuses) # set_counters_maps(@statuses)
set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
next_path = api_v1_home_timeline_url(max_id: @statuses.last.id) unless @statuses.empty? next_path = api_v1_home_timeline_url(max_id: @statuses.last.id) unless @statuses.empty?
prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) unless @statuses.empty? prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
@ -27,8 +27,8 @@ class Api::V1::TimelinesController < ApiController
@statuses = cache_collection(@statuses) @statuses = cache_collection(@statuses)
set_maps(@statuses) set_maps(@statuses)
set_counters_maps(@statuses) # set_counters_maps(@statuses)
set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
next_path = api_v1_public_timeline_url(max_id: @statuses.last.id) unless @statuses.empty? next_path = api_v1_public_timeline_url(max_id: @statuses.last.id) unless @statuses.empty?
prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) unless @statuses.empty? prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
@ -44,8 +44,8 @@ class Api::V1::TimelinesController < ApiController
@statuses = cache_collection(@statuses) @statuses = cache_collection(@statuses)
set_maps(@statuses) set_maps(@statuses)
set_counters_maps(@statuses) # set_counters_maps(@statuses)
set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
next_path = api_v1_hashtag_timeline_url(params[:id], max_id: @statuses.last.id) unless @statuses.empty? next_path = api_v1_hashtag_timeline_url(params[:id], max_id: @statuses.last.id) unless @statuses.empty?
prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) unless @statuses.empty? prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) unless @statuses.empty?

@ -0,0 +1,34 @@
# frozen_string_literal: true
class Settings::ImportsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_account
def show
@import = Import.new
end
def create
@import = Import.new(import_params)
@import.account = @account
if @import.save
ImportWorker.perform_async(@import.id)
redirect_to settings_import_path, notice: I18n.t('imports.success')
else
render action: :show
end
end
private
def set_account
@account = current_user.account
end
def import_params
params.require(:import).permit(:data, :type)
end
end

@ -0,0 +1,14 @@
# frozen_string_literal: true
class Import < ApplicationRecord
self.inheritance_column = false
enum type: [:following, :blocking]
belongs_to :account
FILE_TYPES = ['text/plain', 'text/csv'].freeze
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV.fetch('PAPERCLIP_SECRET')
validates_attachment_content_type :data, content_type: FILE_TYPES
end

@ -12,6 +12,15 @@
.content-wrapper .content-wrapper
.content .content
%h2= yield :page_title %h2= yield :page_title
- if flash[:notice]
.flash-message.notice
%strong= flash[:notice]
- if flash[:alert]
.flash-message.alert
%strong= flash[:alert]
= yield = yield
= render template: "layouts/application", locals: { body_classes: 'admin' } = render template: "layouts/application", locals: { body_classes: 'admin' }

@ -0,0 +1,11 @@
- content_for :page_title do
= t('settings.import')
%p.hint= t('imports.preface')
= simple_form_for @import, url: settings_import_path do |f|
= f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }
= f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data')
.actions
= f.button :button, t('imports.upload'), type: :submit

@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'csv'
class ImportWorker
include Sidekiq::Worker
sidekiq_options retry: false
def perform(import_id)
import = Import.find(import_id)
case import.type
when 'blocking'
process_blocks(import)
when 'following'
process_follows(import)
end
import.destroy
end
private
def process_blocks(import)
from_account = import.account
CSV.foreach(import.data.path) do |row|
next if row.size != 1
begin
target_account = FollowRemoteAccountService.new.call(row[0])
next if target_account.nil?
BlockService.new.call(from_account, target_account)
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
next
end
end
end
def process_follows(import)
from_account = import.account
CSV.foreach(import.data.path) do |row|
next if row.size != 1
begin
FollowService.new.call(from_account, row[0])
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
next
end
end
end
end

@ -85,6 +85,13 @@ en:
validation_errors: validation_errors:
one: Something isn't quite right yet! Please review the error below one: Something isn't quite right yet! Please review the error below
other: Something isn't quite right yet! Please review %{count} errors below other: Something isn't quite right yet! Please review %{count} errors below
imports:
preface: You can import certain data like all the people you are following or blocking into your account on this instance, from files created by an export on another instance.
success: Your data was successfully uploaded and will now be processed in due time
types:
blocking: Blocking list
following: Following list
upload: Upload
landing_strip_html: <strong>%{name}</strong> is a user on <strong>%{domain}</strong>. You can follow them or interact with them if you have an account anywhere in the fediverse. If you don't, you can <a href="%{sign_up_path}">sign up here</a>. landing_strip_html: <strong>%{name}</strong> is a user on <strong>%{domain}</strong>. You can follow them or interact with them if you have an account anywhere in the fediverse. If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
notification_mailer: notification_mailer:
digest: digest:
@ -124,6 +131,7 @@ en:
back: Back to Mastodon back: Back to Mastodon
edit_profile: Edit profile edit_profile: Edit profile
export: Data export export: Data export
import: Import
preferences: Preferences preferences: Preferences
settings: Settings settings: Settings
two_factor_auth: Two-factor Authentication two_factor_auth: Two-factor Authentication

@ -8,12 +8,15 @@ en:
header: PNG, GIF or JPG. At most 2MB. Will be downscaled to 700x335px header: PNG, GIF or JPG. At most 2MB. Will be downscaled to 700x335px
locked: Requires you to manually approve followers and defaults post privacy to followers-only locked: Requires you to manually approve followers and defaults post privacy to followers-only
note: At most 160 characters note: At most 160 characters
imports:
data: CSV file exported from another Mastodon instance
labels: labels:
defaults: defaults:
avatar: Avatar avatar: Avatar
confirm_new_password: Confirm new password confirm_new_password: Confirm new password
confirm_password: Confirm password confirm_password: Confirm password
current_password: Current password current_password: Current password
data: Data
display_name: Display name display_name: Display name
email: E-mail address email: E-mail address
header: Header header: Header
@ -24,6 +27,7 @@ en:
otp_attempt: Two-factor code otp_attempt: Two-factor code
password: Password password: Password
setting_default_privacy: Post privacy setting_default_privacy: Post privacy
type: Import type
username: Username username: Username
interactions: interactions:
must_be_follower: Block notifications from non-followers must_be_follower: Block notifications from non-followers

@ -9,6 +9,7 @@ SimpleNavigation::Configuration.run do |navigation|
settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url
settings.item :password, safe_join([fa_icon('cog fw'), t('auth.change_password')]), edit_user_registration_url settings.item :password, safe_join([fa_icon('cog fw'), t('auth.change_password')]), edit_user_registration_url
settings.item :two_factor_auth, safe_join([fa_icon('mobile fw'), t('settings.two_factor_auth')]), settings_two_factor_auth_url settings.item :two_factor_auth, safe_join([fa_icon('mobile fw'), t('settings.two_factor_auth')]), settings_two_factor_auth_url
settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
end end

@ -51,6 +51,7 @@ Rails.application.routes.draw do
namespace :settings do namespace :settings do
resource :profile, only: [:show, :update] resource :profile, only: [:show, :update]
resource :preferences, only: [:show, :update] resource :preferences, only: [:show, :update]
resource :import, only: [:show, :create]
resource :export, only: [:show] do resource :export, only: [:show] do
collection do collection do

@ -0,0 +1,11 @@
class CreateImports < ActiveRecord::Migration[5.0]
def change
create_table :imports do |t|
t.integer :account_id, null: false
t.integer :type, null: false
t.boolean :approved
t.timestamps
end
end
end

@ -0,0 +1,11 @@
class AddAttachmentDataToImports < ActiveRecord::Migration
def self.up
change_table :imports do |t|
t.attachment :data
end
end
def self.down
remove_attachment :imports, :data
end
end

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170330021336) do ActiveRecord::Schema.define(version: 20170330164118) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -93,6 +93,18 @@ ActiveRecord::Schema.define(version: 20170330021336) do
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true, using: :btree t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true, using: :btree
end end
create_table "imports", force: :cascade do |t|
t.integer "account_id", null: false
t.integer "type", null: false
t.boolean "approved"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "data_file_name"
t.string "data_content_type"
t.integer "data_file_size"
t.datetime "data_updated_at"
end
create_table "media_attachments", force: :cascade do |t| create_table "media_attachments", force: :cascade do |t|
t.bigint "status_id" t.bigint "status_id"
t.string "file_file_name" t.string "file_file_name"

@ -0,0 +1,2 @@
Fabricator(:import) do
end

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe Import, type: :model do
end
Loading…
Cancel
Save