about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/api/v1/timelines_controller.rb12
-rw-r--r--app/controllers/settings/imports_controller.rb34
-rw-r--r--app/models/import.rb14
-rw-r--r--app/views/layouts/admin.html.haml9
-rw-r--r--app/views/settings/imports/show.html.haml11
-rw-r--r--app/workers/import_worker.rb54
-rw-r--r--config/locales/en.yml8
-rw-r--r--config/locales/simple_form.en.yml4
-rw-r--r--config/navigation.rb1
-rw-r--r--config/routes.rb1
-rw-r--r--db/migrate/20170330163835_create_imports.rb11
-rw-r--r--db/migrate/20170330164118_add_attachment_data_to_imports.rb11
-rw-r--r--db/schema.rb14
-rw-r--r--spec/fabricators/import_fabricator.rb2
-rw-r--r--spec/models/import_spec.rb5
15 files changed, 184 insertions, 7 deletions
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: <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:
     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