about summary refs log tree commit diff
path: root/app/lib/activitypub/parser
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-01-19 22:37:27 +0100
committerGitHub <noreply@github.com>2022-01-19 22:37:27 +0100
commit1060666c583670bb3b89ed5154e61038331e30c3 (patch)
tree11713b72bc62cd395dade4cb4fe7e397bf41ffec /app/lib/activitypub/parser
parent2d1f082bb6bee89242ee8042dc19016179078566 (diff)
Add support for editing for published statuses (#16697)
* Add support for editing for published statuses

* Fix references to stripped-out code

* Various fixes and improvements

* Further fixes and improvements

* Fix updates being potentially sent to unauthorized recipients

* Various fixes and improvements

* Fix wrong words in test

* Fix notifying accounts that were tagged but were not in the audience

* Fix mistake
Diffstat (limited to 'app/lib/activitypub/parser')
-rw-r--r--app/lib/activitypub/parser/custom_emoji_parser.rb27
-rw-r--r--app/lib/activitypub/parser/media_attachment_parser.rb58
-rw-r--r--app/lib/activitypub/parser/poll_parser.rb53
-rw-r--r--app/lib/activitypub/parser/status_parser.rb118
4 files changed, 256 insertions, 0 deletions
diff --git a/app/lib/activitypub/parser/custom_emoji_parser.rb b/app/lib/activitypub/parser/custom_emoji_parser.rb
new file mode 100644
index 000000000..724c60215
--- /dev/null
+++ b/app/lib/activitypub/parser/custom_emoji_parser.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class ActivityPub::Parser::CustomEmojiParser
+  include JsonLdHelper
+
+  def initialize(json)
+    @json = json
+  end
+
+  def uri
+    @json['id']
+  end
+
+  def shortcode
+    @json['name']&.delete(':')
+  end
+
+  def image_remote_url
+    @json.dig('icon', 'url')
+  end
+
+  def updated_at
+    @json['updated']&.to_datetime
+  rescue ArgumentError
+    nil
+  end
+end
diff --git a/app/lib/activitypub/parser/media_attachment_parser.rb b/app/lib/activitypub/parser/media_attachment_parser.rb
new file mode 100644
index 000000000..1798e58a4
--- /dev/null
+++ b/app/lib/activitypub/parser/media_attachment_parser.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+class ActivityPub::Parser::MediaAttachmentParser
+  include JsonLdHelper
+
+  def initialize(json)
+    @json = json
+  end
+
+  # @param [MediaAttachment] previous_record
+  def significantly_changes?(previous_record)
+    remote_url != previous_record.remote_url ||
+      thumbnail_remote_url != previous_record.thumbnail_remote_url ||
+      description != previous_record.description
+  end
+
+  def remote_url
+    Addressable::URI.parse(@json['url'])&.normalize&.to_s
+  rescue Addressable::URI::InvalidURIError
+    nil
+  end
+
+  def thumbnail_remote_url
+    Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
+  rescue Addressable::URI::InvalidURIError
+    nil
+  end
+
+  def description
+    @json['summary'].presence || @json['name'].presence
+  end
+
+  def focus
+    @json['focalPoint']
+  end
+
+  def blurhash
+    supported_blurhash? ? @json['blurhash'] : nil
+  end
+
+  def file_content_type
+    @json['mediaType']
+  end
+
+  private
+
+  def supported_blurhash?
+    components = begin
+      blurhash = @json['blurhash']
+
+      if blurhash.present? && /^[\w#$%*+-.:;=?@\[\]^{|}~]+$/.match?(blurhash)
+        Blurhash.components(blurhash)
+      end
+    end
+
+    components.present? && components.none? { |comp| comp > 5 }
+  end
+end
diff --git a/app/lib/activitypub/parser/poll_parser.rb b/app/lib/activitypub/parser/poll_parser.rb
new file mode 100644
index 000000000..758c03f07
--- /dev/null
+++ b/app/lib/activitypub/parser/poll_parser.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class ActivityPub::Parser::PollParser
+  include JsonLdHelper
+
+  def initialize(json)
+    @json = json
+  end
+
+  def valid?
+    equals_or_includes?(@json['type'], 'Question') && items.is_a?(Array)
+  end
+
+  # @param [Poll] previous_record
+  def significantly_changes?(previous_record)
+    options != previous_record.options ||
+      multiple != previous_record.multiple
+  end
+
+  def options
+    items.filter_map { |item| item['name'].presence || item['content'] }
+  end
+
+  def multiple
+    @json['anyOf'].is_a?(Array)
+  end
+
+  def expires_at
+    if @json['closed'].is_a?(String)
+      @json['closed'].to_datetime
+    elsif !@json['closed'].nil? && !@json['closed'].is_a?(FalseClass)
+      Time.now.utc
+    else
+      @json['endTime']&.to_datetime
+    end
+  rescue ArgumentError
+    nil
+  end
+
+  def voters_count
+    @json['votersCount']
+  end
+
+  def cached_tallies
+    items.map { |item| item.dig('replies', 'totalItems') || 0 }
+  end
+
+  private
+
+  def items
+    @json['anyOf'] || @json['oneOf']
+  end
+end
diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb
new file mode 100644
index 000000000..3ba154d01
--- /dev/null
+++ b/app/lib/activitypub/parser/status_parser.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+class ActivityPub::Parser::StatusParser
+  include JsonLdHelper
+
+  # @param [Hash] json
+  # @param [Hash] magic_values
+  # @option magic_values [String] :followers_collection
+  def initialize(json, magic_values = {})
+    @json         = json
+    @object       = json['object'] || json
+    @magic_values = magic_values
+  end
+
+  def uri
+    id = @object['id']
+
+    if id&.start_with?('bear:')
+      Addressable::URI.parse(id).query_values['u']
+    else
+      id
+    end
+  rescue Addressable::URI::InvalidURIError
+    id
+  end
+
+  def url
+    url_to_href(@object['url'], 'text/html') if @object['url'].present?
+  end
+
+  def text
+    if @object['content'].present?
+      @object['content']
+    elsif content_language_map?
+      @object['contentMap'].values.first
+    end
+  end
+
+  def spoiler_text
+    if @object['summary'].present?
+      @object['summary']
+    elsif summary_language_map?
+      @object['summaryMap'].values.first
+    end
+  end
+
+  def title
+    if @object['name'].present?
+      @object['name']
+    elsif name_language_map?
+      @object['nameMap'].values.first
+    end
+  end
+
+  def created_at
+    @object['published']&.to_datetime
+  rescue ArgumentError
+    nil
+  end
+
+  def edited_at
+    @object['updated']&.to_datetime
+  rescue ArgumentError
+    nil
+  end
+
+  def reply
+    @object['inReplyTo'].present?
+  end
+
+  def sensitive
+    @object['sensitive']
+  end
+
+  def visibility
+    if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) }
+      :public
+    elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) }
+      :unlisted
+    elsif audience_to.include?(@magic_values[:followers_collection])
+      :private
+    else
+      :direct
+    end
+  end
+
+  def language
+    if content_language_map?
+      @object['contentMap'].keys.first
+    elsif name_language_map?
+      @object['nameMap'].keys.first
+    elsif summary_language_map?
+      @object['summaryMap'].keys.first
+    end
+  end
+
+  private
+
+  def audience_to
+    as_array(@object['to'] || @json['to']).map { |x| value_or_id(x) }
+  end
+
+  def audience_cc
+    as_array(@object['cc'] || @json['cc']).map { |x| value_or_id(x) }
+  end
+
+  def summary_language_map?
+    @object['summaryMap'].is_a?(Hash) && !@object['summaryMap'].empty?
+  end
+
+  def content_language_map?
+    @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
+  end
+
+  def name_language_map?
+    @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
+  end
+end