diff --git a/app/controllers/api/v1/timelines_controller.rb b/app/controllers/api/v1/timelines_controller.rb
index af6e5b7df2..0446b9e4dd 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 0000000000..cbb5e65da5
--- /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 0000000000..255063c536
--- /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 750d6036f3..59fe078dfc 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 0000000000..8502913dcc
--- /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 0000000000..a3ae2a85a4
--- /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 3e130aaf84..965001e059 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 c4bd0ad96e..df4f6ca007 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 607a0ff102..77556e5aa3 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 cf83649681..bfca5c734b 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 0000000000..d6f74823d7
--- /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 0000000000..4850b0663d
--- /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 7675ed1a97..5a9ca1426d 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 0000000000..e2eb1e0dfb
--- /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 0000000000..fa52077cd1
--- /dev/null
+++ b/spec/models/import_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Import, type: :model do
+
+end