Merge branch 'master' into master

th-downstream
Shel R 8 years ago committed by GitHub
commit 76c073d4fd

@ -5,3 +5,4 @@ public/assets
node_modules node_modules
storybook storybook
neo4j neo4j
vendor/bundle

@ -25,7 +25,11 @@ OTP_SECRET=
# Only allow registrations with the following e-mail domains # Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc # EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# Optionally change default language
# DEFAULT_LOCALE=de
# E-mail configuration # E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
SMTP_SERVER=smtp.mailgun.org SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587 SMTP_PORT=587
SMTP_LOGIN= SMTP_LOGIN=
@ -44,6 +48,16 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_PROTOCOL=http # S3_PROTOCOL=http
# S3_HOSTNAME=192.168.1.123:9000 # S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details)
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=
# S3_ENDPOINT=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST= # S3_CLOUDFRONT_HOST=

@ -1,2 +1,5 @@
node_modules/ node_modules/
.cache/ .cache/
docs/
spec/
storybook/

@ -1,11 +1,16 @@
FROM ruby:2.3.1-alpine FROM ruby:2.3.1-alpine
LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server"
ENV RAILS_ENV=production \ ENV RAILS_ENV=production \
NODE_ENV=production NODE_ENV=production
EXPOSE 3000 4000
WORKDIR /mastodon WORKDIR /mastodon
COPY . /mastodon COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN BUILD_DEPS=" \ RUN BUILD_DEPS=" \
postgresql-dev \ postgresql-dev \
@ -24,8 +29,11 @@ RUN BUILD_DEPS=" \
&& npm install -g npm@3 && npm install -g yarn \ && npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \ && bundle install --deployment --without test development \
&& yarn \ && yarn \
&& npm cache clean \ && yarn cache clean \
&& npm -g cache clean \
&& apk del $BUILD_DEPS \ && apk del $BUILD_DEPS \
&& rm -rf /tmp/* /var/cache/apk/* && rm -rf /tmp/* /var/cache/apk/*
COPY . /mastodon
VOLUME /mastodon/public/system /mastodon/public/assets VOLUME /mastodon/public/system /mastodon/public/assets

@ -34,6 +34,7 @@ gem 'doorkeeper'
gem 'rabl' gem 'rabl'
gem 'rqrcode' gem 'rqrcode'
gem 'twitter-text' gem 'twitter-text'
gem 'ox'
gem 'oj' gem 'oj'
gem 'hiredis' gem 'hiredis'
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']

@ -240,6 +240,7 @@ GEM
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
ox (2.4.11)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -482,6 +483,7 @@ DEPENDENCIES
nokogiri nokogiri
oj oj
ostatus2 ostatus2
ox
paperclip (~> 5.1) paperclip (~> 5.1)
paperclip-av-transcoder paperclip-av-transcoder
pg pg

@ -1,2 +1,2 @@
web: bundle exec puma -C config/puma.rb web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq -q default -q mailers -q push worker: bundle exec sidekiq -q default -q push -q pull -q mailers

@ -65,6 +65,8 @@ Consult the example configuration file, `.env.production.sample` for the full li
## Running with Docker and Docker-Compose ## Running with Docker and Docker-Compose
[![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com")
The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can: The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
docker-compose build docker-compose build

14
Vagrantfile vendored

@ -84,6 +84,16 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "1024"] vb.customize ["modifyvm", :id, "--memory", "1024"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172
vb.customize ["modifyvm", :id, "--natdnsproxy1", "off"]
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "off"]
# Use "virtio" network interfaces for better performance.
vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
end end
config.vm.hostname = "mastodon.dev" config.vm.hostname = "mastodon.dev"
@ -91,9 +101,9 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# This uses the vagrant-hostsupdater plugin, and lets you # This uses the vagrant-hostsupdater plugin, and lets you
# access the development site at http://mastodon.dev. # access the development site at http://mastodon.dev.
# To install: # To install:
# $ vagrant plugin install hostsupdater # $ vagrant plugin install vagrant-hostsupdater
if defined?(VagrantPlugins::HostsUpdater) if defined?(VagrantPlugins::HostsUpdater)
config.vm.network :private_network, ip: "192.168.42.42" config.vm.network :private_network, ip: "192.168.42.42", nictype: "virtio"
config.hostsupdater.remove_on_suspend = false config.hostsupdater.remove_on_suspend = false
end end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 874 KiB

After

Width:  |  Height:  |  Size: 209 KiB

@ -1,5 +1,9 @@
@import 'variables'; @import 'variables';
.app-body{
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.button { .button {
background-color: darken($color4, 3%); background-color: darken($color4, 3%);
font-family: inherit; font-family: inherit;

@ -16,7 +16,8 @@ class AccountsController < ApplicationController
end end
format.atom do format.atom do
@entries = @account.stream_entries.order('id desc').where(activity_type: 'Status').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id]) @entries = @account.stream_entries.order('id desc').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
end end
format.activitystreams2 format.activitystreams2

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Localized
# Prevent CSRF attacks by raising an exception. # Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead. # For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception protect_from_forgery with: :exception
@ -14,7 +16,6 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :set_locale
before_action :set_user_activity before_action :set_user_activity
before_action :check_suspension, if: :user_signed_in? before_action :check_suspension, if: :user_signed_in?
@ -28,12 +29,6 @@ class ApplicationController < ActionController::Base
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end
def set_locale
I18n.locale = current_user.try(:locale) || I18n.default_locale
rescue I18n::InvalidLocale
I18n.locale = I18n.default_locale
end
def require_admin! def require_admin!
redirect_to root_path unless current_user&.admin? redirect_to root_path unless current_user&.admin?
end end

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Localized
extend ActiveSupport::Concern
included do
before_action :set_locale
end
def set_locale
I18n.locale = current_user.try(:locale) || default_locale
rescue I18n::InvalidLocale
I18n.locale = default_locale
end
def default_locale
ENV.fetch('DEFAULT_LOCALE') { I18n.default_locale }
end
end

@ -1,9 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include Localized
skip_before_action :authenticate_resource_owner! skip_before_action :authenticate_resource_owner!
before_action :set_locale
before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
@ -12,10 +13,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
def store_current_location def store_current_location
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end
def set_locale
I18n.locale = current_user.try(:locale) || I18n.default_locale
rescue I18n::InvalidLocale
I18n.locale = I18n.default_locale
end
end end

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
include Localized
skip_before_action :authenticate_resource_owner!
before_action :store_current_location
before_action :authenticate_resource_owner!
private
def store_current_location
store_location_for(:user, request.url)
end
end

@ -19,7 +19,9 @@ class StreamEntriesController < ApplicationController
end end
end end
format.atom format.atom do
render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true))
end
end end
end end

@ -34,10 +34,6 @@ module StreamEntriesHelper
user_signed_in? && @favourited.key?(status.id) ? 'favourited' : '' user_signed_in? && @favourited.key?(status.id) ? 'favourited' : ''
end end
def proper_status(status)
status.reblog? ? status.reblog : status
end
def rtl?(text) def rtl?(text)
return false if text.empty? return false if text.empty?

@ -0,0 +1,351 @@
# frozen_string_literal: true
class AtomSerializer
include RoutingHelper
class << self
def render(element)
document = Ox::Document.new(version: '1.0')
document << element
('<?xml version="1.0"?>' + Ox.dump(element)).force_encoding('UTF-8')
end
end
def author(account)
author = Ox::Element.new('author')
uri = TagManager.instance.uri_for(account)
append_element(author, 'id', uri)
append_element(author, 'activity:object-type', TagManager::TYPES[:person])
append_element(author, 'uri', uri)
append_element(author, 'name', account.username)
append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct)
append_element(author, 'summary', account.note)
append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original)))
append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original)))
append_element(author, 'poco:preferredUsername', account.username)
append_element(author, 'poco:displayName', account.display_name) unless account.display_name.blank?
append_element(author, 'poco:note', Formatter.instance.simplified_format(account).to_str) unless account.note.blank?
append_element(author, 'mastodon:scope', account.locked? ? :private : :public)
author
end
def feed(account, stream_entries)
feed = Ox::Element.new('feed')
add_namespaces(feed)
append_element(feed, 'id', account_url(account, format: 'atom'))
append_element(feed, 'title', account.display_name)
append_element(feed, 'subtitle', account.note)
append_element(feed, 'updated', account.updated_at.iso8601)
append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
feed << author(account)
append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom'))
append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20
append_element(feed, 'link', nil, rel: :hub, href: api_push_url)
append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id))
stream_entries.each do |stream_entry|
feed << entry(stream_entry)
end
feed
end
def entry(stream_entry, root = false)
entry = Ox::Element.new('entry')
add_namespaces(entry) if root
append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type))
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)
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])
entry << object(stream_entry.target) if stream_entry.targeted?
serialize_status_attributes(entry, stream_entry.status) unless stream_entry.status.nil?
append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: account_stream_entry_url(stream_entry.account, stream_entry))
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?
entry
end
def object(status)
object = Ox::Element.new('activity:object')
append_element(object, 'id', 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])
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)) if status.reply? && !status.thread.nil?
object
end
def follow_salmon(follow)
entry = Ox::Element.new('entry')
add_namespaces(entry)
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, '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])
object = author(follow.target_account)
object.value = 'activity:object'
entry << object
entry
end
def follow_request_salmon(follow_request)
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, '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])
object = author(follow_request.target_account)
object.value = 'activity:object'
entry << object
entry
end
def authorize_follow_request_salmon(follow_request)
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, '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])
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])
inner_object = author(follow_request.target_account)
inner_object.value = 'activity:object'
object << inner_object
entry << object
entry
end
def reject_follow_request_salmon(follow_request)
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, '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])
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])
inner_object = author(follow_request.target_account)
inner_object.value = 'activity:object'
object << inner_object
entry << object
entry
end
def unfollow_salmon(follow)
entry = Ox::Element.new('entry')
add_namespaces(entry)
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, '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])
object = author(follow.target_account)
object.value = 'activity:object'
entry << object
entry
end
def block_salmon(block)
entry = Ox::Element.new('entry')
add_namespaces(entry)
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, 'title', description)
entry << author(block.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:block])
object = author(block.target_account)
object.value = 'activity:object'
entry << object
entry
end
def unblock_salmon(block)
entry = Ox::Element.new('entry')
add_namespaces(entry)
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, 'title', description)
entry << author(block.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:unblock])
object = author(block.target_account)
object.value = 'activity:object'
entry << object
entry
end
def favourite_salmon(favourite)
entry = Ox::Element.new('entry')
add_namespaces(entry)
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, '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])
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))
entry
end
def unfavourite_salmon(favourite)
entry = Ox::Element.new('entry')
add_namespaces(entry)
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, '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])
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))
entry
end
private
def append_element(parent, name, content = nil, attributes = {})
element = Ox::Element.new(name)
attributes.each { |k, v| element[k] = v.to_s }
element << content.to_s unless content.nil?
parent << element
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
end
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.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))
end
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:collection], href: TagManager::COLLECTIONS[:public]) if status.public_visibility?
status.tags.each do |tag|
append_element(entry, 'category', nil, term: tag.name)
end
append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive?
status.media_attachments.each do |media|
append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false)))
end
append_element(entry, 'mastodon:scope', status.visibility)
end
end

@ -78,6 +78,8 @@ class TagManager
case target.object_type case target.object_type
when :person when :person
account_url(target) account_url(target)
when :note, :comment, :activity
unique_tag(target.created_at, target.id, 'Status')
else else
unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type) unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type)
end end

@ -125,11 +125,11 @@ class Account < ApplicationRecord
end end
def favourited?(status) def favourited?(status)
(status.reblog? ? status.reblog : status).favourites.where(account: self).count.positive? status.proper.favourites.where(account: self).count.positive?
end end
def reblogged?(status) def reblogged?(status)
(status.reblog? ? status.reblog : status).reblogs.where(account: self).count.positive? status.proper.reblogs.where(account: self).count.positive?
end end
def keypair def keypair

@ -62,8 +62,12 @@ class Status < ApplicationRecord
reply? ? :comment : :note reply? ? :comment : :note
end end
def proper
reblog? ? reblog : self
end
def content def content
reblog? ? reblog.text : text proper.text
end end
def target def target

@ -5,25 +5,21 @@ class StreamEntry < ApplicationRecord
belongs_to :account, inverse_of: :stream_entries belongs_to :account, inverse_of: :stream_entries
belongs_to :activity, polymorphic: true belongs_to :activity, polymorphic: true
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry
validates :account, :activity, presence: true validates :account, :activity, presence: true
STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, mentions: :account], thread: [:stream_entry, :account]].freeze STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account], thread: [:stream_entry, :account]].freeze
default_scope { where(activity_type: 'Status') }
scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) } scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) }
def object_type def object_type
if orphaned? orphaned? || targeted? ? :activity : status.object_type
:activity
else
targeted? ? :activity : activity.object_type
end
end end
def verb def verb
orphaned? ? :delete : activity.verb orphaned? ? :delete : status.verb
end end
def targeted? def targeted?
@ -31,15 +27,15 @@ class StreamEntry < ApplicationRecord
end end
def target def target
orphaned? ? nil : activity.target orphaned? ? nil : status.target
end end
def title def title
orphaned? ? nil : activity.title orphaned? ? nil : status.title
end end
def content def content
orphaned? ? nil : activity.content orphaned? ? nil : status.content
end end
def threaded? def threaded?
@ -47,20 +43,16 @@ class StreamEntry < ApplicationRecord
end end
def thread def thread
orphaned? ? nil : activity.thread orphaned? ? nil : status.thread
end end
def mentions def mentions
activity.respond_to?(:mentions) ? activity.mentions.map(&:account) : [] orphaned? ? [] : status.mentions.map(&:account)
end
def activity
!new_record? ? send(activity_type.underscore) || super : super
end end
private private
def orphaned? def orphaned?
activity.nil? status.nil?
end end
end end

@ -9,20 +9,20 @@ class AfterBlockService < BaseService
private private
def clear_timelines(account, target_account) def clear_timelines(account, target_account)
mentions_key = FeedManager.instance.key(:mentions, account.id)
home_key = FeedManager.instance.key(:home, account.id) home_key = FeedManager.instance.key(:home, account.id)
redis.pipelined do
target_account.statuses.select('id').find_each do |status| target_account.statuses.select('id').find_each do |status|
redis.zrem(mentions_key, status.id)
redis.zrem(home_key, status.id) redis.zrem(home_key, status.id)
end end
end end
end
def clear_notifications(account, target_account) def clear_notifications(account, target_account)
Notification.where(account: account).joins(:follow).where(activity_type: 'Follow', follows: { account_id: target_account.id }).destroy_all Notification.where(account: account).joins(:follow).where(activity_type: 'Follow', follows: { account_id: target_account.id }).delete_all
Notification.where(account: account).joins(mention: :status).where(activity_type: 'Mention', statuses: { account_id: target_account.id }).destroy_all Notification.where(account: account).joins(mention: :status).where(activity_type: 'Mention', statuses: { account_id: target_account.id }).delete_all
Notification.where(account: account).joins(:favourite).where(activity_type: 'Favourite', favourites: { account_id: target_account.id }).destroy_all Notification.where(account: account).joins(:favourite).where(activity_type: 'Favourite', favourites: { account_id: target_account.id }).delete_all
Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).destroy_all Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).delete_all
end end
def redis def redis

@ -10,31 +10,6 @@ class AuthorizeFollowService < BaseService
private private
def build_xml(follow_request) def build_xml(follow_request)
Nokogiri::XML::Builder.new do |xml| AtomSerializer.render(AtomSerializer.new.authorize_follow_request_salmon(follow_request))
entry(xml, true) do
unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest'
title xml, "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}"
author(xml) do
include_author xml, follow_request.target_account
end
object_type xml, :activity
verb xml, :authorize
target(xml) do
author(xml) do
include_author xml, follow_request.account
end
object_type xml, :activity
verb xml, :request_friend
target(xml) do
include_author xml, follow_request.target_account
end
end
end
end.to_xml
end end
end end

@ -18,22 +18,6 @@ class BlockService < BaseService
private private
def build_xml(block) def build_xml(block)
Nokogiri::XML::Builder.new do |xml| AtomSerializer.render(AtomSerializer.new.block_salmon(block))
entry(xml, true) do
unique_id xml, block.created_at, block.id, 'Block'
title xml, "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}"
author(xml) do
include_author xml, block.account
end
object_type xml, :activity
verb xml, :block
target(xml) do
include_author xml, block.target_account
end
end
end.to_xml
end end
end end

@ -2,7 +2,6 @@
module StreamEntryRenderer module StreamEntryRenderer
def stream_entry_to_xml(stream_entry) def stream_entry_to_xml(stream_entry)
renderer = StreamEntriesController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https) AtomSerializer.render(AtomSerializer.new.entry(stream_entry, true))
renderer.render(:show, assigns: { stream_entry: stream_entry }, formats: [:atom])
end end
end end

@ -51,21 +51,22 @@ class FanOutOnWriteService < BaseService
def render_anonymous_payload(status) def render_anonymous_payload(status)
@payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show') @payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show')
@payload = Oj.dump(event: :update, payload: @payload)
end end
def deliver_to_hashtags(status) def deliver_to_hashtags(status)
Rails.logger.debug "Delivering status #{status.id} to hashtags" Rails.logger.debug "Delivering status #{status.id} to hashtags"
status.tags.pluck(:name).each do |hashtag| status.tags.pluck(:name).each do |hashtag|
Redis.current.publish("hashtag:#{hashtag}", Oj.dump(event: :update, payload: @payload)) Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
Redis.current.publish("hashtag:#{hashtag}:local", Oj.dump(event: :update, payload: @payload)) if status.account.local? Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
end end
end end
def deliver_to_public(status) def deliver_to_public(status)
Rails.logger.debug "Delivering status #{status.id} to public timeline" Rails.logger.debug "Delivering status #{status.id} to public timeline"
Redis.current.publish('public', Oj.dump(event: 'update', payload: @payload)) Redis.current.publish('timeline:public', @payload)
Redis.current.publish('public:local', Oj.dump(event: 'update', payload: @payload)) if status.account.local? Redis.current.publish('timeline:public:local', @payload) if status.local?
end end
end end

@ -22,26 +22,6 @@ class FavouriteService < BaseService
private private
def build_xml(favourite) def build_xml(favourite)
description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}" AtomSerializer.render(AtomSerializer.new.favourite_salmon(favourite))
Nokogiri::XML::Builder.new do |xml|
entry(xml, true) do
unique_id xml, favourite.created_at, favourite.id, 'Favourite'
title xml, description
content xml, description
author(xml) do
include_author xml, favourite.account
end
object_type xml, :activity
verb xml, :favorite
in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status)
target(xml) do
include_target xml, favourite.status
end
end
end.to_xml
end end
end end

@ -55,48 +55,10 @@ class FollowService < BaseService
end end
def build_follow_request_xml(follow_request) def build_follow_request_xml(follow_request)
description = "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}" AtomSerializer.render(AtomSerializer.new.follow_request_salmon(follow_request))
Nokogiri::XML::Builder.new do |xml|
entry(xml, true) do
unique_id xml, follow_request.created_at, follow_request.id, 'FollowRequest'
title xml, description
content xml, description
author(xml) do
include_author xml, follow_request.account
end
object_type xml, :activity
verb xml, :request_friend
target(xml) do
include_author xml, follow_request.target_account
end
end
end.to_xml
end end
def build_follow_xml(follow) def build_follow_xml(follow)
description = "#{follow.account.acct} started following #{follow.target_account.acct}" AtomSerializer.render(AtomSerializer.new.follow_salmon(follow))
Nokogiri::XML::Builder.new do |xml|
entry(xml, true) do
unique_id xml, follow.created_at, follow.id, 'Follow'
title xml, description
content xml, description
author(xml) do
include_author xml, follow.account
end
object_type xml, :activity
verb xml, :follow
target(xml) do
include_author xml, follow.target_account
end
end
end.to_xml
end end
end end

@ -50,7 +50,7 @@ class NotifyService < BaseService
def create_notification def create_notification
@notification.save! @notification.save!
return unless @notification.browserable? return unless @notification.browserable?
Redis.current.publish(@recipient.id, Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show'))) Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show')))
end end
def send_email def send_email

@ -37,11 +37,11 @@ class PostStatusService < BaseService
def validate_media!(media_ids) def validate_media!(media_ids)
return if media_ids.nil? || !media_ids.is_a?(Enumerable) 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)) 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 media
end end

@ -10,31 +10,6 @@ class RejectFollowService < BaseService
private private
def build_xml(follow_request) def build_xml(follow_request)
Nokogiri::XML::Builder.new do |xml| AtomSerializer.render(AtomSerializer.new.reject_follow_request_salmon(follow_request))
entry(xml, true) do
unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest'
title xml, "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}"
author(xml) do
include_author xml, follow_request.target_account
end
object_type xml, :activity
verb xml, :reject
target(xml) do
author(xml) do
include_author xml, follow_request.account
end
object_type xml, :activity
verb xml, :request_friend
target(xml) do
include_author xml, follow_request.target_account
end
end
end
end.to_xml
end end
end end

@ -4,6 +4,8 @@ class RemoveStatusService < BaseService
include StreamEntryRenderer include StreamEntryRenderer
def call(status) def call(status)
@payload = Oj.dump(event: :delete, payload: status.id)
remove_from_self(status) if status.account.local? remove_from_self(status) if status.account.local?
remove_from_followers(status) remove_from_followers(status)
remove_from_mentioned(status) remove_from_mentioned(status)
@ -25,27 +27,25 @@ class RemoveStatusService < BaseService
end end
def remove_from_followers(status) def remove_from_followers(status)
status.account.followers.each do |follower| status.account.followers.where(domain: nil).each do |follower|
next unless follower.local?
unpush(:home, follower, status) unpush(:home, follower, status)
end end
end end
def remove_from_mentioned(status) def remove_from_mentioned(status)
return unless status.local?
notified_domains = [] notified_domains = []
status.mentions.each do |mention| status.mentions.each do |mention|
mentioned_account = mention.account mentioned_account = mention.account
if mentioned_account.local? next if mentioned_account.local?
unpush(:mentions, mentioned_account, status)
else
next if notified_domains.include?(mentioned_account.domain) next if notified_domains.include?(mentioned_account.domain)
notified_domains << mentioned_account.domain notified_domains << mentioned_account.domain
send_delete_salmon(mentioned_account, status) send_delete_salmon(mentioned_account, status)
end end
end end
end
def send_delete_salmon(account, status) def send_delete_salmon(account, status)
return unless status.local? return unless status.local?
@ -65,17 +65,19 @@ class RemoveStatusService < BaseService
redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id) redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id)
end end
Redis.current.publish(receiver.id, Oj.dump(event: :delete, payload: status.id)) Redis.current.publish("timeline:#{receiver.id}", @payload)
end end
def remove_from_hashtags(status) def remove_from_hashtags(status)
status.tags.each do |tag| status.tags.pluck(:name) do |hashtag|
Redis.current.publish("hashtag:#{tag.name}", Oj.dump(event: :delete, payload: status.id)) Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
end end
end end
def remove_from_public(status) def remove_from_public(status)
Redis.current.publish('public', Oj.dump(event: :delete, payload: status.id)) Redis.current.publish('timeline:public', @payload)
Redis.current.publish('timeline:public:local', @payload) if status.local?
end end
def redis def redis

@ -11,22 +11,6 @@ class UnblockService < BaseService
private private
def build_xml(block) def build_xml(block)
Nokogiri::XML::Builder.new do |xml| AtomSerializer.render(AtomSerializer.new.unblock_salmon(block))
entry(xml, true) do
unique_id xml, Time.now.utc, block.id, 'Block'
title xml, "#{block.account.acct} no longer blocks #{block.target_account.acct}"
author(xml) do
include_author xml, block.account
end
object_type xml, :activity
verb xml, :unblock
target(xml) do
include_author xml, block.target_account
end
end
end.to_xml
end end
end end

@ -13,26 +13,6 @@ class UnfavouriteService < BaseService
private private
def build_xml(favourite) def build_xml(favourite)
description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}" AtomSerializer.render(AtomSerializer.new.unfavourite_salmon(favourite))
Nokogiri::XML::Builder.new do |xml|
entry(xml, true) do
unique_id xml, Time.now.utc, favourite.id, 'Favourite'
title xml, description
content xml, description
author(xml) do
include_author xml, favourite.account
end
object_type xml, :activity
verb xml, :unfavorite
in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status)
target(xml) do
include_target xml, favourite.status
end
end
end.to_xml
end end
end end

@ -13,25 +13,6 @@ class UnfollowService < BaseService
private private
def build_xml(follow) def build_xml(follow)
description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}" AtomSerializer.render(AtomSerializer.new.unfollow_salmon(follow))
Nokogiri::XML::Builder.new do |xml|
entry(xml, true) do
unique_id xml, Time.now.utc, follow.id, 'Follow'
title xml, description
content xml, description
author(xml) do
include_author xml, follow.account
end
object_type xml, :activity
verb xml, :unfollow
target(xml) do
include_author xml, follow.target_account
end
end
end.to_xml
end end
end end

@ -1,27 +0,0 @@
# frozen_string_literal: true
Nokogiri::XML::Builder.new do |xml|
feed(xml) do
simple_id xml, account_url(@account, format: 'atom')
title xml, @account.display_name
subtitle xml, @account.note
updated_at xml, stream_updated_at
logo xml, full_asset_url(@account.avatar.url(:original))
author(xml) do
include_author xml, @account
end
link_alternate xml, TagManager.instance.url_for(@account)
link_self xml, account_url(@account, format: 'atom')
link_next xml, account_url(@account, format: 'atom', max_id: @entries.last.id) if @entries.size == 20
link_hub xml, api_push_url
link_salmon xml, api_salmon_url(@account.id)
@entries.each do |stream_entry|
entry(xml, false) do
include_entry xml, stream_entry
end
end
end
end.to_xml

@ -11,8 +11,10 @@
%meta{:name => "theme-color", :content => "#282c37"}/ %meta{:name => "theme-color", :content => "#282c37"}/
%meta{:name => "apple-mobile-web-app-capable", :content => "yes"}/ %meta{:name => "apple-mobile-web-app-capable", :content => "yes"}/
%title %title<
= "#{yield(:page_title)} - " if content_for?(:page_title) - if content_for?(:page_title)
= yield(:page_title)
= ' - '
= Setting.site_title = Setting.site_title
= stylesheet_link_tag 'application', media: 'all' = stylesheet_link_tag 'application', media: 'all'

@ -16,7 +16,7 @@
%strong= display_name(status.account) %strong= display_name(status.account)
= t('stream_entries.reblogged') = 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 - if include_threads
= render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true } = render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true }

@ -1,9 +0,0 @@
Nokogiri::XML::Builder.new do |xml|
entry(xml, true) do
author(xml) do
include_author xml, @stream_entry.account
end
include_entry xml, @stream_entry
end
end.to_xml

@ -0,0 +1,5 @@
<p>Tervetuloa <%= @resource.email %>!</p>
<p>Voit vahvistaa Mastodon tilisi klikkaamalla alla olevaa linkkiä:</p>
<p><%= link_to 'Varmista tilini', confirmation_url(@resource, confirmation_token: @token) %></p>

@ -0,0 +1,5 @@
Tervetuloa <%= @resource.email %>!
Voit vahvistaa Mastodon tilisi klikkaamalla alla olevaa linkkiä:
<%= confirmation_url(@resource, confirmation_token: @token) %>

@ -0,0 +1,3 @@
<p>Hei <%= @resource.email %>!</p>
<p>Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu.</p>

@ -0,0 +1,3 @@
Hei <%= @resource.email %>!
Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu.

@ -0,0 +1,8 @@
<p>Hei <%= @resource.email %>!</p>
<p>Joku on pyytänyt salasanvaihto Mastodonissa. Voit tehdä sen allaolevassa linkissä.</p>
<p><%= link_to 'Vaihda salasanani', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>Jos et pyytänyt vaihtoa, poista tämä viesti.</p>
<p>Salasanaasi ei vaihdeta ennen kuin menet ylläolevaan linkkiin ja luot uuden.</p>

@ -0,0 +1,8 @@
Hei <%= @resource.email %>!
Joku on pyytänyt salasanvaihto Mastodonissa. Voit tehdä sen allaolevassa linkissä.
<%= edit_password_url(@resource, reset_password_token: @token) %>
Jos et pyytänyt vaihtoa, poista tämä viesti.
Salasanaasi ei vaihdeta ennen kuin menet ylläolevaan linkkiin ja luot uuden.

@ -46,7 +46,7 @@ class ImportWorker
begin begin
FollowService.new.call(from_account, row[0]) FollowService.new.call(from_account, row[0])
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
next next
end end
end end

@ -13,6 +13,9 @@ class Pubsubhubbub::DeliveryWorker
def perform(subscription_id, payload) def perform(subscription_id, payload)
subscription = Subscription.find(subscription_id) subscription = Subscription.find(subscription_id)
headers = {} headers = {}
host = Addressable::URI.parse(subscription.callback_url).host
return if DomainBlock.blocked?(host)
headers['User-Agent'] = 'Mastodon/PubSubHubbub' headers['User-Agent'] = 'Mastodon/PubSubHubbub'
headers['Link'] = LinkHeader.new([[api_push_url, [%w(rel hub)]], [account_url(subscription.account, format: :atom), [%w(rel self)]]]).to_s headers['Link'] = LinkHeader.new([[api_push_url, [%w(rel hub)]], [account_url(subscription.account, format: :atom), [%w(rel self)]]]).to_s

@ -11,13 +11,9 @@ class Pubsubhubbub::DistributionWorker
return if stream_entry.hidden? return if stream_entry.hidden?
account = stream_entry.account account = stream_entry.account
renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https) payload = AtomSerializer.render(AtomSerializer.new.feed(account, [stream_entry]))
payload = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
# domains = account.followers_domains
Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription| Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
host = Addressable::URI.parse(subscription.callback_url).host
next if DomainBlock.blocked?(host) # || !domains.include?(host)
Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload) Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound

@ -163,3 +163,7 @@ en:
invalid_otp_token: Invalid two-factor code invalid_otp_token: Invalid two-factor code
will_paginate: will_paginate:
page_gap: "&hellip;" 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

@ -16,18 +16,18 @@ fi:
chronology: Aikajana on kronologisessa järjestyksessä chronology: Aikajana on kronologisessa järjestyksessä
ethics: 'Eettinen suunnittelu: ei mainoksia, no seurantaa' ethics: 'Eettinen suunnittelu: ei mainoksia, no seurantaa'
gifv: GIFV settejä ja lyhyitä videoita gifv: GIFV settejä ja lyhyitä videoita
privacy: Julkaisu kohtainen yksityisyys aseuts privacy: Julkaisu kohtainen yksityisyys asetus
public: Julkiset aikajanat public: Julkiset aikajanat
features_headline: Mikä erottaa Mastodonin muista features_headline: Mikä erottaa Mastodonin muista
get_started: Aloita käyttö get_started: Aloita käyttö
links: Linkit links: Linkit
other_instances: muuhun palvelimeen other_instances: Muut palvelimet
source_code: Lähdekoodi source_code: Lähdekoodi
status_count_after: statusta status_count_after: statusta
status_count_before: Ovat luoneet status_count_before: Ovat luoneet
terms: Ehdot terms: Ehdot
user_count_after: käyttäjää user_count_after: käyttäjälle
user_count_before: Koti käyttäjälle user_count_before: Koti
accounts: accounts:
follow: Seuraa follow: Seuraa
followers: Seuraajat followers: Seuraajat
@ -130,8 +130,8 @@ fi:
authorized_apps: Valtuutetut ohjelmat authorized_apps: Valtuutetut ohjelmat
back: Takaisin Mastodoniin back: Takaisin Mastodoniin
edit_profile: Muokkaa profiilia edit_profile: Muokkaa profiilia
export: Datan vienti export: Vie dataa
import: Datan tuonti import: Tuo dataa
preferences: Ominaisuudet preferences: Ominaisuudet
settings: Asetukset settings: Asetukset
two_factor_auth: Kaksivaiheinen tunnistus two_factor_auth: Kaksivaiheinen tunnistus

@ -9,7 +9,7 @@ preload_app!
on_worker_boot do on_worker_boot do
if ENV['HEROKU'] # Spawn the workers from Puma, to only use one dyno if ENV['HEROKU'] # Spawn the workers from Puma, to only use one dyno
@sidekiq_pid ||= spawn('bundle exec sidekiq -q default -q mailers -q push') @sidekiq_pid ||= spawn('bundle exec sidekiq -q default -q push -q pull -q mailers ')
end end
ActiveRecord::Base.establish_connection if defined?(ActiveRecord) ActiveRecord::Base.establish_connection if defined?(ActiveRecord)

@ -11,7 +11,7 @@ Rails.application.routes.draw do
end end
use_doorkeeper do use_doorkeeper do
controllers authorizations: 'oauth/authorizations' controllers authorizations: 'oauth/authorizations', authorized_applications: 'oauth/authorized_applications'
end end
get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta

@ -0,0 +1,7 @@
class AddNotificationsAndFavouritesIndices < ActiveRecord::Migration[5.0]
def change
add_index :notifications, [:activity_id, :activity_type]
add_index :accounts, :url
add_index :favourites, :status_id
end
end

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170405112956) do ActiveRecord::Schema.define(version: 20170406215816) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -49,6 +49,7 @@ ActiveRecord::Schema.define(version: 20170405112956) do
t.integer "following_count", default: 0, null: false t.integer "following_count", default: 0, null: false
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", using: :btree t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", using: :btree
t.index ["url"], name: "index_accounts_on_url", using: :btree
t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true, using: :btree t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true, using: :btree
end end
@ -75,6 +76,7 @@ ActiveRecord::Schema.define(version: 20170405112956) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id", "status_id"], name: "index_favourites_on_account_id_and_status_id", unique: true, using: :btree t.index ["account_id", "status_id"], name: "index_favourites_on_account_id_and_status_id", unique: true, using: :btree
t.index ["status_id"], name: "index_favourites_on_status_id", using: :btree
end end
create_table "follow_requests", force: :cascade do |t| create_table "follow_requests", force: :cascade do |t|
@ -128,6 +130,7 @@ ActiveRecord::Schema.define(version: 20170405112956) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree
t.index ["status_id"], name: "index_mentions_on_status_id", using: :btree t.index ["status_id"], name: "index_mentions_on_status_id", using: :btree
t.index ["status_id"], name: "mentions_status_id_index", using: :btree
end end
create_table "mutes", force: :cascade do |t| create_table "mutes", force: :cascade do |t|
@ -146,6 +149,7 @@ ActiveRecord::Schema.define(version: 20170405112956) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "from_account_id" t.integer "from_account_id"
t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true, using: :btree t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true, using: :btree
t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type", using: :btree
end end
create_table "oauth_access_grants", force: :cascade do |t| create_table "oauth_access_grants", force: :cascade do |t|

@ -7,7 +7,7 @@ So, you have a working Mastodon instance... now what?
The following rake task: The following rake task:
rake mastodon:make_admin USERNAME=alice RAILS_ENV=production bundle exec rails mastodon:make_admin USERNAME=alice
Would turn the local user "alice" into an admin. Would turn the local user "alice" into an admin.

@ -3,13 +3,50 @@ 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) [![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. ## Basic setup
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.
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.
To make yourself an admin, you may need to use the `heroku` CLI application after creating an account online:
`heroku rake mastodon:make_admin USERNAME=yourUsername`

@ -24,7 +24,7 @@ server {
ssl_protocols TLSv1.2; ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+AES; ssl_ciphers EECDH+AESGCM:EECDH+AES;
ssl_ecdh_curve secp384r1; ssl_ecdh_curve prime256v1;
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@ -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 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 - curl -sL https://deb.nodesource.com/setup_4.x | sudo bash -
apt-get intall nodejs apt-get install nodejs
sudo npm install -g yarn sudo npm install -g yarn
## Redis ## Redis

@ -8,6 +8,6 @@ Scalingo guide
* You can use a .scalingo.io domain, which will be simple to set up, or you can use a custom domain. * You can use a .scalingo.io domain, which will be simple to set up, or you can use a custom domain.
* 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. * 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. * 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. 3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Scalingo dashboard.
You may need to use the `scalingo` CLI application to run `USERNAME=yourUsername rails mastodon:make_admin` to make yourself an admin. To make yourself an admin, you can use the `scalingo` CLI: `scalingo run -e USERNAME=yourusername rails mastodon:make_admin`.

@ -17,6 +17,8 @@ To create and provision a new virtual machine for Mastodon development:
cd mastodon cd mastodon
vagrant up vagrant up
**Note:** On Linux hosts, you will need to [enable NFS support](https://www.vagrantup.com/docs/synced-folders/nfs.html).
Running `vagrant up` for the first time will run provisioning, which will: Running `vagrant up` for the first time will run provisioning, which will:
- Download the Ubuntu 14.04 base image, if there isn't already a copy on your machine - Download the Ubuntu 14.04 base image, if there isn't already a copy on your machine

@ -13,5 +13,6 @@ Some people have started working on apps for the Mastodon API. Here is a list of
|Albatross|iOS||[@goldie_ice@mastodon.social](https://mastodon.social/users/goldie_ice)| |Albatross|iOS||[@goldie_ice@mastodon.social](https://mastodon.social/users/goldie_ice)|
|Tooter|Chrome|<https://github.com/ineffyble/tooter>|[@effy@mastodon.social](https://mastodon.social/users/effy)| |Tooter|Chrome|<https://github.com/ineffyble/tooter>|[@effy@mastodon.social](https://mastodon.social/users/effy)|
|tootstream|CLI|<https://github.com/magicalraccoon/tootstream>|[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)| |tootstream|CLI|<https://github.com/magicalraccoon/tootstream>|[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)|
|HackerNewsBot|CLI|<https://github.com/raymestalez/mastodon-hnbot>|[@rayalez@hackertribe.io](https://hackertribe.io/users/rayalez)|
If you have a project like this, let me know so I can add it to the list! If you have a project like this, let me know so I can add it to the list!

@ -36,6 +36,7 @@ While Mastodon is compatible with GNU social in terms of server to server commun
Because Mastodon has been created from a blank slate, it is much simpler to have the API mirror internal structures as closely as possible, rather than build an emulation layer. Secondly, the GNU social client API is actually a half-way implementation of the legacy Twitter API - that's the reason why it works with some older Twitter client apps. However, many of those apps are not maintained anymore, the GNU social API does not actually keep up with the real Twitter API and never fully implemented all its features; at the same time, the Twitter API was never meant for a federated service and so obscures some of the functionality. Because Mastodon has been created from a blank slate, it is much simpler to have the API mirror internal structures as closely as possible, rather than build an emulation layer. Secondly, the GNU social client API is actually a half-way implementation of the legacy Twitter API - that's the reason why it works with some older Twitter client apps. However, many of those apps are not maintained anymore, the GNU social API does not actually keep up with the real Twitter API and never fully implemented all its features; at the same time, the Twitter API was never meant for a federated service and so obscures some of the functionality.
#### How is Mastodon funded? #### How is Mastodon funded?
Development of Mastodon and hosting of mastodon.social is funded through my [Patreon (also BTC/PayPal donations)](https://www.patreon.com/user?u=619786). Beyond that, I am not interested in VC funding, monetizing, advertising, or anything of that sort. I could offer setup/maintenance services on demand. Development of Mastodon and hosting of mastodon.social is funded through my [Patreon (also BTC/PayPal donations)](https://www.patreon.com/user?u=619786). Beyond that, I am not interested in VC funding, monetizing, advertising, or anything of that sort. I could offer setup/maintenance services on demand.

@ -7,6 +7,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| -------------|-------------|---|---| | -------------|-------------|---|---|
| [mastodon.social](https://mastodon.social) |Flagship, quick updates|No|No| | [mastodon.social](https://mastodon.social) |Flagship, quick updates|No|No|
| [securitymastod.one](https://securitymastod.one/) |Information security enthusiasts and pros|Yes|Yes| | [securitymastod.one](https://securitymastod.one/) |Information security enthusiasts and pros|Yes|Yes|
| [mastodon.nuzgo.net](https://mastodon.nuzgo.net/) |Mastodon instance hosted in Paris |Yes|No|
| [mastodon.cx](https://mastodon.cx/) |Alternative Mastodon instance hosted in France|Yes|Yes| | [mastodon.cx](https://mastodon.cx/) |Alternative Mastodon instance hosted in France|Yes|Yes|
| [mastodon.network](https://mastodon.network) |N/A|Yes|Yes| | [mastodon.network](https://mastodon.network) |N/A|Yes|Yes|
| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No| | [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No|
@ -47,6 +48,18 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| [status.dissidence.ovh](https://status.dissidence.ovh)|N/A|Yes|Yes| | [status.dissidence.ovh](https://status.dissidence.ovh)|N/A|Yes|Yes|
| [mastodon.cc](https://mastodon.cc)|Art|Yes|No| | [mastodon.cc](https://mastodon.cc)|Art|Yes|No|
| [mastodon.technology](https://mastodon.technology)|Open registrations, federates everywhere, for tech folks|Yes|No| | [mastodon.technology](https://mastodon.technology)|Open registrations, federates everywhere, for tech folks|Yes|No|
| [mastodon.systemlab.fr](https://mastodon.systemlab.fr/)|Le mastodon Français, informatique, jeux-vidéos, gaming et hébergement.|Yes|No| | [mastodon.systemlab.fr](https://mastodon.systemlab.fr/)|Le mastodon Français, informatique, jeux-vidéos, gaming et hébergement.|Yes|
| [mastodon.top](https://mastodon.top) |N/A|Yes|Yes|
| [niu.moe](https://niu.moe/)|:dolls: The most cutest node ever, FR/EN, anime and computer :balloon:|Yes|Yes|
| [im-in.space](https://im-in.space/)|SPAAAAACE! Probably with a lot of French people. (Invite-only, might randomly open registrations)|No|Yes|
| [social.bytestemplar.com](https://social.bytestemplar.com)|N/A|Yes|No|
| [digitalhumanities.club](http://www.digitalhumanities.club)|[Digital humanities](http://whatisdigitalhumanities.com) community; invitations will open once code of conduct drafted.|No|No
| [design.vu](https://design.vu)|— what's your design view‽|Yes|No|
| [masto.raildecake.fr](https://masto.raildecake.fr)|Hebergé chez un FAI associatif dans le sud de la france, grillons & pins en options|Yes|No|
| [good-dragon.com](https://good-dragon.com/)|Quick updates, Relaxed Moderation, Federates Everywhere, Furries|Yes|No|
| [rich.gop](https://rich.gop/)|Federates everywhere, Open registration, Privacy respected|Yes|Yes|
| [social.nowa.re](https://social.nowa.re)|Open Registration|Yes|No|
| [mastodon.ml](http://mastodon.ml) |A chill place to hangout and chat about anime, programming and movies.|Yes|Yes|
Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request). Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request).

@ -1,7 +1,7 @@
{ {
"name": "Mastodon", "name": "Mastodon",
"description": "A GNU Social-compatible microblogging server", "description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/johnsudaar/mastodon", "repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png", "logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png",
"env": { "env": {
"LOCAL_DOMAIN": { "LOCAL_DOMAIN": {

@ -1,3 +1,3 @@
Fabricator(:media_attachment) do Fabricator(:media_attachment) do
account
end end

@ -1,3 +1,4 @@
Fabricator(:status) do Fabricator(:status) do
account
text "Lorem ipsum dolor sit amet" text "Lorem ipsum dolor sit amet"
end end

@ -99,11 +99,75 @@ RSpec.describe Account, type: :model do
end end
describe '#favourited?' do 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 end
describe '#reblogged?' do 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 end
describe '.find_local' do describe '.find_local' do

@ -91,10 +91,31 @@ RSpec.describe Status, type: :model do
end end
describe '#reblogs_count' do 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 end
describe '#favourites_count' do 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
end end

@ -3,8 +3,168 @@ require 'rails_helper'
RSpec.describe PostStatusService do RSpec.describe PostStatusService do
subject { PostStatusService.new } subject { PostStatusService.new }
it 'creates a new status' it 'creates a new status' do
it 'creates a new response status' account = Fabricate(:account)
it 'processes mentions' text = "test status update"
it 'pings PuSH hubs'
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 end

Loading…
Cancel
Save