about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/helpers/stream_entries_helper.rb4
-rw-r--r--app/lib/atom_serializer.rb2
-rw-r--r--app/models/account.rb4
-rw-r--r--app/models/status.rb6
-rw-r--r--app/services/post_status_service.rb4
-rw-r--r--app/views/stream_entries/_status.html.haml2
-rw-r--r--config/locales/en.yml4
-rw-r--r--docs/Running-Mastodon/Heroku-guide.md55
-rw-r--r--docs/Running-Mastodon/Production-guide.md2
-rw-r--r--spec/fabricators/media_attachment_fabricator.rb2
-rw-r--r--spec/fabricators/status_fabricator.rb1
-rw-r--r--spec/models/account_spec.rb68
-rw-r--r--spec/models/status_spec.rb25
-rw-r--r--spec/services/post_status_service_spec.rb168
14 files changed, 318 insertions, 29 deletions
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index a26e912a3..38e63ed8d 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -34,10 +34,6 @@ module StreamEntriesHelper
     user_signed_in? && @favourited.key?(status.id) ? 'favourited' : ''
   end
 
-  def proper_status(status)
-    status.reblog? ? status.reblog : status
-  end
-
   def rtl?(text)
     return false if text.empty?
 
diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb
index be1cced8b..b9dcee6b3 100644
--- a/app/lib/atom_serializer.rb
+++ b/app/lib/atom_serializer.rb
@@ -328,7 +328,7 @@ class AtomSerializer
 
   def serialize_status_attributes(entry, status)
     append_element(entry, 'summary', status.spoiler_text) unless status.spoiler_text.blank?
-    append_element(entry, 'content', Formatter.instance.format(status.reblog? ? status.reblog : status).to_str, type: 'html')
+    append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html')
 
     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))
diff --git a/app/models/account.rb b/app/models/account.rb
index 6968607a2..cbba8b5b6 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -125,11 +125,11 @@ class Account < ApplicationRecord
   end
 
   def favourited?(status)
-    (status.reblog? ? status.reblog : status).favourites.where(account: self).count.positive?
+    status.proper.favourites.where(account: self).count.positive?
   end
 
   def reblogged?(status)
-    (status.reblog? ? status.reblog : status).reblogs.where(account: self).count.positive?
+    status.proper.reblogs.where(account: self).count.positive?
   end
 
   def keypair
diff --git a/app/models/status.rb b/app/models/status.rb
index 6948ad77c..7e3dd3e28 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -62,8 +62,12 @@ class Status < ApplicationRecord
     reply? ? :comment : :note
   end
 
+  def proper
+    reblog? ? reblog : self
+  end
+
   def content
-    reblog? ? reblog.text : text
+    proper.text
   end
 
   def target
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index b8179f7dc..221aa42a3 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -37,11 +37,11 @@ class PostStatusService < BaseService
   def validate_media!(media_ids)
     return if media_ids.nil? || !media_ids.is_a?(Enumerable)
 
-    raise Mastodon::ValidationError, 'Cannot attach more than 4 files' if media_ids.size > 4
+    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4
 
     media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i))
 
-    raise Mastodon::ValidationError, 'Cannot attach a video to a toot that already contains images' if media.size > 1 && media.find(&:video?)
+    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media.size > 1 && media.find(&:video?)
 
     media
   end
diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml
index cdd0dde3b..434c5c8da 100644
--- a/app/views/stream_entries/_status.html.haml
+++ b/app/views/stream_entries/_status.html.haml
@@ -16,7 +16,7 @@
           %strong= display_name(status.account)
         = t('stream_entries.reblogged')
 
-  = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: proper_status(status) }
+  = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper }
 
 - if include_threads
   = render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true }
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 742219df9..aa3a732f9 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -163,3 +163,7 @@ en:
     invalid_otp_token: Invalid two-factor code
   will_paginate:
     page_gap: "&hellip;"
+  media_attachments:
+    validations:
+      too_many: Cannot attach more than 4 files
+      images_and_video: Cannot attach a video to a status that already contains images
diff --git a/docs/Running-Mastodon/Heroku-guide.md b/docs/Running-Mastodon/Heroku-guide.md
index 0de26230c..754f923ed 100644
--- a/docs/Running-Mastodon/Heroku-guide.md
+++ b/docs/Running-Mastodon/Heroku-guide.md
@@ -3,13 +3,52 @@ Heroku guide
 
 [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?button-url=https://github.com/tootsuite/mastodon&template=https://github.com/tootsuite/mastodon)
 
-Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. It should be noted this has limited testing and could have unpredictable results.
+Mastodon can be run on a free [Heroku](https://heroku.com) app. It should be
+noted this has limited testing and could have unpredictable results.
 
-1. Click the above button.
-2. Fill in the options requested.
-  * You can use a .herokuapp.com domain, which will be simple to set up, or you can use a custom domain. If you want a custom domain and HTTPS, you will need to upgrade to a paid plan (to use Heroku's SSL features), or set up [CloudFlare](https://cloudflare.com) who offer free "Flexible SSL" (note: CloudFlare have some undefined limits on WebSockets. So far, no one has reported hitting concurrent connection limits).
-  * You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details.
-  * If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests.
-3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Heroku dashboard.
+## Basic setup
 
-You may need to use the `heroku` CLI application to run `USERNAME=yourUsername rails mastodon:make_admin` to make yourself an admin.
+Click the button above to start creating a Heroku app with the Mastodon repo as
+the source. This tells Heroku to use the `app.json` file which does things like
+prompt for config variables, set up the right buildpacks, run a postdeploy task,
+and add the appropriate addons.
+
+If you don't use the deploy button and app.json approach, you will need to do
+some of that manually.
+
+## Domain names and SSL
+
+You can add your domain name to the Heroku app's setting, and then also use
+Heroku's (free) auto renewal program for Lets Encrypt certificates, by
+requesting a cert from the settings screen. You'll have to point your hostname
+DNS at Heroku using the values heroku gives you on this screen, using whatever
+method is appropriate for your DNS setup.
+
+You should set the Heroku config vars of `LOCAL_DOMAIN` to your hostname, and
+`LOCAL_HTTPS` to "true" as well.
+
+## Email
+
+Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans
+that should suit your interests. Look in `production.rb` to see which config
+variables need to be set on Heroku for outgoing email to work.
+
+## File storage
+
+You will want Amazon S3 for file storage. The only exception is for development
+purposes, where you may not care if files are not saved. Follow a guide online
+for creating a free Amazon S3 bucket and Access Key, then enter the details.
+
+## Deployment
+
+You can deploy from the Heroku web interface or from the command line. Run:
+
+  `heroku run rails db:migrate`
+
+after you first deploy to set up the first database.
+
+You may need to use the `heroku` CLI application to run:
+
+  `USERNAME=yourUsername rails mastodon:make_admin`
+
+to make yourself an admin.
diff --git a/docs/Running-Mastodon/Production-guide.md b/docs/Running-Mastodon/Production-guide.md
index 1dba56131..af21af546 100644
--- a/docs/Running-Mastodon/Production-guide.md
+++ b/docs/Running-Mastodon/Production-guide.md
@@ -90,7 +90,7 @@ It is recommended to create a special user for mastodon on the server (you could
 
     sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file git curl
     curl -sL https://deb.nodesource.com/setup_4.x | sudo bash -
-    apt-get intall nodejs
+    apt-get install nodejs
     sudo npm install -g yarn
 
 ## Redis
diff --git a/spec/fabricators/media_attachment_fabricator.rb b/spec/fabricators/media_attachment_fabricator.rb
index 59db2440d..dc91d708f 100644
--- a/spec/fabricators/media_attachment_fabricator.rb
+++ b/spec/fabricators/media_attachment_fabricator.rb
@@ -1,3 +1,3 @@
 Fabricator(:media_attachment) do
-
+  account
 end
diff --git a/spec/fabricators/status_fabricator.rb b/spec/fabricators/status_fabricator.rb
index df222fc9d..8ec5f4ba7 100644
--- a/spec/fabricators/status_fabricator.rb
+++ b/spec/fabricators/status_fabricator.rb
@@ -1,3 +1,4 @@
 Fabricator(:status) do
+  account
   text "Lorem ipsum dolor sit amet"
 end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index d7f59adb8..93a45459d 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -99,11 +99,75 @@ RSpec.describe Account, type: :model do
   end
 
   describe '#favourited?' do
-    pending
+    let(:original_status) do
+      author = Fabricate(:account, username: 'original')
+      Fabricate(:status, account: author)
+    end
+
+    context 'when the status is a reblog of another status' do
+      let(:original_reblog) do
+        author = Fabricate(:account, username: 'original_reblogger')
+        Fabricate(:status, reblog: original_status, account: author)
+      end
+
+      it 'is is true when this account has favourited it' do
+        Fabricate(:favourite, status: original_reblog, account: subject)
+
+        expect(subject.favourited?(original_status)).to eq true
+      end
+
+      it 'is false when this account has not favourited it' do
+        expect(subject.favourited?(original_status)).to eq false
+      end
+    end
+
+    context 'when the status is an original status' do
+      it 'is is true when this account has favourited it' do
+        Fabricate(:favourite, status: original_status, account: subject)
+
+        expect(subject.favourited?(original_status)).to eq true
+      end
+
+      it 'is false when this account has not favourited it' do
+        expect(subject.favourited?(original_status)).to eq false
+      end
+    end
   end
 
   describe '#reblogged?' do
-    pending
+    let(:original_status) do
+      author = Fabricate(:account, username: 'original')
+      Fabricate(:status, account: author)
+    end
+
+    context 'when the status is a reblog of another status'do
+      let(:original_reblog) do
+        author = Fabricate(:account, username: 'original_reblogger')
+        Fabricate(:status, reblog: original_status, account: author)
+      end
+
+      it 'is true when this account has reblogged it' do
+        Fabricate(:status, reblog: original_reblog, account: subject)
+
+        expect(subject.reblogged?(original_reblog)).to eq true
+      end
+
+      it 'is false when this account has not reblogged it' do
+        expect(subject.reblogged?(original_reblog)).to eq false
+      end
+    end
+
+    context 'when the status is an original status' do
+      it 'is true when this account has reblogged it' do
+        Fabricate(:status, reblog: original_status, account: subject)
+
+        expect(subject.reblogged?(original_status)).to eq true
+      end
+
+      it 'is false when this account has not reblogged it' do
+        expect(subject.reblogged?(original_status)).to eq false
+      end
+    end
   end
 
   describe '.find_local' do
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index b9d079521..000bee0f5 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -91,10 +91,31 @@ RSpec.describe Status, type: :model do
   end
 
   describe '#reblogs_count' do
-    pending
+    it 'is the number of reblogs' do
+      Fabricate(:status, account: bob, reblog: subject)
+      Fabricate(:status, account: alice, reblog: subject)
+
+      expect(subject.reblogs_count).to eq 2
+    end
   end
 
   describe '#favourites_count' do
-    pending
+    it 'is the number of favorites' do
+      Fabricate(:favourite, account: bob, status: subject)
+      Fabricate(:favourite, account: alice, status: subject)
+
+      expect(subject.favourites_count).to eq 2
+    end
+  end
+
+  describe '#proper' do
+    it 'is itself for original statuses' do
+      expect(subject.proper).to eq subject
+    end
+
+    it 'is the source status for reblogs' do
+      subject.reblog = other
+      expect(subject.proper).to eq other
+    end
   end
 end
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 9ee4daf6f..0e39cd969 100644
--- a/spec/services/post_status_service_spec.rb
+++ b/spec/services/post_status_service_spec.rb
@@ -3,8 +3,168 @@ require 'rails_helper'
 RSpec.describe PostStatusService do
   subject { PostStatusService.new }
 
-  it 'creates a new status'
-  it 'creates a new response status'
-  it 'processes mentions'
-  it 'pings PuSH hubs'
+  it 'creates a new status' do
+    account = Fabricate(:account)
+    text = "test status update"
+
+    status = subject.call(account, text)
+
+    expect(status).to be_persisted
+    expect(status.text).to eq text
+  end
+
+  it 'creates a new response status' do
+    in_reply_to_status = Fabricate(:status)
+    account = Fabricate(:account)
+    text = "test status update"
+
+    status = subject.call(account, text, in_reply_to_status)
+
+    expect(status).to be_persisted
+    expect(status.text).to eq text
+    expect(status.thread).to eq in_reply_to_status
+  end
+
+  it 'creates a sensitive status' do
+    status = create_status_with_options(sensitive: true)
+
+    expect(status).to be_persisted
+    expect(status).to be_sensitive
+  end
+
+  it 'creates a status with spoiler text' do
+    spoiler_text = "spoiler text"
+
+    status = create_status_with_options(spoiler_text: spoiler_text)
+
+    expect(status).to be_persisted
+    expect(status.spoiler_text).to eq spoiler_text
+  end
+
+  it 'creates a status with empty default spoiler text' do
+    status = create_status_with_options(spoiler_text: nil)
+
+    expect(status).to be_persisted
+    expect(status.spoiler_text).to eq ''
+  end
+
+  it 'creates a status with the given visibility' do
+    status = create_status_with_options(visibility: :private)
+
+    expect(status).to be_persisted
+    expect(status.visibility).to eq "private"
+  end
+
+  it 'creates a status for the given application' do
+    application = Fabricate(:application)
+
+    status = create_status_with_options(application: application)
+
+    expect(status).to be_persisted
+    expect(status.application).to eq application
+  end
+
+  it 'processes mentions' do
+    mention_service = double(:process_mentions_service)
+    allow(mention_service).to receive(:call)
+    allow(ProcessMentionsService).to receive(:new).and_return(mention_service)
+    account = Fabricate(:account)
+
+    status = subject.call(account, "test status update")
+
+    expect(ProcessMentionsService).to have_received(:new)
+    expect(mention_service).to have_received(:call).with(status)
+  end
+
+  it 'processes hashtags' do
+    hashtags_service = double(:process_hashtags_service)
+    allow(hashtags_service).to receive(:call)
+    allow(ProcessHashtagsService).to receive(:new).and_return(hashtags_service)
+    account = Fabricate(:account)
+
+    status = subject.call(account, "test status update")
+
+    expect(ProcessHashtagsService).to have_received(:new)
+    expect(hashtags_service).to have_received(:call).with(status)
+  end
+
+  it 'pings PuSH hubs' do
+    allow(DistributionWorker).to receive(:perform_async)
+    allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async)
+    account = Fabricate(:account)
+
+    status = subject.call(account, "test status update")
+
+    expect(DistributionWorker).to have_received(:perform_async).with(status.id)
+    expect(Pubsubhubbub::DistributionWorker).
+      to have_received(:perform_async).with(status.stream_entry.id)
+  end
+
+  it 'crawls links' do
+    allow(LinkCrawlWorker).to receive(:perform_async)
+    account = Fabricate(:account)
+
+    status = subject.call(account, "test status update")
+
+    expect(LinkCrawlWorker).to have_received(:perform_async).with(status.id)
+  end
+
+  it 'attaches the given media to the created status' do
+    account = Fabricate(:account)
+    media = Fabricate(:media_attachment)
+
+    status = subject.call(
+      account,
+      "test status update",
+      nil,
+      media_ids: [media.id],
+    )
+
+    expect(media.reload.status).to eq status
+  end
+
+  it 'does not allow attaching more than 4 files' do
+    account = Fabricate(:account)
+
+    expect do
+      subject.call(
+        account,
+        "test status update",
+        nil,
+        media_ids: [
+          Fabricate(:media_attachment, account: account),
+          Fabricate(:media_attachment, account: account),
+          Fabricate(:media_attachment, account: account),
+          Fabricate(:media_attachment, account: account),
+          Fabricate(:media_attachment, account: account),
+        ].map(&:id),
+      )
+    end.to raise_error(
+      Mastodon::ValidationError,
+      I18n.t('media_attachments.validations.too_many'),
+    )
+  end
+
+  it 'does not allow attaching both videos and images' do
+    account = Fabricate(:account)
+
+    expect do
+      subject.call(
+        account,
+        "test status update",
+        nil,
+        media_ids: [
+          Fabricate(:media_attachment, type: :video, account: account),
+          Fabricate(:media_attachment, type: :image, account: account),
+        ].map(&:id),
+      )
+    end.to raise_error(
+      Mastodon::ValidationError,
+      I18n.t('media_attachments.validations.images_and_video'),
+    )
+  end
+
+  def create_status_with_options(options = {})
+    subject.call(Fabricate(:account), "test", nil, options)
+  end
 end