@@ -18,9 +18,29 @@ Metrics/MethodLength: | |||
CountComments: false | |||
Max: 10 | |||
Metrics/ModuleLength: | |||
Metrics/AbcSize: | |||
Max: 100 | |||
Metrics/BlockNesting: | |||
Max: 3 | |||
Metrics/ClassLength: | |||
CountComments: false | |||
Max: 200 | |||
Metrics/CyclomaticComplexity: | |||
Max: 15 | |||
Metrics/MethodLength: | |||
Max: 55 | |||
Metrics/ModuleLength: | |||
CountComments: false | |||
Max: 200 | |||
Metrics/PerceivedComplexity: | |||
Max: 10 | |||
Metrics/ParameterLists: | |||
Max: 4 | |||
CountKeywordArgs: true | |||
@@ -37,10 +57,10 @@ Style/Documentation: | |||
Enabled: false | |||
Style/DoubleNegation: | |||
Enabled: false | |||
Enabled: true | |||
Style/FrozenStringLiteralComment: | |||
Enabled: false | |||
Enabled: true | |||
Style/SpaceInsideHashLiteralBraces: | |||
EnforcedStyle: space | |||
@@ -51,10 +71,18 @@ Style/TrailingCommaInLiteral: | |||
Style/RegexpLiteral: | |||
Enabled: false | |||
Style/Lambda: | |||
Enabled: false | |||
Rails/HasAndBelongsToMany: | |||
Enabled: false | |||
AllCops: | |||
TargetRubyVersion: 2.2 | |||
TargetRubyVersion: 2.3 | |||
Exclude: | |||
- 'spec/**/*' | |||
- 'db/**/*' | |||
- 'app/views/**/*' | |||
- 'config/**/*' | |||
- 'bin/*' | |||
- 'Rakefile' |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
source 'https://rubygems.org' | |||
gem 'rails', '5.0.0.1' |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
module ApplicationCable | |||
class Channel < ActionCable::Channel::Base | |||
protected |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
module ApplicationCable | |||
class Connection < ActionCable::Connection::Base | |||
identified_by :current_user |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class HashtagChannel < ApplicationCable::Channel | |||
def subscribed | |||
tag = params[:tag].downcase |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class PublicChannel < ApplicationCable::Channel | |||
def subscribed | |||
stream_from 'timeline:public', lambda { |encoded_message| |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class TimelineChannel < ApplicationCable::Channel | |||
def subscribed | |||
stream_from "timeline:#{current_user.account_id}" |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class AboutController < ApplicationController | |||
before_action :set_body_classes | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class AccountsController < ApplicationController | |||
layout 'public' | |||
@@ -41,10 +43,7 @@ class AccountsController < ApplicationController | |||
end | |||
def set_link_headers | |||
response.headers['Link'] = LinkHeader.new([ | |||
[webfinger_account_url, [['rel', 'lrdd'], ['type', 'application/xrd+xml']]], | |||
[account_url(@account, format: 'atom'), [['rel', 'alternate'], ['type', 'application/atom+xml']]] | |||
]) | |||
response.headers['Link'] = LinkHeader.new([[webfinger_account_url, [%w(rel lrdd), %w(type application/xrd+xml)]], [account_url(@account, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]]) | |||
end | |||
def webfinger_account_url |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Api::SalmonController < ApiController | |||
before_action :set_account | |||
respond_to :txt |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Api::SubscriptionsController < ApiController | |||
before_action :set_account | |||
respond_to :txt |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::AccountsController < ApiController | |||
before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock] | |||
before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock] | |||
@@ -20,7 +22,7 @@ class Api::V1::AccountsController < ApiController | |||
@accounts = results.map { |f| accounts[f.target_account_id] } | |||
next_path = following_api_v1_account_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT | |||
prev_path = following_api_v1_account_url(since_id: results.first.id) if results.size > 0 | |||
prev_path = following_api_v1_account_url(since_id: results.first.id) unless results.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
@@ -33,7 +35,7 @@ class Api::V1::AccountsController < ApiController | |||
@accounts = results.map { |f| accounts[f.account_id] } | |||
next_path = followers_api_v1_account_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT | |||
prev_path = followers_api_v1_account_url(since_id: results.first.id) if results.size > 0 | |||
prev_path = followers_api_v1_account_url(since_id: results.first.id) unless results.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
@@ -56,7 +58,7 @@ class Api::V1::AccountsController < ApiController | |||
set_maps(@statuses) | |||
next_path = statuses_api_v1_account_url(max_id: @statuses.last.id) if @statuses.size == DEFAULT_STATUSES_LIMIT | |||
prev_path = statuses_api_v1_account_url(since_id: @statuses.first.id) if @statuses.size > 0 | |||
prev_path = statuses_api_v1_account_url(since_id: @statuses.first.id) unless @statuses.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::AppsController < ApiController | |||
respond_to :json | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::FollowsController < ApiController | |||
before_action -> { doorkeeper_authorize! :follow } | |||
before_action :require_user! |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::MediaController < ApiController | |||
before_action -> { doorkeeper_authorize! :write } | |||
before_action :require_user! |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::StatusesController < ApiController | |||
before_action -> { doorkeeper_authorize! :read }, except: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite] | |||
before_action -> { doorkeeper_authorize! :write }, only: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite] | |||
@@ -10,7 +12,7 @@ class Api::V1::StatusesController < ApiController | |||
end | |||
def context | |||
@context = OpenStruct.new({ ancestors: @status.ancestors, descendants: @status.descendants }) | |||
@context = OpenStruct.new(ancestors: @status.ancestors, descendants: @status.descendants) | |||
set_maps([@status] + @context[:ancestors] + @context[:descendants]) | |||
end | |||
@@ -20,7 +22,7 @@ class Api::V1::StatusesController < ApiController | |||
@accounts = results.map { |r| accounts[r.account_id] } | |||
next_path = reblogged_by_api_v1_status_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT | |||
prev_path = reblogged_by_api_v1_status_url(since_id: results.first.id) if results.size > 0 | |||
prev_path = reblogged_by_api_v1_status_url(since_id: results.first.id) unless results.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
@@ -33,7 +35,7 @@ class Api::V1::StatusesController < ApiController | |||
@accounts = results.map { |f| accounts[f.account_id] } | |||
next_path = favourited_by_api_v1_status_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT | |||
prev_path = favourited_by_api_v1_status_url(since_id: results.first.id) if results.size > 0 | |||
prev_path = favourited_by_api_v1_status_url(since_id: results.first.id) unless results.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::TimelinesController < ApiController | |||
before_action -> { doorkeeper_authorize! :read } | |||
before_action :require_user!, only: [:home, :mentions] | |||
@@ -10,7 +12,7 @@ class Api::V1::TimelinesController < ApiController | |||
set_maps(@statuses) | |||
next_path = api_v1_home_timeline_url(max_id: @statuses.last.id) if @statuses.size == DEFAULT_STATUSES_LIMIT | |||
prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) if @statuses.size > 0 | |||
prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) unless @statuses.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
@@ -23,7 +25,7 @@ class Api::V1::TimelinesController < ApiController | |||
set_maps(@statuses) | |||
next_path = api_v1_mentions_timeline_url(max_id: @statuses.last.id) if @statuses.size == DEFAULT_STATUSES_LIMIT | |||
prev_path = api_v1_mentions_timeline_url(since_id: @statuses.first.id) if @statuses.size > 0 | |||
prev_path = api_v1_mentions_timeline_url(since_id: @statuses.first.id) unless @statuses.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
@@ -36,7 +38,7 @@ class Api::V1::TimelinesController < ApiController | |||
set_maps(@statuses) | |||
next_path = api_v1_public_timeline_url(max_id: @statuses.last.id) if @statuses.size == DEFAULT_STATUSES_LIMIT | |||
prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) if @statuses.size > 0 | |||
prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) unless @statuses.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
@@ -50,7 +52,7 @@ class Api::V1::TimelinesController < ApiController | |||
set_maps(@statuses) | |||
next_path = api_v1_hashtag_timeline_url(params[:id], max_id: @statuses.last.id) if @statuses.size == DEFAULT_STATUSES_LIMIT | |||
prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) if @statuses.size > 0 | |||
prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) unless @statuses.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class ApiController < ApplicationController | |||
DEFAULT_STATUSES_LIMIT = 20 | |||
DEFAULT_ACCOUNTS_LIMIT = 40 | |||
@@ -51,8 +53,8 @@ class ApiController < ApplicationController | |||
def set_pagination_headers(next_path = nil, prev_path = nil) | |||
links = [] | |||
links << [next_path, [['rel', 'next']]] if next_path | |||
links << [prev_path, [['rel', 'prev']]] if prev_path | |||
links << [next_path, [%w(rel next)]] if next_path | |||
links << [prev_path, [%w(rel prev)]] if prev_path | |||
response.headers['Link'] = LinkHeader.new(links) | |||
end | |||
@@ -76,7 +78,7 @@ class ApiController < ApplicationController | |||
render json: {}, status: 200 | |||
end | |||
def set_maps(statuses) | |||
def set_maps(statuses) # rubocop:disable Style/AccessorMethodName | |||
if current_account.nil? | |||
@reblogs_map = {} | |||
@favourites_map = {} |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class ApplicationController < ActionController::Base | |||
# Prevent CSRF attacks by raising an exception. | |||
# For APIs, you may want to use :null_session instead. |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Auth::ConfirmationsController < Devise::ConfirmationsController | |||
layout 'auth' | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Auth::PasswordsController < Devise::PasswordsController | |||
layout 'auth' | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Auth::RegistrationsController < Devise::RegistrationsController | |||
layout 'auth' | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Auth::SessionsController < Devise::SessionsController | |||
include Devise::Controllers::Rememberable | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class HomeController < ApplicationController | |||
before_action :authenticate_user! | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class MediaController < ApplicationController | |||
before_action :set_media_attachment | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController | |||
skip_before_action :authenticate_resource_owner! | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Settings::PreferencesController < ApplicationController | |||
layout 'auth' | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Settings::ProfilesController < ApplicationController | |||
layout 'auth' | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class StreamEntriesController < ApplicationController | |||
layout 'public' | |||
@@ -29,9 +31,7 @@ class StreamEntriesController < ApplicationController | |||
end | |||
def set_link_headers | |||
response.headers['Link'] = LinkHeader.new([ | |||
[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [['rel', 'alternate'], ['type', 'application/atom+xml']]] | |||
]) | |||
response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]]) | |||
end | |||
def set_stream_entry |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class TagsController < ApplicationController | |||
layout 'public' | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class XrdController < ApplicationController | |||
before_action :set_default_format_json, only: :webfinger | |||
before_action :set_default_format_xml, only: :host_meta | |||
@@ -26,11 +28,11 @@ class XrdController < ApplicationController | |||
private | |||
def set_default_format_xml | |||
request.format = 'xml' if request.headers["HTTP_ACCEPT"].nil? && params[:format].nil? | |||
request.format = 'xml' if request.headers['HTTP_ACCEPT'].nil? && params[:format].nil? | |||
end | |||
def set_default_format_json | |||
request.format = 'json' if request.headers["HTTP_ACCEPT"].nil? && params[:format].nil? | |||
request.format = 'json' if request.headers['HTTP_ACCEPT'].nil? && params[:format].nil? | |||
end | |||
def username_from_resource | |||
@@ -44,14 +46,14 @@ class XrdController < ApplicationController | |||
def pem_to_magic_key(public_key) | |||
modulus, exponent = [public_key.n, public_key.e].map do |component| | |||
result = '' | |||
result = [] | |||
until component.zero? | |||
result << [component % 256].pack('C') | |||
component >>= 8 | |||
end | |||
result.reverse! | |||
result.reverse.join | |||
end | |||
(['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.') |
@@ -1,2 +1,4 @@ | |||
# frozen_string_literal: true | |||
module AboutHelper | |||
end |
@@ -1,10 +1,12 @@ | |||
# frozen_string_literal: true | |||
module AccountsHelper | |||
def pagination_options | |||
{ | |||
previous_label: "#{fa_icon('chevron-left')} Prev".html_safe, | |||
next_label: "Next #{fa_icon('chevron-right')}".html_safe, | |||
previous_label: safe_join([fa_icon('chevron-left'), 'Prev'], ' '), | |||
next_label: safe_join(['Next', fa_icon('chevron-right')], ' '), | |||
inner_window: 1, | |||
outer_window: 0 | |||
outer_window: 0, | |||
} | |||
end | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
module ApplicationHelper | |||
def active_nav_class(path) | |||
current_page?(path) ? 'active' : '' |
@@ -1,6 +1,12 @@ | |||
# frozen_string_literal: true | |||
module AtomBuilderHelper | |||
def stream_updated_at | |||
@account.stream_entries.last ? (@account.updated_at > @account.stream_entries.last.created_at ? @account.updated_at : @account.stream_entries.last.created_at) : @account.updated_at | |||
if @account.stream_entries.last | |||
(@account.updated_at > @account.stream_entries.last.created_at ? @account.updated_at : @account.stream_entries.last.created_at) | |||
else | |||
@account.updated_at | |||
end | |||
end | |||
def entry(xml, is_root = false, &block) | |||
@@ -98,7 +104,7 @@ module AtomBuilderHelper | |||
end | |||
def in_reply_to(xml, uri, url) | |||
xml['thr'].send('in-reply-to', { ref: uri, href: url, type: 'text/html' }) | |||
xml['thr'].send('in-reply-to', ref: uri, href: url, type: 'text/html') | |||
end | |||
def link_mention(xml, account) |
@@ -1,8 +1,10 @@ | |||
# frozen_string_literal: true | |||
module HomeHelper | |||
def default_props | |||
{ | |||
token: @token, | |||
account: render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json) | |||
account: render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json), | |||
} | |||
end | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
module RoutingHelper | |||
extend ActiveSupport::Concern | |||
include Rails.application.routes.url_helpers |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
module StreamEntriesHelper | |||
def display_name(account) | |||
account.display_name.blank? ? account.username : account.display_name |
@@ -1,2 +1,4 @@ | |||
# frozen_string_literal: true | |||
module TagsHelper | |||
end |
@@ -1,2 +1,4 @@ | |||
# frozen_string_literal: true | |||
module XrdHelper | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
require 'singleton' | |||
class FeedManager | |||
@@ -60,29 +62,29 @@ class FeedManager | |||
private | |||
def redis | |||
$redis | |||
Redis.current | |||
end | |||
def filter_from_home?(status, receiver) | |||
should_filter = false | |||
if status.reply? && !status.thread.account.nil? # Filter out if it's a reply | |||
should_filter = !receiver.following?(status.thread.account) # and I'm not following the person it's a reply to | |||
should_filter = should_filter && !(receiver.id == status.thread.account_id) # and it's not a reply to me | |||
should_filter = should_filter && !(status.account_id == status.thread.account_id) # and it's not a self-reply | |||
elsif status.reblog? # Filter out a reblog | |||
should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person | |||
if status.reply? && !status.thread.account.nil? # Filter out if it's a reply | |||
should_filter = !receiver.following?(status.thread.account) # and I'm not following the person it's a reply to | |||
should_filter &&= !(receiver.id == status.thread.account_id) # and it's not a reply to me | |||
should_filter &&= !(status.account_id == status.thread.account_id) # and it's not a self-reply | |||
elsif status.reblog? # Filter out a reblog | |||
should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person | |||
end | |||
should_filter | |||
end | |||
def filter_from_mentions?(status, receiver) | |||
should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself | |||
should_filter = should_filter || receiver.blocking?(status.account) # or it's from someone I blocked | |||
should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself | |||
should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked | |||
if status.reply? && !status.thread.account.nil? # or it's a reply | |||
should_filter = should_filter || receiver.blocking?(status.thread.account) # to a user I blocked | |||
if status.reply? && !status.thread.account.nil? # or it's a reply | |||
should_filter ||= receiver.blocking?(status.thread.account) # to a user I blocked | |||
end | |||
should_filter | |||
@@ -92,9 +94,9 @@ class FeedManager | |||
should_filter = receiver.blocking?(status.account) | |||
if status.reply? && !status.thread.account.nil? | |||
should_filter = should_filter || receiver.blocking?(status.thread.account) | |||
should_filter ||= receiver.blocking?(status.thread.account) | |||
elsif status.reblog? | |||
should_filter = should_filter || receiver.blocking?(status.reblog.account) | |||
should_filter ||= receiver.blocking?(status.reblog.account) | |||
end | |||
should_filter |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
require 'singleton' | |||
class Formatter | |||
@@ -17,7 +19,7 @@ class Formatter | |||
html = link_mentions(html, status.mentions) | |||
html = link_hashtags(html) | |||
html.html_safe | |||
html.html_safe # rubocop:disable Rails/OutputSafety | |||
end | |||
def reformat(html) | |||
@@ -30,7 +32,7 @@ class Formatter | |||
html = encode(account.note) | |||
html = link_urls(html) | |||
html.html_safe | |||
html.html_safe # rubocop:disable Rails/OutputSafety | |||
end | |||
private |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
require 'singleton' | |||
class TagManager | |||
@@ -18,7 +20,7 @@ class TagManager | |||
end | |||
def local_domain?(domain) | |||
domain.nil? || domain.gsub(/[\/]/, '').downcase == Rails.configuration.x.local_domain.downcase | |||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero? | |||
end | |||
def uri_for(target) |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class ApplicationMailer < ActionMailer::Base | |||
default from: (ENV['SMTP_FROM_ADDRESS'] || 'notifications@localhost') | |||
layout 'mailer' |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class NotificationMailer < ApplicationMailer | |||
helper StreamEntriesHelper | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Account < ApplicationRecord | |||
include Targetable | |||
include PgSearch | |||
@@ -92,11 +94,11 @@ class Account < ApplicationRecord | |||
end | |||
def favourited?(status) | |||
(status.reblog? ? status.reblog : status).favourites.where(account: self).count > 0 | |||
(status.reblog? ? status.reblog : status).favourites.where(account: self).count.positive? | |||
end | |||
def reblogged?(status) | |||
(status.reblog? ? status.reblog : status).reblogs.where(account: self).count > 0 | |||
(status.reblog? ? status.reblog : status).reblogs.where(account: self).count.positive? | |||
end | |||
def keypair | |||
@@ -115,8 +117,8 @@ class Account < ApplicationRecord | |||
def avatar_remote_url=(url) | |||
self.avatar = URI.parse(url) unless self[:avatar_remote_url] == url | |||
self[:avatar_remote_url] = url | |||
rescue OpenURI::HTTPError | |||
# | |||
rescue OpenURI::HTTPError => e | |||
Rails.logger.debug "Error fetching remote avatar: #{e}" | |||
end | |||
def object_type |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class ApplicationRecord < ActiveRecord::Base | |||
self.abstract_class = true | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Block < ApplicationRecord | |||
belongs_to :account | |||
belongs_to :target_account, class_name: 'Account' |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
module Paginable | |||
extend ActiveSupport::Concern | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
module Streamable | |||
extend ActiveSupport::Concern | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
module Targetable | |||
extend ActiveSupport::Concern | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class DomainBlock < ApplicationRecord | |||
validates :domain, presence: true, uniqueness: true | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Favourite < ApplicationRecord | |||
include Paginable | |||
include Streamable |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Feed | |||
def initialize(type, account) | |||
@type = type | |||
@@ -28,6 +30,6 @@ class Feed | |||
end | |||
def redis | |||
$redis | |||
Redis.current | |||
end | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Follow < ApplicationRecord | |||
include Paginable | |||
include Streamable |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class FollowSuggestion | |||
class << self | |||
def get(for_account_id, limit = 10) |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class MediaAttachment < ApplicationRecord | |||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze | |||
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze | |||
@@ -6,9 +8,9 @@ class MediaAttachment < ApplicationRecord | |||
belongs_to :status, inverse_of: :media_attachments | |||
has_attached_file :file, | |||
styles: -> (f) { file_styles f }, | |||
processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] }, | |||
convert_options: { all: "-strip" } | |||
styles: -> (f) { file_styles f }, | |||
processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] }, | |||
convert_options: { all: '-strip' } | |||
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES | |||
validates_attachment_size :file, less_than: 4.megabytes | |||
@@ -20,8 +22,8 @@ class MediaAttachment < ApplicationRecord | |||
def file_remote_url=(url) | |||
self.file = URI.parse(url) | |||
rescue OpenURI::HTTPError | |||
# | |||
rescue OpenURI::HTTPError => e | |||
Rails.logger.debug "Error fetching remote attachment: #{e}" | |||
end | |||
def image? | |||
@@ -43,19 +45,19 @@ class MediaAttachment < ApplicationRecord | |||
if f.instance.image? | |||
{ | |||
original: '100%', | |||
small: '510x680>' | |||
small: '510x680>', | |||
} | |||
else | |||
{ | |||
small: { | |||
convert_options: { | |||
output: { | |||
vf: 'scale=\'min(510\, iw):min(680\, ih)\':force_original_aspect_ratio=decrease' | |||
} | |||
vf: 'scale=\'min(510\, iw):min(680\, ih)\':force_original_aspect_ratio=decrease', | |||
}, | |||
}, | |||
format: 'png', | |||
time: 1 | |||
} | |||
time: 1, | |||
}, | |||
} | |||
end | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Mention < ApplicationRecord | |||
belongs_to :account, inverse_of: :mentions | |||
belongs_to :status |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Status < ApplicationRecord | |||
include Paginable | |||
include Streamable | |||
@@ -89,22 +91,17 @@ class Status < ApplicationRecord | |||
def as_public_timeline(account = nil) | |||
query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id').where('accounts.silenced = FALSE') | |||
unless account.nil? | |||
query = filter_timeline(query, account) | |||
end | |||
query = filter_timeline(query, account) unless account.nil? | |||
query.with_includes.with_counters | |||
end | |||
def as_tag_timeline(tag, account = nil) | |||
query = tag.statuses | |||
.joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') | |||
.where('accounts.silenced = FALSE') | |||
.joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') | |||
.where('accounts.silenced = FALSE') | |||
unless account.nil? | |||
query = filter_timeline(query, account) | |||
end | |||
query = filter_timeline(query, account) unless account.nil? | |||
query.with_includes.with_counters | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class StreamEntry < ApplicationRecord | |||
include Paginable | |||
@@ -15,7 +17,11 @@ class StreamEntry < ApplicationRecord | |||
scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES, favourite: [:account, :stream_entry, status: STATUS_INCLUDES], follow: [:target_account, :stream_entry]) } | |||
def object_type | |||
orphaned? ? :activity : (targeted? ? :activity : activity.object_type) | |||
if orphaned? | |||
:activity | |||
else | |||
targeted? ? :activity : activity.object_type | |||
end | |||
end | |||
def verb |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class Tag < ApplicationRecord | |||
has_and_belongs_to_many :statuses | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class User < ApplicationRecord | |||
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class BaseService | |||
include ActionView::Helpers::TextHelper | |||
include ActionView::Helpers::SanitizeHelper |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class BlockDomainService < BaseService | |||
def call(domain) | |||
DomainBlock.find_or_create_by!(domain: domain) |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class BlockService < BaseService | |||
def call(account, target_account) | |||
return if account.id == target_account.id | |||
@@ -20,6 +22,6 @@ class BlockService < BaseService | |||
end | |||
def redis | |||
$redis | |||
Redis.current | |||
end | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class FanOutOnWriteService < BaseService | |||
# Push a status into home and mentions feeds | |||
# @param [Status] status |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class FavouriteService < BaseService | |||
# Favourite a status and notify remote user | |||
# @param [Account] account |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class FetchAtomService < BaseService | |||
def call(url) | |||
response = http_client.head(url) | |||
@@ -9,15 +11,9 @@ class FetchAtomService < BaseService | |||
Rails.logger.debug "Remote status GET request returned code #{response.code}" | |||
return nil if response.code != 200 | |||
if response.mime_type == 'application/atom+xml' | |||
return [url, fetch(url)] | |||
elsif !response['Link'].blank? | |||
return process_headers(url, response) | |||
else | |||
return process_html(fetch(url)) | |||
end | |||
return [url, fetch(url)] if response.mime_type == 'application/atom+xml' | |||
return process_headers(url, response) unless response['Link'].blank? | |||
process_html(fetch(url)) | |||
rescue OpenSSL::SSL::SSLError => e | |||
Rails.logger.debug "SSL error: #{e}" | |||
end | |||
@@ -31,17 +27,17 @@ class FetchAtomService < BaseService | |||
alternate_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' } | |||
return nil if alternate_link.nil? | |||
return [alternate_link['href'], fetch(alternate_link['href'])] | |||
[alternate_link['href'], fetch(alternate_link['href'])] | |||
end | |||
def process_headers(url, response) | |||
Rails.logger.debug 'Processing link header' | |||
link_header = LinkHeader.parse(response['Link'].is_a?(Array) ? response['Link'].first : response['Link']) | |||
alternate_link = link_header.find_link(['rel', 'alternate'], ['type', 'application/atom+xml']) | |||
alternate_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml)) | |||
return process_html(fetch(url)) if alternate_link.nil? | |||
return [alternate_link.href, fetch(alternate_link.href)] | |||
[alternate_link.href, fetch(alternate_link.href)] | |||
end | |||
def fetch(url) |
@@ -1,9 +1,11 @@ | |||
# frozen_string_literal: true | |||
class FetchRemoteAccountService < BaseService | |||
def call(url) | |||
atom_url, body = FetchAtomService.new.call(url) | |||
return nil if atom_url.nil? | |||
return process_atom(atom_url, body) | |||
process_atom(atom_url, body) | |||
end | |||
private | |||
@@ -25,7 +27,7 @@ class FetchRemoteAccountService < BaseService | |||
Rails.logger.debug "Unparseable URL given: #{url}" | |||
nil | |||
rescue Nokogiri::XML::XPath::SyntaxError | |||
Rails.logger.debug "Invalid XML or missing namespace" | |||
Rails.logger.debug 'Invalid XML or missing namespace' | |||
nil | |||
end | |||
end |
@@ -1,9 +1,11 @@ | |||
# frozen_string_literal: true | |||
class FetchRemoteStatusService < BaseService | |||
def call(url) | |||
atom_url, body = FetchAtomService.new.call(url) | |||
return nil if atom_url.nil? | |||
return process_atom(atom_url, body) | |||
process_atom(atom_url, body) | |||
end | |||
private | |||
@@ -20,7 +22,7 @@ class FetchRemoteStatusService < BaseService | |||
statuses = ProcessFeedService.new.call(body, account) | |||
return statuses.first | |||
statuses.first | |||
end | |||
def extract_author(url, xml) | |||
@@ -34,7 +36,7 @@ class FetchRemoteStatusService < BaseService | |||
return FollowRemoteAccountService.new.call("#{username}@#{domain}") | |||
rescue Nokogiri::XML::XPath::SyntaxError | |||
Rails.logger.debug "Invalid XML or missing namespace" | |||
Rails.logger.debug 'Invalid XML or missing namespace' | |||
nil | |||
end | |||
end |
@@ -1,7 +1,9 @@ | |||
# frozen_string_literal: true | |||
class FollowRemoteAccountService < BaseService | |||
include OStatus2::MagicKey | |||
DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'.freeze | |||
DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' | |||
# Find or create a local account for a remote user. | |||
# When creating, look up the user's webfinger and fetch all | |||
@@ -49,7 +51,7 @@ class FollowRemoteAccountService < BaseService | |||
get_profile(xml, account) | |||
account.save! | |||
return account | |||
account | |||
end | |||
private |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class FollowService < BaseService | |||
# Follow a remote user, notify remote user about the follow | |||
# @param [Account] source_account From which to follow | |||
@@ -35,7 +37,7 @@ class FollowService < BaseService | |||
end | |||
def redis | |||
$redis | |||
Redis.current | |||
end | |||
def follow_remote_account_service |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class PostStatusService < BaseService | |||
# Post a text status update, fetch and notify remote users mentioned | |||
# @param [Account] account Account from which to post |
@@ -1,10 +1,10 @@ | |||
# frozen_string_literal: true | |||
class PrecomputeFeedService < BaseService | |||
# Fill up a user's home/mentions feed from DB and return a subset | |||
# @param [Symbol] type :home or :mentions | |||
# @param [Account] account | |||
def call(type, account) | |||
instant_return = [] | |||
Status.send("as_#{type}_timeline", account).limit(FeedManager::MAX_ITEMS).each do |status| | |||
next if FeedManager.instance.filter?(type, status, account) | |||
redis.zadd(FeedManager.instance.key(type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) | |||
@@ -14,6 +14,6 @@ class PrecomputeFeedService < BaseService | |||
private | |||
def redis | |||
$redis | |||
Redis.current | |||
end | |||
end |
@@ -1,6 +1,8 @@ | |||
# frozen_string_literal: true | |||
class ProcessFeedService < BaseService | |||
ACTIVITY_NS = 'http://activitystrea.ms/spec/1.0/'.freeze | |||
THREAD_NS = 'http://purl.org/syndication/thread/1.0'.freeze | |||
ACTIVITY_NS = 'http://activitystrea.ms/spec/1.0/' | |||
THREAD_NS = 'http://purl.org/syndication/thread/1.0' | |||
def call(body, account) | |||
xml = Nokogiri::XML(body) | |||
@@ -89,13 +91,13 @@ class ProcessFeedService < BaseService | |||
account = @account | |||
end | |||
status = Status.create!({ | |||
status = Status.create!( | |||
uri: id(entry), | |||
url: url(entry), | |||
account: account, | |||
text: content(entry), | |||
created_at: published(entry), | |||
}) | |||
created_at: published(entry) | |||
) | |||
if thread?(entry) | |||
Rails.logger.debug "Trying to attach #{status.id} (#{id(entry)}) to #{thread(entry).first}" |
@@ -1,8 +1,8 @@ | |||
# frozen_string_literal: true | |||
class ProcessHashtagsService < BaseService | |||
def call(status, tags = []) | |||
if status.local? | |||
tags = status.text.scan(Tag::HASHTAG_RE).map(&:first) | |||
end | |||
tags = status.text.scan(Tag::HASHTAG_RE).map(&:first) if status.local? | |||
tags.map(&:downcase).uniq.each do |tag| | |||
status.tags << Tag.where(name: tag).first_or_initialize(name: tag) |
@@ -1,5 +1,7 @@ | |||
# frozen_string_literal: true | |||
class ProcessInteractionService < BaseService | |||
ACTIVITY_NS = 'http://activitystrea.ms/spec/1.0/'.freeze | |||
ACTIVITY_NS = 'http://activitystrea.ms/spec/1.0/' | |||
# Record locally the remote interaction with our user | |||
# @param [String] envelope Salmon envelope | |||
@@ -76,9 +78,7 @@ class ProcessInteractionService < BaseService | |||
return if status.nil? | |||
if account.id == status.account_id | |||
remove_status_service.call(status) | |||
end | |||
remove_status_service.call(status) if account.id == status.account_id | |||
end | |||
def favourite!(xml, from_account) |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class ProcessMentionsService < BaseService | |||
# Scan status for mentions and fetch remote mentioned users, create | |||
# local mention pointers, send Salmon notifications to mentioned |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class ReblogService < BaseService | |||
# Reblog a status and notify its remote author | |||
# @param [Account] account Account to reblog from |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class RemoveStatusService < BaseService | |||
def call(status) | |||
remove_from_self(status) if status.account.local? | |||
@@ -62,6 +64,6 @@ class RemoveStatusService < BaseService | |||
end | |||
def redis | |||
$redis | |||
Redis.current | |||
end | |||
end |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class SearchService < BaseService | |||
def call(query, limit, resolve = false) | |||
return if query.blank? | |||
@@ -5,10 +7,10 @@ class SearchService < BaseService | |||
username, domain = query.split('@') | |||
results = if domain.nil? | |||
Account.search_for(username) | |||
else | |||
Account.search_for("#{username} #{domain}") | |||
end | |||
Account.search_for(username) | |||
else | |||
Account.search_for("#{username} #{domain}") | |||
end | |||
results = results.limit(limit).with_counters | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class SendInteractionService < BaseService | |||
# Send an Atom representation of an interaction to a remote Salmon endpoint | |||
# @param [StreamEntry] stream_entry |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class SubscribeService < BaseService | |||
def call(account) | |||
account.secret = SecureRandom.hex |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class UnblockService < BaseService | |||
def call(account, target_account) | |||
account.unblock!(target_account) if account.blocking?(target_account) |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class UnfavouriteService < BaseService | |||
def call(account, status) | |||
favourite = Favourite.find_by!(account: account, status: status) |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class UnfollowService < BaseService | |||
# Unfollow and notify the remote user | |||
# @param [Account] source_account Where to unfollow from | |||
@@ -21,6 +23,6 @@ class UnfollowService < BaseService | |||
end | |||
def redis | |||
$redis | |||
Redis.current | |||
end | |||
end |
@@ -1,14 +1,16 @@ | |||
# frozen_string_literal: true | |||
class UpdateRemoteProfileService < BaseService | |||
POCO_NS = 'http://portablecontacts.net/spec/1.0' | |||
def call(author_xml, account) | |||
return if author_xml.nil? | |||
if author_xml.at_xpath('./poco:displayName', poco: POCO_NS).nil? | |||
account.display_name = account.username | |||
else | |||
account.display_name = author_xml.at_xpath('./poco:displayName', poco: POCO_NS).content | |||
end | |||
account.display_name = if author_xml.at_xpath('./poco:displayName', poco: POCO_NS).nil? | |||
account.username | |||
else | |||
author_xml.at_xpath('./poco:displayName', poco: POCO_NS).content | |||
end | |||
unless author_xml.at_xpath('./poco:note').nil? | |||
account.note = author_xml.at_xpath('./poco:note', poco: POCO_NS).content |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class DistributionWorker | |||
include Sidekiq::Worker | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class HubPingWorker | |||
include Sidekiq::Worker | |||
include RoutingHelper |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class NotificationWorker | |||
include Sidekiq::Worker | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class ProcessingWorker | |||
include Sidekiq::Worker | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class RegenerationWorker | |||
include Sidekiq::Worker | |||
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
class ThreadResolveWorker | |||
include Sidekiq::Worker | |||
@@ -1,3 +1,4 @@ | |||
# frozen_string_literal: true | |||
# This file is used by Rack-based servers to start the application. | |||
require ::File.expand_path('../config/environment', __FILE__) |
@@ -1,5 +1,7 @@ | |||
$redis = Redis.new({ | |||
# frozen_string_literal: true | |||
Redis.current = Redis.new( | |||
host: ENV.fetch('REDIS_HOST') { 'localhost' }, | |||
port: ENV.fetch('REDIS_PORT') { 6379 }, | |||
driver: :hiredis | |||
}) | |||
) |
@@ -1,3 +1,5 @@ | |||
# frozen_string_literal: true | |||
namespace :mastodon do | |||
namespace :media do | |||
desc 'Removes media attachments that have not been assigned to any status for longer than a day' | |||
@@ -28,7 +30,7 @@ namespace :mastodon do | |||
task refresh: :environment do | |||
Account.expiring(1.day.from_now).find_each do |a| | |||
Rails.logger.debug "PuSH re-subscribing to #{a.acct}" | |||
SubscribeService.new.(a) | |||
SubscribeService.new.call(a) | |||
end | |||
end | |||
end | |||
@@ -36,7 +38,7 @@ namespace :mastodon do | |||
namespace :feeds do | |||
desc 'Clears all timelines so that they would be regenerated on next hit' | |||
task clear: :environment do | |||
$redis.keys('feed:*').each { |key| $redis.del(key) } | |||
Redis.current.keys('feed:*').each { |key| Redis.current.del(key) } | |||
end | |||
end | |||