From 230a012f0090c496fc5cdb011bcc8ed732fd0f5c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 3 Mar 2019 22:18:23 +0100 Subject: Add polls (#10111) * Add polls Fix #1629 * Add tests * Fixes * Change API for creating polls * Use name instead of content for votes * Remove poll validation for remote polls * Add polls to public pages * When updating the poll, update options just in case they were changed * Fix public pages showing both poll and other media --- app/lib/activitypub/activity.rb | 2 +- app/lib/activitypub/activity/create.rb | 38 +++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) (limited to 'app/lib') diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 11fa3363a..54b175613 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -4,7 +4,7 @@ class ActivityPub::Activity include JsonLdHelper include Redisable - SUPPORTED_TYPES = %w(Note).freeze + SUPPORTED_TYPES = %w(Note Question).freeze CONVERTED_TYPES = %w(Image Video Article Page).freeze def initialize(json, account, **options) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 0980f94ba..793e20dbe 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -6,7 +6,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity RedisLock.acquire(lock_options) do |lock| if lock.acquired? - return if delete_arrived_first?(object_uri) + return if delete_arrived_first?(object_uri) || poll_vote? @status = find_existing_status @@ -68,6 +68,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity thread: replied_to_status, conversation: conversation_from_uri(@object['conversation']), media_attachment_ids: process_attachments.take(4).map(&:id), + owned_poll: process_poll, } end end @@ -209,6 +210,41 @@ class ActivityPub::Activity::Create < ActivityPub::Activity media_attachments end + def process_poll + return unless @object['type'] == 'Question' && (@object['anyOf'].is_a?(Array) || @object['oneOf'].is_a?(Array)) + + expires_at = begin + if @object['closed'].is_a?(String) + @object['closed'] + elsif !@object['closed'].is_a?(FalseClass) + Time.now.utc + else + @object['endTime'] + end + end + + if @object['anyOf'].is_a?(Array) + multiple = true + items = @object['anyOf'] + else + multiple = false + items = @object['oneOf'] + end + + Poll.new( + account: @account, + multiple: multiple, + expires_at: expires_at, + options: items.map { |item| item['name'].presence || item['content'] }, + cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } + ) + end + + def poll_vote? + return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name']) + replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name'])) + end + def resolve_thread(status) return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri) ThreadResolveWorker.perform_async(status.id, in_reply_to_uri) -- cgit From ae1b9cf70a5c7426054947bef8cc836fd402c173 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Mar 2019 00:44:34 +0100 Subject: Fix remote poll expiration time (#10144) --- app/lib/activitypub/activity/create.rb | 2 +- app/services/activitypub/fetch_remote_poll_service.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app/lib') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 793e20dbe..08c46be44 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -216,7 +216,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity expires_at = begin if @object['closed'].is_a?(String) @object['closed'] - elsif !@object['closed'].is_a?(FalseClass) + elsif !@object['closed'].nil? && !@object['closed'].is_a?(FalseClass) Time.now.utc else @object['endTime'] diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb index ea75e8ef9..2f40625d6 100644 --- a/app/services/activitypub/fetch_remote_poll_service.rb +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -11,7 +11,7 @@ class ActivityPub::FetchRemotePollService < BaseService expires_at = begin if @json['closed'].is_a?(String) @json['closed'] - elsif !@json['closed'].is_a?(FalseClass) + elsif !@object['closed'].nil? && !@object['closed'].is_a?(FalseClass) Time.now.utc else @json['endTime'] -- cgit From 0e6998da3cdc0ac73845d1c3c3c4c75972ea28ee Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Mar 2019 01:13:42 +0100 Subject: Add tests for ActivityPub poll processing (#10143) --- app/lib/activitypub/activity/create.rb | 3 +- spec/lib/activitypub/activity/create_spec.rb | 42 +++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) (limited to 'app/lib') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 08c46be44..fc4c45692 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -231,8 +231,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity items = @object['oneOf'] end - Poll.new( - account: @account, + @account.polls.new( multiple: multiple, expires_at: expires_at, options: items.map { |item| item['name'].presence || item['content'] }, diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 26cb84871..ac6237c86 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Create do - let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') } + let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') } let(:json) do { @@ -407,6 +407,46 @@ RSpec.describe ActivityPub::Activity::Create do expect(status).to_not be_nil end end + + context 'with poll' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Question', + content: 'Which color was the submarine?', + oneOf: [ + { + name: 'Yellow', + replies: { + type: 'Collection', + totalItems: 10, + }, + }, + { + name: 'Blue', + replies: { + type: 'Collection', + totalItems: 3, + } + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + expect(status.poll).to_not be_nil + end + + it 'creates a poll' do + poll = sender.polls.first + expect(poll).to_not be_nil + expect(poll.status).to_not be_nil + expect(poll.options).to eq %w(Yellow Blue) + expect(poll.cached_tallies).to eq [10, 3] + end + end end context 'when sender is followed by local users' do -- cgit From 833ffce2df68ae3b673e230fcb273da5d8c4681f Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 22:51:23 +0100 Subject: Store remote votes URI (#10158) * Store remote votes URI * Add spec for accepting remote votes * Make poll vote id generation work the same way as follows --- app/lib/activitypub/activity/create.rb | 2 +- app/models/poll_vote.rb | 3 +++ app/serializers/activitypub/vote_serializer.rb | 2 +- db/migrate/20190304152020_add_uri_to_poll_votes.rb | 5 +++++ db/schema.rb | 3 ++- spec/lib/activitypub/activity/create_spec.rb | 21 +++++++++++++++++++++ 6 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20190304152020_add_uri_to_poll_votes.rb (limited to 'app/lib') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index fc4c45692..07ef16bf3 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -241,7 +241,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def poll_vote? return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name']) - replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name'])) + replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id']) end def resolve_thread(status) diff --git a/app/models/poll_vote.rb b/app/models/poll_vote.rb index 57781d616..aec678968 100644 --- a/app/models/poll_vote.rb +++ b/app/models/poll_vote.rb @@ -9,6 +9,7 @@ # choice :integer default(0), not null # created_at :datetime not null # updated_at :datetime not null +# uri :string # class PollVote < ApplicationRecord @@ -20,6 +21,8 @@ class PollVote < ApplicationRecord after_create_commit :increment_counter_cache + delegate :local?, to: :account + private def increment_counter_cache diff --git a/app/serializers/activitypub/vote_serializer.rb b/app/serializers/activitypub/vote_serializer.rb index 655d04d22..248190404 100644 --- a/app/serializers/activitypub/vote_serializer.rb +++ b/app/serializers/activitypub/vote_serializer.rb @@ -6,7 +6,7 @@ class ActivityPub::VoteSerializer < ActiveModel::Serializer :in_reply_to, :to def id - [ActivityPub::TagManager.instance.uri_for(object.account), '#votes/', object.id].join + ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#votes/', object.id].join end def type diff --git a/db/migrate/20190304152020_add_uri_to_poll_votes.rb b/db/migrate/20190304152020_add_uri_to_poll_votes.rb new file mode 100644 index 000000000..f6b81f1ba --- /dev/null +++ b/db/migrate/20190304152020_add_uri_to_poll_votes.rb @@ -0,0 +1,5 @@ +class AddUriToPollVotes < ActiveRecord::Migration[5.2] + def change + add_column :poll_votes, :uri, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index d5d516e06..161619dcf 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: 2019_02_26_003449) do +ActiveRecord::Schema.define(version: 2019_03_04_152020) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -447,6 +447,7 @@ ActiveRecord::Schema.define(version: 2019_02_26_003449) do t.integer "choice", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "uri" t.index ["account_id"], name: "index_poll_votes_on_account_id" t.index ["poll_id"], name: "index_poll_votes_on_poll_id" end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index ac6237c86..4780c29c8 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -447,6 +447,27 @@ RSpec.describe ActivityPub::Activity::Create do expect(poll.cached_tallies).to eq [10, 3] end end + + context 'when a vote to a local poll' do + let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } + let!(:local_status) { Fabricate(:status, owned_poll: poll) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + name: 'Yellow', + inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status) + } + end + + it 'adds a vote to the poll with correct uri' do + vote = poll.votes.first + expect(vote).to_not be_nil + expect(vote.uri).to eq object_json[:id] + expect(poll.reload.cached_tallies).to eq [1, 0] + end + end end context 'when sender is followed by local users' do -- cgit From 636db1f54fbd1690d083276b18f980cc5a51bcf0 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 5 Mar 2019 21:09:18 +0100 Subject: When serializing polls over OStatus, serialize poll options to text (#10160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * When serializing polls over OStatus, serialize poll options to text * Do the same for RSS feeds * Use “[ ] ” as a prefix for poll options instead of “- ” --- app/lib/formatter.rb | 4 ++++ app/lib/ostatus/atom_serializer.rb | 2 +- app/serializers/rss/account_serializer.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) (limited to 'app/lib') diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 0653214f5..b9845cb45 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -19,6 +19,10 @@ class Formatter raw_content = status.text + if options[:inline_poll_options] && status.poll + raw_content = raw_content + '\n\n' + status.poll.options.map { |title| "[ ] #{title}" }.join('\n') + end + return '' if raw_content.blank? unless status.local? diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 7a181fb40..9a05d96cf 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -352,7 +352,7 @@ class OStatus::AtomSerializer append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(status)) if status.account.local? append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text? - append_element(entry, 'content', Formatter.instance.format(status).to_str || '.', type: 'html', 'xml:lang': status.language) + append_element(entry, 'content', Formatter.instance.format(status, inline_poll_options: true).to_str || '.', type: 'html', 'xml:lang': status.language) status.active_mentions.sort_by(&:id).each do |mentioned| append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account)) diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb index bde360a41..712b1347a 100644 --- a/app/serializers/rss/account_serializer.rb +++ b/app/serializers/rss/account_serializer.rb @@ -22,7 +22,7 @@ class RSS::AccountSerializer item.title(status.title) .link(TagManager.instance.url_for(status)) .pub_date(status.created_at) - .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str) + .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str) status.media_attachments.each do |media| item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, length: media.file.size) -- cgit