From 9239e4ce4d4e958e62552d4a01183d0295c020f5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 14 Sep 2017 00:04:30 +0200 Subject: Uploads for admin site settings (#4913) * Improve OpenGraph tags for about pages * Add thumbnail admin setting * Fix error * Fix up --- app/serializers/rest/instance_serializer.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'app/serializers/rest') diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index a97137909..2898011fd 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class REST::InstanceSerializer < ActiveModel::Serializer + include RoutingHelper + attributes :uri, :title, :description, :email, - :version, :urls, :stats + :version, :urls, :stats, :thumbnail def uri Rails.configuration.x.local_domain @@ -24,6 +26,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer Mastodon::Version.to_s end + def thumbnail + full_asset_url(instance_presenter.thumbnail.file.url) if instance_presenter.thumbnail + end + def stats { user_count: instance_presenter.user_count, -- cgit From 54edb4b853522b322922b65989ddfc5fdf928014 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 16 Sep 2017 03:01:45 +0200 Subject: When accessing uncached media attachment, redownload it (#4955) * When accessing uncached media attachment, redownload it * Prevent re-download of rejected media --- app/controllers/media_proxy_controller.rb | 40 ++++++++++++++++++++++ app/models/media_attachment.rb | 10 ++++-- .../rest/media_attachment_serializer.rb | 12 +++++-- config/routes.rb | 2 ++ lib/tasks/mastodon.rake | 1 - 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 app/controllers/media_proxy_controller.rb (limited to 'app/serializers/rest') diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb new file mode 100644 index 000000000..6a83cf9dc --- /dev/null +++ b/app/controllers/media_proxy_controller.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class MediaProxyController < ApplicationController + include RoutingHelper + + def show + RedisLock.acquire(lock_options) do |lock| + if lock.acquired? + @media_attachment = MediaAttachment.remote.find(params[:id]) + redownload! if @media_attachment.needs_redownload? && !reject_media? + end + end + + redirect_to full_asset_url(@media_attachment.file.url(version)) + end + + private + + def redownload! + @media_attachment.file_remote_url = @media_attachment.remote_url + @media_attachment.touch(:created_at) + @media_attachment.save! + end + + def version + if request.path.ends_with?('/small') + :small + else + :original + end + end + + def lock_options + { redis: Redis.current, key: "media_download:#{params[:id]}" } + end + + def reject_media? + DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media? + end +end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index d83ca44f1..d913e7372 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -56,15 +56,21 @@ class MediaAttachment < ApplicationRecord validates :account, presence: true - scope :attached, -> { where.not(status_id: nil) } + scope :attached, -> { where.not(status_id: nil) } scope :unattached, -> { where(status_id: nil) } - scope :local, -> { where(remote_url: '') } + scope :local, -> { where(remote_url: '') } + scope :remote, -> { where.not(remote_url: '') } + default_scope { order(id: :asc) } def local? remote_url.blank? end + def needs_redownload? + file.blank? && remote_url.present? + end + def to_param shortcode end diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb index 9055b8db4..31189406a 100644 --- a/app/serializers/rest/media_attachment_serializer.rb +++ b/app/serializers/rest/media_attachment_serializer.rb @@ -7,11 +7,19 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer :remote_url, :text_url, :meta def url - full_asset_url(object.file.url(:original)) + if object.needs_redownload? + media_proxy_url(object.id, :original) + else + full_asset_url(object.file.url(:original)) + end end def preview_url - full_asset_url(object.file.url(:small)) + if object.needs_redownload? + media_proxy_url(object.id, :small) + else + full_asset_url(object.file.url(:small)) + end end def text_url diff --git a/config/routes.rb b/config/routes.rb index 63e94fb49..bf5428869 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -98,6 +98,8 @@ Rails.application.routes.draw do resources :media, only: [:show] resources :tags, only: [:show] + get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy + # Remote follow resource :authorize_follow, only: [:show, :create] resource :share, only: [:show, :create] diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 2aef752ca..5614ddf48 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -83,7 +83,6 @@ namespace :mastodon do MediaAttachment.where.not(remote_url: '').where('created_at < ?', time_ago).find_each do |media| media.file.destroy - media.type = :unknown media.save end end -- cgit From 81cec35dbf1b348d23363559e3f4e6b1ec3415c5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 19 Sep 2017 02:42:40 +0200 Subject: Custom emoji (#4988) * Custom emoji - In OStatus: `` - In ActivityPub: `{ type: "Emoji", name: ":coolcat:", href: "http://..." }` - In REST API: Status object includes `emojis` array (`shortcode`, `url`) - Domain blocks with reject media stop emojis - Emoji file up to 50KB - Web UI handles custom emojis - Static pages render custom emojis as `` tags Side effects: - Undo #4500 optimization, as I needed to modify it to restore shortcode handling in emojify() - Formatter#plaintext should now make sure stripped out line-breaks and paragraphs are replaced with newlines * Fix emoji at the start not being converted --- app/javascript/mastodon/emoji.js | 60 ++++++++++------ app/javascript/mastodon/reducers/statuses.js | 9 ++- app/lib/activitypub/activity/create.rb | 13 ++++ app/lib/formatter.rb | 54 +++++++++++++- app/lib/ostatus/activity/creation.rb | 20 ++++++ app/lib/ostatus/atom_serializer.rb | 4 ++ app/models/custom_emoji.rb | 38 ++++++++++ app/models/status.rb | 4 ++ app/serializers/activitypub/note_serializer.rb | 20 +++++- app/serializers/rest/status_serializer.rb | 11 +++ .../stream_entries/_detailed_status.html.haml | 2 +- app/views/stream_entries/_simple_status.html.haml | 2 +- db/migrate/20170917153509_create_custom_emojis.rb | 13 ++++ db/schema.rb | 14 +++- spec/fabricators/custom_emoji_fabricator.rb | 5 ++ spec/fixtures/files/emojo.png | Bin 0 -> 29814 bytes spec/lib/activitypub/activity/create_spec.rb | 25 +++++++ spec/lib/formatter_spec.rb | 78 +++++++++++++++++++++ spec/lib/ostatus/atom_serializer_spec.rb | 16 ++++- spec/models/custom_emoji_spec.rb | 25 +++++++ 20 files changed, 382 insertions(+), 31 deletions(-) create mode 100644 app/models/custom_emoji.rb create mode 100644 db/migrate/20170917153509_create_custom_emojis.rb create mode 100644 spec/fabricators/custom_emoji_fabricator.rb create mode 100644 spec/fixtures/files/emojo.png create mode 100644 spec/models/custom_emoji_spec.rb (limited to 'app/serializers/rest') diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index a41dfdd1d..865b85b61 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -3,28 +3,48 @@ import Trie from 'substring-trie'; const trie = new Trie(Object.keys(unicodeMapping)); -const emojify = str => { - let rtn = ''; - for (;;) { - let match, i = 0; - while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) { - i += str.codePointAt(i) < 65536 ? 1 : 2; - } - if (i === str.length) - break; - else if (str[i] === '<') { - let tagend = str.indexOf('>', i + 1) + 1; - if (!tagend) - break; - rtn += str.slice(0, tagend); - str = str.slice(tagend); - } else { - const [filename, shortCode] = unicodeMapping[match]; - rtn += str.slice(0, i) + `${match}`; - str = str.slice(i + match.length); +const emojify = (str, customEmojis = {}) => { + // This walks through the string from start to end, ignoring any tags (

,
, etc.) + // and replacing valid unicode strings + // that _aren't_ within tags with an version. + // The goal is to be the same as an emojione.regUnicode replacement, but faster. + let i = -1; + let insideTag = false; + let insideShortname = false; + let shortnameStartIndex = -1; + let match; + while (++i < str.length) { + const char = str.charAt(i); + if (insideShortname && char === ':') { + const shortname = str.substring(shortnameStartIndex, i + 1); + if (shortname in customEmojis) { + const replacement = `${shortname}`; + str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1); + i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string + } else { + i--; + } + insideShortname = false; + } else if (insideTag && char === '>') { + insideTag = false; + } else if (char === '<') { + insideTag = true; + insideShortname = false; + } else if (!insideTag && char === ':') { + insideShortname = true; + shortnameStartIndex = i; + } else if (!insideTag && (match = trie.search(str.substring(i)))) { + const unicodeStr = match; + if (unicodeStr in unicodeMapping) { + const [filename, shortCode] = unicodeMapping[unicodeStr]; + const alt = unicodeStr; + const replacement = `${alt}`; + str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); + i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string + } } } - return rtn + str; + return str; }; export default emojify; diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 7f906bef6..38b23504e 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -58,9 +58,14 @@ const normalizeStatus = (state, status) => { } const searchContent = [status.spoiler_text, status.content].join(' ').replace(/
/g, '\n').replace(/<\/p>

/g, '\n\n'); + const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji.url; + return obj; + }, {}); + normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || '')); + normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); }; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 894759d9a..41f2b0bad 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -61,6 +61,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity process_hashtag tag, status when 'Mention' process_mention tag, status + when 'Emoji' + process_emoji tag, status end end end @@ -79,6 +81,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity account.mentions.create(status: status) end + def process_emoji(tag, _status) + shortcode = tag['name'].delete(':') + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain) + + return if !emoji.nil? || skip_download? + + emoji = CustomEmoji.new(domain: @account.domain, shortcode: shortcode) + emoji.image_remote_url = tag['href'] + emoji.save + end + def process_attachments(status) return unless @object['attachment'].is_a?(Array) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 575830190..29fea27de 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -9,7 +9,7 @@ class Formatter include ActionView::Helpers::TextHelper - def format(status) + def format(status, options = {}) if status.reblog? prepend_reblog = status.reblog.account.acct status = status.proper @@ -19,7 +19,11 @@ class Formatter raw_content = status.text - return reformat(raw_content) unless status.local? + unless status.local? + html = reformat(raw_content) + html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify] + return html + end linkable_accounts = status.mentions.map(&:account) linkable_accounts << status.account @@ -27,6 +31,7 @@ class Formatter html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog html = encode_and_link_urls(html, linkable_accounts) + html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify] html = simple_format(html, {}, sanitize: false) html = html.delete("\n") @@ -39,7 +44,9 @@ class Formatter def plaintext(status) return status.text if status.local? - strip_tags(status.text) + + text = status.text.gsub(/(
|
|<\/p>)+/) { |match| "#{match}\n" } + strip_tags(text) end def simplified_format(account) @@ -76,6 +83,47 @@ class Formatter end end + def encode_custom_emojis(html, emojis) + return html if emojis.empty? + + emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h + + i = -1 + inside_tag = false + inside_shortname = false + shortname_start_index = -1 + + while i + 1 < html.size + i += 1 + + if inside_shortname && html[i] == ':' + shortcode = html[shortname_start_index + 1..i - 1] + emoji = emoji_map[shortcode] + + if emoji + replacement = "\":#{shortcode}:\"" + before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : '' + html = before_html + replacement + html[i + 1..-1] + i += replacement.size - (shortcode.size + 2) - 1 + else + i -= 1 + end + + inside_shortname = false + elsif inside_tag && html[i] == '>' + inside_tag = false + elsif html[i] == '<' + inside_tag = true + inside_shortname = false + elsif !inside_tag && html[i] == ':' + inside_shortname = true + shortname_start_index = i + end + end + + html + end + def rewrite(text, entities) chars = text.to_s.to_char_a diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index 1a23c9efa..d3f1629c4 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -42,6 +42,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base save_mentions(status) save_hashtags(status) save_media(status) + save_emojis(status) end if thread? && status.thread.nil? @@ -150,6 +151,25 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end end + def save_emojis(parent) + do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + + return if do_not_download + + @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link| + next unless link['href'] && link['name'] + + shortcode = link['name'].delete(':') + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) + + next unless emoji.nil? + + emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) + emoji.image_remote_url = link['href'] + emoji.save + end + end + def account_from_href(href) url = Addressable::URI.parse(href).normalize diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index b8e22a381..a6a5cb0c4 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -368,5 +368,9 @@ class OStatus::AtomSerializer end append_element(entry, 'mastodon:scope', status.visibility) + + status.emojis.each do |emoji| + append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode) + end end end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb new file mode 100644 index 000000000..f4d3b16a0 --- /dev/null +++ b/app/models/custom_emoji.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: custom_emojis +# +# id :integer not null, primary key +# shortcode :string default(""), not null +# domain :string +# image_file_name :string +# image_content_type :string +# image_file_size :integer +# image_updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class CustomEmoji < ApplicationRecord + SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}' + + SCAN_RE = /(?<=[^[:alnum:]:]|\n|^) + :(#{SHORTCODE_RE_FRAGMENT}): + (?=[^[:alnum:]:]|$)/x + + has_attached_file :image + + validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes } + validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 } + + include Remotable + + class << self + def from_text(text, domain) + return [] if text.blank? + shortcodes = text.scan(SCAN_RE).map(&:first) + where(shortcode: shortcodes, domain: domain) + end + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 2a2cdcf6e..326d128d6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -131,6 +131,10 @@ class Status < ApplicationRecord !sensitive? && media_attachments.any? end + def emojis + CustomEmoji.from_text(text, account.domain) + end + after_create :store_uri, if: :local? before_validation :prepare_contents, if: :local? diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 166214eee..e5d8e3f03 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -57,7 +57,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end def virtual_tags - object.mentions + object.tags + object.mentions + object.tags + object.emojis end def atom_uri @@ -137,4 +137,22 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer "##{object.name}" end end + + class CustomEmojiSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :type, :href, :name + + def type + 'Emoji' + end + + def href + full_asset_url(object.image.url) + end + + def name + ":#{object.shortcode}:" + end + end end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 298a3bb40..d8efa8e60 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -17,6 +17,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :media_attachments, serializer: REST::MediaAttachmentSerializer has_many :mentions has_many :tags + has_many :emojis def current_user? !current_user.nil? @@ -106,4 +107,14 @@ class REST::StatusSerializer < ActiveModel::Serializer tag_url(object) end end + + class CustomEmojiSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :shortcode, :url + + def url + full_asset_url(object.image.url) + end + end end diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index dd9456260..692d5a6d5 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -17,7 +17,7 @@ %p{ style: 'margin-bottom: 0' }< %span.p-summary> #{status.spoiler_text}  %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') - .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) + .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true) - if !status.media_attachments.empty? - if status.media_attachments.first.video? diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index 55aa97f32..f9a530d38 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -18,7 +18,7 @@ %p{ style: 'margin-bottom: 0' }< %span.p-summary> #{status.spoiler_text}  %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') - .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) + .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true) - unless status.media_attachments.empty? - if status.media_attachments.first.video? diff --git a/db/migrate/20170917153509_create_custom_emojis.rb b/db/migrate/20170917153509_create_custom_emojis.rb new file mode 100644 index 000000000..4040c8312 --- /dev/null +++ b/db/migrate/20170917153509_create_custom_emojis.rb @@ -0,0 +1,13 @@ +class CreateCustomEmojis < ActiveRecord::Migration[5.1] + def change + create_table :custom_emojis do |t| + t.string :shortcode, null: false, default: '' + t.string :domain + t.attachment :image + + t.timestamps + end + + add_index :custom_emojis, [:shortcode, :domain], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index f2ca2af69..9f42d46dd 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: 20170913000752) do +ActiveRecord::Schema.define(version: 20170917153509) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -89,6 +89,18 @@ ActiveRecord::Schema.define(version: 20170913000752) do t.index ["uri"], name: "index_conversations_on_uri", unique: true end + create_table "custom_emojis", force: :cascade do |t| + t.string "shortcode", default: "", null: false + t.string "domain" + t.string "image_file_name" + t.string "image_content_type" + t.integer "image_file_size" + t.datetime "image_updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true + end + create_table "domain_blocks", id: :serial, force: :cascade do |t| t.string "domain", default: "", null: false t.datetime "created_at", null: false diff --git a/spec/fabricators/custom_emoji_fabricator.rb b/spec/fabricators/custom_emoji_fabricator.rb new file mode 100644 index 000000000..18a7d23dc --- /dev/null +++ b/spec/fabricators/custom_emoji_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:custom_emoji) do + shortcode 'coolcat' + domain nil + image { File.open(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png')) } +end diff --git a/spec/fixtures/files/emojo.png b/spec/fixtures/files/emojo.png new file mode 100644 index 000000000..cb5993499 Binary files /dev/null and b/spec/fixtures/files/emojo.png differ diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index fcb044ebc..1a9520f04 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -17,6 +17,7 @@ RSpec.describe ActivityPub::Activity::Create do before do stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt')) + stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png')) end describe '#perform' do @@ -217,5 +218,29 @@ RSpec.describe ActivityPub::Activity::Create do expect(status.tags.map(&:name)).to include('test') end end + + context 'with emojis' do + let(:object_json) do + { + id: 'bar', + type: 'Note', + content: 'Lorem ipsum :tinking:', + tag: [ + { + type: 'Emoji', + href: 'http://example.com/emoji.png', + name: 'tinking', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.emojis.map(&:shortcode)).to include('tinking') + end + end end end diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb index b714b317a..71b6b78d2 100644 --- a/spec/lib/formatter_spec.rb +++ b/spec/lib/formatter_spec.rb @@ -223,6 +223,45 @@ RSpec.describe Formatter do include_examples 'encode and link URLs' end + + context 'with custom_emojify option' do + let!(:emoji) { Fabricate(:custom_emoji) } + let(:status) { Fabricate(:status, account: local_account, text: text) } + + subject { Formatter.instance.format(status, custom_emojify: true) } + + context 'with emoji at the start' do + let(:text) { ':coolcat: Beep boop' } + + it 'converts shortcode to image tag' do + is_expected.to match(/

:coolcat::coolcat: Beep boop
' } + + it 'converts shortcode to image tag' do + is_expected.to match(/

:coolcat:Beep :coolcat: boop

' } + + it 'converts shortcode to image tag' do + is_expected.to match(/Beep :coolcat::coolcat::coolcat:

' } + + it 'does not touch the shortcodes' do + is_expected.to match(/

:coolcat::coolcat:<\/p>/) + end + end + + context 'with emoji at the end' do + let(:text) { '

Beep boop
:coolcat:

' } + + it 'converts shortcode to image tag' do + is_expected.to match(/
:coolcat:Hello :coolcat:

' } + + it 'returns records used via shortcodes in text' do + is_expected.to include(emojo) + end + end + end +end -- cgit From bb4d005a8381091911697175416eb9c37379155e Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Wed, 20 Sep 2017 01:08:08 +0900 Subject: Introduce OStatus::TagManager (#5008) --- app/lib/activitypub/activity/create.rb | 2 +- app/lib/activitypub/tag_manager.rb | 4 +- app/lib/ostatus/activity/base.rb | 18 ++-- app/lib/ostatus/activity/creation.rb | 30 +++--- app/lib/ostatus/activity/share.rb | 2 +- app/lib/ostatus/atom_serializer.rb | 106 ++++++++++----------- app/lib/ostatus/tag_manager.rb | 73 ++++++++++++++ app/lib/tag_manager.rb | 67 ------------- app/models/remote_profile.rb | 12 +-- app/serializers/activitypub/delete_serializer.rb | 2 +- app/serializers/activitypub/note_serializer.rb | 6 +- app/serializers/rest/status_serializer.rb | 2 +- app/services/concerns/author_extractor.rb | 6 +- app/services/fetch_remote_account_service.rb | 2 +- app/services/fetch_remote_status_service.rb | 2 +- app/services/process_feed_service.rb | 2 +- app/services/process_interaction_service.rb | 16 ++-- app/services/verify_salmon_service.rb | 2 +- spec/lib/activitypub/tag_manager_spec.rb | 2 +- spec/lib/ostatus/atom_serializer_spec.rb | 90 ++++++++--------- spec/lib/ostatus/tag_manager_spec.rb | 70 ++++++++++++++ spec/lib/tag_manager_spec.rb | 65 ------------- spec/services/authorize_follow_service_spec.rb | 2 +- .../services/batched_remove_status_service_spec.rb | 4 +- spec/services/block_service_spec.rb | 2 +- spec/services/favourite_service_spec.rb | 2 +- spec/services/follow_service_spec.rb | 4 +- spec/services/reject_follow_service_spec.rb | 2 +- spec/services/remove_status_service_spec.rb | 4 +- spec/services/unblock_service_spec.rb | 2 +- spec/services/unfollow_service_spec.rb | 2 +- 31 files changed, 308 insertions(+), 297 deletions(-) create mode 100644 app/lib/ostatus/tag_manager.rb create mode 100644 spec/lib/ostatus/tag_manager_spec.rb (limited to 'app/serializers/rest') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 41f2b0bad..0964c9f53 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -115,7 +115,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def conversation_from_uri(uri) return nil if uri.nil? - return Conversation.find_by(id: TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if TagManager.instance.local_id?(uri) + return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri) Conversation.find_by(uri: uri) || Conversation.create!(uri: uri) end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 1b4e271db..4ec3b8c56 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -98,8 +98,8 @@ class ActivityPub::TagManager else StatusFinder.new(uri).status end - elsif ::TagManager.instance.local_id?(uri) - klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s)) + elsif OStatus::TagManager.instance.local_id?(uri) + klass.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s)) else klass.find_by(uri: uri.split('#').first) end diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb index 1dc7abee3..039381397 100644 --- a/app/lib/ostatus/activity/base.rb +++ b/app/lib/ostatus/activity/base.rb @@ -11,30 +11,30 @@ class OStatus::Activity::Base end def verb - raw = @xml.at_xpath('./activity:verb', activity: TagManager::AS_XMLNS).content - TagManager::VERBS.key(raw) + raw = @xml.at_xpath('./activity:verb', activity: OStatus::TagManager::AS_XMLNS).content + OStatus::TagManager::VERBS.key(raw) rescue :post end def type - raw = @xml.at_xpath('./activity:object-type', activity: TagManager::AS_XMLNS).content - TagManager::TYPES.key(raw) + raw = @xml.at_xpath('./activity:object-type', activity: OStatus::TagManager::AS_XMLNS).content + OStatus::TagManager::TYPES.key(raw) rescue :activity end def id - @xml.at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content + @xml.at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content end def url - link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' } + link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' } link.nil? ? nil : link['href'] end def activitypub_uri - link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) } + link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) } link.nil? ? nil : link['href'] end @@ -45,8 +45,8 @@ class OStatus::Activity::Base private def find_status(uri) - if TagManager.instance.local_id?(uri) - local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status') + if OStatus::TagManager.instance.local_id?(uri) + local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status') return Status.find_by(id: local_id) elsif ActivityPub::TagManager.instance.local_uri?(uri) local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri) diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index d3f1629c4..4f4ef2971 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -63,42 +63,42 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end def content - @xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content + @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS).content end def content_language - @xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en' + @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS)['xml:lang']&.presence || 'en' end def content_warning - @xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || '' + @xml.at_xpath('./xmlns:summary', xmlns: OStatus::TagManager::XMLNS)&.content || '' end def visibility_scope - @xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public + @xml.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content&.to_sym || :public end def published - @xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content + @xml.at_xpath('./xmlns:published', xmlns: OStatus::TagManager::XMLNS).content end def thread? - !@xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS).nil? + !@xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS).nil? end def thread - thr = @xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS) + thr = @xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS) [thr['ref'], thr['href']] end private def find_or_create_conversation - uri = @xml.at_xpath('./ostatus:conversation', ostatus: TagManager::OS_XMLNS)&.attribute('ref')&.content + uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content return if uri.nil? - if TagManager.instance.local_id?(uri) - local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Conversation') + if OStatus::TagManager.instance.local_id?(uri) + local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation') return Conversation.find_by(id: local_id) end @@ -108,8 +108,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base def save_mentions(parent) processed_account_ids = [] - @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link| - next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type'] + @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each do |link| + next if [OStatus::TagManager::TYPES[:group], OStatus::TagManager::TYPES[:collection]].include? link['ostatus:object-type'] mentioned_account = account_from_href(link['href']) @@ -123,14 +123,14 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end def save_hashtags(parent) - tags = @xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?) + tags = @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).map { |category| category['term'] }.select(&:present?) ProcessHashtagsService.new.call(parent, tags) end def save_media(parent) do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? - @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link| + @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link| next unless link['href'] media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href']) @@ -156,7 +156,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base return if do_not_download - @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link| + @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS).each do |link| next unless link['href'] && link['name'] shortcode = link['name'].delete(':') diff --git a/app/lib/ostatus/activity/share.rb b/app/lib/ostatus/activity/share.rb index 290008021..5ca601415 100644 --- a/app/lib/ostatus/activity/share.rb +++ b/app/lib/ostatus/activity/share.rb @@ -10,7 +10,7 @@ class OStatus::Activity::Share < OStatus::Activity::Creation end def object - @xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS) + @xml.at_xpath('.//activity:object', activity: OStatus::TagManager::AS_XMLNS) end private diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index a6a5cb0c4..a1ac11a51 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -15,10 +15,10 @@ class OStatus::AtomSerializer def author(account) author = Ox::Element.new('author') - uri = TagManager.instance.uri_for(account) + uri = OStatus::TagManager.instance.uri_for(account) append_element(author, 'id', uri) - append_element(author, 'activity:object-type', TagManager::TYPES[:person]) + append_element(author, 'activity:object-type', OStatus::TagManager::TYPES[:person]) append_element(author, 'uri', uri) append_element(author, 'name', account.username) append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct) @@ -65,15 +65,15 @@ class OStatus::AtomSerializer add_namespaces(entry) if root - append_element(entry, 'id', TagManager.instance.uri_for(stream_entry.status)) + append_element(entry, 'id', OStatus::TagManager.instance.uri_for(stream_entry.status)) append_element(entry, 'published', stream_entry.created_at.iso8601) append_element(entry, 'updated', stream_entry.updated_at.iso8601) append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status") entry << author(stream_entry.account) if root - append_element(entry, 'activity:object-type', TagManager::TYPES[stream_entry.object_type]) - append_element(entry, 'activity:verb', TagManager::VERBS[stream_entry.verb]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[stream_entry.object_type]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[stream_entry.verb]) entry << object(stream_entry.target) if stream_entry.targeted? @@ -88,7 +88,7 @@ class OStatus::AtomSerializer append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(stream_entry.status)) append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')) - append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded? + append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded? append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil? entry @@ -97,20 +97,20 @@ class OStatus::AtomSerializer def object(status) object = Ox::Element.new('activity:object') - append_element(object, 'id', TagManager.instance.uri_for(status)) + append_element(object, 'id', OStatus::TagManager.instance.uri_for(status)) append_element(object, 'published', status.created_at.iso8601) append_element(object, 'updated', status.updated_at.iso8601) append_element(object, 'title', status.title) object << author(status.account) - append_element(object, 'activity:object-type', TagManager::TYPES[status.object_type]) - append_element(object, 'activity:verb', TagManager::VERBS[status.verb]) + append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[status.object_type]) + append_element(object, 'activity:verb', OStatus::TagManager::VERBS[status.verb]) serialize_status_attributes(object, status) append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(status)) - append_element(object, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(status.thread), href: TagManager.instance.url_for(status.thread)) unless status.thread.nil? + append_element(object, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(status.thread), href: TagManager.instance.url_for(status.thread)) unless status.thread.nil? append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil? object @@ -122,14 +122,14 @@ class OStatus::AtomSerializer description = "#{follow.account.acct} started following #{follow.target_account.acct}" - append_element(entry, 'id', TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow')) append_element(entry, 'title', description) append_element(entry, 'content', description, type: :html) entry << author(follow.account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:follow]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:follow]) object = author(follow.target_account) object.value = 'activity:object' @@ -142,13 +142,13 @@ class OStatus::AtomSerializer entry = Ox::Element.new('entry') add_namespaces(entry) - append_element(entry, 'id', TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest')) append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}") entry << author(follow_request.account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:request_friend]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) object = author(follow_request.target_account) object.value = 'activity:object' @@ -161,19 +161,19 @@ class OStatus::AtomSerializer entry = Ox::Element.new('entry') add_namespaces(entry) - append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}") entry << author(follow_request.target_account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:authorize]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:authorize]) object = Ox::Element.new('activity:object') object << author(follow_request.account) - append_element(object, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(object, 'activity:verb', TagManager::VERBS[:request_friend]) + append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) inner_object = author(follow_request.target_account) inner_object.value = 'activity:object' @@ -187,19 +187,19 @@ class OStatus::AtomSerializer entry = Ox::Element.new('entry') add_namespaces(entry) - append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}") entry << author(follow_request.target_account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:reject]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:reject]) object = Ox::Element.new('activity:object') object << author(follow_request.account) - append_element(object, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(object, 'activity:verb', TagManager::VERBS[:request_friend]) + append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) inner_object = author(follow_request.target_account) inner_object.value = 'activity:object' @@ -215,14 +215,14 @@ class OStatus::AtomSerializer description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}" - append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow')) append_element(entry, 'title', description) append_element(entry, 'content', description, type: :html) entry << author(follow.account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:unfollow]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfollow]) object = author(follow.target_account) object.value = 'activity:object' @@ -237,13 +237,13 @@ class OStatus::AtomSerializer description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}" - append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) append_element(entry, 'title', description) entry << author(block.account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:block]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:block]) object = author(block.target_account) object.value = 'activity:object' @@ -258,13 +258,13 @@ class OStatus::AtomSerializer description = "#{block.account.acct} no longer blocks #{block.target_account.acct}" - append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) append_element(entry, 'title', description) entry << author(block.account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:unblock]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unblock]) object = author(block.target_account) object.value = 'activity:object' @@ -279,18 +279,18 @@ class OStatus::AtomSerializer description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}" - append_element(entry, 'id', TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite')) append_element(entry, 'title', description) append_element(entry, 'content', description, type: :html) entry << author(favourite.account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:favorite]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:favorite]) entry << object(favourite.status) - append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status)) + append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status)) entry end @@ -301,18 +301,18 @@ class OStatus::AtomSerializer description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}" - append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite')) + append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite')) append_element(entry, 'title', description) append_element(entry, 'content', description, type: :html) entry << author(favourite.account) - append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', TagManager::VERBS[:unfavorite]) + append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfavorite]) entry << object(favourite.status) - append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status)) + append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status)) entry end @@ -332,17 +332,17 @@ class OStatus::AtomSerializer def conversation_uri(conversation) return conversation.uri if conversation.uri? - TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation') + OStatus::TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation') end def add_namespaces(parent) - parent['xmlns'] = TagManager::XMLNS - parent['xmlns:thr'] = TagManager::THR_XMLNS - parent['xmlns:activity'] = TagManager::AS_XMLNS - parent['xmlns:poco'] = TagManager::POCO_XMLNS - parent['xmlns:media'] = TagManager::MEDIA_XMLNS - parent['xmlns:ostatus'] = TagManager::OS_XMLNS - parent['xmlns:mastodon'] = TagManager::MTDN_XMLNS + parent['xmlns'] = OStatus::TagManager::XMLNS + parent['xmlns:thr'] = OStatus::TagManager::THR_XMLNS + parent['xmlns:activity'] = OStatus::TagManager::AS_XMLNS + parent['xmlns:poco'] = OStatus::TagManager::POCO_XMLNS + parent['xmlns:media'] = OStatus::TagManager::MEDIA_XMLNS + parent['xmlns:ostatus'] = OStatus::TagManager::OS_XMLNS + parent['xmlns:mastodon'] = OStatus::TagManager::MTDN_XMLNS end def serialize_status_attributes(entry, status) @@ -352,10 +352,10 @@ class OStatus::AtomSerializer append_element(entry, 'content', Formatter.instance.format(status).to_str, type: 'html', 'xml:lang': status.language) status.mentions.each do |mentioned| - append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account)) + append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account)) end - append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:collection], href: TagManager::COLLECTIONS[:public]) if status.public_visibility? + append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:collection], href: OStatus::TagManager::COLLECTIONS[:public]) if status.public_visibility? status.tags.each do |tag| append_element(entry, 'category', nil, term: tag.name) diff --git a/app/lib/ostatus/tag_manager.rb b/app/lib/ostatus/tag_manager.rb new file mode 100644 index 000000000..4f4501312 --- /dev/null +++ b/app/lib/ostatus/tag_manager.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +class OStatus::TagManager + include Singleton + include RoutingHelper + + VERBS = { + post: 'http://activitystrea.ms/schema/1.0/post', + share: 'http://activitystrea.ms/schema/1.0/share', + favorite: 'http://activitystrea.ms/schema/1.0/favorite', + unfavorite: 'http://activitystrea.ms/schema/1.0/unfavorite', + delete: 'http://activitystrea.ms/schema/1.0/delete', + follow: 'http://activitystrea.ms/schema/1.0/follow', + request_friend: 'http://activitystrea.ms/schema/1.0/request-friend', + authorize: 'http://activitystrea.ms/schema/1.0/authorize', + reject: 'http://activitystrea.ms/schema/1.0/reject', + unfollow: 'http://ostatus.org/schema/1.0/unfollow', + block: 'http://mastodon.social/schema/1.0/block', + unblock: 'http://mastodon.social/schema/1.0/unblock', + }.freeze + + TYPES = { + activity: 'http://activitystrea.ms/schema/1.0/activity', + note: 'http://activitystrea.ms/schema/1.0/note', + comment: 'http://activitystrea.ms/schema/1.0/comment', + person: 'http://activitystrea.ms/schema/1.0/person', + collection: 'http://activitystrea.ms/schema/1.0/collection', + group: 'http://activitystrea.ms/schema/1.0/group', + }.freeze + + COLLECTIONS = { + public: 'http://activityschema.org/collection/public', + }.freeze + + XMLNS = 'http://www.w3.org/2005/Atom' + MEDIA_XMLNS = 'http://purl.org/syndication/atommedia' + AS_XMLNS = 'http://activitystrea.ms/spec/1.0/' + THR_XMLNS = 'http://purl.org/syndication/thread/1.0' + POCO_XMLNS = 'http://portablecontacts.net/spec/1.0' + DFRN_XMLNS = 'http://purl.org/macgirvin/dfrn/1.0' + OS_XMLNS = 'http://ostatus.org/schema/1.0' + MTDN_XMLNS = 'http://mastodon.social/schema/1.0' + + def unique_tag(date, id, type) + "tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}" + end + + def unique_tag_to_local_id(tag, expected_type) + return nil unless local_id?(tag) + + if ActivityPub::TagManager.instance.local_uri?(tag) + ActivityPub::TagManager.instance.uri_to_local_id(tag) + else + matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) + return matches[1] unless matches.nil? + end + end + + def local_id?(id) + id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id) + end + + def uri_for(target) + return target.uri if target.respond_to?(:local?) && !target.local? + + case target.object_type + when :person + account_url(target) + when :note, :comment, :activity + target.uri || unique_tag(target.created_at, target.id, 'Status') + end + end +end diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index 1d0a24e42..fb364cb98 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -6,62 +6,6 @@ class TagManager include Singleton include RoutingHelper - VERBS = { - post: 'http://activitystrea.ms/schema/1.0/post', - share: 'http://activitystrea.ms/schema/1.0/share', - favorite: 'http://activitystrea.ms/schema/1.0/favorite', - unfavorite: 'http://activitystrea.ms/schema/1.0/unfavorite', - delete: 'http://activitystrea.ms/schema/1.0/delete', - follow: 'http://activitystrea.ms/schema/1.0/follow', - request_friend: 'http://activitystrea.ms/schema/1.0/request-friend', - authorize: 'http://activitystrea.ms/schema/1.0/authorize', - reject: 'http://activitystrea.ms/schema/1.0/reject', - unfollow: 'http://ostatus.org/schema/1.0/unfollow', - block: 'http://mastodon.social/schema/1.0/block', - unblock: 'http://mastodon.social/schema/1.0/unblock', - }.freeze - - TYPES = { - activity: 'http://activitystrea.ms/schema/1.0/activity', - note: 'http://activitystrea.ms/schema/1.0/note', - comment: 'http://activitystrea.ms/schema/1.0/comment', - person: 'http://activitystrea.ms/schema/1.0/person', - collection: 'http://activitystrea.ms/schema/1.0/collection', - group: 'http://activitystrea.ms/schema/1.0/group', - }.freeze - - COLLECTIONS = { - public: 'http://activityschema.org/collection/public', - }.freeze - - XMLNS = 'http://www.w3.org/2005/Atom' - MEDIA_XMLNS = 'http://purl.org/syndication/atommedia' - AS_XMLNS = 'http://activitystrea.ms/spec/1.0/' - THR_XMLNS = 'http://purl.org/syndication/thread/1.0' - POCO_XMLNS = 'http://portablecontacts.net/spec/1.0' - DFRN_XMLNS = 'http://purl.org/macgirvin/dfrn/1.0' - OS_XMLNS = 'http://ostatus.org/schema/1.0' - MTDN_XMLNS = 'http://mastodon.social/schema/1.0' - - def unique_tag(date, id, type) - "tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}" - end - - def unique_tag_to_local_id(tag, expected_type) - return nil unless local_id?(tag) - - if ActivityPub::TagManager.instance.local_uri?(tag) - ActivityPub::TagManager.instance.uri_to_local_id(tag) - else - matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) - return matches[1] unless matches.nil? - end - end - - def local_id?(id) - id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id) - end - def web_domain?(domain) domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero? end @@ -90,17 +34,6 @@ class TagManager TagManager.instance.web_domain?(domain) end - def uri_for(target) - return target.uri if target.respond_to?(:local?) && !target.local? - - case target.object_type - when :person - account_url(target) - when :note, :comment, :activity - target.uri || unique_tag(target.created_at, target.id, 'Status') - end - end - def url_for(target) return target.url if target.respond_to?(:local?) && !target.local? diff --git a/app/models/remote_profile.rb b/app/models/remote_profile.rb index 93c759930..613911c57 100644 --- a/app/models/remote_profile.rb +++ b/app/models/remote_profile.rb @@ -10,11 +10,11 @@ class RemoteProfile end def root - @root ||= document.at_xpath('/atom:feed|/atom:entry', atom: TagManager::XMLNS) + @root ||= document.at_xpath('/atom:feed|/atom:entry', atom: OStatus::TagManager::XMLNS) end def author - @author ||= root.at_xpath('./atom:author|./dfrn:owner', atom: TagManager::XMLNS, dfrn: TagManager::DFRN_XMLNS) + @author ||= root.at_xpath('./atom:author|./dfrn:owner', atom: OStatus::TagManager::XMLNS, dfrn: OStatus::TagManager::DFRN_XMLNS) end def hub_link @@ -22,15 +22,15 @@ class RemoteProfile end def display_name - @display_name ||= author.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS)&.content + @display_name ||= author.at_xpath('./poco:displayName', poco: OStatus::TagManager::POCO_XMLNS)&.content end def note - @note ||= author.at_xpath('./atom:summary|./poco:note', atom: TagManager::XMLNS, poco: TagManager::POCO_XMLNS)&.content + @note ||= author.at_xpath('./atom:summary|./poco:note', atom: OStatus::TagManager::XMLNS, poco: OStatus::TagManager::POCO_XMLNS)&.content end def scope - @scope ||= author.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content + @scope ||= author.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content end def avatar @@ -48,6 +48,6 @@ class RemoteProfile private def link_href_from_xml(xml, type) - xml.at_xpath(%(./atom:link[@rel="#{type}"]/@href), atom: TagManager::XMLNS)&.content + xml.at_xpath(%(./atom:link[@rel="#{type}"]/@href), atom: OStatus::TagManager::XMLNS)&.content end end diff --git a/app/serializers/activitypub/delete_serializer.rb b/app/serializers/activitypub/delete_serializer.rb index 87a43b95d..2bb65135f 100644 --- a/app/serializers/activitypub/delete_serializer.rb +++ b/app/serializers/activitypub/delete_serializer.rb @@ -13,7 +13,7 @@ class ActivityPub::DeleteSerializer < ActiveModel::Serializer end def atom_uri - ::TagManager.instance.uri_for(object) + OStatus::TagManager.instance.uri_for(object) end end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index e5d8e3f03..f94c3b9dc 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -63,13 +63,13 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer def atom_uri return unless object.local? - ::TagManager.instance.uri_for(object) + OStatus::TagManager.instance.uri_for(object) end def in_reply_to_atom_uri return unless object.reply? && !object.thread.nil? - ::TagManager.instance.uri_for(object.thread) + OStatus::TagManager.instance.uri_for(object.thread) end def conversation @@ -78,7 +78,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer if object.conversation.uri? object.conversation.uri else - TagManager.instance.unique_tag(object.conversation.created_at, object.conversation.id, 'Conversation') + OStatus::TagManager.instance.unique_tag(object.conversation.created_at, object.conversation.id, 'Conversation') end end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index d8efa8e60..066d65d9e 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -24,7 +24,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def uri - TagManager.instance.uri_for(object) + OStatus::TagManager.instance.uri_for(object) end def content diff --git a/app/services/concerns/author_extractor.rb b/app/services/concerns/author_extractor.rb index 867d6dc25..c2366188a 100644 --- a/app/services/concerns/author_extractor.rb +++ b/app/services/concerns/author_extractor.rb @@ -5,12 +5,12 @@ module AuthorExtractor return nil if xml.nil? # Try for acct - acct = xml.at_xpath('./xmlns:author/xmlns:email', xmlns: TagManager::XMLNS)&.content + acct = xml.at_xpath('./xmlns:author/xmlns:email', xmlns: OStatus::TagManager::XMLNS)&.content # Try + if acct.blank? - username = xml.at_xpath('./xmlns:author/xmlns:name', xmlns: TagManager::XMLNS)&.content - uri = xml.at_xpath('./xmlns:author/xmlns:uri', xmlns: TagManager::XMLNS)&.content + username = xml.at_xpath('./xmlns:author/xmlns:name', xmlns: OStatus::TagManager::XMLNS)&.content + uri = xml.at_xpath('./xmlns:author/xmlns:uri', xmlns: OStatus::TagManager::XMLNS)&.content return nil if username.blank? || uri.blank? diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index 7c618a0b0..bd98e70d1 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -25,7 +25,7 @@ class FetchRemoteAccountService < BaseService xml = Nokogiri::XML(body) xml.encoding = 'utf-8' - account = author_from_xml(xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS), false) + account = author_from_xml(xml.at_xpath('/xmlns:feed', xmlns: OStatus::TagManager::XMLNS), false) UpdateRemoteProfileService.new.call(xml, account) unless account.nil? diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index 18af18059..1b90854c4 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -27,7 +27,7 @@ class FetchRemoteStatusService < BaseService xml = Nokogiri::XML(body) xml.encoding = 'utf-8' - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)) + account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) domain = Addressable::URI.parse(url).normalized_host return nil unless !account.nil? && confirmed_domain?(domain, account) diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index 31191a818..2a5f1e2bc 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -16,7 +16,7 @@ class ProcessFeedService < BaseService end def process_entries(xml, account) - xml.xpath('//xmlns:entry', xmlns: TagManager::XMLNS).reverse_each.map { |entry| process_entry(entry, account) }.compact + xml.xpath('//xmlns:entry', xmlns: OStatus::TagManager::XMLNS).reverse_each.map { |entry| process_entry(entry, account) }.compact end def process_entry(xml, account) diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb index d04e926e7..1fca3832b 100644 --- a/app/services/process_interaction_service.rb +++ b/app/services/process_interaction_service.rb @@ -13,7 +13,7 @@ class ProcessInteractionService < BaseService xml = Nokogiri::XML(body) xml.encoding = 'utf-8' - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)) + account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) return if account.nil? || account.suspended? @@ -54,13 +54,13 @@ class ProcessInteractionService < BaseService private def mentions_account?(xml, account) - xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each { |mention_link| return true if [TagManager.instance.uri_for(account), TagManager.instance.url_for(account)].include?(mention_link.attribute('href').value) } + xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each { |mention_link| return true if [OStatus::TagManager.instance.uri_for(account), OStatus::TagManager.instance.url_for(account)].include?(mention_link.attribute('href').value) } false end def verb(xml) - raw = xml.at_xpath('//activity:verb', activity: TagManager::AS_XMLNS).content - TagManager::VERBS.key(raw) + raw = xml.at_xpath('//activity:verb', activity: OStatus::TagManager::AS_XMLNS).content + OStatus::TagManager::VERBS.key(raw) rescue :post end @@ -104,7 +104,7 @@ class ProcessInteractionService < BaseService end def delete_post!(xml, account) - status = Status.find(xml.at_xpath('//xmlns:id', xmlns: TagManager::XMLNS).content) + status = Status.find(xml.at_xpath('//xmlns:id', xmlns: OStatus::TagManager::XMLNS).content) return if status.nil? @@ -137,12 +137,12 @@ class ProcessInteractionService < BaseService def status(xml) uri = activity_id(xml) - return nil unless TagManager.instance.local_id?(uri) - Status.find(TagManager.instance.unique_tag_to_local_id(uri, 'Status')) + return nil unless OStatus::TagManager.instance.local_id?(uri) + Status.find(OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status')) end def activity_id(xml) - xml.at_xpath('//activity:object', activity: TagManager::AS_XMLNS).at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content + xml.at_xpath('//activity:object', activity: OStatus::TagManager::AS_XMLNS).at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content end def salmon diff --git a/app/services/verify_salmon_service.rb b/app/services/verify_salmon_service.rb index cd674837d..205b35d8b 100644 --- a/app/services/verify_salmon_service.rb +++ b/app/services/verify_salmon_service.rb @@ -9,7 +9,7 @@ class VerifySalmonService < BaseService xml = Nokogiri::XML(body) xml.encoding = 'utf-8' - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)) + account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) if account.nil? false diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index dea8abc65..0d1665216 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -108,7 +108,7 @@ RSpec.describe ActivityPub::TagManager do it 'returns the local status for OStatus tag: URI' do status = Fabricate(:status) - expect(subject.uri_to_resource(::TagManager.instance.uri_for(status), Status)).to eq status + expect(subject.uri_to_resource(OStatus::TagManager.instance.uri_for(status), Status)).to eq status end it 'returns the local status for OStatus StreamEntry URL' do diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb index b2480a53b..00e6f09dc 100644 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ b/spec/lib/ostatus/atom_serializer_spec.rb @@ -17,7 +17,7 @@ RSpec.describe OStatus::AtomSerializer do follow_request_salmon = serialize(follow_request) object_type = follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:activity] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with request_friend type' do @@ -26,7 +26,7 @@ RSpec.describe OStatus::AtomSerializer do follow_request_salmon = serialize(follow_request) verb = follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:request_friend] + expect(verb.text).to eq OStatus::TagManager::VERBS[:request_friend] end it 'appends activity:object with target account' do @@ -44,13 +44,13 @@ RSpec.describe OStatus::AtomSerializer do it 'adds namespaces' do element = serialize - expect(element['xmlns']).to eq TagManager::XMLNS - expect(element['xmlns:thr']).to eq TagManager::THR_XMLNS - expect(element['xmlns:activity']).to eq TagManager::AS_XMLNS - expect(element['xmlns:poco']).to eq TagManager::POCO_XMLNS - expect(element['xmlns:media']).to eq TagManager::MEDIA_XMLNS - expect(element['xmlns:ostatus']).to eq TagManager::OS_XMLNS - expect(element['xmlns:mastodon']).to eq TagManager::MTDN_XMLNS + expect(element['xmlns']).to eq OStatus::TagManager::XMLNS + expect(element['xmlns:thr']).to eq OStatus::TagManager::THR_XMLNS + expect(element['xmlns:activity']).to eq OStatus::TagManager::AS_XMLNS + expect(element['xmlns:poco']).to eq OStatus::TagManager::POCO_XMLNS + expect(element['xmlns:media']).to eq OStatus::TagManager::MEDIA_XMLNS + expect(element['xmlns:ostatus']).to eq OStatus::TagManager::OS_XMLNS + expect(element['xmlns:mastodon']).to eq OStatus::TagManager::MTDN_XMLNS end end @@ -98,7 +98,7 @@ RSpec.describe OStatus::AtomSerializer do mentioned = element.nodes.find do |node| node.name == 'link' && node[:rel] == 'mentioned' && - node['ostatus:object-type'] == TagManager::TYPES[:person] + node['ostatus:object-type'] == OStatus::TagManager::TYPES[:person] end expect(mentioned[:href]).to eq 'https://cb6e6126.ngrok.io/users/username' @@ -188,7 +188,7 @@ RSpec.describe OStatus::AtomSerializer do author = OStatus::AtomSerializer.new.author(account) object_type = author.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:person] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:person] end it 'appends email element with username and domain for local account' do @@ -358,9 +358,9 @@ RSpec.describe OStatus::AtomSerializer do mentioned_person = entry.nodes.find do |node| node.name == 'link' && node[:rel] == 'mentioned' && - node['ostatus:object-type'] == TagManager::TYPES[:collection] + node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection] end - expect(mentioned_person[:href]).to eq TagManager::COLLECTIONS[:public] + expect(mentioned_person[:href]).to eq OStatus::TagManager::COLLECTIONS[:public] end it 'does not append link element for the public collection if status is not publicly visible' do @@ -371,8 +371,8 @@ RSpec.describe OStatus::AtomSerializer do entry.nodes.each do |node| if node.name == 'link' && node[:rel] == 'mentioned' && - node['ostatus:object-type'] == TagManager::TYPES[:collection] - expect(mentioned_collection[:href]).not_to eq TagManager::COLLECTIONS[:public] + node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection] + expect(mentioned_collection[:href]).not_to eq OStatus::TagManager::COLLECTIONS[:public] end end end @@ -506,7 +506,7 @@ RSpec.describe OStatus::AtomSerializer do status = Fabricate(:status) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) object_type = entry.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:note] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:note] end it 'appends activity:verb element with object type' do @@ -515,7 +515,7 @@ RSpec.describe OStatus::AtomSerializer do entry = OStatus::AtomSerializer.new.entry(status.stream_entry) object_type = entry.nodes.find { |node| node.name == 'activity:verb' } - expect(object_type.text).to eq TagManager::VERBS[:post] + expect(object_type.text).to eq OStatus::TagManager::VERBS[:post] end it 'appends activity:object element with target if present' do @@ -739,8 +739,8 @@ RSpec.describe OStatus::AtomSerializer do time_after = Time.now expect(block_salmon.id.text).to( - eq(TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) - .or(eq(TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) + eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) + .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) ) end @@ -769,7 +769,7 @@ RSpec.describe OStatus::AtomSerializer do block_salmon = OStatus::AtomSerializer.new.block_salmon(block) object_type = block_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:activity] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with block' do @@ -778,7 +778,7 @@ RSpec.describe OStatus::AtomSerializer do block_salmon = OStatus::AtomSerializer.new.block_salmon(block) verb = block_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:block] + expect(verb.text).to eq OStatus::TagManager::VERBS[:block] end it 'appends activity:object element with target account' do @@ -826,8 +826,8 @@ RSpec.describe OStatus::AtomSerializer do time_after = Time.now expect(unblock_salmon.id.text).to( - eq(TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) - .or(eq(TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) + eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) + .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) ) end @@ -856,7 +856,7 @@ RSpec.describe OStatus::AtomSerializer do unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) object_type = unblock_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:activity] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with block' do @@ -865,7 +865,7 @@ RSpec.describe OStatus::AtomSerializer do unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) verb = unblock_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:unblock] + expect(verb.text).to eq OStatus::TagManager::VERBS[:unblock] end it 'appends activity:object element with target account' do @@ -934,7 +934,7 @@ RSpec.describe OStatus::AtomSerializer do favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) verb = favourite_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:favorite] + expect(verb.text).to eq OStatus::TagManager::VERBS[:favorite] end it 'appends activity:object element with status' do @@ -1005,8 +1005,8 @@ RSpec.describe OStatus::AtomSerializer do time_after = Time.now expect(unfavourite_salmon.id.text).to( - eq(TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite')) - .or(eq(TagManager.instance.unique_tag(time_after.utc, favourite.id, 'Favourite'))) + eq(OStatus::TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite')) + .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, favourite.id, 'Favourite'))) ) end @@ -1034,7 +1034,7 @@ RSpec.describe OStatus::AtomSerializer do unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) verb = unfavourite_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:unfavorite] + expect(verb.text).to eq OStatus::TagManager::VERBS[:unfavorite] end it 'appends activity:object element with status' do @@ -1117,7 +1117,7 @@ RSpec.describe OStatus::AtomSerializer do follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) object_type = follow_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:activity] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with follow' do @@ -1126,7 +1126,7 @@ RSpec.describe OStatus::AtomSerializer do follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) verb = follow_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:follow] + expect(verb.text).to eq OStatus::TagManager::VERBS[:follow] end it 'appends activity:object element with target account' do @@ -1190,8 +1190,8 @@ RSpec.describe OStatus::AtomSerializer do time_after = Time.now expect(unfollow_salmon.id.text).to( - eq(TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow')) - .or(eq(TagManager.instance.unique_tag(time_after.utc, follow.id, 'Follow'))) + eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow')) + .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow.id, 'Follow'))) ) end @@ -1234,7 +1234,7 @@ RSpec.describe OStatus::AtomSerializer do unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) object_type = unfollow_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:activity] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with follow' do @@ -1244,7 +1244,7 @@ RSpec.describe OStatus::AtomSerializer do unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) verb = unfollow_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:unfollow] + expect(verb.text).to eq OStatus::TagManager::VERBS[:unfollow] end it 'appends activity:object element with target account' do @@ -1338,8 +1338,8 @@ RSpec.describe OStatus::AtomSerializer do time_after = Time.now expect(authorize_follow_request_salmon.id.text).to( - eq(TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) - .or(eq(TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest'))) + eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) + .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest'))) ) end @@ -1359,7 +1359,7 @@ RSpec.describe OStatus::AtomSerializer do authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) object_type = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:activity] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with authorize' do @@ -1368,7 +1368,7 @@ RSpec.describe OStatus::AtomSerializer do authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:authorize] + expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize] end it 'returns element whose rendered view creates follow from follow request when processed' do @@ -1407,8 +1407,8 @@ RSpec.describe OStatus::AtomSerializer do time_after = Time.now expect(reject_follow_request_salmon.id.text).to( - eq(TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) - .or(TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest')) + eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) + .or(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest')) ) end @@ -1424,14 +1424,14 @@ RSpec.describe OStatus::AtomSerializer do follow_request = Fabricate(:follow_request) reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) object_type = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:activity] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with authorize' do follow_request = Fabricate(:follow_request) reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq TagManager::VERBS[:reject] + expect(verb.text).to eq OStatus::TagManager::VERBS[:reject] end it 'returns element whose rendered view deletes follow request when processed' do @@ -1503,7 +1503,7 @@ RSpec.describe OStatus::AtomSerializer do entry = OStatus::AtomSerializer.new.object(status) object_type = entry.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq TagManager::TYPES[:note] + expect(object_type.text).to eq OStatus::TagManager::TYPES[:note] end it 'appends activity:verb element with verb' do @@ -1512,7 +1512,7 @@ RSpec.describe OStatus::AtomSerializer do entry = OStatus::AtomSerializer.new.object(status) object_type = entry.nodes.find { |node| node.name == 'activity:verb' } - expect(object_type.text).to eq TagManager::VERBS[:post] + expect(object_type.text).to eq OStatus::TagManager::VERBS[:post] end it 'appends link element for an alternative' do diff --git a/spec/lib/ostatus/tag_manager_spec.rb b/spec/lib/ostatus/tag_manager_spec.rb new file mode 100644 index 000000000..31195bae2 --- /dev/null +++ b/spec/lib/ostatus/tag_manager_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe OStatus::TagManager do + describe '#unique_tag' do + it 'returns a unique tag' do + expect(OStatus::TagManager.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status' + end + end + + describe '#unique_tag_to_local_id' do + it 'returns the ID part' do + expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12' + end + + it 'returns nil if it is not local id' do + expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to eq nil + end + + it 'returns nil if it is not expected type' do + expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to eq nil + end + + it 'returns nil if it does not have object ID' do + expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to eq nil + end + end + + describe '#local_id?' do + it 'returns true for a local ID' do + expect(OStatus::TagManager.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true + end + + it 'returns false for a foreign ID' do + expect(OStatus::TagManager.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false + end + end + + describe '#uri_for' do + subject { OStatus::TagManager.instance.uri_for(target) } + + context 'comment object' do + let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) } + + it 'returns the unique tag for status' do + expect(target.object_type).to eq :comment + is_expected.to eq target.uri + end + end + + context 'note object' do + let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) } + + it 'returns the unique tag for status' do + expect(target.object_type).to eq :note + is_expected.to eq target.uri + end + end + + context 'person object' do + let(:target) { Fabricate(:account, username: 'alice') } + + it 'returns the URL for account' do + expect(target.object_type).to eq :person + is_expected.to eq 'https://cb6e6126.ngrok.io/users/alice' + end + end + end +end diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb index 6c7830231..5427a2929 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -120,71 +120,6 @@ RSpec.describe TagManager do end end - describe '#unique_tag' do - it 'returns a unique tag' do - expect(TagManager.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status' - end - end - - describe '#unique_tag_to_local_id' do - it 'returns the ID part' do - expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12' - end - - it 'returns nil if it is not local id' do - expect(TagManager.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to eq nil - end - - it 'returns nil if it is not expected type' do - expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to eq nil - end - - it 'returns nil if it does not have object ID' do - expect(TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to eq nil - end - end - - describe '#local_id?' do - it 'returns true for a local ID' do - expect(TagManager.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true - end - - it 'returns false for a foreign ID' do - expect(TagManager.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false - end - end - - describe '#uri_for' do - subject { TagManager.instance.uri_for(target) } - - context 'comment object' do - let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :comment - is_expected.to eq target.uri - end - end - - context 'note object' do - let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :note - is_expected.to eq target.uri - end - end - - context 'person object' do - let(:target) { Fabricate(:account, username: 'alice') } - - it 'returns the URL for account' do - expect(target.object_type).to eq :person - is_expected.to eq 'https://cb6e6126.ngrok.io/users/alice' - end - end - end - describe '#url_for' do let(:alice) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb index d74eb41a2..6ea4d83da 100644 --- a/spec/services/authorize_follow_service_spec.rb +++ b/spec/services/authorize_follow_service_spec.rb @@ -42,7 +42,7 @@ RSpec.describe AuthorizeFollowService do it 'sends a follow request authorization salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:authorize]) + xml.match(OStatus::TagManager::VERBS[:authorize]) }).to have_been_made.once end end diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index b1e9ac567..f5c9adfb5 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -50,14 +50,14 @@ RSpec.describe BatchedRemoveStatusService do it 'sends PuSH update to PuSH subscribers' do expect(a_request(:post, 'http://example.com/push').with { |req| - matches = req.body.match(TagManager::VERBS[:delete]) + matches = req.body.match(OStatus::TagManager::VERBS[:delete]) }).to have_been_made.at_least_once end it 'sends Salmon slap to previously mentioned users' do expect(a_request(:post, "http://example.com/salmon").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:delete]) + xml.match(OStatus::TagManager::VERBS[:delete]) }).to have_been_made.once end diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb index bd2ab3d53..c69ff7804 100644 --- a/spec/services/block_service_spec.rb +++ b/spec/services/block_service_spec.rb @@ -32,7 +32,7 @@ RSpec.describe BlockService do it 'sends a block salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:block]) + xml.match(OStatus::TagManager::VERBS[:block]) }).to have_been_made.once end end diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb index 2ab1f32ca..5bf2c74a9 100644 --- a/spec/services/favourite_service_spec.rb +++ b/spec/services/favourite_service_spec.rb @@ -34,7 +34,7 @@ RSpec.describe FavouriteService do it 'sends a salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:favorite]) + xml.match(OStatus::TagManager::VERBS[:favorite]) }).to have_been_made.once end end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 1e2378031..ceb39e5e6 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -60,7 +60,7 @@ RSpec.describe FollowService do it 'sends a follow request salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:request_friend]) + xml.match(OStatus::TagManager::VERBS[:request_friend]) }).to have_been_made.once end end @@ -81,7 +81,7 @@ RSpec.describe FollowService do it 'sends a follow salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:follow]) + xml.match(OStatus::TagManager::VERBS[:follow]) }).to have_been_made.once end diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb index 2e06345b3..bf49dd2c9 100644 --- a/spec/services/reject_follow_service_spec.rb +++ b/spec/services/reject_follow_service_spec.rb @@ -42,7 +42,7 @@ RSpec.describe RejectFollowService do it 'sends a follow request rejection salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:reject]) + xml.match(OStatus::TagManager::VERBS[:reject]) }).to have_been_made.once end end diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 8b34bdb6b..b60015928 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -34,7 +34,7 @@ RSpec.describe RemoveStatusService do it 'sends PuSH update to PuSH subscribers' do expect(a_request(:post, 'http://example.com/push').with { |req| - req.body.match(TagManager::VERBS[:delete]) + req.body.match(OStatus::TagManager::VERBS[:delete]) }).to have_been_made end @@ -45,7 +45,7 @@ RSpec.describe RemoveStatusService do it 'sends Salmon slap to previously mentioned users' do expect(a_request(:post, "http://example.com/salmon").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:delete]) + xml.match(OStatus::TagManager::VERBS[:delete]) }).to have_been_made.once end diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb index def4981e7..ca7a6b77e 100644 --- a/spec/services/unblock_service_spec.rb +++ b/spec/services/unblock_service_spec.rb @@ -34,7 +34,7 @@ RSpec.describe UnblockService do it 'sends an unblock salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:unblock]) + xml.match(OStatus::TagManager::VERBS[:unblock]) }).to have_been_made.once end end diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb index 29040431e..021e76782 100644 --- a/spec/services/unfollow_service_spec.rb +++ b/spec/services/unfollow_service_spec.rb @@ -34,7 +34,7 @@ RSpec.describe UnfollowService do it 'sends an unfollow salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(TagManager::VERBS[:unfollow]) + xml.match(OStatus::TagManager::VERBS[:unfollow]) }).to have_been_made.once end end -- cgit From 669fe9ee06a82482201377abd303492eb7fa7d94 Mon Sep 17 00:00:00 2001 From: aschmitz Date: Wed, 20 Sep 2017 07:53:48 -0500 Subject: Change IDs to strings rather than numbers in API JSON output (#5019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix JavaScript interface with long IDs Somewhat predictably, the JS interface handled IDs as numbers, which in JS are IEEE double-precision floats. This loses some precision when working with numbers as large as those generated by the new ID scheme, so we instead handle them here as strings. This is relatively simple, and doesn't appear to have caused any problems, but should definitely be tested more thoroughly than the built-in tests. Several days of use appear to support this working properly. BREAKING CHANGE: The major(!) change here is that IDs are now returned as strings by the REST endpoints, rather than as integers. In practice, relatively few changes were required to make the existing JS UI work with this change, but it will likely hit API clients pretty hard: it's an entirely different type to consume. (The one API client I tested, Tusky, handles this with no problems, however.) Twitter ran into this issue when introducing Snowflake IDs, and decided to instead introduce an `id_str` field in JSON responses. I have opted to *not* do that, and instead force all IDs to 64-bit integers represented by strings in one go. (I believe Twitter exacerbated their problem by rolling out the changes three times: once for statuses, once for DMs, and once for user IDs, as well as by leaving an integer ID value in JSON. As they said, "If you’re using the `id` field with JSON in a Javascript-related language, there is a very high likelihood that the integers will be silently munged by Javascript interpreters. In most cases, this will result in behavior such as being unable to load or delete a specific direct message, because the ID you're sending to the API is different than the actual identifier associated with the message." [1]) However, given that this is a significant change for API users, alternatives or a transition time may be appropriate. 1: https://blog.twitter.com/developer/en_us/a/2011/direct-messages-going-snowflake-on-sep-30-2011.html * Additional fixes for stringified IDs in JSON These should be the last two. These were identified using eslint to try to identify any plain casts to JavaScript numbers. (Some such casts are legitimate, but these were not.) Adding the following to .eslintrc.yml will identify casts to numbers: ~~~ no-restricted-syntax: - warn - selector: UnaryExpression[operator='+'] > :not(Literal) message: Avoid the use of unary + - selector: CallExpression[callee.name='Number'] message: Casting with Number() may coerce string IDs to numbers ~~~ The remaining three casts appear legitimate: two casts to array indices, one in a server to turn an environment variable into a number. * Back out RelationshipsController Change This was made to make a test a bit less flakey, but has nothing to do with this branch. * Change internal streaming payloads to stringified IDs as well Per https://github.com/tootsuite/mastodon/pull/5019#issuecomment-330736452 we need these changes to send deleted status IDs as strings, not integers. --- app/javascript/mastodon/actions/store.js | 3 +-- app/javascript/mastodon/components/account.js | 2 +- .../mastodon/components/autosuggest_textarea.js | 2 +- app/javascript/mastodon/components/status.js | 4 ++-- .../mastodon/components/status_action_bar.js | 2 +- .../mastodon/features/account/components/action_bar.js | 2 +- .../mastodon/features/account/components/header.js | 2 +- .../mastodon/features/account_gallery/index.js | 16 ++++++++-------- .../features/account_timeline/components/header.js | 2 +- .../account_timeline/containers/header_container.js | 2 +- .../mastodon/features/account_timeline/index.js | 18 +++++++++--------- .../features/compose/components/compose_form.js | 2 +- .../features/compose/components/upload_form.js | 2 +- app/javascript/mastodon/features/favourites/index.js | 6 +++--- app/javascript/mastodon/features/followers/index.js | 16 ++++++++-------- app/javascript/mastodon/features/following/index.js | 16 ++++++++-------- app/javascript/mastodon/features/reblogs/index.js | 6 +++--- .../mastodon/features/status/components/action_bar.js | 2 +- app/javascript/mastodon/features/status/index.js | 12 ++++++------ app/serializers/initial_state_serializer.rb | 10 +++++----- app/serializers/rest/account_serializer.rb | 4 ++++ app/serializers/rest/application_serializer.rb | 6 +++++- app/serializers/rest/media_attachment_serializer.rb | 4 ++++ app/serializers/rest/notification_serializer.rb | 4 ++++ app/serializers/rest/relationship_serializer.rb | 4 ++++ app/serializers/rest/report_serializer.rb | 4 ++++ app/serializers/rest/status_serializer.rb | 14 +++++++++++++- app/services/batched_remove_status_service.rb | 2 +- app/services/remove_status_service.rb | 2 +- .../api/v1/accounts/relationships_controller_spec.rb | 4 ++-- spec/controllers/api/v1/media_controller_spec.rb | 6 +++--- .../api/v1/statuses/favourites_controller_spec.rb | 2 +- .../api/v1/statuses/pins_controller_spec.rb | 2 +- .../api/v1/statuses/reblogs_controller_spec.rb | 2 +- 34 files changed, 111 insertions(+), 76 deletions(-) (limited to 'app/serializers/rest') diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 0597d265e..a1db0fdd5 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -5,8 +5,7 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => fromJS(rawState, (k, v) => - Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => - Number.isNaN(x * 1) ? x : x * 1)); + Iterable.isIndexed(v) ? v.toList() : v.toMap()); export function hydrateStore(rawState) { const state = convertState(rawState); diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 6456c12ba..d614a52c9 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -21,7 +21,7 @@ export default class Account extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - me: PropTypes.number.isRequired, + me: PropTypes.string.isRequired, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index 35b37600f..30e3049df 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -128,7 +128,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } onSuggestionClick = (e) => { - const suggestion = Number(e.currentTarget.getAttribute('data-index')); + const suggestion = e.currentTarget.getAttribute('data-index'); e.preventDefault(); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.textarea.focus(); diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 82359156d..3716d522e 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -34,7 +34,7 @@ export default class Status extends ImmutablePureComponent { onBlock: PropTypes.func, onEmbed: PropTypes.func, onHeightChange: PropTypes.func, - me: PropTypes.number, + me: PropTypes.string, boostModal: PropTypes.bool, autoPlayGif: PropTypes.bool, muted: PropTypes.bool, @@ -70,7 +70,7 @@ export default class Status extends ImmutablePureComponent { handleAccountClick = (e) => { if (this.context.router && e.button === 0) { - const id = Number(e.currentTarget.getAttribute('data-id')); + const id = e.currentTarget.getAttribute('data-id'); e.preventDefault(); this.context.router.history.push(`/accounts/${id}`); } diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 692b1ef2b..803b730d9 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -46,7 +46,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onEmbed: PropTypes.func, onMuteConversation: PropTypes.func, onPin: PropTypes.func, - me: PropTypes.number, + me: PropTypes.string, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, }; diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index c12c0889e..9e8fea69d 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -26,7 +26,7 @@ export default class ActionBar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - me: PropTypes.number.isRequired, + me: PropTypes.string.isRequired, onFollow: PropTypes.func, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 6eb51a5c7..9ee7a56d9 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -77,7 +77,7 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, - me: PropTypes.number.isRequired, + me: PropTypes.string.isRequired, onFollow: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, autoPlayGif: PropTypes.bool.isRequired, diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index 0cfd98f23..2a88addc4 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -16,9 +16,9 @@ import { ScrollContainer } from 'react-router-scroll'; import LoadMore from '../../components/load_more'; const mapStateToProps = (state, props) => ({ - medias: getAccountGallery(state, Number(props.params.accountId)), - isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}:media`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}:media`, 'next']), + medias: getAccountGallery(state, props.params.accountId), + isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), + hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']), autoPlayGif: state.getIn(['meta', 'auto_play_gif']), }); @@ -35,20 +35,20 @@ export default class AccountGallery extends ImmutablePureComponent { }; componentDidMount () { - this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(refreshAccountMediaTimeline(Number(this.props.params.accountId))); + this.props.dispatch(fetchAccount(this.props.params.accountId)); + this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId)); } componentWillReceiveProps (nextProps) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(refreshAccountMediaTimeline(Number(this.props.params.accountId))); + this.props.dispatch(fetchAccount(nextProps.params.accountId)); + this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId)); } } handleScrollToBottom = () => { if (this.props.hasMore) { - this.props.dispatch(expandAccountMediaTimeline(Number(this.props.params.accountId))); + this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); } } diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 167a2097e..edfedb864 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -10,7 +10,7 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, - me: PropTypes.number.isRequired, + me: PropTypes.string.isRequired, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index dcee78b3e..ab75b40de 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -27,7 +27,7 @@ const makeMapStateToProps = () => { const getAccount = makeGetAccount(); const mapStateToProps = (state, { accountId }) => ({ - account: getAccount(state, Number(accountId)), + account: getAccount(state, accountId), me: state.getIn(['meta', 'me']), unfollowModal: state.getIn(['meta', 'unfollow_modal']), }); diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 3c8b63114..fe92216d5 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -13,9 +13,9 @@ import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ - statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], ImmutableList()), - isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']), + statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()), + isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']), + hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']), me: state.getIn(['meta', 'me']), }); @@ -28,24 +28,24 @@ export default class AccountTimeline extends ImmutablePureComponent { statusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, hasMore: PropTypes.bool, - me: PropTypes.number.isRequired, + me: PropTypes.string.isRequired, }; componentWillMount () { - this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(refreshAccountTimeline(Number(this.props.params.accountId))); + this.props.dispatch(fetchAccount(this.props.params.accountId)); + this.props.dispatch(refreshAccountTimeline(this.props.params.accountId)); } componentWillReceiveProps (nextProps) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(refreshAccountTimeline(Number(nextProps.params.accountId))); + this.props.dispatch(fetchAccount(nextProps.params.accountId)); + this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId)); } } handleScrollToBottom = () => { if (!this.props.isLoading && this.props.hasMore) { - this.props.dispatch(expandAccountTimeline(Number(this.props.params.accountId))); + this.props.dispatch(expandAccountTimeline(this.props.params.accountId)); } } diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index f3320a42b..413142d81 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -42,7 +42,7 @@ export default class ComposeForm extends ImmutablePureComponent { preselectDate: PropTypes.instanceOf(Date), is_submitting: PropTypes.bool, is_uploading: PropTypes.bool, - me: PropTypes.number, + me: PropTypes.string, onChange: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, onClearSuggestions: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js index 78473dab4..cf2d2658a 100644 --- a/app/javascript/mastodon/features/compose/components/upload_form.js +++ b/app/javascript/mastodon/features/compose/components/upload_form.js @@ -21,7 +21,7 @@ export default class UploadForm extends React.PureComponent { }; onRemoveFile = (e) => { - const id = Number(e.currentTarget.parentElement.getAttribute('data-id')); + const id = e.currentTarget.parentElement.getAttribute('data-id'); this.props.onRemoveFile(id); } diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js index dc8109d16..4dbfefd87 100644 --- a/app/javascript/mastodon/features/favourites/index.js +++ b/app/javascript/mastodon/features/favourites/index.js @@ -11,7 +11,7 @@ import ColumnBackButton from '../../components/column_back_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'favourited_by', Number(props.params.statusId)]), + accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), }); @connect(mapStateToProps) @@ -24,12 +24,12 @@ export default class Favourites extends ImmutablePureComponent { }; componentWillMount () { - this.props.dispatch(fetchFavourites(Number(this.props.params.statusId))); + this.props.dispatch(fetchFavourites(this.props.params.statusId)); } componentWillReceiveProps (nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchFavourites(Number(nextProps.params.statusId))); + this.props.dispatch(fetchFavourites(nextProps.params.statusId)); } } diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index 2d85b9cc0..89445559f 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -17,8 +17,8 @@ import ColumnBackButton from '../../components/column_back_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items']), - hasMore: !!state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'next']), + accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), + hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), }); @connect(mapStateToProps) @@ -32,14 +32,14 @@ export default class Followers extends ImmutablePureComponent { }; componentWillMount () { - this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(fetchFollowers(Number(this.props.params.accountId))); + this.props.dispatch(fetchAccount(this.props.params.accountId)); + this.props.dispatch(fetchFollowers(this.props.params.accountId)); } componentWillReceiveProps (nextProps) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(fetchFollowers(Number(nextProps.params.accountId))); + this.props.dispatch(fetchAccount(nextProps.params.accountId)); + this.props.dispatch(fetchFollowers(nextProps.params.accountId)); } } @@ -47,13 +47,13 @@ export default class Followers extends ImmutablePureComponent { const { scrollTop, scrollHeight, clientHeight } = e.target; if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) { - this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); + this.props.dispatch(expandFollowers(this.props.params.accountId)); } } handleLoadMore = (e) => { e.preventDefault(); - this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); + this.props.dispatch(expandFollowers(this.props.params.accountId)); } render () { diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index e4e2a4811..c34830276 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -17,8 +17,8 @@ import ColumnBackButton from '../../components/column_back_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items']), - hasMore: !!state.getIn(['user_lists', 'following', Number(props.params.accountId), 'next']), + accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']), + hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), }); @connect(mapStateToProps) @@ -32,14 +32,14 @@ export default class Following extends ImmutablePureComponent { }; componentWillMount () { - this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(fetchFollowing(Number(this.props.params.accountId))); + this.props.dispatch(fetchAccount(this.props.params.accountId)); + this.props.dispatch(fetchFollowing(this.props.params.accountId)); } componentWillReceiveProps (nextProps) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(fetchFollowing(Number(nextProps.params.accountId))); + this.props.dispatch(fetchAccount(nextProps.params.accountId)); + this.props.dispatch(fetchFollowing(nextProps.params.accountId)); } } @@ -47,13 +47,13 @@ export default class Following extends ImmutablePureComponent { const { scrollTop, scrollHeight, clientHeight } = e.target; if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) { - this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); + this.props.dispatch(expandFollowing(this.props.params.accountId)); } } handleLoadMore = (e) => { e.preventDefault(); - this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); + this.props.dispatch(expandFollowing(this.props.params.accountId)); } render () { diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js index dc940ae01..f1904786a 100644 --- a/app/javascript/mastodon/features/reblogs/index.js +++ b/app/javascript/mastodon/features/reblogs/index.js @@ -11,7 +11,7 @@ import ColumnBackButton from '../../components/column_back_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'reblogged_by', Number(props.params.statusId)]), + accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), }); @connect(mapStateToProps) @@ -24,12 +24,12 @@ export default class Reblogs extends ImmutablePureComponent { }; componentWillMount () { - this.props.dispatch(fetchReblogs(Number(this.props.params.statusId))); + this.props.dispatch(fetchReblogs(this.props.params.statusId)); } componentWillReceiveProps(nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchReblogs(Number(nextProps.params.statusId))); + this.props.dispatch(fetchReblogs(nextProps.params.statusId)); } } diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index c303caf10..034cc9854 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -36,7 +36,7 @@ export default class ActionBar extends React.PureComponent { onReport: PropTypes.func, onPin: PropTypes.func, onEmbed: PropTypes.func, - me: PropTypes.number.isRequired, + me: PropTypes.string.isRequired, intl: PropTypes.object.isRequired, }; diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index c614f6acb..8da6e743c 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -38,9 +38,9 @@ const makeMapStateToProps = () => { const getStatus = makeGetStatus(); const mapStateToProps = (state, props) => ({ - status: getStatus(state, Number(props.params.statusId)), - ancestorsIds: state.getIn(['contexts', 'ancestors', Number(props.params.statusId)]), - descendantsIds: state.getIn(['contexts', 'descendants', Number(props.params.statusId)]), + status: getStatus(state, props.params.statusId), + ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), + descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), me: state.getIn(['meta', 'me']), boostModal: state.getIn(['meta', 'boost_modal']), deleteModal: state.getIn(['meta', 'delete_modal']), @@ -64,7 +64,7 @@ export default class Status extends ImmutablePureComponent { status: ImmutablePropTypes.map, ancestorsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list, - me: PropTypes.number, + me: PropTypes.string, boostModal: PropTypes.bool, deleteModal: PropTypes.bool, autoPlayGif: PropTypes.bool, @@ -72,12 +72,12 @@ export default class Status extends ImmutablePureComponent { }; componentWillMount () { - this.props.dispatch(fetchStatus(Number(this.props.params.statusId))); + this.props.dispatch(fetchStatus(this.props.params.statusId)); } componentWillReceiveProps (nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchStatus(Number(nextProps.params.statusId))); + this.props.dispatch(fetchStatus(nextProps.params.statusId)); } } diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 32ffcc688..9ee9bd29c 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -10,11 +10,11 @@ class InitialStateSerializer < ActiveModel::Serializer access_token: object.token, locale: I18n.locale, domain: Rails.configuration.x.local_domain, - admin: object.admin&.id, + admin: object.admin&.id&.to_s, } if object.current_account - store[:me] = object.current_account.id + store[:me] = object.current_account.id.to_s store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal store[:boost_modal] = object.current_account.user.setting_boost_modal store[:delete_modal] = object.current_account.user.setting_delete_modal @@ -28,7 +28,7 @@ class InitialStateSerializer < ActiveModel::Serializer store = {} if object.current_account - store[:me] = object.current_account.id + store[:me] = object.current_account.id.to_s store[:default_privacy] = object.current_account.user.setting_default_privacy store[:default_sensitive] = object.current_account.user.setting_default_sensitive end @@ -40,8 +40,8 @@ class InitialStateSerializer < ActiveModel::Serializer def accounts store = {} - store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account - store[object.admin.id] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin + store[object.current_account.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account + store[object.admin.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin store end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 012a4fd18..65fdb0308 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -7,6 +7,10 @@ class REST::AccountSerializer < ActiveModel::Serializer :note, :url, :avatar, :avatar_static, :header, :header_static, :followers_count, :following_count, :statuses_count + def id + object.id.to_s + end + def note Formatter.instance.simplified_format(object) end diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb index 868a62f1e..5eb03a513 100644 --- a/app/serializers/rest/application_serializer.rb +++ b/app/serializers/rest/application_serializer.rb @@ -4,8 +4,12 @@ class REST::ApplicationSerializer < ActiveModel::Serializer attributes :id, :name, :website, :redirect_uri, :client_id, :client_secret + def id + object.id.to_s + end + def client_id - object.uid + object.uid.to_s end def client_secret diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb index 31189406a..f6e7c79d1 100644 --- a/app/serializers/rest/media_attachment_serializer.rb +++ b/app/serializers/rest/media_attachment_serializer.rb @@ -6,6 +6,10 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer attributes :id, :type, :url, :preview_url, :remote_url, :text_url, :meta + def id + object.id.to_s + end + def url if object.needs_redownload? media_proxy_url(object.id, :original) diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index f95d099a3..541a6b8b5 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -6,6 +6,10 @@ class REST::NotificationSerializer < ActiveModel::Serializer belongs_to :from_account, key: :account, serializer: REST::AccountSerializer belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer + def id + object.id.to_s + end + def status_type? [:favourite, :reblog, :mention].include?(object.type) end diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb index 1d431aa1b..998727e37 100644 --- a/app/serializers/rest/relationship_serializer.rb +++ b/app/serializers/rest/relationship_serializer.rb @@ -4,6 +4,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer attributes :id, :following, :followed_by, :blocking, :muting, :requested, :domain_blocking + def id + object.id.to_s + end + def following instance_options[:relationships].following[object.id] || false end diff --git a/app/serializers/rest/report_serializer.rb b/app/serializers/rest/report_serializer.rb index 0c6bd6556..ecb88d653 100644 --- a/app/serializers/rest/report_serializer.rb +++ b/app/serializers/rest/report_serializer.rb @@ -2,4 +2,8 @@ class REST::ReportSerializer < ActiveModel::Serializer attributes :id, :action_taken + + def id + object.id.to_s + end end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 066d65d9e..e0fd1c77e 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -19,6 +19,18 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :tags has_many :emojis + def id + object.id.to_s + end + + def in_reply_to_id + object.in_reply_to_id.to_s + end + + def in_reply_to_account_id + object.in_reply_to_account_id.to_s + end + def current_user? !current_user.nil? end @@ -82,7 +94,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attributes :id, :username, :url, :acct def id - object.account_id + object.account_id.to_s end def username diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 86eaa5735..e1e845bc0 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -18,7 +18,7 @@ class BatchedRemoveStatusService < BaseService @stream_entry_batches = [] @salmon_batches = [] @activity_json_batches = [] - @json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h + @json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h @activity_json = {} @activity_xml = {} diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 83fc77043..5f45fb3ab 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -4,7 +4,7 @@ class RemoveStatusService < BaseService include StreamEntryRenderer def call(status) - @payload = Oj.dump(event: :delete, payload: status.id) + @payload = Oj.dump(event: :delete, payload: status.id.to_s) @status = status @account = status.account @tags = status.tags.pluck(:name).to_a diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb index a9073b197..431fc2194 100644 --- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb @@ -50,14 +50,14 @@ describe Api::V1::Accounts::RelationshipsController do json = body_as_json expect(json).to be_a Enumerable - expect(json.first[:id]).to eq simon.id + expect(json.first[:id]).to eq simon.id.to_s expect(json.first[:following]).to be true expect(json.first[:followed_by]).to be false expect(json.first[:muting]).to be false expect(json.first[:requested]).to be false expect(json.first[:domain_blocking]).to be false - expect(json.second[:id]).to eq lewis.id + expect(json.second[:id]).to eq lewis.id.to_s expect(json.second[:following]).to be false expect(json.second[:followed_by]).to be true expect(json.second[:muting]).to be false diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb index 6bad3f05d..baa22d7e4 100644 --- a/spec/controllers/api/v1/media_controller_spec.rb +++ b/spec/controllers/api/v1/media_controller_spec.rb @@ -53,7 +53,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do end it 'returns media ID in JSON' do - expect(body_as_json[:id]).to eq MediaAttachment.first.id + expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s end end @@ -75,7 +75,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do end it 'returns media ID in JSON' do - expect(body_as_json[:id]).to eq MediaAttachment.first.id + expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s end end @@ -97,7 +97,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do end xit 'returns media ID in JSON' do - expect(body_as_json[:id]).to eq MediaAttachment.first.id + expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s end end end diff --git a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb index 2a029230d..aba7cd458 100644 --- a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb @@ -36,7 +36,7 @@ describe Api::V1::Statuses::FavouritesController do it 'return json with updated attributes' do hash_body = body_as_json - expect(hash_body[:id]).to eq status.id + expect(hash_body[:id]).to eq status.id.to_s expect(hash_body[:favourites_count]).to eq 1 expect(hash_body[:favourited]).to be true end diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb index 2e170da24..79005c9de 100644 --- a/spec/controllers/api/v1/statuses/pins_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb @@ -32,7 +32,7 @@ describe Api::V1::Statuses::PinsController do it 'return json with updated attributes' do hash_body = body_as_json - expect(hash_body[:id]).to eq status.id + expect(hash_body[:id]).to eq status.id.to_s expect(hash_body[:pinned]).to be true end end diff --git a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb index d6d36c1b2..7417ff672 100644 --- a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb @@ -36,7 +36,7 @@ describe Api::V1::Statuses::ReblogsController do it 'return json with updated attributes' do hash_body = body_as_json - expect(hash_body[:reblog][:id]).to eq status.id + expect(hash_body[:reblog][:id]).to eq status.id.to_s expect(hash_body[:reblog][:reblogs_count]).to eq 1 expect(hash_body[:reblog][:reblogged]).to be true end -- cgit From 293972f716476933df2b665ad755cafe4d29d82d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 23 Sep 2017 01:57:23 +0200 Subject: New API: GET /api/v1/custom_emojis to get a server's custom emojis (#5051) --- app/controllers/admin/custom_emojis_controller.rb | 2 +- app/controllers/api/v1/custom_emojis_controller.rb | 9 +++++++++ app/models/custom_emoji.rb | 2 ++ app/serializers/rest/custom_emoji_serializer.rb | 11 +++++++++++ app/serializers/rest/status_serializer.rb | 12 +----------- config/routes.rb | 1 + .../api/v1/custom_emojis_controller_spec.rb | 18 ++++++++++++++++++ 7 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 app/controllers/api/v1/custom_emojis_controller.rb create mode 100644 app/serializers/rest/custom_emoji_serializer.rb create mode 100644 spec/controllers/api/v1/custom_emojis_controller_spec.rb (limited to 'app/serializers/rest') diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 572ad1ac2..d70514d9a 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -3,7 +3,7 @@ module Admin class CustomEmojisController < BaseController def index - @custom_emojis = CustomEmoji.where(domain: nil) + @custom_emojis = CustomEmoji.local end def new diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb new file mode 100644 index 000000000..4dd77fb55 --- /dev/null +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Api::V1::CustomEmojisController < Api::BaseController + respond_to :json + + def index + render json: CustomEmoji.local, each_serializer: REST::CustomEmojiSerializer + end +end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index f4d3b16a0..aff9f8dfa 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -26,6 +26,8 @@ class CustomEmoji < ApplicationRecord validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes } validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 } + scope :local, -> { where(domain: nil) } + include Remotable class << self diff --git a/app/serializers/rest/custom_emoji_serializer.rb b/app/serializers/rest/custom_emoji_serializer.rb new file mode 100644 index 000000000..b744dd4ec --- /dev/null +++ b/app/serializers/rest/custom_emoji_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class REST::CustomEmojiSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :shortcode, :url + + def url + full_asset_url(object.image.url) + end +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index e0fd1c77e..ef3c325ba 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -17,7 +17,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :media_attachments, serializer: REST::MediaAttachmentSerializer has_many :mentions has_many :tags - has_many :emojis + has_many :emojis, serializer: REST::CustomEmojiSerializer def id object.id.to_s @@ -119,14 +119,4 @@ class REST::StatusSerializer < ActiveModel::Serializer tag_url(object) end end - - class CustomEmojiSerializer < ActiveModel::Serializer - include RoutingHelper - - attributes :shortcode, :url - - def url - full_asset_url(object.image.url) - end - end end diff --git a/config/routes.rb b/config/routes.rb index d38f5308a..cb7e84d7b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -188,6 +188,7 @@ Rails.application.routes.draw do end resources :streaming, only: [:index] + resources :custom_emojis, only: [:index] get '/search', to: 'search#index', as: :search diff --git a/spec/controllers/api/v1/custom_emojis_controller_spec.rb b/spec/controllers/api/v1/custom_emojis_controller_spec.rb new file mode 100644 index 000000000..9f3522812 --- /dev/null +++ b/spec/controllers/api/v1/custom_emojis_controller_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::CustomEmojisController, type: :controller do + render_views + + describe 'GET #index' do + before do + Fabricate(:custom_emoji) + get :index + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + end +end -- cgit From adc38078dde102d4b6e98d364ada47fa3dd3f22c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 24 Sep 2017 04:09:32 +0200 Subject: Make sure nil IDs to not get cast to empty string in REST API (#5068) --- app/serializers/rest/application_serializer.rb | 2 +- app/serializers/rest/status_serializer.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app/serializers/rest') diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb index 5eb03a513..a8945f66e 100644 --- a/app/serializers/rest/application_serializer.rb +++ b/app/serializers/rest/application_serializer.rb @@ -9,7 +9,7 @@ class REST::ApplicationSerializer < ActiveModel::Serializer end def client_id - object.uid.to_s + object.uid end def client_secret diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index ef3c325ba..e6270f902 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -24,11 +24,11 @@ class REST::StatusSerializer < ActiveModel::Serializer end def in_reply_to_id - object.in_reply_to_id.to_s + object.in_reply_to_id&.to_s end def in_reply_to_account_id - object.in_reply_to_account_id.to_s + object.in_reply_to_account_id&.to_s end def current_user? -- cgit