Merge branch 'master' into fix/cache_blocking

feature-private-federation
Effy Elden 2017-04-17 01:41:33 +10:00 committed by GitHub
commit acd33101c5
48 changed files with 453 additions and 184 deletions

View File

@ -14,6 +14,7 @@ addons:
postgresql: 9.4
rvm:
- 2.3.4
- 2.4.1
services:

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '2.4.1'
ruby '>= 2.3.0', '< 2.5.0'
gem 'pkg-config'
@ -88,7 +88,7 @@ group :development do
gem 'bullet'
gem 'active_record_query_trace'
gem 'capistrano'
gem 'capistrano', '3.8.0'
gem 'capistrano-rails'
gem 'capistrano-rbenv'
gem 'capistrano-yarn'

View File

@ -41,7 +41,7 @@ GEM
tzinfo (~> 1.1)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
airbrussh (1.1.2)
airbrussh (1.2.0)
sshkit (>= 1.6.1, != 1.7.0)
arel (7.1.4)
ast (2.3.0)
@ -469,7 +469,7 @@ DEPENDENCIES
binding_of_caller
browserify-rails
bullet
capistrano
capistrano (= 3.8.0)
capistrano-faster-assets (~> 1.0)
capistrano-rails
capistrano-rbenv

View File

@ -3,3 +3,4 @@
* * * *
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate.
- [ ] This bug happens on a [tagged release](https://github.com/tootsuite/mastodon/releases) and not on `master` (If you're a user, don't worry about this).

View File

@ -48,6 +48,14 @@ If you would like, you can [support the development of this project on Patreon][
- **Deployable via Docker**
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
## Checking out
If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version:
git clone https://github.com/tootsuite/mastodon.git
cd mastodon
git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
## Configuration
- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related

View File

@ -24,8 +24,10 @@ const makeGetStatusIds = () => createSelector([
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
try {
if (showStatus) {
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
showStatus = showStatus && !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'content']) : statusForId.get('content'));
showStatus = !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'unescaped_content']) : statusForId.get('unescaped_content'));
}
} catch(e) {
// Bad regex, don't affect filters
}

View File

@ -75,6 +75,7 @@ const fr = {
"navigation_bar.favourites": "Favoris",
"navigation_bar.info": "Plus d'informations",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Utilisateurs muets",
"navigation_bar.follow_requests": "Demandes de suivi",
"reply_indicator.cancel": "Annuler",
"search.placeholder": "Rechercher",

View File

@ -1,121 +1,125 @@
const ja = {
"column_back_button.label": "戻る",
"lightbox.close": "閉じる",
"loading_indicator.label": "読み込み中...",
"status.mention": "@{name} さんへの返信",
"status.delete": "削除",
"status.reply": "返信",
"status.reblog": "ブースト",
"status.favourite": "お気に入り",
"status.reblogged_by": "{name} さんにブーストされました",
"status.sensitive_warning": "不適切なコンテンツ",
"status.sensitive_toggle": "クリックして表示",
"status.show_more": "もっと見る",
"status.load_more": "もっと見る",
"status.show_less": "隠す",
"status.open": "Expand this status",
"status.report": "@{name} さんを通報",
"status.media_hidden": "非表示のメデイア",
"video_player.toggle_sound": "音の切り替え",
"account.mention": "@{name} さんに返信",
"account.block": "@{name} さんをブロック",
"account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。",
"account.edit_profile": "プロフィールを編集",
"account.follow": "フォロー",
"account.followers": "フォロワー",
"account.follows": "フォロー",
"account.follows_you": "フォローされています",
"account.mention": "@{name} さんに返信",
"account.mute": "ミュート",
"account.posts": "投稿",
"account.report": "@{name}を通報する",
"account.requested": "承認待ち",
"account.unblock": "@{name} さんのブロックを解除",
"account.unfollow": "フォロー解除",
"account.block": "@{name} さんをブロック",
"account.mute": "ミュート",
"account.unmute": "ミュート解除",
"account.follow": "フォロー",
"account.report": "@{name}を通報する",
"account.posts": "投稿",
"account.follows": "フォロー",
"account.followers": "フォロワー",
"account.follows_you": "フォローされています",
"account.requested": "承認待ち",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.heading": "スタート",
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub{github})から開発に参加したり、問題を報告したりできます。 {apps}",
"getting_started.apps": "さまざまなアプリで利用できます。",
"column.home": "ホーム",
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
"column.blocks": "ブロックしたユーザー",
"column.community": "ローカルタイムライン",
"column.public": "連合タイムライン",
"column.notifications": "通知",
"column.favourites": "お気に入り",
"tabs_bar.compose": "投稿",
"tabs_bar.home": "ホーム",
"tabs_bar.mentions": "返信",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.federated_timeline": "連合",
"tabs_bar.notifications": "通知",
"column.follow_requests": "フォローリクエスト",
"column.home": "ホーム",
"column.mutes": "ミュートしたユーザー",
"column.notifications": "通知",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る",
"compose_form.placeholder": "今なにしてる?",
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザーat {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
"compose_form.publish": "トゥート",
"compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
"compose_form.spoiler": "テキストを隠す",
"compose_form.spoiler_placeholder": "内容注意メッセージ",
"compose_form.private": "非公開にする",
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザーat {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
"compose_form.unlisted": "公開タイムラインに表示しない",
"privacy.public.short": "公開",
"privacy.public.long": "公開TLに投稿する",
"privacy.unlisted.short": "未収載",
"privacy.unlisted.long": "公開TLで表示しない",
"privacy.private.short": "非公開",
"privacy.private.long": "フォロワーだけに公開",
"privacy.direct.short": "ダイレクト",
"privacy.direct.long": "含んだユーザーだけに公開",
"privacy.change": "投稿のプライバシーを変更",
"report.heading": "新規通報",
"report.placeholder": "コメント",
"report.target": "問題のユーザー",
"report.submit": "通報する",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.logout": "ログアウト",
"navigation_bar.favourites": "お気に入り",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.info": "サーバー情報",
"reply_indicator.cancel": "キャンセル",
"search.placeholder": "検索",
"search.account": "アカウント",
"search.hashtag": "ハッシュタグ",
"search.status_by": "{uuuname}からの投稿",
"search_results.total": "{count} 件",
"upload_area.title": "ファイルをこちらにドラッグしてください",
"upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す",
"notification.follow": "{name} さんにフォローされました",
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
"notification.mention": "{name} さんがあなたに返信しました",
"notifications.clear": "通知を片付ける",
"notifications.clear_confirmation": "通知を全部片付けます。大丈夫ですか?",
"notifications.column_settings.alert": "デスクトップ通知",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.sound": "通知音を再生",
"compose_form.spoiler_placeholder": "閲覧注意",
"emoji_button.label": "絵文字を追加",
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
"empty_column.home.public_timeline": "連合タイムライン",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
"empty_column.hashtag": "このハッシュタグはまだ使っていません。",
"upload_progress.label": "アップロード中…",
"emoji_button.label": "絵文字を追加",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.apps": "さまざまなアプリで利用できます。",
"getting_started.heading": "スタート",
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub{github})から開発に参加したり、問題を報告したりできます。 {apps}",
"home.column_settings.advanced": "上級者向け",
"home.column_settings.basic": "シンプル",
"home.column_settings.advanced": "エキスパート",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.column_settings.show_reblogs": "ブースト表示",
"home.column_settings.show_replies": "返信表示",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.settings": "カラム設定",
"notifications.settings": "カラム設定",
"lightbox.close": "閉じる",
"loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え",
"missing_indicator.label": "見つかりません",
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.favourites": "お気に入り",
"navigation_bar.follow_requests": "フォローリクエスト",
"navigation_bar.info": "サーバー情報",
"navigation_bar.logout": "ログアウト",
"navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン",
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
"notification.follow": "{name} さんにフォローされました",
"notification.mention": "{name} さんがあなたに返信しました",
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
"notifications.clear": "通知を消去",
"notifications.clear_confirmation": "本当に通知を消去しますか?",
"notifications.column_settings.alert": "デスクトップ通知",
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.sound": "通知音を再生",
"notifications.settings": "カラム設定",
"privacy.change": "投稿のプライバシーを変更",
"privacy.direct.long": "メンションしたユーザーだけに公開",
"privacy.direct.short": "ダイレクト",
"privacy.private.long": "フォロワーだけに公開",
"privacy.private.short": "非公開",
"privacy.public.long": "公開TLに投稿する",
"privacy.public.short": "公開",
"privacy.unlisted.long": "公開TLで表示しない",
"privacy.unlisted.short": "未収載",
"reply_indicator.cancel": "キャンセル",
"report.heading": "新規通報",
"report.placeholder": "コメント",
"report.submit": "通報する",
"report.target": "問題のユーザー",
"search.placeholder": "検索",
"search.status_by": "{name}からの投稿",
"search_results.total": "{count} {count, plural, one {result} other {results}} 件",
"status.delete": "削除",
"status.favourite": "お気に入り",
"status.load_more": "もっと見る",
"status.media_hidden": "非表示のメデイア",
"status.mention": "@{name} さんへの返信",
"status.open": "詳細を表示",
"status.reblog": "ブースト",
"status.reblogged_by": "{name} さんにブーストされました",
"status.reply": "返信",
"status.report": "@{name} さんを通報",
"status.sensitive_toggle": "クリックして表示",
"status.sensitive_warning": "不適切なコンテンツ",
"status.show_less": "隠す",
"status.show_more": "もっと見る",
"tabs_bar.compose": "投稿",
"tabs_bar.federated_timeline": "連合",
"tabs_bar.home": "ホーム",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.notifications": "通知",
"upload_area.title": "ドラッグ&ドロップでアップロード",
"upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す",
"upload_progress.label": "アップロード中…",
"video_player.expand": "動画の詳細",
"video_player.toggle_sound": "音の切り替え",
"video_player.toggle_visible": "表示切り替え",
"video_player.video_error": "動画の再生に失敗しました",
};
export default ja;

View File

@ -48,6 +48,9 @@ const normalizeStatus = (state, status) => {
normalStatus.reblog = status.reblog.id;
}
const linebreakComplemented = status.content.replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
normalStatus.unescaped_content = new DOMParser().parseFromString(linebreakComplemented, 'text/html').documentElement.textContent;
return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus)));
};

View File

@ -1203,6 +1203,10 @@ a.status__content__spoiler-link {
&:focus {
outline: 0;
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.spoiler-input__input {
@ -1267,6 +1271,10 @@ a.status__content__spoiler-link {
color: $color5;
border-bottom-color: $color4;
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
@import 'boost';
@ -1906,6 +1914,10 @@ button.icon-button.active i.fa-retweet {
&:focus {
background: lighten($color1, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.search__icon {

View File

@ -15,16 +15,26 @@ module Admin
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_block.created_msg')
else
render action: :new
end
end
def show
@domain_block = DomainBlock.find(params[:id])
end
def destroy
@domain_block = DomainBlock.find(params[:id])
UnblockDomainService.new.call(@domain_block, resource_params[:retroactive])
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_block.destroyed_msg')
end
private
def resource_params
params.require(:domain_block).permit(:domain, :severity)
params.require(:domain_block).permit(:domain, :severity, :reject_media, :retroactive)
end
end
end

View File

@ -8,7 +8,9 @@ class ApplicationController < ActionController::Base
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
include Localized
helper_method :current_account, :single_user_mode?
helper_method :current_account
helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found

View File

@ -3,6 +3,8 @@
class DomainBlock < ApplicationRecord
enum severity: [:silence, :suspend]
attr_accessor :retroactive
validates :domain, presence: true, uniqueness: true
def self.blocked?(domain)

View File

@ -1,13 +1,15 @@
# frozen_string_literal: true
class Import < ApplicationRecord
FILE_TYPES = ['text/plain', 'text/csv'].freeze
self.inheritance_column = false
belongs_to :account, required: true
enum type: [:following, :blocking, :muting]
belongs_to :account
FILE_TYPES = ['text/plain', 'text/csv'].freeze
validates :type, presence: true
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
validates_attachment_content_type :data, content_type: FILE_TYPES

View File

@ -110,6 +110,10 @@ class Status < ApplicationRecord
results
end
def non_sensitive_with_media?
!sensitive? && media_attachments.any?
end
class << self
def as_home_timeline(account)
where(account: [account] + account.following)

View File

@ -3,12 +3,34 @@
class BlockDomainService < BaseService
def call(domain_block)
if domain_block.silence?
Account.where(domain: domain_block.domain).update_all(silenced: true)
silence_accounts!(domain_block.domain)
clear_media!(domain_block.domain) if domain_block.reject_media?
else
Account.where(domain: domain_block.domain).find_each do |account|
suspend_accounts!(domain_block.domain)
end
end
private
def silence_accounts!(domain)
Account.where(domain: domain).update_all(silenced: true)
end
def clear_media!(domain)
Account.where(domain: domain).find_each do |account|
account.avatar.destroy
account.header.destroy
end
MediaAttachment.where(account: Account.where(domain: domain)).find_each do |attachment|
attachment.file.destroy
end
end
def suspend_accounts!(domain)
Account.where(domain: domain).where(suspended: false).find_each do |account|
account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
SuspendAccountService.new.call(account)
end
end
end
end

View File

@ -16,7 +16,7 @@ class FollowRemoteAccountService < BaseService
return Account.find_local(username) if TagManager.instance.local_domain?(domain)
account = Account.find_remote(username, domain)
return account unless account&.last_webfingered_at.nil? || 1.day.from_now(account.last_webfingered_at) < Time.now.utc
return account unless account_needs_webfinger_update?(account)
Rails.logger.debug "Looking up webfinger for #{uri}"
@ -62,6 +62,10 @@ class FollowRemoteAccountService < BaseService
private
def account_needs_webfinger_update?(account)
account&.last_webfingered_at.nil? || account.last_webfingered_at <= 1.day.ago
end
def get_feed(url)
response = http_client.get(Addressable::URI.parse(url))
[response.to_s, Nokogiri::XML(response)]

View File

@ -179,12 +179,12 @@ class ProcessFeedService < BaseService
end
def hashtags_from_xml(parent, xml)
tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select { |t| !t.blank? }
tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
ProcessHashtagsService.new.call(parent, tags)
end
def media_from_xml(parent, xml)
return if DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link|
next unless link['href']
@ -192,7 +192,11 @@ class ProcessFeedService < BaseService
media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
parsed_url = URI.parse(link['href'])
next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
next if !%w[http https].include?(parsed_url.scheme) || parsed_url.host.empty?
media.save
next if do_not_download
begin
media.file_remote_url = link['href']

View File

@ -13,6 +13,7 @@ class SuspendAccountService < BaseService
def purge_content
@account.statuses.reorder(nil).find_each do |status|
# This federates out deletes to previous followers
RemoveStatusService.new.call(status)
end
@ -29,9 +30,7 @@ class SuspendAccountService < BaseService
@account.display_name = ''
@account.note = ''
@account.avatar.destroy
@account.avatar.clear
@account.header.destroy
@account.header.clear
@account.save!
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class UnblockDomainService < BaseService
def call(domain_block, retroactive)
if retroactive
if domain_block.silence?
Account.where(domain: domain_block.domain).update_all(silenced: false)
else
Account.where(domain: domain_block.domain).update_all(suspended: false)
end
end
domain_block.destroy
end
end

View File

@ -6,6 +6,7 @@ class UnfollowService < BaseService
# @param [Account] target_account Which to unfollow
def call(source_account, target_account)
follow = source_account.unfollow!(target_account)
return unless follow
NotificationWorker.perform_async(build_xml(follow), source_account.id, target_account.id) unless target_account.local?
UnmergeWorker.perform_async(target_account.id, source_account.id)
end

View File

@ -1,34 +1,34 @@
.card.h-card.p-author{ style: "background-image: url(#{@account.header.url( :original)})" }
- if user_signed_in? && current_account.id != @account.id && !current_account.requested?(@account)
.card.h-card.p-author{ style: "background-image: url(#{account.header.url( :original)})" }
- if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
.controls
- if current_account.following?(@account)
= link_to t('accounts.unfollow'), unfollow_account_path(@account), data: { method: :post }, class: 'button'
- if current_account.following?(account)
= link_to t('accounts.unfollow'), unfollow_account_path(account), data: { method: :post }, class: 'button'
- else
= link_to t('accounts.follow'), follow_account_path(@account), data: { method: :post }, class: 'button'
= link_to t('accounts.follow'), follow_account_path(account), data: { method: :post }, class: 'button'
- elsif !user_signed_in?
.controls
.remote-follow
= link_to t('accounts.remote_follow'), account_remote_follow_path(@account), class: 'button'
.avatar= image_tag @account.avatar.url(:original), class: 'u-photo'
= link_to t('accounts.remote_follow'), account_remote_follow_path(account), class: 'button'
.avatar= image_tag account.avatar.url(:original), class: 'u-photo'
%h1.name
%span.p-name.emojify= display_name(@account)
%span.p-name.emojify= display_name(account)
%small
%span= "@#{@account.username}"
= fa_icon('lock') if @account.locked?
%span= "@#{account.username}"
= fa_icon('lock') if account.locked?
.details
.bio
.account__header__content.p-note.emojify= Formatter.instance.simplified_format(@account)
.account__header__content.p-note.emojify= Formatter.instance.simplified_format(account)
.details-counters
.counter{ class: active_nav_class(short_account_url(@account)) }
= link_to short_account_url(@account), class: 'u-url u-uid' do
.counter{ class: active_nav_class(short_account_url(account)) }
= link_to short_account_url(account), class: 'u-url u-uid' do
%span.counter-label= t('accounts.posts')
%span.counter-number= number_with_delimiter @account.statuses_count
.counter{ class: active_nav_class(following_account_url(@account)) }
= link_to following_account_url(@account) do
%span.counter-number= number_with_delimiter account.statuses_count
.counter{ class: active_nav_class(following_account_url(account)) }
= link_to following_account_url(account) do
%span.counter-label= t('accounts.following')
%span.counter-number= number_with_delimiter @account.following_count
.counter{ class: active_nav_class(followers_account_url(@account)) }
= link_to followers_account_url(@account) do
%span.counter-number= number_with_delimiter account.following_count
.counter{ class: active_nav_class(followers_account_url(account)) }
= link_to followers_account_url(account) do
%span.counter-label= t('accounts.followers')
%span.counter-number= number_with_delimiter @account.followers_count
%span.counter-number= number_with_delimiter account.followers_count

View File

@ -1,7 +1,7 @@
- content_for :page_title do
= t('accounts.people_who_follow', name: display_name(@account))
= render partial: 'header'
= render 'header', account: @account
.accounts-grid
- if @followers.empty?

View File

@ -1,7 +1,7 @@
- content_for :page_title do
= t('accounts.people_followed_by', name: display_name(@account))
= render partial: 'header'
= render 'header', account: @account
.accounts-grid
- if @following.empty?

View File

@ -20,7 +20,7 @@
.h-feed
%data.p-name{ value: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
= render partial: 'header'
= render 'header', account: @account
- if @statuses.empty?
.accounts-grid

View File

@ -6,12 +6,19 @@
%tr
%th= t('admin.domain_block.domain')
%th= t('admin.domain_block.severity')
%th= t('admin.domain_block.reject_media')
%th
%tbody
- @blocks.each do |block|
%tr
%td
%samp= block.domain
%td= block.severity
%td= t("admin.domain_block.severities.#{block.severity}")
%td
- if block.reject_media? || block.suspend?
%i.fa.fa-check
%td
= table_link_to 'undo', t('admin.domain_block.undo'), admin_domain_block_path(block)
= paginate @blocks
= link_to t('admin.domain_block.add_new'), new_admin_domain_block_path, class: 'button'

View File

@ -10,5 +10,8 @@
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("admin.domain_block.new.severity.#{type}") }
%p.hint= t('admin.domain_block.new.severity.desc_html')
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_block.reject_media'), hint: I18n.t('admin.domain_block.reject_media_hint')
.actions
= f.button :button, t('admin.domain_block.new.create'), type: :submit

View File

@ -0,0 +1,9 @@
- content_for :page_title do
= t('admin.domain_block.show.title', domain: @domain_block.domain)
= simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f|
= f.input :retroactive, as: :boolean, wrapper: :with_label, label: I18n.t("admin.domain_block.show.retroactive.#{@domain_block.severity}"), hint: I18n.t('admin.domain_block.show.affected_accounts', count: Account.where(domain: @domain_block.domain).count)
.actions
= f.button :button, t('admin.domain_block.show.undo'), type: :submit

View File

@ -1,5 +1,5 @@
attributes :id, :remote_url, :type
node(:url) { |media| full_asset_url(media.file.url(:original)) }
node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
node(:url) { |media| media.file.blank? ? media.remote_url : full_asset_url(media.file.url(:original)) }
node(:preview_url) { |media| media.file.blank? ? media.remote_url : full_asset_url(media.file.url(:small)) }
node(:text_url) { |media| media.local? ? medium_url(media) : nil }

View File

@ -1,6 +1,6 @@
%p.hint= t('two_factor_auth.recovery_instructions')
%ol.recovery-codes
- @codes.each do |code|
- recovery_codes.each do |code|
%li
%samp= code

View File

@ -1,4 +1,4 @@
- content_for :page_title do
= t('settings.two_factor_auth')
= render 'recovery_codes'
= render partial: 'recovery_codes', object: @codes

View File

@ -1,4 +1,4 @@
- content_for :page_title do
= t('settings.two_factor_auth')
= render 'recovery_codes'
= render partial: 'recovery_codes', object: @codes

View File

@ -0,0 +1,4 @@
- if activity.is_a?(Status) && activity.spoiler_text?
%meta{ property: 'og:description', content: activity.spoiler_text }/
- else
%meta{ property: 'og:description', content: activity.content }/

View File

@ -0,0 +1,6 @@
- if activity.is_a?(Status) && activity.non_sensitive_with_media?
%meta{ property: 'og:image', content: full_asset_url(activity.media_attachments.first.file.url(:small)) }/
- else
%meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/
%meta{ property: 'og:image:width', content: '120' }/
%meta{ property: 'og:image:height', content: '120' }/

View File

@ -6,17 +6,8 @@
%meta{ property: 'og:type', content: 'article' }/
%meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
- if @stream_entry.activity.is_a?(Status) && !@stream_entry.activity.spoiler_text.blank?
%meta{ property: 'og:description', content: @stream_entry.activity.spoiler_text }/
- else
%meta{ property: 'og:description', content: @stream_entry.activity.content }/
- if @stream_entry.activity.is_a?(Status) && !@stream_entry.activity.sensitive? && @stream_entry.activity.media_attachments.size > 0
%meta{ property: 'og:image', content: full_asset_url(@stream_entry.activity.media_attachments.first.file.url(:small)) }/
- else
%meta{ property: 'og:image', content: full_asset_url(@account.avatar.url(:original)) }/
%meta{ property: 'og:image:width', content: '120' }/
%meta{ property: 'og:image:height', content: '120' }/
= render 'stream_entries/og_description', activity: @stream_entry.activity
= render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account
%meta{ property: 'twitter:card', content: 'summary' }/

17
bin/rspec Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rspec' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rspec-core", "rspec")

View File

@ -1,4 +1,7 @@
lock '3.7.2'
# frozen_string_literal: true
lock '3.8.0'
set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
set :branch, ENV.fetch('BRANCH', 'master')

View File

@ -81,6 +81,8 @@ en:
web: Web
domain_block:
add_new: Add new
created_msg: Domain block is now being processed
destroyed_msg: Domain block has been undone
domain: Domain
new:
create: Create block
@ -90,8 +92,22 @@ en:
silence: Silence
suspend: Suspend
title: New domain block
reject_media: Reject media files
reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions
severities:
silence: Silence
suspend: Suspend
severity: Severity
show:
affected_accounts:
one: One account in the database affected
other: "%{count} accounts in the database affected"
retroactive:
silence: Unsilence all existing accounts from this domain
suspend: Unsuspend all existing accounts from this domain
title: Undo domain block for %{domain}
title: Domain Blocks
undo: Undo
pubsubhubbub:
callback_url: Callback URL
confirmed: Confirmed

View File

@ -71,6 +71,7 @@ ja:
profile_url: プロフィールURL
public: パブリック
push_subscription_expires: PuSH購読期限切れ
reset_password: パスワード再設定
salmon_url: Salmon URL
silence: サイレンス
statuses: トゥート数
@ -81,6 +82,8 @@ ja:
web: Web
domain_block:
add_new: 新規追加
created_msg: ドメインブロック処理を完了しました
destroyed_msg: ドメインブロックを外しました
domain: ドメイン
new:
create: ブロックを作成
@ -90,8 +93,21 @@ ja:
silence: サイレンス
suspend: 停止
title: 新規ドメインブロック
reject_media: メディアファイルを拒否
reject_media_hint: ローカルに保村されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。
severities:
silence: サイレンス
suspend: 停止
severity: 深刻度
show:
affected_accounts: "データベース中の%{count}個のアカウントに影響します"
retroactive:
silence: このドメインからの存在するすべてのアカウントのサイレンスを戻す
suspend: このドメインからの存在するすべてのアカウントの停止を戻す
title: "%{domain}のドメインブロックを戻す"
undo: 元に戻す
title: ドメインブロック
undo: 元に戻す
pubsubhubbub:
callback_url: コールバックURL
confirmed: 確認済み
@ -106,7 +122,7 @@ ja:
delete: 削除
id: ID
mark_as_resolved: 解決済みとしてマーク
report: 'レポート#%{id}'
report: レポート#%{id}
reported_account: 報告対象アカウント
reported_by: 報告者
resolved: 解決済み
@ -290,8 +306,13 @@ ja:
disable: 無効
enable: 有効
enabled_success: 二段階認証が有効になりました
generate_recovery_codes: 復元コードを生成
instructions_html: "<strong>Google Authenticatorか、もしくはほかのTOTPアプリでこのQRコードをスキャンしてください。</strong>これ以降、ログインするときはそのアプリで生成されるコードが必要になります。"
lost_recovery_codes: リカバリコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリコードは無効になります。
manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
recovery_codes: リカバリーコード
recovery_codes_regenerated: リカバリーコードが再生成されました。
recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリコードを使用してアカウントへアクセスすることができます。 リカバリコードは印刷して安全に保管してください。
setup: 初期設定
warning: 現在認証アプリを設定できない場合、無効に設定して、有効にしないでください。
wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。

View File

@ -2,28 +2,97 @@
pt:
about:
about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas.
about_this: Sobre essa instância
get_started: Como começar
apps: Aplicações
business_email: 'Email comercial:'
closed_registrations: Registros estão fechadas para essa instância.
contact: Contato
description_headline: O que é %{domain}?
domain_count_after: outras instâncias
domain_count_before: Conectado a
features:
api: Aberto para API de aplicações e serviços
blocks: Bloqueos e ferramentas para mudar
characters: 500 caracteres por post
chronology: Timeline são cronologicas
ethics: 'Design ético: sem propaganda, sem tracking'
gifv: GIFV e vídeos curtos
privacy: Granular, privacidade setada por post
public: Timelines públicas
features_headline: O que torna Mastodon diferente
get_started: Comece aqui
links: Links
source_code: Source code
other_instances: Outras instâncias
terms: Termos
user_count_after: usuários
user_count_before: Lugar de
accounts:
follow: Seguir
followers: Seguidores
following: Following
following: Seguindo
nothing_here: Não há nada aqui!
people_followed_by: Pessoas seguidas por %{name}
people_who_follow: Pessoas que seguem %{name}
posts: Posts
remote_follow: Acesso remoto
unfollow: Unfollow
admin:
accounts:
are_you_sure: Você tem certeza?
display_name: Nome mostrado
domain: Domain
edit: Editar
email: E-mail
feed_url: URL do Feed
followers: Seguidores
follows: Seguindo
location:
all: Todos
local: Local
remote: Remoto
title: Local
media_attachments: Mídia anexadas
moderation:
all: Todos
silenced: Silenciado
suspended: Supenso
title: Moderação
most_recent_activity: Atividade mais recente
most_recent_ip: IP mais recente
not_subscribed: Não inscrito
order:
alphabetic: Alfabética
most_recent: Mais recente
title: Ordem
perform_full_suspension: Fazer suspensão completa
profile_url: URL do perfil
public: Público
push_subscription_expires: PuSH subscription expires
salmon_url: Salmon URL
silence: Silêncio
statuses: Status
title: Contas
undo_silenced: Desfazer silenciar
undo_suspension: Desfazer supensão
username: Usuário
web: Web
domain_block:
add_new: Adicionar nova
created_msg: Bloqueio do domínio está sendo processado
destroyed_msg: Bloqueio de domínio está sendo desfeito
domain: Domínio
application_mailer:
signature: notificações Mastodon de %{instance}
auth:
change_password: Mudar password
change_password: Mudar senha
didnt_get_confirmation: Não recebeu instruções de confirmação?
forgot_password: Esqueceu a password?
forgot_password: Esqueceu a senha?
login: Entrar
register: Registar
resend_confirmation: Reenviar instruções de confirmação
reset_password: Reset password
reset_password: Resetar senha
set_new_password: Editar password
generic:
changes_saved_msg: Mudanças guardadas!

View File

@ -10,11 +10,13 @@ ja:
note: プロフィールは160文字まで設定することができます。
imports:
data: 他の Mastodon サーバーからエクスポートしたCSVファイルを選択して下さい
sessions:
otp: 携帯電話に表示された2段階認証コードを入力するか、生成したリカバリーコードを使用してください。
labels:
defaults:
avatar: アイコン
confirm_new_password: 新しいパスワード(確認用)
confirm_password: 新しいパスワード
confirm_password: パスワード(確認用)
current_password: 現在のパスワード
data: データ
display_name: 表示名
@ -22,12 +24,13 @@ ja:
header: ヘッダー
locale: 言語
locked: 非公開アカウントにする
new_password: パスワード
new_password: 新しいパスワード
note: プロフィール
otp_attempt: 二段階認証コード
password: パスワード
setting_boost_modal: ブーストする前に確認ダイアログを表示する
setting_default_privacy: 投稿の公開範囲
severity: 重大性
type: インポートする項目
username: ユーザー名
interactions:

View File

@ -4,17 +4,17 @@ pt:
labels:
defaults:
avatar: Avatar
confirm_new_password: Confirme nova password
confirm_password: Confirme a password
current_password: Password atual
confirm_new_password: Confirme nova senha
confirm_password: Confirme a senha
current_password: Senha atual
display_name: Nome
email: Endereço de email
header: Header
locale: Linguagem
new_password: Nova password
new_password: Nova senha
note: Biografia
password: Password
username: Username
password: Senha
username: Usuário
interactions:
must_be_follower: Bloquear notificações de não-seguidores
must_be_following: Bloquear notificações de pessoas que você

View File

@ -78,7 +78,7 @@ Rails.application.routes.draw do
namespace :admin do
resources :pubsubhubbub, only: [:index]
resources :domain_blocks, only: [:index, :new, :create]
resources :domain_blocks, only: [:index, :new, :create, :show, :destroy]
resources :settings, only: [:index, :update]
resources :reports, only: [:index, :show, :update] do

View File

@ -1,6 +1,16 @@
# frozen_string_literal: true
namespace :mastodon do
desc 'Execute daily tasks'
task :daily do
Rake::Task['mastodon:feeds:clear'].invoke
Rake::Task['mastodon:media:clear'].invoke
Rake::Task['mastodon:users:clear'].invoke
Rake::Task['mastodon:push:refresh'].invoke
end
desc 'Turn a user into an admin, identified by the USERNAME environment variable'
task make_admin: :environment do
include RoutingHelper
@ -13,12 +23,13 @@ namespace :mastodon do
desc 'Manually confirms a user with associated user email address stored in USER_EMAIL environment variable.'
task confirm_email: :environment do
email = ENV.fetch('USER_EMAIL')
user = User.where(email: email).first
user = User.find_by(email: email)
if user
user.update(confirmed_at: Time.now.utc)
puts "User #{email} confirmed."
puts "#{email} confirmed"
else
abort "User #{email} not found."
abort "#{email} not found"
end
end
@ -32,6 +43,13 @@ namespace :mastodon do
task remove_silenced: :environment do
MediaAttachment.where(account: Account.silenced).find_each(&:destroy)
end
desc 'Remove cached remote media attachments that are older than a week'
task remove_remote: :environment do
MediaAttachment.where.not(remote_url: '').where('created_at < ?', 1.week.ago).find_each do |media|
media.file.destroy
end
end
end
namespace :push do
@ -60,7 +78,7 @@ namespace :mastodon do
end
end
desc 'Clears all timelines so that they would be regenerated on next hit'
desc 'Clears all timelines'
task clear_all: :environment do
Redis.current.keys('feed:*').each { |key| Redis.current.del(key) }
end
@ -126,8 +144,13 @@ namespace :mastodon do
Rails.logger.debug 'Generating static avatars/headers for GIF ones...'
Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account|
begin
account.avatar.reprocess!
account.header.reprocess!
rescue StandardError => e
Rails.logger.error "Error while generating static avatars/headers for account #{account.id}: #{e}"
next
end
end
Rails.logger.debug 'Done!'

View File

@ -1,5 +1,6 @@
{
"name": "mastodon",
"license" : "AGPL-3.0",
"scripts": {
"start": "babel-node ./streaming/index.js --presets es2015,stage-2",
"storybook": "start-storybook -p 9001 -c storybook",

View File

@ -61,5 +61,4 @@ describe 'stream_entries/show.html.haml' do
expect(mf2.entry.in_reply_to.format.author.format.name.to_s).to eq alice.display_name
expect(mf2.entry.in_reply_to.format.author.format.url.to_s).not_to be_empty
end
end