Browse Source

Merge pull request #87 from tootsuite/master

merge upstream
closed-social-glitch-2
beatrix 7 years ago
committed by GitHub
parent
commit
e7edb4d1ee
64 changed files with 497 additions and 330 deletions
  1. +1
    -0
      Gemfile
  2. +3
    -0
      Gemfile.lock
  3. +1
    -1
      app/controllers/accounts_controller.rb
  4. +1
    -5
      app/controllers/api/base_controller.rb
  5. +17
    -0
      app/controllers/settings/sessions_controller.rb
  6. +1
    -1
      app/controllers/stream_entries_controller.rb
  7. +2
    -2
      app/helpers/routing_helper.rb
  8. +1
    -1
      app/javascript/mastodon/actions/statuses.js
  9. +1
    -1
      app/javascript/mastodon/actions/timelines.js
  10. +13
    -9
      app/javascript/mastodon/components/status_list.js
  11. +5
    -5
      app/javascript/mastodon/emoji.js
  12. +2
    -2
      app/javascript/mastodon/emojione_light.js
  13. +1
    -1
      app/javascript/mastodon/locales/ja.json
  14. +7
    -7
      app/javascript/mastodon/locales/pl.json
  15. +1
    -1
      app/javascript/packs/public.js
  16. +4
    -0
      app/javascript/styles/components.scss
  17. +10
    -0
      app/lib/exceptions.rb
  18. +1
    -1
      app/lib/ostatus/activity/base.rb
  19. +1
    -1
      app/lib/ostatus/activity/creation.rb
  20. +1
    -1
      app/lib/ostatus/activity/deletion.rb
  21. +4
    -4
      app/lib/ostatus/activity/general.rb
  22. +1
    -1
      app/lib/ostatus/activity/post.rb
  23. +1
    -1
      app/lib/ostatus/activity/remote.rb
  24. +2
    -2
      app/lib/ostatus/activity/share.rb
  25. +1
    -1
      app/lib/ostatus/atom_serializer.rb
  26. +7
    -0
      app/models/account.rb
  27. +8
    -10
      app/models/web/push_subscription.rb
  28. +1
    -1
      app/services/authorize_follow_service.rb
  29. +1
    -1
      app/services/block_service.rb
  30. +1
    -1
      app/services/concerns/stream_entry_renderer.rb
  31. +1
    -1
      app/services/favourite_service.rb
  32. +0
    -3
      app/services/fetch_remote_account_service.rb
  33. +0
    -3
      app/services/fetch_remote_status_service.rb
  34. +2
    -2
      app/services/follow_service.rb
  35. +6
    -1
      app/services/notify_service.rb
  36. +2
    -2
      app/services/process_feed_service.rb
  37. +1
    -1
      app/services/process_interaction_service.rb
  38. +1
    -1
      app/services/reject_follow_service.rb
  39. +115
    -58
      app/services/resolve_remote_account_service.rb
  40. +2
    -2
      app/services/send_interaction_service.rb
  41. +3
    -1
      app/services/subscribe_service.rb
  42. +1
    -1
      app/services/unblock_service.rb
  43. +1
    -1
      app/services/unfavourite_service.rb
  44. +1
    -1
      app/services/unfollow_service.rb
  45. +2
    -0
      app/services/unsubscribe_service.rb
  46. +4
    -0
      app/views/auth/registrations/_sessions.html.haml
  47. +3
    -3
      app/workers/import_worker.rb
  48. +1
    -1
      app/workers/pubsubhubbub/delivery_worker.rb
  49. +2
    -2
      app/workers/pubsubhubbub/distribution_worker.rb
  50. +1
    -0
      config/initializers/inflections.rb
  51. +3
    -1
      config/locales/en.yml
  52. +1
    -1
      config/locales/ja.yml
  53. +19
    -2
      config/locales/pl.yml
  54. +1
    -0
      config/locales/simple_form.ja.yml
  55. +3
    -0
      config/locales/simple_form.pl.yml
  56. +2
    -0
      config/routes.rb
  57. +9
    -0
      db/migrate/20170718211102_add_activitypub_to_accounts.rb
  58. +6
    -1
      db/schema.rb
  59. +1
    -1
      spec/controllers/api/base_controller_spec.rb
  60. +8
    -8
      spec/javascript/components/emojify.test.js
  61. +165
    -165
      spec/lib/ostatus/atom_serializer_spec.rb
  62. +26
    -3
      spec/services/resolve_remote_account_service_spec.rb
  63. +2
    -2
      spec/services/subscribe_service_spec.rb
  64. +1
    -1
      spec/workers/pubsubhubbub/delivery_worker_spec.rb

+ 1
- 0
Gemfile 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'

+ 3
- 0
Gemfile.lock 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)

+ 1
- 1
app/controllers/accounts_controller.rb 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

+ 1
- 5
app/controllers/api/base_controller.rb 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
render json: { error: 'Remote account could not be resolved' }, status: 422
end
rescue_from HTTP::Error do
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError 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

+ 17
- 0
app/controllers/settings/sessions_controller.rb 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

+ 1
- 1
app/controllers/stream_entries_controller.rb 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

+ 2
- 2
app/helpers/routing_helper.rb View File

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

+ 1
- 1
app/javascript/mastodon/actions/statuses.js 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));
} }

+ 1
- 1
app/javascript/mastodon/actions/timelines.js 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,
}; };
}; };

+ 13
- 9
app/javascript/mastodon/components/status_list.js View File

@ -30,8 +30,8 @@ export default class StatusList extends ImmutablePureComponent {
intersectionObserverWrapper = new IntersectionObserverWrapper(); intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = debounce((e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
handleScroll = debounce(() => {
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,18 +49,22 @@ 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 &&
prevProps.statusIds.first() !== this.props.statusIds.first() &&
this._oldScrollPosition &&
this.node.scrollTop > 0) {
let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
} }
} }
} }

+ 5
- 5
app/javascript/mastodon/emoji.js 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) {
const filename = unicodeToFilename[unicodeStr];
if (unicodeStr in unicodeMapping) {
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
} }

+ 2
- 2
app/javascript/mastodon/emojione_light.js 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), { });

+ 1
- 1
app/javascript/mastodon/locales/ja.json 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": "食べ物",

+ 7
- 7
app/javascript/mastodon/locales/pl.json 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.message": "Are you sure you want to unfollow {name}?",
"confirmations.unfollow.confirm": "Przestań śledzić",
"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_meta": "This device",
"notifications.column_settings.push": "Powiadomienia push",
"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ść",

+ 1
- 1
app/javascript/packs/public.js 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);
}); });
}); });

+ 4
- 0
app/javascript/styles/components.scss 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;

+ 10
- 0
app/lib/exceptions.rb 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

+ 1
- 1
app/lib/ostatus/activity/base.rb 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

+ 1
- 1
app/lib/ostatus/activity/creation.rb 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"

+ 1
- 1
app/lib/ostatus/activity/deletion.rb 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)

+ 4
- 4
app/lib/ostatus/activity/general.rb 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

+ 1
- 1
app/lib/ostatus/activity/post.rb 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

+ 1
- 1
app/lib/ostatus/activity/remote.rb 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

+ 2
- 2
app/lib/ostatus/activity/share.rb 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

+ 1
- 1
app/lib/ostatus/atom_serializer.rb 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

+ 7
- 0
app/models/account.rb 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

+ 8
- 10
app/models/web/push_subscription.rb 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'),
badge: full_asset_url('badge.png'),
icon: full_asset_url('android-chrome-192x192.png', skip_pipeline: true),
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: [],

+ 1
- 1
app/services/authorize_follow_service.rb 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

+ 1
- 1
app/services/block_service.rb 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

+ 1
- 1
app/services/concerns/stream_entry_renderer.rb 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

+ 1
- 1
app/services/favourite_service.rb 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

+ 0
- 3
app/services/fetch_remote_account_service.rb 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

+ 0
- 3
app/services/fetch_remote_status_service.rb 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)

+ 2
- 2
app/services/follow_service.rb 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

+ 6
- 1
app/services/notify_service.rb 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]

+ 2
- 2
app/services/process_feed_service.rb 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

+ 1
- 1
app/services/process_interaction_service.rb 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

+ 1
- 1
app/services/reject_follow_service.rb 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

+ 115
- 58
app/services/resolve_remote_account_service.rb 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)
return account unless account_needs_webfinger_update?(account)
@account = Account.find_remote(@username, @domain)
Rails.logger.debug "Looking up webfinger for #{uri}"
return @account unless webfinger_update_due?
data = Goldfinger.finger("acct:#{uri}")
Rails.logger.debug "Looking up webfinger for #{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?
@webfinger = Goldfinger.finger("acct:#{uri}")
# Disallow account hijacking
confirmed_username, confirmed_domain = data.subject.gsub(/\Aacct:/, '').split('@')
confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
unless confirmed_username.casecmp(username).zero? && confirmed_domain.casecmp(domain).zero?
return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true) if redirected.nil?
raise Goldfinger::Error, 'Requested and returned acct URI do not match'
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
@username = confirmed_username
@domain = confirmed_domain
elsif redirected.nil?
return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true)
else
Rails.logger.debug 'Requested and returned acct URIs do not match'
return
end end
return Account.find_local(confirmed_username) if TagManager.instance.local_domain?(confirmed_domain)
return if links_missing?
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
confirmed_account = Account.find_remote(confirmed_username, confirmed_domain)
if confirmed_account.nil?
Rails.logger.debug "Creating new remote account for #{uri}"
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@account = Account.find_remote(@username, @domain)
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
account = confirmed_account
create_account if @account.nil?
update_account
update_account_profile if update_profile
end
end end
account.last_webfingered_at = Time.now.utc
@account
rescue Goldfinger::Error => e
Rails.logger.debug "Webfinger query for #{uri} unsuccessful: #{e}"
nil
end
account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href
account.salmon_url = data.link('salmon').href
account.url = data.link('http://webfinger.net/rel/profile-page').href
account.public_key = magic_key_to_pem(data.link('magic-public-key').href)
private
body, xml = get_feed(account.remote_url)
hubs = get_hubs(xml)
def links_missing?
@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
account.uri = get_account_uri(xml)
account.hub_url = hubs.first.attribute('href').value
def webfinger_update_due?
@account.nil? || @account.last_webfingered_at.nil? || @account.last_webfingered_at <= 1.day.ago
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
def create_account
Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"
account
@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
private
def update_account
@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 account_needs_webfinger_update?(account)
account&.last_webfingered_at.nil? || account.last_webfingered_at <= 1.day.ago
def domain_block
return @domain_block if defined?(@domain_block)
@domain_block = DomainBlock.find_by(domain: @domain)
end end
def get_feed(url)
response = Request.new(:get, url).perform
raise Goldfinger::Error, "Feed attempt failed for #{url}: HTTP #{response.code}" unless response.code == 200
[response.to_s, Nokogiri::XML(response)]
def atom_url
@atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href
end end
def get_hubs(xml)
hubs = xml.xpath('//xmlns:link[@rel="hub"]')
raise Goldfinger::Error, 'No PubSubHubbub hubs found' if hubs.empty? || hubs.first.attribute('href').nil?
hubs
def salmon_url
@salmon_url ||= @webfinger.link('salmon').href
end end
def get_account_uri(xml)
author_uri = xml.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri')
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?
author_uri.content
@canonical_uri = author_uri.nil? ? nil : author_uri.content
end
def hub_url
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 end
def get_profile(body, account)
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), false)
def lock_options
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
end end
end end

+ 2
- 2
app/services/send_interaction_service.rb 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

+ 3
- 1
app/services/subscribe_service.rb 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

+ 1
- 1
app/services/unblock_service.rb 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

+ 1
- 1
app/services/unfavourite_service.rb 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

+ 1
- 1
app/services/unfollow_service.rb 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

+ 2
- 0
app/services/unsubscribe_service.rb 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

+ 4
- 0
app/views/auth/registrations/_sessions.html.haml 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

+ 3
- 3
app/workers/import_worker.rb 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

+ 1
- 1
app/workers/pubsubhubbub/delivery_worker.rb 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

+ 2
- 2
app/workers/pubsubhubbub/distribution_worker.rb 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|

+ 1
- 0
config/initializers/inflections.rb 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'

+ 3
- 1
config/locales/en.yml 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:

+ 1
- 1
config/locales/ja.yml 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:

+ 19
- 2
config/locales/pl.yml 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:

+ 1
- 0
config/locales/simple_form.ja.yml 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: インポートする項目

+ 3
- 0
config/locales/simple_form.pl.yml 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

+ 2
- 0
config/routes.rb 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]

+ 9
- 0
db/migrate/20170718211102_add_activitypub_to_accounts.rb 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

+ 6
- 1
db/schema.rb 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"

+ 1
- 1
spec/controllers/api/base_controller_spec.rb 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,

+ 8
- 8
spec/javascript/components/emojify.test.js 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" />');
expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('<img draggable="false" class="emojione" alt="👩👩👦" src="/emoji/1f469-1f469-1f466.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="👩👩👦" 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', () => {

+ 165
- 165
spec/lib/ostatus/atom_serializer_spec.rb
File diff suppressed because it is too large
View File


+ 26
- 3
spec/services/resolve_remote_account_service_spec.rb 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

+ 2
- 2
spec/services/subscribe_service_spec.rb 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

+ 1
- 1
spec/workers/pubsubhubbub/delivery_worker_spec.rb 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

Loading…
Cancel
Save