about summary refs log tree commit diff
path: root/app/services/import_service.rb
blob: 2f48abc364a2a3bfb9c275f53e438e8ca9265295 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# frozen_string_literal: true

require 'csv'

class ImportService < BaseService
  ROWS_PROCESSING_LIMIT = 20_000

  def call(import)
    @import  = import
    @account = @import.account

    case @import.type
    when 'following'
      import_follows!
    when 'blocking'
      import_blocks!
    when 'muting'
      import_mutes!
    when 'domain_blocking'
      import_domain_blocks!
    when 'bookmarks'
      import_bookmarks!
    end
  end

  private

  def import_follows!
    parse_import_data!(['Account address'])
    import_relationships!('follow', 'unfollow', @account.following, ROWS_PROCESSING_LIMIT, reblogs: { header: 'Show boosts', default: true }, notify: { header: 'Notify on new posts', default: false }, languages: { header: 'Languages', default: nil })
  end

  def import_blocks!
    parse_import_data!(['Account address'])
    import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT)
  end

  def import_mutes!
    parse_import_data!(['Account address'])
    import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: { header: 'Hide notifications', default: true })
  end

  def import_domain_blocks!
    parse_import_data!(['#domain'])
    items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#domain'].strip }

    if @import.overwrite?
      presence_hash = items.index_with(true)

      @account.domain_blocks.find_each do |domain_block|
        if presence_hash[domain_block.domain]
          items.delete(domain_block.domain)
        else
          @account.unblock_domain!(domain_block.domain)
        end
      end
    end

    items.each do |domain|
      @account.block_domain!(domain)
    end

    AfterAccountDomainBlockWorker.push_bulk(items) do |domain|
      [@account.id, domain]
    end
  end

  def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
    local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
    items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, field_settings| [key, row[field_settings[:header]]&.strip || field_settings[:default]] }]] }.reject { |(id, _)| id.blank? }

    if @import.overwrite?
      presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }

      overwrite_scope.find_each do |target_account|
        if presence_hash[target_account.acct]
          items.delete(target_account.acct)
          extra = presence_hash[target_account.acct][1]
          Import::RelationshipWorker.perform_async(@account.id, target_account.acct, action, extra.stringify_keys)
        else
          Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
        end
      end
    end

    head_items = items.uniq { |acct, _| acct.split('@')[1] }
    tail_items = items - head_items

    Import::RelationshipWorker.push_bulk(head_items + tail_items) do |acct, extra|
      [@account.id, acct, action, extra.stringify_keys]
    end
  end

  def import_bookmarks!
    parse_import_data!(['#uri'])
    items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#uri'].strip }

    if @import.overwrite?
      presence_hash = items.index_with(true)

      @account.bookmarks.find_each do |bookmark|
        if presence_hash[bookmark.status.uri]
          items.delete(bookmark.status.uri)
        else
          bookmark.destroy!
        end
      end
    end

    statuses = items.filter_map do |uri|
      status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
      next if status.nil? && ActivityPub::TagManager.instance.local_uri?(uri)

      status || ActivityPub::FetchRemoteStatusService.new.call(uri)
    rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::UnexpectedResponseError
      nil
    rescue StandardError => e
      Rails.logger.warn "Unexpected error when importing bookmark: #{e}"
      nil
    end

    account_ids         = statuses.map(&:account_id)
    preloaded_relations = relations_map_for_account(@account, account_ids)

    statuses.keep_if { |status| StatusPolicy.new(@account, status, preloaded_relations).show? }

    statuses.each do |status|
      @account.bookmarks.find_or_create_by!(account: @account, status: status)
    end
  end

  def parse_import_data!(default_headers)
    data = CSV.parse(import_data, headers: true)
    data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(' ')
    @data = data.reject(&:blank?)
  end

  def import_data
    Paperclip.io_adapters.for(@import.data).read.force_encoding(Encoding::UTF_8)
  end

  def relations_map_for_account(account, account_ids)
    {
      blocking: {},
      blocked_by: Account.blocked_by_map(account_ids, account.id),
      muting: {},
      following: Account.following_map(account_ids, account.id),
      domain_blocking_by_domain: {},
    }
  end
end