about summary refs log tree commit diff
path: root/app/services/process_feed_service.rb
blob: db69dfc6a28619487cef7444a9736e4e3af28481 (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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class ProcessFeedService < BaseService
  # Create local statuses from an Atom feed
  # @param [String] body Atom feed
  # @param [Account] account Account this feed belongs to
  def call(body, account)
    xml = Nokogiri::XML(body)

    # If we got a full feed, make sure the account's profile is up to date
    unless xml.at_xpath('/xmlns:feed').nil?
      update_remote_profile_service.(xml.at_xpath('/xmlns:feed/xmlns:author'), account)
    end

    # Process entries
    xml.xpath('//xmlns:entry').each do |entry|
      next unless [:note, :comment, :activity].include? object_type(entry)

      status = Status.find_by(uri: activity_id(entry))

      # If we already have a post and the verb is now "delete", we gotta delete it and move on!
      if !status.nil? && verb(entry) == :delete
        delete_post!(status)
        next
      end

      next unless status.nil?

      status = Status.new(uri: activity_id(entry), url: activity_link(entry), account: account, text: content(entry), created_at: published(entry), updated_at: updated(entry))

      if verb(entry) == :share
        add_reblog!(entry, status)
      elsif verb(entry) == :post
        if thread_id(entry).nil?
          add_post!(entry, status)
        else
          add_reply!(entry, status)
        end
      end

      # If we added a status, go through accounts it mentions and create respective relations
      unless status.new_record?
        entry.xpath('./xmlns:link[@rel="mentioned"]').each do |mention_link|
          # Here we have to do a reverse lookup of local accounts by their URL!
          # It's not pretty at all! I really wish all these protocols sticked to
          # using acct:username@domain only! It would make things so much easier
          # and tidier

          href = Addressable::URI.parse(mention_link.attribute('href').value)

          if href.host == Rails.configuration.x.local_domain
            mentioned_account = Account.find_local(href.path.gsub('/users/', ''))

            unless mentioned_account.nil?
              mentioned_account.mentions.where(status: status).first_or_create(status: status)
              NotificationMailer.mention(mentioned_account, status).deliver_later
            end
          end
        end

        fan_out_on_write_service.(status)
      end
    end
  end

  private

  def add_post!(_entry, status)
    status.save!
  end

  def add_reblog!(entry, status)
    status.reblog = find_original_status(entry, target_id(entry))

    if status.reblog.nil?
      status.reblog = fetch_remote_status(entry)
    end

    if !status.reblog.nil?
      status.save!
      NotificationMailer.reblog(status.reblog, status.account).deliver_later
    end
  end

  def add_reply!(entry, status)
    status.thread = find_original_status(entry, thread_id(entry))
    status.save!
  end

  def delete_post!(status)
    status.destroy!
  end

  def find_original_status(_xml, id)
    return nil if id.nil?

    if local_id?(id)
      Status.find(unique_tag_to_local_id(id, 'Status'))
    else
      Status.find_by(uri: id)
    end
  end

  def fetch_remote_status(xml)
    username = xml.at_xpath('./activity:object/xmlns:author/xmlns:name').content
    url      = xml.at_xpath('./activity:object/xmlns:author/xmlns:uri').content
    domain   = Addressable::URI.parse(url).host
    account  = Account.find_by(username: username, domain: domain)

    if account.nil?
      account = follow_remote_account_service.("#{username}@#{domain}", false)
      return nil if account.nil?
    end

    Status.new(account: account, uri: target_id(xml), text: target_content(xml), url: target_url(xml))
  end

  def published(xml)
    xml.at_xpath('./xmlns:published').content
  end

  def updated(xml)
    xml.at_xpath('./xmlns:updated').content
  end

  def content(xml)
    xml.at_xpath('./xmlns:content').content
  end

  def thread_id(xml)
    xml.at_xpath('./thr:in-reply-to').attribute('ref').value
  rescue
    nil
  end

  def target_id(xml)
    xml.at_xpath('.//activity:object/xmlns:id').content
  rescue
    nil
  end

  def activity_id(xml)
    xml.at_xpath('./xmlns:id').content
  end

  def activity_link(xml)
    xml.at_xpath('./xmlns:link[@rel="alternate"]').attribute('href').value
  rescue
    ''
  end

  def target_content(xml)
    xml.at_xpath('.//activity:object/xmlns:content').content
  end

  def target_url(xml)
    xml.at_xpath('.//activity:object/xmlns:link[@rel="alternate"]').attribute('href').value
  end

  def object_type(xml)
    xml.at_xpath('./activity:object-type').content.gsub('http://activitystrea.ms/schema/1.0/', '').to_sym
  rescue
    :note
  end

  def verb(xml)
    xml.at_xpath('./activity:verb').content.gsub('http://activitystrea.ms/schema/1.0/', '').to_sym
  rescue
    :post
  end

  def follow_remote_account_service
    @follow_remote_account_service ||= FollowRemoteAccountService.new
  end

  def update_remote_profile_service
    @update_remote_profile_service ||= UpdateRemoteProfileService.new
  end

  def fan_out_on_write_service
    @fan_out_on_write_service ||= FanOutOnWriteService.new
  end
end