Merge branch 'master' into patch-1
This commit is contained in:
commit
bc237d17a7
63 changed files with 854 additions and 326 deletions
|
@ -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=
|
||||||
|
|
||||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -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
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -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
|
||||||
|
|
2
Procfile
2
Procfile
|
@ -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
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 |
|
@ -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
|
||||||
|
|
19
app/controllers/concerns/localized.rb
Normal file
19
app/controllers/concerns/localized.rb
Normal file
|
@ -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
|
||||||
|
|
16
app/controllers/oauth/authorized_applications_controller.rb
Normal file
16
app/controllers/oauth/authorized_applications_controller.rb
Normal file
|
@ -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?
|
||||||
|
|
||||||
|
|
351
app/lib/atom_serializer.rb
Normal file
351
app/lib/atom_serializer.rb
Normal file
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) %>
|
3
app/views/user_mailer/password_change.fi.html.erb
Normal file
3
app/views/user_mailer/password_change.fi.html.erb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<p>Hei <%= @resource.email %>!</p>
|
||||||
|
|
||||||
|
<p>Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu.</p>
|
3
app/views/user_mailer/password_change.fi.text.erb
Normal file
3
app/views/user_mailer/password_change.fi.text.erb
Normal file
|
@ -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.
|
|
@ -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: "…"
|
page_gap: "…"
|
||||||
|
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`.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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…
Reference in a new issue