Merge pull request #87 from tootsuite/master

merge upstream
This commit is contained in:
beatrix 2017-07-20 11:24:32 -04:00 committed by GitHub
commit e7edb4d1ee
64 changed files with 499 additions and 332 deletions

View File

@ -52,6 +52,7 @@ gem 'rack-timeout', '~> 0.4'
gem 'rails-i18n', '~> 5.0' gem 'rails-i18n', '~> 5.0'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10' gem 'rqrcode', '~> 0.10'
gem 'ruby-oembed', '~> 0.12', require: 'oembed' gem 'ruby-oembed', '~> 0.12', require: 'oembed'
gem 'sanitize', '~> 4.4' gem 'sanitize', '~> 4.4'

View File

@ -242,6 +242,8 @@ GEM
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.6) mail (2.6.6)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5)
method_source (0.8.2) method_source (0.8.2)
microformats (4.0.7) microformats (4.0.7)
json json
@ -535,6 +537,7 @@ DEPENDENCIES
letter_opener_web (~> 1.3) letter_opener_web (~> 1.3)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.5) lograge (~> 0.5)
mario-redis-lock (~> 1.2)
microformats (~> 4.0) microformats (~> 4.0)
mime-types (~> 3.1) mime-types (~> 3.1)
nokogiri (~> 1.7) nokogiri (~> 1.7)

View File

@ -13,7 +13,7 @@ class AccountsController < ApplicationController
format.atom do format.atom do
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id]) @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.feed(@account, @entries.to_a)) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.to_a))
end end
format.json do format.json do

View File

@ -17,11 +17,7 @@ class Api::BaseController < ApplicationController
render json: { error: 'Record not found' }, status: 404 render json: { error: 'Record not found' }, status: 404
end end
rescue_from Goldfinger::Error do rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
render json: { error: 'Remote account could not be resolved' }, status: 422
end
rescue_from HTTP::Error do
render json: { error: 'Remote data could not be fetched' }, status: 503 render json: { error: 'Remote data could not be fetched' }, status: 503
end end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Settings::SessionsController < ApplicationController
before_action :set_session, only: :destroy
def destroy
@session.destroy!
flash[:notice] = I18n.t('sessions.revoke_success')
redirect_to edit_user_registration_path
end
private
def set_session
@session = current_user.session_activations.find(params[:id])
end
end

View File

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

View File

@ -11,7 +11,7 @@ module RoutingHelper
end end
end end
def full_asset_url(source) def full_asset_url(source, options = {})
Rails.configuration.x.use_s3 ? source : URI.join(root_url, ActionController::Base.helpers.asset_url(source)).to_s Rails.configuration.x.use_s3 ? source : URI.join(root_url, ActionController::Base.helpers.asset_url(source, options)).to_s
end end
end end

View File

@ -113,7 +113,7 @@ export function fetchContext(id) {
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants)); dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
}).catch(error => { }).catch(error => {
if (error.response.status === 404) { if (error.response && error.response.status === 404) {
dispatch(deleteFromTimelines(id)); dispatch(deleteFromTimelines(id));
} }

View File

@ -105,7 +105,7 @@ export function refreshTimelineFail(timeline, error, skipLoading) {
timeline, timeline,
error, error,
skipLoading, skipLoading,
skipAlert: error.response.status === 404, skipAlert: error.response && error.response.status === 404,
}; };
}; };

View File

@ -30,8 +30,8 @@ export default class StatusList extends ImmutablePureComponent {
intersectionObserverWrapper = new IntersectionObserverWrapper(); intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = debounce((e) => { handleScroll = debounce(() => {
const { scrollTop, scrollHeight, clientHeight } = e.target; const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight; const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop; this._oldScrollPosition = scrollHeight - scrollTop;
@ -49,19 +49,23 @@ export default class StatusList extends ImmutablePureComponent {
componentDidMount () { componentDidMount () {
this.attachScrollListener(); this.attachScrollListener();
this.attachIntersectionObserver(); this.attachIntersectionObserver();
// Handle initial scroll posiiton
this.handleScroll();
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
// Reset the scroll position when a new toot comes in in order not to // Reset the scroll position when a new toot comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page. // jerk the scrollbar around if you're already scrolled down the page.
if (prevProps.statusIds.size < this.props.statusIds.size && if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
prevProps.statusIds.first() !== this.props.statusIds.first() && if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
this._oldScrollPosition &&
this.node.scrollTop > 0) {
let newScrollTop = this.node.scrollHeight - this._oldScrollPosition; let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) { if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop; this.node.scrollTop = newScrollTop;
} }
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
}
} }
} }

View File

@ -1,7 +1,7 @@
import { unicodeToFilename } from './emojione_light'; import { unicodeMapping } from './emojione_light';
import Trie from 'substring-trie'; import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeToFilename)); const trie = new Trie(Object.keys(unicodeMapping));
function emojify(str) { function emojify(str) {
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
@ -19,10 +19,10 @@ function emojify(str) {
insideTag = true; insideTag = true;
} else if (!insideTag && (match = trie.search(str.substring(i)))) { } else if (!insideTag && (match = trie.search(str.substring(i)))) {
const unicodeStr = match; const unicodeStr = match;
if (unicodeStr in unicodeToFilename) { if (unicodeStr in unicodeMapping) {
const filename = unicodeToFilename[unicodeStr]; const [filename, shortCode] = unicodeMapping[unicodeStr];
const alt = unicodeStr; const alt = unicodeStr;
const replacement = `<img draggable="false" class="emojione" alt="${alt}" src="/emoji/${filename}.svg" />`; const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
} }

View File

@ -5,7 +5,7 @@ const emojione = require('emojione');
const mappedUnicode = emojione.mapUnicodeToShort(); const mappedUnicode = emojione.mapUnicodeToShort();
module.exports.unicodeToFilename = Object.keys(emojione.jsEscapeMap) module.exports.unicodeMapping = Object.keys(emojione.jsEscapeMap)
.map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]]) .map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]])
.map(([unicodeStr, shortCode]) => ({ [unicodeStr]: emojione.emojioneList[shortCode].fname })) .map(([unicodeStr, shortCode]) => ({ [unicodeStr]: [emojione.emojioneList[shortCode].fname, shortCode.slice(1, shortCode.length - 1)] }))
.reduce((x, y) => Object.assign(x, y), { }); .reduce((x, y) => Object.assign(x, y), { });

View File

@ -56,7 +56,7 @@
"confirmations.mute.confirm": "ミュート", "confirmations.mute.confirm": "ミュート",
"confirmations.mute.message": "本当に{name}をミュートしますか?", "confirmations.mute.message": "本当に{name}をミュートしますか?",
"confirmations.unfollow.confirm": "フォロー解除", "confirmations.unfollow.confirm": "フォロー解除",
"confirmations.unfollow.message": "本当に{name}のフォローを解除しますか?", "confirmations.unfollow.message": "本当に{name}をフォロー解除しますか?",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.flags": "国旗", "emoji_button.flags": "国旗",
"emoji_button.food": "食べ物", "emoji_button.food": "食べ物",

View File

@ -55,8 +55,8 @@
"confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.", "confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
"confirmations.mute.confirm": "Wycisz", "confirmations.mute.confirm": "Wycisz",
"confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?", "confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Przestań śledzić",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Czy na pewno zamierzasz przestać śledzić {name}?",
"emoji_button.activity": "Aktywność", "emoji_button.activity": "Aktywność",
"emoji_button.flags": "Flagi", "emoji_button.flags": "Flagi",
"emoji_button.food": "Żywność i napoje", "emoji_button.food": "Żywność i napoje",
@ -111,8 +111,8 @@
"notifications.column_settings.favourite": "Ulubione:", "notifications.column_settings.favourite": "Ulubione:",
"notifications.column_settings.follow": "Nowi śledzący:", "notifications.column_settings.follow": "Nowi śledzący:",
"notifications.column_settings.mention": "Wspomniali:", "notifications.column_settings.mention": "Wspomniali:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Powiadomienia push",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "To urządzenie",
"notifications.column_settings.reblog": "Podbili:", "notifications.column_settings.reblog": "Podbili:",
"notifications.column_settings.show": "Pokaż w kolumnie", "notifications.column_settings.show": "Pokaż w kolumnie",
"notifications.column_settings.sound": "Odtwarzaj dźwięk", "notifications.column_settings.sound": "Odtwarzaj dźwięk",
@ -125,7 +125,7 @@
"onboarding.page_one.handle": "Jesteś na domenie {domain}, więc Twój pełny adres to {handle}", "onboarding.page_one.handle": "Jesteś na domenie {domain}, więc Twój pełny adres to {handle}",
"onboarding.page_one.welcome": "Witamy w Mastodon!", "onboarding.page_one.welcome": "Witamy w Mastodon!",
"onboarding.page_six.admin": "Administratorem tej instancji jest {admin}.", "onboarding.page_six.admin": "Administratorem tej instancji jest {admin}.",
"onboarding.page_six.almost_done": "Prawie gotowe...", "onboarding.page_six.almost_done": "Prawie gotowe",
"onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "Są dostępne {apps} dla Androida, iOS i innych platform.", "onboarding.page_six.apps_available": "Są dostępne {apps} dla Androida, iOS i innych platform.",
"onboarding.page_six.github": "Mastodon jest oprogramowaniem otwartoźródłwym. Możesz zgłaszać błędy, proponować funkcje i pomóc w rozwoju na {github}.", "onboarding.page_six.github": "Mastodon jest oprogramowaniem otwartoźródłwym. Możesz zgłaszać błędy, proponować funkcje i pomóc w rozwoju na {github}.",
@ -151,7 +151,7 @@
"report.target": "Zgłaszanie {target}", "report.target": "Zgłaszanie {target}",
"search.placeholder": "Szukaj", "search.placeholder": "Szukaj",
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}", "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "Spojrzenie wgłąb…",
"status.cannot_reblog": "Ten post nie może zostać podbity", "status.cannot_reblog": "Ten post nie może zostać podbity",
"status.delete": "Usuń", "status.delete": "Usuń",
"status.favourite": "Ulubione", "status.favourite": "Ulubione",
@ -178,7 +178,7 @@
"upload_area.title": "Przeciągnij i upuść aby wysłać", "upload_area.title": "Przeciągnij i upuść aby wysłać",
"upload_button.label": "Dodaj zawartość multimedialną", "upload_button.label": "Dodaj zawartość multimedialną",
"upload_form.undo": "Cofnij", "upload_form.undo": "Cofnij",
"upload_progress.label": "Wysyłanie...", "upload_progress.label": "Wysyłanie",
"video_player.expand": "Przełącz wideo", "video_player.expand": "Przełącz wideo",
"video_player.toggle_sound": "Przełącz dźwięk", "video_player.toggle_sound": "Przełącz dźwięk",
"video_player.toggle_visible": "Przełącz widoczność", "video_player.toggle_visible": "Przełącz widoczność",

View File

@ -36,7 +36,7 @@ function main() {
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => { [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime')); const datetime = new Date(content.getAttribute('datetime'));
content.textContent = relativeFormat.format(datetime);; content.textContent = relativeFormat.format(datetime);
}); });
}); });

View File

@ -4091,6 +4091,10 @@ button.icon-button.active i.fa-retweet {
} }
} }
::-webkit-scrollbar-thumb {
border-radius: 0;
}
noscript { noscript {
text-align: center; text-align: center;

View File

@ -5,4 +5,14 @@ module Mastodon
class NotPermittedError < Error; end class NotPermittedError < Error; end
class ValidationError < Error; end class ValidationError < Error; end
class RaceConditionError < Error; end class RaceConditionError < Error; end
class UnexpectedResponseError < Error
def initialize(response = nil)
@response = response
end
def to_s
"#{@response.uri} returned code #{@response.code}"
end
end
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Ostatus::Activity::Base class OStatus::Activity::Base
def initialize(xml, account = nil) def initialize(xml, account = nil)
@xml = xml @xml = xml
@account = account @account = account

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Ostatus::Activity::Creation < Ostatus::Activity::Base class OStatus::Activity::Creation < OStatus::Activity::Base
def perform def perform
if redis.exists("delete_upon_arrival:#{@account.id}:#{id}") if redis.exists("delete_upon_arrival:#{@account.id}:#{id}")
Rails.logger.debug "Delete for status #{id} was queued, ignoring" Rails.logger.debug "Delete for status #{id} was queued, ignoring"

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Ostatus::Activity::Deletion < Ostatus::Activity::Base class OStatus::Activity::Deletion < OStatus::Activity::Base
def perform def perform
Rails.logger.debug "Deleting remote status #{id}" Rails.logger.debug "Deleting remote status #{id}"
status = Status.find_by(uri: id, account: @account) status = Status.find_by(uri: id, account: @account)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Ostatus::Activity::General < Ostatus::Activity::Base class OStatus::Activity::General < OStatus::Activity::Base
def specialize def specialize
special_class&.new(@xml, @account) special_class&.new(@xml, @account)
end end
@ -10,11 +10,11 @@ class Ostatus::Activity::General < Ostatus::Activity::Base
def special_class def special_class
case verb case verb
when :post when :post
Ostatus::Activity::Post OStatus::Activity::Post
when :share when :share
Ostatus::Activity::Share OStatus::Activity::Share
when :delete when :delete
Ostatus::Activity::Deletion OStatus::Activity::Deletion
end end
end end
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Ostatus::Activity::Post < Ostatus::Activity::Creation class OStatus::Activity::Post < OStatus::Activity::Creation
def perform def perform
status, just_created = super status, just_created = super

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Ostatus::Activity::Remote < Ostatus::Activity::Base class OStatus::Activity::Remote < OStatus::Activity::Base
def perform def perform
find_status(id) || FetchRemoteStatusService.new.call(url) find_status(id) || FetchRemoteStatusService.new.call(url)
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Ostatus::Activity::Share < Ostatus::Activity::Creation class OStatus::Activity::Share < OStatus::Activity::Creation
def perform def perform
return if reblog.nil? return if reblog.nil?
@ -18,7 +18,7 @@ class Ostatus::Activity::Share < Ostatus::Activity::Creation
def reblog def reblog
return @reblog if defined? @reblog return @reblog if defined? @reblog
original_status = Ostatus::Activity::Remote.new(object).perform original_status = OStatus::Activity::Remote.new(object).perform
return if original_status.nil? return if original_status.nil?
@reblog = original_status.reblog? ? original_status.reblog : original_status @reblog = original_status.reblog? ? original_status.reblog : original_status

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Ostatus::AtomSerializer class OStatus::AtomSerializer
include RoutingHelper include RoutingHelper
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper

View File

@ -36,6 +36,11 @@
# followers_count :integer default(0), not null # followers_count :integer default(0), not null
# following_count :integer default(0), not null # following_count :integer default(0), not null
# last_webfingered_at :datetime # last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# #
class Account < ApplicationRecord class Account < ApplicationRecord
@ -49,6 +54,8 @@ class Account < ApplicationRecord
include Remotable include Remotable
include EmojiHelper include EmojiHelper
enum protocol: [:ostatus, :activitypub]
# Local users # Local users
has_one :user, inverse_of: :account has_one :user, inverse_of: :account

View File

@ -26,8 +26,6 @@ class Web::PushSubscription < ApplicationRecord
before_create :send_welcome_notification before_create :send_welcome_notification
def push(notification) def push(notification)
return unless pushable? notification
name = display_name notification.from_account name = display_name notification.from_account
title = title_str(name, notification) title = title_str(name, notification)
body = body_str notification body = body_str notification
@ -45,7 +43,7 @@ class Web::PushSubscription < ApplicationRecord
title: title, title: title,
dir: dir, dir: dir,
image: image, image: image,
badge: full_asset_url('badge.png'), badge: full_asset_url('badge.png', skip_pipeline: true),
tag: notification.id, tag: notification.id,
timestamp: notification.created_at, timestamp: notification.created_at,
icon: notification.from_account.avatar_static_url, icon: notification.from_account.avatar_static_url,
@ -69,6 +67,10 @@ class Web::PushSubscription < ApplicationRecord
) )
end end
def pushable?(notification)
data && data.key?('alerts') && data['alerts'][notification.type.to_s]
end
def as_payload def as_payload
payload = { payload = {
id: id, id: id,
@ -115,7 +117,7 @@ class Web::PushSubscription < ApplicationRecord
when :mention then [ when :mention then [
{ {
title: translate('push_notifications.mention.action_favourite'), title: translate('push_notifications.mention.action_favourite'),
icon: full_asset_url('emoji/2764.png'), icon: full_asset_url('emoji/2764.png', skip_pipeline: true),
todo: 'request', todo: 'request',
method: 'POST', method: 'POST',
action: "/api/v1/statuses/#{notification.target_status.id}/favourite", action: "/api/v1/statuses/#{notification.target_status.id}/favourite",
@ -148,16 +150,12 @@ class Web::PushSubscription < ApplicationRecord
rtl?(body) ? 'rtl' : 'ltr' rtl?(body) ? 'rtl' : 'ltr'
end end
def pushable?(notification)
data && data.key?('alerts') && data['alerts'][notification.type.to_s]
end
def send_welcome_notification def send_welcome_notification
Webpush.payload_send( Webpush.payload_send(
message: JSON.generate( message: JSON.generate(
title: translate('push_notifications.subscribed.title'), title: translate('push_notifications.subscribed.title'),
icon: full_asset_url('android-chrome-192x192.png'), icon: full_asset_url('android-chrome-192x192.png', skip_pipeline: true),
badge: full_asset_url('badge.png'), badge: full_asset_url('badge.png', skip_pipeline: true),
data: { data: {
content: translate('push_notifications.subscribed.body'), content: translate('push_notifications.subscribed.body'),
actions: [], actions: [],

View File

@ -10,6 +10,6 @@ class AuthorizeFollowService < BaseService
private private
def build_xml(follow_request) def build_xml(follow_request)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request))
end end
end end

View File

@ -18,6 +18,6 @@ class BlockService < BaseService
private private
def build_xml(block) def build_xml(block)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.block_salmon(block)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block))
end end
end end

View File

@ -2,6 +2,6 @@
module StreamEntryRenderer module StreamEntryRenderer
def stream_entry_to_xml(stream_entry) def stream_entry_to_xml(stream_entry)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.entry(stream_entry, true)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(stream_entry, true))
end end
end end

View File

@ -28,6 +28,6 @@ class FavouriteService < BaseService
private private
def build_xml(favourite) def build_xml(favourite)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.favourite_salmon(favourite)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite))
end end
end end

View File

@ -32,8 +32,5 @@ class FetchRemoteAccountService < BaseService
rescue Nokogiri::XML::XPath::SyntaxError rescue Nokogiri::XML::XPath::SyntaxError
Rails.logger.debug 'Invalid XML or missing namespace' Rails.logger.debug 'Invalid XML or missing namespace'
nil nil
rescue Goldfinger::NotFoundError, Goldfinger::Error
Rails.logger.debug 'Exceptions related to Goldfinger occurs'
nil
end end
end end

View File

@ -33,9 +33,6 @@ class FetchRemoteStatusService < BaseService
rescue Nokogiri::XML::XPath::SyntaxError rescue Nokogiri::XML::XPath::SyntaxError
Rails.logger.debug 'Invalid XML or missing namespace' Rails.logger.debug 'Invalid XML or missing namespace'
nil nil
rescue Goldfinger::NotFoundError, Goldfinger::Error
Rails.logger.debug 'Exceptions related to Goldfinger occurs'
nil
end end
def confirmed_domain?(domain, account) def confirmed_domain?(domain, account)

View File

@ -57,10 +57,10 @@ class FollowService < BaseService
end end
def build_follow_request_xml(follow_request) def build_follow_request_xml(follow_request)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.follow_request_salmon(follow_request)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request))
end end
def build_follow_xml(follow) def build_follow_xml(follow)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.follow_salmon(follow)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow))
end end
end end

View File

@ -65,7 +65,12 @@ class NotifyService < BaseService
end end
def send_push_notifications def send_push_notifications
sessions_with_subscriptions_ids = @recipient.user.session_activations.where.not(web_push_subscription: nil).pluck(:id) # HACK: Can be caused by quickly unfavouriting a status, since creating
# a favourite and creating a notification are not wrapped in a transaction.
return if @notification.activity.nil?
sessions_with_subscriptions = @recipient.user.session_activations.where.not(web_push_subscription: nil)
sessions_with_subscriptions_ids = sessions_with_subscriptions.select { |session| session.web_push_subscription.pushable? @notification }.map(&:id)
WebPushNotificationWorker.push_bulk(sessions_with_subscriptions_ids) do |session_activation_id| WebPushNotificationWorker.push_bulk(sessions_with_subscriptions_ids) do |session_activation_id|
[session_activation_id, @notification.id] [session_activation_id, @notification.id]

View File

@ -20,10 +20,10 @@ class ProcessFeedService < BaseService
end end
def process_entry(xml, account) def process_entry(xml, account)
activity = Ostatus::Activity::General.new(xml, account) activity = OStatus::Activity::General.new(xml, account)
activity.specialize&.perform if activity.status? activity.specialize&.perform if activity.status?
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
Rails.logger.debug "Nothing was saved for #{id} because: #{e}" Rails.logger.debug "Nothing was saved for #{activity.id} because: #{e}"
nil nil
end end
end end

View File

@ -47,7 +47,7 @@ class ProcessInteractionService < BaseService
reflect_unblock!(account, target_account) reflect_unblock!(account, target_account)
end end
end end
rescue Goldfinger::Error, HTTP::Error, OStatus2::BadSalmonError, Mastodon::NotPermittedError rescue HTTP::Error, OStatus2::BadSalmonError, Mastodon::NotPermittedError
nil nil
end end

View File

@ -10,6 +10,6 @@ class RejectFollowService < BaseService
private private
def build_xml(follow_request) def build_xml(follow_request)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request))
end end
end end

View File

@ -11,97 +11,154 @@ class ResolveRemoteAccountService < BaseService
# @param [String] uri User URI in the form of username@domain # @param [String] uri User URI in the form of username@domain
# @return [Account] # @return [Account]
def call(uri, update_profile = true, redirected = nil) def call(uri, update_profile = true, redirected = nil)
username, domain = uri.split('@') @username, @domain = uri.split('@')
return Account.find_local(username) if TagManager.instance.local_domain?(domain) return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
account = Account.find_remote(username, domain) @account = Account.find_remote(@username, @domain)
return account unless account_needs_webfinger_update?(account)
return @account unless webfinger_update_due?
Rails.logger.debug "Looking up webfinger for #{uri}" Rails.logger.debug "Looking up webfinger for #{uri}"
data = Goldfinger.finger("acct:#{uri}") @webfinger = Goldfinger.finger("acct:#{uri}")
raise Goldfinger::Error, 'Missing resource links' if data.link('http://schemas.google.com/g/2010#updates-from').nil? || data.link('salmon').nil? || data.link('http://webfinger.net/rel/profile-page').nil? || data.link('magic-public-key').nil? confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
# Disallow account hijacking if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
confirmed_username, confirmed_domain = data.subject.gsub(/\Aacct:/, '').split('@') @username = confirmed_username
@domain = confirmed_domain
unless confirmed_username.casecmp(username).zero? && confirmed_domain.casecmp(domain).zero? elsif redirected.nil?
return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true) if redirected.nil? return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true)
raise Goldfinger::Error, 'Requested and returned acct URI do not match'
end
return Account.find_local(confirmed_username) if TagManager.instance.local_domain?(confirmed_domain)
confirmed_account = Account.find_remote(confirmed_username, confirmed_domain)
if confirmed_account.nil?
Rails.logger.debug "Creating new remote account for #{uri}"
domain_block = DomainBlock.find_by(domain: domain)
account = Account.new(username: confirmed_username, domain: confirmed_domain)
account.suspended = true if domain_block && domain_block.suspend?
account.silenced = true if domain_block && domain_block.silence?
account.private_key = nil
else else
account = confirmed_account Rails.logger.debug 'Requested and returned acct URIs do not match'
return
end end
account.last_webfingered_at = Time.now.utc return if links_missing?
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href RedisLock.acquire(lock_options) do |lock|
account.salmon_url = data.link('salmon').href if lock.acquired?
account.url = data.link('http://webfinger.net/rel/profile-page').href @account = Account.find_remote(@username, @domain)
account.public_key = magic_key_to_pem(data.link('magic-public-key').href)
body, xml = get_feed(account.remote_url) create_account if @account.nil?
hubs = get_hubs(xml) update_account
account.uri = get_account_uri(xml) update_account_profile if update_profile
account.hub_url = hubs.first.attribute('href').value end
begin
account.save!
get_profile(body, account) if update_profile
rescue ActiveRecord::RecordNotUnique
# The account has been added by another worker!
return Account.find_remote(confirmed_username, confirmed_domain)
end end
account @account
rescue Goldfinger::Error => e
Rails.logger.debug "Webfinger query for #{uri} unsuccessful: #{e}"
nil
end end
private private
def account_needs_webfinger_update?(account) def links_missing?
account&.last_webfingered_at.nil? || account.last_webfingered_at <= 1.day.ago @webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? ||
@webfinger.link('salmon').nil? ||
@webfinger.link('http://webfinger.net/rel/profile-page').nil? ||
@webfinger.link('magic-public-key').nil? ||
canonical_uri.nil? ||
hub_url.nil?
end end
def get_feed(url) def webfinger_update_due?
response = Request.new(:get, url).perform @account.nil? || @account.last_webfingered_at.nil? || @account.last_webfingered_at <= 1.day.ago
raise Goldfinger::Error, "Feed attempt failed for #{url}: HTTP #{response.code}" unless response.code == 200
[response.to_s, Nokogiri::XML(response)]
end end
def get_hubs(xml) def create_account
hubs = xml.xpath('//xmlns:link[@rel="hub"]') Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"
raise Goldfinger::Error, 'No PubSubHubbub hubs found' if hubs.empty? || hubs.first.attribute('href').nil?
hubs @account = Account.new(username: @username, domain: @domain)
@account.suspended = true if auto_suspend?
@account.silenced = true if auto_silence?
@account.private_key = nil
end end
def get_account_uri(xml) def update_account
author_uri = xml.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri') @account.last_webfingered_at = Time.now.utc
@account.remote_url = atom_url
@account.salmon_url = salmon_url
@account.url = url
@account.public_key = public_key
@account.uri = canonical_uri
@account.hub_url = hub_url
@account.save!
end
def auto_suspend?
domain_block && domain_block.suspend?
end
def auto_silence?
domain_block && domain_block.silence?
end
def domain_block
return @domain_block if defined?(@domain_block)
@domain_block = DomainBlock.find_by(domain: @domain)
end
def atom_url
@atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href
end
def salmon_url
@salmon_url ||= @webfinger.link('salmon').href
end
def url
@url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href
end
def public_key
@public_key ||= magic_key_to_pem(@webfinger.link('magic-public-key').href)
end
def canonical_uri
return @canonical_uri if defined?(@canonical_uri)
author_uri = atom.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri')
if author_uri.nil? if author_uri.nil?
owner = xml.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS) owner = atom.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS)
author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil? author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil?
end end
raise Goldfinger::Error, 'Author URI could not be found' if author_uri.nil? @canonical_uri = author_uri.nil? ? nil : author_uri.content
author_uri.content
end end
def get_profile(body, account) def hub_url
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), false) return @hub_url if defined?(@hub_url)
hubs = atom.xpath('//xmlns:link[@rel="hub"]')
@hub_url = hubs.empty? || hubs.first['href'].nil? ? nil : hubs.first['href']
end
def atom_body
return @atom_body if defined?(@atom_body)
response = Request.new(:get, atom_url).perform
raise Mastodon::UnexpectedResponseError, response unless response.code == 200
@atom_body = response.to_s
end
def atom
return @atom if defined?(@atom)
@atom = Nokogiri::XML(atom_body)
end
def update_account_profile
RemoteProfileUpdateWorker.perform_async(@account.id, atom_body.force_encoding('UTF-8'), false)
end
def lock_options
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
end end
end end

View File

@ -10,11 +10,11 @@ class SendInteractionService < BaseService
@source_account = source_account @source_account = source_account
@target_account = target_account @target_account = target_account
return if block_notification? return if !target_account.ostatus? || block_notification?
delivery = build_request.perform delivery = build_request.perform
raise "Delivery failed for #{target_account.salmon_url}: HTTP #{delivery.code}" unless delivery.code > 199 && delivery.code < 300 raise Mastodon::UnexpectedResponseError, delivery unless delivery.code > 199 && delivery.code < 300
end end
private private

View File

@ -2,6 +2,8 @@
class SubscribeService < BaseService class SubscribeService < BaseService
def call(account) def call(account)
return unless account.ostatus?
@account = account @account = account
@account.secret = SecureRandom.hex @account.secret = SecureRandom.hex
@response = build_request.perform @response = build_request.perform
@ -16,7 +18,7 @@ class SubscribeService < BaseService
else else
# The response was either a 429 rate limit, or a 5xx error. # The response was either a 429 rate limit, or a 5xx error.
# We need to retry at a later time. Fail loudly! # We need to retry at a later time. Fail loudly!
raise "Subscription attempt failed for #{@account.acct} (#{@account.hub_url}): HTTP #{@response.code}" raise Mastodon::UnexpectedResponseError, @response
end end
end end

View File

@ -11,6 +11,6 @@ class UnblockService < BaseService
private private
def build_xml(block) def build_xml(block)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unblock_salmon(block)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block))
end end
end end

View File

@ -13,6 +13,6 @@ class UnfavouriteService < BaseService
private private
def build_xml(favourite) def build_xml(favourite)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unfavourite_salmon(favourite)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite))
end end
end end

View File

@ -14,6 +14,6 @@ class UnfollowService < BaseService
private private
def build_xml(follow) def build_xml(follow)
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unfollow_salmon(follow)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow))
end end
end end

View File

@ -2,6 +2,8 @@
class UnsubscribeService < BaseService class UnsubscribeService < BaseService
def call(account) def call(account)
return unless account.ostatus?
@account = account @account = account
@response = build_request.perform @response = build_request.perform

View File

@ -7,6 +7,7 @@
%th= t 'sessions.browser' %th= t 'sessions.browser'
%th= t 'sessions.ip' %th= t 'sessions.ip'
%th= t 'sessions.activity' %th= t 'sessions.activity'
%td
%tbody %tbody
- @sessions.each do |session| - @sessions.each do |session|
%tr %tr
@ -22,3 +23,6 @@
= t 'sessions.current_session' = t 'sessions.current_session'
- else - else
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at) %time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at)
%td
- if request.session['auth_id'] != session.session_id
= table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete

View File

@ -44,7 +44,7 @@ class ImportWorker
target_account = ResolveRemoteAccountService.new.call(row.first) target_account = ResolveRemoteAccountService.new.call(row.first)
next if target_account.nil? next if target_account.nil?
MuteService.new.call(from_account, target_account) MuteService.new.call(from_account, target_account)
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError rescue Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
next next
end end
end end
@ -56,7 +56,7 @@ class ImportWorker
target_account = ResolveRemoteAccountService.new.call(row.first) target_account = ResolveRemoteAccountService.new.call(row.first)
next if target_account.nil? next if target_account.nil?
BlockService.new.call(from_account, target_account) BlockService.new.call(from_account, target_account)
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError rescue Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
next next
end end
end end
@ -66,7 +66,7 @@ class ImportWorker
import_rows.each do |row| import_rows.each do |row|
begin begin
FollowService.new.call(from_account, row.first) FollowService.new.call(from_account, row.first)
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
next next
end end
end end

View File

@ -23,7 +23,7 @@ class Pubsubhubbub::DeliveryWorker
def process_delivery def process_delivery
payload_delivery payload_delivery
raise "Delivery failed for #{subscription.callback_url}: HTTP #{payload_delivery.code}" unless response_successful? raise Mastodon::UnexpectedResponseError, payload_delivery unless response_successful?
subscription.touch(:last_successful_delivery_at) subscription.touch(:last_successful_delivery_at)
end end

View File

@ -22,7 +22,7 @@ class Pubsubhubbub::DistributionWorker
def distribute_public!(stream_entries) def distribute_public!(stream_entries)
return if stream_entries.empty? return if stream_entries.empty?
@payload = Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.feed(@account, stream_entries)) @payload = OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, stream_entries))
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription| Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription|
[subscription.id, @payload] [subscription.id, @payload]
@ -32,7 +32,7 @@ class Pubsubhubbub::DistributionWorker
def distribute_hidden!(stream_entries) def distribute_hidden!(stream_entries)
return if stream_entries.empty? return if stream_entries.empty?
@payload = Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.feed(@account, stream_entries)) @payload = OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, stream_entries))
@domains = @account.followers.domains @domains = @account.followers.domains
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.reject { |s| !allowed_to_receive?(s.callback_url, s.domain) }) do |subscription| Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.reject { |s| !allowed_to_receive?(s.callback_url, s.domain) }) do |subscription|

View File

@ -13,6 +13,7 @@
ActiveSupport::Inflector.inflections(:en) do |inflect| ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'StatsD' inflect.acronym 'StatsD'
inflect.acronym 'OEmbed' inflect.acronym 'OEmbed'
inflect.acronym 'OStatus'
inflect.acronym 'ActivityPub' inflect.acronym 'ActivityPub'
inflect.acronym 'PubSubHubbub' inflect.acronym 'PubSubHubbub'
inflect.acronym 'ActivityStreams' inflect.acronym 'ActivityStreams'

View File

@ -397,6 +397,8 @@ en:
windows: Windows windows: Windows
windows_mobile: Windows Mobile windows_mobile: Windows Mobile
windows_phone: Windows Phone windows_phone: Windows Phone
revoke: Revoke
revoke_success: Session successfully revoked
title: Sessions title: Sessions
settings: settings:
authorized_apps: Authorized apps authorized_apps: Authorized apps
@ -492,7 +494,7 @@ en:
<p>This document is CC-BY-SA. It was last updated May 31, 2013.</p> <p>This document is CC-BY-SA. It was last updated May 31, 2013.</p>
<p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>. <p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
title: "%{instance} Terms of Service and Privacy Policy" title: "%{instance} Terms of Service and Privacy Policy"
time: time:
formats: formats:

View File

@ -492,7 +492,7 @@ ja:
<p>この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。</p> <p>この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。</p>
<p>オリジナルの出典 <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>. <p>オリジナルの出典 <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
title: "%{instance} 利用規約・プライバシーポリシー" title: "%{instance} 利用規約・プライバシーポリシー"
time: time:
formats: formats:

View File

@ -176,7 +176,7 @@ pl:
title: Opis instancji title: Opis instancji
site_description_extended: site_description_extended:
desc_html: Dobre miejsce na zasady użytkowania, wprowadzenie i inne rzeczy, które wyróżniają tą instancję. Możesz korzystać z tagów HTML desc_html: Dobre miejsce na zasady użytkowania, wprowadzenie i inne rzeczy, które wyróżniają tą instancję. Możesz korzystać z tagów HTML
title: Niestandrdowy opis stronyv title: Niestandrdowy opis strony
site_terms: site_terms:
desc_html: Miejsce na własną politykę prywatności, zasady użytkowania i inne unormowania prawne. Możesz używać tagów HTML desc_html: Miejsce na własną politykę prywatności, zasady użytkowania i inne unormowania prawne. Możesz używać tagów HTML
title: Niestandardowe zasady użytkowania title: Niestandardowe zasady użytkowania
@ -185,6 +185,21 @@ pl:
desc_html: Wyświetlaj publiczną oś czasu na stronie widocznej dla niezalogowanych desc_html: Wyświetlaj publiczną oś czasu na stronie widocznej dla niezalogowanych
title: Podgląd osi czasu title: Podgląd osi czasu
title: Ustawienia strony title: Ustawienia strony
statuses:
back_to_account: Wróć na konto
batch:
delete: Usuń
nsfw_off: Cofnij NSFW
nsfw_on: Oznacz jako NSFW
execute: Wykonaj
failed_to_execute: Nie udało się wykonać
media:
hide: Ukryj zawartość multimedialną
show: Pokaż zawartość multimedialną
title: Media
no_media: Bez zawartości multimedialnej
with_media: Z zawartością multimedialną
title: Statusy konta
subscriptions: subscriptions:
callback_url: URL zwrotny callback_url: URL zwrotny
confirmed: Potwierdzono confirmed: Potwierdzono
@ -386,6 +401,8 @@ pl:
windows: Windows windows: Windows
windows_mobile: Windows Mobile windows_mobile: Windows Mobile
windows_phone: Windows Phone windows_phone: Windows Phone
revoke: Unieważnij
revoke_success: Pomyślnie unieważniono sesję
title: Sesje title: Sesje
settings: settings:
authorized_apps: Uwierzytelnione aplikacje authorized_apps: Uwierzytelnione aplikacje
@ -481,7 +498,7 @@ pl:
<p>Dokument jest dostępny na licencji CC-BY-SA. Ostatnio modyfikowany 31 maja 2013, przetłumaczony 4 lipca 2017. Tłumaczenie (mimo dołożenia wszelkich starań) może nie być w pełni poprawne.</p> <p>Dokument jest dostępny na licencji CC-BY-SA. Ostatnio modyfikowany 31 maja 2013, przetłumaczony 4 lipca 2017. Tłumaczenie (mimo dołożenia wszelkich starań) może nie być w pełni poprawne.</p>
<p>Tekst bazuje na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>. <p>Tekst bazuje na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>.</p>
title: Zasady korzystania i polityka prywatności %{instance} title: Zasady korzystania i polityka prywatności %{instance}
time: time:
formats: formats:

View File

@ -39,6 +39,7 @@ ja:
setting_default_sensitive: メディアを常に閲覧注意としてマークする setting_default_sensitive: メディアを常に閲覧注意としてマークする
setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する
setting_system_font_ui: システムのデフォルトフォントを使う setting_system_font_ui: システムのデフォルトフォントを使う
setting_unfollow_modal: フォロー解除する前に確認ダイアログを表示する
setting_noindex: 検索エンジンによるインデックスを拒否する setting_noindex: 検索エンジンによるインデックスを拒否する
severity: 重大性 severity: 重大性
type: インポートする項目 type: インポートする項目

View File

@ -40,11 +40,14 @@ pl:
otp_attempt: Kod uwierzytelnienia dwustopniowego otp_attempt: Kod uwierzytelnienia dwustopniowego
password: Hasło password: Hasło
setting_auto_play_gif: Automatycznie odtwarzaj animowane GIFy setting_auto_play_gif: Automatycznie odtwarzaj animowane GIFy
setting_boost_modal: Pytaj o potwierdzenie przed podbiciem setting_boost_modal: Pytaj o potwierdzenie przed podbiciem
setting_default_privacy: Widoczność posta setting_default_privacy: Widoczność posta
setting_default_sensitive: Zawsze oznaczaj zawartość multimedialną jako wrażliwą setting_default_sensitive: Zawsze oznaczaj zawartość multimedialną jako wrażliwą
setting_delete_modal: Pytaj o potwierdzenie przed usunięciem postu setting_delete_modal: Pytaj o potwierdzenie przed usunięciem postu
setting_system_font_ui: Używaj domyślnej czcionki systemu setting_system_font_ui: Używaj domyślnej czcionki systemu
setting_unfollow_modal: Pytaj o potwierdzenie przed usunięciem śledzenia
setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych
severity: Priorytet severity: Priorytet
type: Typ importu type: Typ importu
username: Nazwa użytkownika username: Nazwa użytkownika

View File

@ -74,6 +74,8 @@ Rails.application.routes.draw do
resource :follower_domains, only: [:show, :update] resource :follower_domains, only: [:show, :update]
resource :delete, only: [:show, :destroy] resource :delete, only: [:show, :destroy]
resources :sessions, only: [:destroy]
end end
resources :media, only: [:show] resources :media, only: [:show]

View File

@ -0,0 +1,9 @@
class AddActivityPubToAccounts < ActiveRecord::Migration[5.1]
def change
add_column :accounts, :inbox_url, :string, null: false, default: ''
add_column :accounts, :outbox_url, :string, null: false, default: ''
add_column :accounts, :shared_inbox_url, :string, null: false, default: ''
add_column :accounts, :followers_url, :string, null: false, default: ''
add_column :accounts, :protocol, :integer, null: false, default: 0
end
end

View File

@ -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: 20170714184731) do ActiveRecord::Schema.define(version: 20170718211102) 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"
@ -56,6 +56,11 @@ ActiveRecord::Schema.define(version: 20170714184731) do
t.integer "followers_count", default: 0, null: false t.integer "followers_count", default: 0, null: false
t.integer "following_count", default: 0, null: false t.integer "following_count", default: 0, null: false
t.datetime "last_webfingered_at" t.datetime "last_webfingered_at"
t.string "inbox_url", default: "", null: false
t.string "outbox_url", default: "", null: false
t.string "shared_inbox_url", default: "", null: false
t.string "followers_url", default: "", null: false
t.integer "protocol", 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" t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower"
t.index ["uri"], name: "index_accounts_on_uri" t.index ["uri"], name: "index_accounts_on_uri"

View File

@ -32,7 +32,7 @@ describe Api::BaseController do
ActiveRecord::RecordInvalid => 422, ActiveRecord::RecordInvalid => 422,
Mastodon::ValidationError => 422, Mastodon::ValidationError => 422,
ActiveRecord::RecordNotFound => 404, ActiveRecord::RecordNotFound => 404,
Goldfinger::Error => 422, Mastodon::UnexpectedResponseError => 503,
HTTP::Error => 503, HTTP::Error => 503,
OpenSSL::SSL::SSLError => 503, OpenSSL::SSL::SSLError => 503,
Mastodon::NotPermittedError => 403, Mastodon::NotPermittedError => 403,

View File

@ -22,23 +22,23 @@ describe('emojify', () => {
it('does unicode', () => { it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal( expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal(
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" src="/emoji/1f469-1f469-1f466-1f466.svg" />'); '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":family_wwbb:" src="/emoji/1f469-1f469-1f466-1f466.svg" />');
expect(emojify('\uD83D\uDC68\uD83D\uDC69\uD83D\uDC67\uD83D\uDC67')).to.equal( expect(emojify('\uD83D\uDC68\uD83D\uDC69\uD83D\uDC67\uD83D\uDC67')).to.equal(
'<img draggable="false" class="emojione" alt="👨👩👧👧" src="/emoji/1f468-1f469-1f467-1f467.svg" />'); '<img draggable="false" class="emojione" alt="👨👩👧👧" title=":family_mwgg:" src="/emoji/1f468-1f469-1f467-1f467.svg" />');
expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('<img draggable="false" class="emojione" alt="👩👩👦" src="/emoji/1f469-1f469-1f466.svg" />'); expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('<img draggable="false" class="emojione" alt="👩👩👦" title=":family_wwb:" src="/emoji/1f469-1f469-1f466.svg" />');
expect(emojify('\u2757')).to.equal( expect(emojify('\u2757')).to.equal(
'<img draggable="false" class="emojione" alt="❗" src="/emoji/2757.svg" />'); '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
}); });
it('does multiple unicode', () => { it('does multiple unicode', () => {
expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal( expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal(
'<img draggable="false" class="emojione" alt="❗" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" src="/emoji/0023-20e3.svg" />'); '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" />');
expect(emojify('\u2757#\uFE0F\u20E3')).to.equal( expect(emojify('\u2757#\uFE0F\u20E3')).to.equal(
'<img draggable="false" class="emojione" alt="❗" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" src="/emoji/0023-20e3.svg" />'); '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" />');
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal( expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal(
'<img draggable="false" class="emojione" alt="❗" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" src="/emoji/0023-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" src="/emoji/2757.svg" />'); '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal( expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal(
'foo <img draggable="false" class="emojione" alt="❗" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" src="/emoji/0023-20e3.svg" /> bar'); 'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> bar');
}); });
it('ignores unicode inside of tags', () => { it('ignores unicode inside of tags', () => {

File diff suppressed because it is too large Load Diff

View File

@ -22,11 +22,11 @@ RSpec.describe ResolveRemoteAccountService do
end end
it 'raises error if no such user can be resolved via webfinger' do it 'raises error if no such user can be resolved via webfinger' do
expect { subject.call('catsrgr8@quitter.no') }.to raise_error Goldfinger::Error expect(subject.call('catsrgr8@quitter.no')).to be_nil
end end
it 'raises error if the domain does not have webfinger' do it 'raises error if the domain does not have webfinger' do
expect { subject.call('catsrgr8@example.com') }.to raise_error Goldfinger::Error expect(subject.call('catsrgr8@example.com')).to be_nil
end end
it 'returns an already existing remote account' do it 'returns an already existing remote account' do
@ -58,7 +58,7 @@ RSpec.describe ResolveRemoteAccountService do
end end
it 'prevents hijacking inexisting accounts' do it 'prevents hijacking inexisting accounts' do
expect { subject.call('hacker2@redirected.com') }.to raise_error Goldfinger::Error expect(subject.call('hacker2@redirected.com')).to be_nil
end end
it 'returns a new remote account' do it 'returns a new remote account' do
@ -68,4 +68,27 @@ RSpec.describe ResolveRemoteAccountService do
expect(account.domain).to eq 'localdomain.com' expect(account.domain).to eq 'localdomain.com'
expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom' expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom'
end end
it 'processes one remote account at a time using locks' do
wait_for_start = true
fail_occurred = false
return_values = []
threads = Array.new(5) do
Thread.new do
true while wait_for_start
begin
return_values << subject.call('foo@localdomain.com')
rescue ActiveRecord::RecordNotUnique
fail_occurred = true
end
end
end
wait_for_start = false
threads.each(&:join)
expect(fail_occurred).to be false
expect(return_values).to_not include(nil)
end
end end

View File

@ -33,11 +33,11 @@ RSpec.describe SubscribeService do
it 'fails loudly if PuSH hub is unavailable' do it 'fails loudly if PuSH hub is unavailable' do
stub_request(:post, 'http://hub.example.com/').to_return(status: 503) stub_request(:post, 'http://hub.example.com/').to_return(status: 503)
expect { subject.call(account) }.to raise_error(/Subscription attempt failed/) expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError
end end
it 'fails loudly if rate limited' do it 'fails loudly if rate limited' do
stub_request(:post, 'http://hub.example.com/').to_return(status: 429) stub_request(:post, 'http://hub.example.com/').to_return(status: 429)
expect { subject.call(account) }.to raise_error(/Subscription attempt failed/) expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError
end end
end end

View File

@ -26,7 +26,7 @@ describe Pubsubhubbub::DeliveryWorker do
subscription = Fabricate(:subscription) subscription = Fabricate(:subscription)
stub_request_to_respond_with(subscription, 500) stub_request_to_respond_with(subscription, 500)
expect { subject.perform(subscription.id, payload) }.to raise_error(/Delivery failed/) expect { subject.perform(subscription.id, payload) }.to raise_error Mastodon::UnexpectedResponseError
end end
it 'updates subscriptions when delivery succeeds' do it 'updates subscriptions when delivery succeeds' do