From e8875c6046615778c7ae6f1fc0c4a195fb5d3a03 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 30 Mar 2017 19:42:33 +0200 Subject: [PATCH] Import feature for following/blocking lists (addresses #62, #177, #201, #454) --- .../api/v1/timelines_controller.rb | 12 ++--- .../settings/imports_controller.rb | 34 ++++++++++++ app/models/import.rb | 14 +++++ app/views/layouts/admin.html.haml | 9 ++++ app/views/settings/imports/show.html.haml | 11 ++++ app/workers/import_worker.rb | 54 +++++++++++++++++++ config/locales/en.yml | 8 +++ config/locales/simple_form.en.yml | 4 ++ config/navigation.rb | 1 + config/routes.rb | 1 + db/migrate/20170330163835_create_imports.rb | 11 ++++ ...30164118_add_attachment_data_to_imports.rb | 11 ++++ db/schema.rb | 14 ++++- spec/fabricators/import_fabricator.rb | 2 + spec/models/import_spec.rb | 5 ++ 15 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 app/controllers/settings/imports_controller.rb create mode 100644 app/models/import.rb create mode 100644 app/views/settings/imports/show.html.haml create mode 100644 app/workers/import_worker.rb create mode 100644 db/migrate/20170330163835_create_imports.rb create mode 100644 db/migrate/20170330164118_add_attachment_data_to_imports.rb create mode 100644 spec/fabricators/import_fabricator.rb create mode 100644 spec/models/import_spec.rb diff --git a/app/controllers/api/v1/timelines_controller.rb b/app/controllers/api/v1/timelines_controller.rb index af6e5b7df..0446b9e4d 100644 --- a/app/controllers/api/v1/timelines_controller.rb +++ b/app/controllers/api/v1/timelines_controller.rb @@ -11,8 +11,8 @@ class Api::V1::TimelinesController < ApiController @statuses = cache_collection(@statuses) 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) + # set_counters_maps(@statuses) + # 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? 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) 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) + # set_counters_maps(@statuses) + # 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? 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) 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) + # set_counters_maps(@statuses) + # 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? prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) unless @statuses.empty? diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb new file mode 100644 index 000000000..cbb5e65da --- /dev/null +++ b/app/controllers/settings/imports_controller.rb @@ -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 diff --git a/app/models/import.rb b/app/models/import.rb new file mode 100644 index 000000000..255063c53 --- /dev/null +++ b/app/models/import.rb @@ -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 diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 750d6036f..59fe078df 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -12,6 +12,15 @@ .content-wrapper .content %h2= yield :page_title + + - if flash[:notice] + .flash-message.notice + %strong= flash[:notice] + + - if flash[:alert] + .flash-message.alert + %strong= flash[:alert] + = yield = render template: "layouts/application", locals: { body_classes: 'admin' } diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml new file mode 100644 index 000000000..8502913dc --- /dev/null +++ b/app/views/settings/imports/show.html.haml @@ -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 diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb new file mode 100644 index 000000000..a3ae2a85a --- /dev/null +++ b/app/workers/import_worker.rb @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 3e130aaf8..965001e05 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -85,6 +85,13 @@ en: validation_errors: one: Something isn't quite right yet! Please review the error 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: %{name} is a user on %{domain}. You can follow them or interact with them if you have an account anywhere in the fediverse. If you don't, you can sign up here. notification_mailer: digest: @@ -124,6 +131,7 @@ en: back: Back to Mastodon edit_profile: Edit profile export: Data export + import: Import preferences: Preferences settings: Settings two_factor_auth: Two-factor Authentication diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index c4bd0ad96..df4f6ca00 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -8,12 +8,15 @@ en: 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 note: At most 160 characters + imports: + data: CSV file exported from another Mastodon instance labels: defaults: avatar: Avatar confirm_new_password: Confirm new password confirm_password: Confirm password current_password: Current password + data: Data display_name: Display name email: E-mail address header: Header @@ -24,6 +27,7 @@ en: otp_attempt: Two-factor code password: Password setting_default_privacy: Post privacy + type: Import type username: Username interactions: must_be_follower: Block notifications from non-followers diff --git a/config/navigation.rb b/config/navigation.rb index 607a0ff10..77556e5aa 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -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 :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 :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 :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url end diff --git a/config/routes.rb b/config/routes.rb index cf8364968..bfca5c734 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,6 +51,7 @@ Rails.application.routes.draw do namespace :settings do resource :profile, only: [:show, :update] resource :preferences, only: [:show, :update] + resource :import, only: [:show, :create] resource :export, only: [:show] do collection do diff --git a/db/migrate/20170330163835_create_imports.rb b/db/migrate/20170330163835_create_imports.rb new file mode 100644 index 000000000..d6f74823d --- /dev/null +++ b/db/migrate/20170330163835_create_imports.rb @@ -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 diff --git a/db/migrate/20170330164118_add_attachment_data_to_imports.rb b/db/migrate/20170330164118_add_attachment_data_to_imports.rb new file mode 100644 index 000000000..4850b0663 --- /dev/null +++ b/db/migrate/20170330164118_add_attachment_data_to_imports.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 7675ed1a9..5a9ca1426 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: 20170330021336) do +ActiveRecord::Schema.define(version: 20170330164118) do # These are extensions that must be enabled in order to support this database 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 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| t.bigint "status_id" t.string "file_file_name" diff --git a/spec/fabricators/import_fabricator.rb b/spec/fabricators/import_fabricator.rb new file mode 100644 index 000000000..e2eb1e0df --- /dev/null +++ b/spec/fabricators/import_fabricator.rb @@ -0,0 +1,2 @@ +Fabricator(:import) do +end diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb new file mode 100644 index 000000000..fa52077cd --- /dev/null +++ b/spec/models/import_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Import, type: :model do + +end