Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master

main
Jenkins 7 years ago
commit 82236a3703

@ -89,7 +89,8 @@ module Admin
:username,
:display_name,
:email,
:ip
:ip,
:staff
)
end
end

@ -92,7 +92,9 @@ module Admin
def filter_params
params.permit(
:local,
:remote
:remote,
:by_domain,
:shortcode
)
end
end

@ -1,11 +1,12 @@
# frozen_string_literal: true
module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params)

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
export default class Avatar extends React.PureComponent {
@ -8,12 +9,12 @@ export default class Avatar extends React.PureComponent {
account: ImmutablePropTypes.map.isRequired,
size: PropTypes.number.isRequired,
style: PropTypes.object,
animate: PropTypes.bool,
inline: PropTypes.bool,
animate: PropTypes.bool,
};
static defaultProps = {
animate: false,
animate: autoPlayGif,
size: 20,
inline: false,
};

@ -1,22 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
export default class AvatarOverlay extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
};
render() {
const { account, friend } = this.props;
const { account, friend, animate } = this.props;
const baseStyle = {
backgroundImage: `url(${account.get('avatar_static')})`,
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
const overlayStyle = {
backgroundImage: `url(${friend.get('avatar_static')})`,
backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (

@ -156,6 +156,8 @@ export default class ComposeForm extends ImmutablePureComponent {
return (
<div className='compose-form'>
<WarningContainer />
<Collapsable isVisible={this.props.spoiler} fullHeight={50}>
<div className='spoiler-input'>
<label>
@ -165,8 +167,6 @@ export default class ComposeForm extends ImmutablePureComponent {
</div>
</Collapsable>
<WarningContainer />
<ReplyIndicatorContainer />
<div className='compose-form__autosuggest-wrapper'>
@ -199,11 +199,11 @@ export default class ComposeForm extends ImmutablePureComponent {
<SensitiveButtonContainer />
<SpoilerButtonContainer />
</div>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
</div>
<div className='compose-form__publish'>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
</div>
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
</div>
</div>
);

@ -6,6 +6,7 @@ import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { isRtl } from '../../../rtl';
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@ -42,7 +43,10 @@ export default class ReplyIndicator extends ImmutablePureComponent {
return null;
}
const content = { __html: status.get('contentHtml') };
const content = { __html: status.get('contentHtml') };
const style = {
direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
};
return (
<div className='reply-indicator'>
@ -55,7 +59,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
</a>
</div>
<div className='reply-indicator__content' dangerouslySetInnerHTML={content} />
<div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
</div>
);
}

@ -62,7 +62,7 @@ export default class Upload extends ImmutablePureComponent {
render () {
const { intl, media } = this.props;
const active = this.state.hovered || this.state.focused;
const description = this.state.dirtyDescription || media.get('description') || '';
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
return (
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>

@ -161,7 +161,7 @@ export default class ListTimeline extends React.PureComponent {
scrollKey={`list_timeline-${columnId}`}
timelineId={`list:${id}`}
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
/>
</Column>
);

@ -8,6 +8,7 @@ import {
} from '../../../actions/timelines';
import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';
import { connectHashtagStream } from '../../../actions/streaming';
@connect()
export default class HashtagTimeline extends React.PureComponent {
@ -29,16 +30,13 @@ export default class HashtagTimeline extends React.PureComponent {
const { dispatch, hashtag } = this.props;
dispatch(refreshHashtagTimeline(hashtag));
this.polling = setInterval(() => {
dispatch(refreshHashtagTimeline(hashtag));
}, 10000);
this.disconnect = dispatch(connectHashtagStream(hashtag));
}
componentWillUnmount () {
if (typeof this.polling !== 'undefined') {
clearInterval(this.polling);
this.polling = null;
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

@ -9,6 +9,7 @@ import {
import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';
import { defineMessages, injectIntl } from 'react-intl';
import { connectPublicStream } from '../../../actions/streaming';
const messages = defineMessages({
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
@ -35,16 +36,13 @@ export default class PublicTimeline extends React.PureComponent {
const { dispatch } = this.props;
dispatch(refreshPublicTimeline());
this.polling = setInterval(() => {
dispatch(refreshPublicTimeline());
}, 3000);
this.disconnect = dispatch(connectPublicStream());
}
componentWillUnmount () {
if (typeof this.polling !== 'undefined') {
clearInterval(this.polling);
this.polling = null;
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

@ -27,6 +27,8 @@ const componentMap = {
'LIST': ListTimeline,
};
const isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
@component => injectIntl(component, { withRef: true })
export default class ColumnsArea extends ImmutablePureComponent {
@ -79,7 +81,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleChildrenContentChange() {
if (!this.props.singleColumn) {
this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
const modifier = isRtlLayout ? -1 : 1;
this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}

@ -36,9 +36,9 @@
"column.favourites": "المفضلة",
"column.follow_requests": "طلبات المتابعة",
"column.home": "الرئيسية",
"column.lists": "Lists",
"column.lists": "القوائم",
"column.mutes": "الحسابات المكتومة",
"column.notifications": "الإشعارات",
"column.notifications": "الإخطارات",
"column.pins": "التبويقات المثبتة",
"column.public": "الخيط العام الموحد",
"column_back_button.label": "العودة",
@ -64,7 +64,7 @@
"confirmations.delete.confirm": "حذف",
"confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟",
"confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "أكتم",
@ -109,32 +109,32 @@
"home.settings": "إعدادات العمود",
"keyboard_shortcuts.back": "للعودة",
"keyboard_shortcuts.boost": "للترقية",
"keyboard_shortcuts.column": "to focus a status in one of the columns",
"keyboard_shortcuts.compose": "to focus the compose textarea",
"keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة",
"keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص",
"keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.down": "للإنتقال إلى أسفل القائمة",
"keyboard_shortcuts.enter": "to open status",
"keyboard_shortcuts.favourite": "to favourite",
"keyboard_shortcuts.favourite": "للإضافة إلى المفضلة",
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.hotkey": "مفتاح الإختصار",
"keyboard_shortcuts.legend": "لعرض هذا المفتاح",
"keyboard_shortcuts.mention": "لذِكر الناشر",
"keyboard_shortcuts.reply": "للردّ",
"keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.search": "للتركيز على البحث",
"keyboard_shortcuts.toot": "لتحرير تبويق جديد",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
"lightbox.close": "إغلاق",
"lightbox.next": "التالي",
"lightbox.previous": "العودة",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.account.add": "أضف إلى القائمة",
"lists.account.remove": "إحذف من القائمة",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"lists.edit": "تعديل القائمة",
"lists.new.create": "إنشاء قائمة",
"lists.new.title_placeholder": "عنوان القائمة الجديدة",
"lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
"lists.subheading": "قوائمك",
"loading_indicator.label": "تحميل ...",
"media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "تعذر العثور عليه",
@ -146,7 +146,7 @@
"navigation_bar.follow_requests": "طلبات المتابعة",
"navigation_bar.info": "معلومات إضافية",
"navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح",
"navigation_bar.lists": "Lists",
"navigation_bar.lists": "القوائم",
"navigation_bar.logout": "خروج",
"navigation_bar.mutes": "الحسابات المكتومة",
"navigation_bar.pins": "التبويقات المثبتة",
@ -209,7 +209,7 @@
"search_popout.search_format": "نمط البحث المتقدم",
"search_popout.tips.hashtag": "وسم",
"search_popout.tips.status": "حالة",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.text": "جملة قصيرة تُمكّنُك من عرض أسماء و حسابات و كلمات رمزية",
"search_popout.tips.user": "مستخدِم",
"search_results.total": "{count, number} {count, plural, one {result} و {results}}",
"standalone.public_title": "نظرة على ...",

@ -36,7 +36,7 @@
"column.favourites": "Favorits",
"column.follow_requests": "Peticions per seguir-te",
"column.home": "Inici",
"column.lists": "Lists",
"column.lists": "Llistes",
"column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions",
"column.pins": "Toot fixat",
@ -64,7 +64,7 @@
"confirmations.delete.confirm": "Esborrar",
"confirmations.delete.message": "Estàs segur que vols esborrar aquest estat?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.message": "Estàs segur que vols esborrar permanenment aquesta llista?",
"confirmations.domain_block.confirm": "Amagar tot el domini",
"confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
"confirmations.mute.confirm": "Silenciar",
@ -127,14 +127,14 @@
"lightbox.close": "Tancar",
"lightbox.next": "Següent",
"lightbox.previous": "Anterior",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.account.add": "Afegir a la llista",
"lists.account.remove": "Treure de la llista",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"lists.edit": "Editar llista",
"lists.new.create": "Afegir llista",
"lists.new.title_placeholder": "Nou títol de llista",
"lists.search": "Cercar entre les persones que segueixes",
"lists.subheading": "Les teves llistes",
"loading_indicator.label": "Carregant...",
"media_gallery.toggle_visible": "Alternar visibilitat",
"missing_indicator.label": "No trobat",
@ -146,7 +146,7 @@
"navigation_bar.follow_requests": "Sol·licituds de seguiment",
"navigation_bar.info": "Informació addicional",
"navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
"navigation_bar.lists": "Lists",
"navigation_bar.lists": "Llistes",
"navigation_bar.logout": "Tancar sessió",
"navigation_bar.mutes": "Usuaris silenciats",
"navigation_bar.pins": "Toots fixats",

@ -93,7 +93,7 @@
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.list": "There is nothing in this list yet.",
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",

@ -91,7 +91,7 @@
"empty_column.hashtag": "Il ny a encore aucun contenu associé à ce hashtag",
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à dautres utilisateur⋅ice⋅s.",
"empty_column.home.public_timeline": "le fil public",
"empty_column.list": "Il n'y a rien dans cette liste pour l'instant.",
"empty_column.list": "Il n'y a rien dans cette liste pour l'instant. Dès que des personnes de cette listes publierons de nouveaux statuts ils apparaîtront ici.",
"empty_column.notifications": "Vous navez pas encore de notification. Interagissez avec dautres utilisateur⋅ice⋅s pour débuter la conversation.",
"empty_column.public": "Il ny a rien ici! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s dautres instances pour remplir le fil public.",
"follow_request.authorize": "Accepter",

@ -36,7 +36,7 @@
"column.favourites": "Favoritas",
"column.follow_requests": "Peticións de seguimento",
"column.home": "Inicio",
"column.lists": "Lists",
"column.lists": "Listas",
"column.mutes": "Usuarias acaladas",
"column.notifications": "Notificacións",
"column.pins": "Mensaxes fixadas",
@ -64,7 +64,7 @@
"confirmations.delete.confirm": "Borrar",
"confirmations.delete.message": "Está segura de que quere eliminar este estado?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.message": "Estás seguro de que queres eliminar permanentemente esta lista?",
"confirmations.domain_block.confirm": "Agochar un dominio completo",
"confirmations.domain_block.message": "Realmente está segura de que quere bloquear por completo o dominio {domain}? Normalmente é suficiente, e preferible, bloquear de xeito selectivo varios elementos.",
"confirmations.mute.confirm": "Acalar",
@ -127,14 +127,14 @@
"lightbox.close": "Fechar",
"lightbox.next": "Seguinte",
"lightbox.previous": "Anterior",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.account.add": "Engadir á lista",
"lists.account.remove": "Eliminar da lista",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"lists.edit": "Editar lista",
"lists.new.create": "Engadir lista",
"lists.new.title_placeholder": "Novo título da lista",
"lists.search": "Procurar entre a xente que segues",
"lists.subheading": "As túas listas",
"loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Dar visibilidade",
"missing_indicator.label": "Non atopado",
@ -146,7 +146,7 @@
"navigation_bar.follow_requests": "Peticións de seguimento",
"navigation_bar.info": "Sobre esta instancia",
"navigation_bar.keyboard_shortcuts": "Atallos do teclado",
"navigation_bar.lists": "Lists",
"navigation_bar.lists": "Listas",
"navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Usuarias acaladas",
"navigation_bar.pins": "Mensaxes fixadas",

@ -95,7 +95,7 @@
"empty_column.home.public_timeline": "連合タイムライン",
"empty_column.list": "このリストにはまだなにもありません。",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう",
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.appsshort": "アプリ",
@ -162,12 +162,12 @@
"notifications.clear": "通知を消去",
"notifications.clear_confirmation": "本当に通知を消去しますか?",
"notifications.column_settings.alert": "デスクトップ通知",
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.favourite": "お気に入り:",
"notifications.column_settings.follow": "新しいフォロワー:",
"notifications.column_settings.mention": "返信:",
"notifications.column_settings.push": "プッシュ通知",
"notifications.column_settings.push_meta": "このデバイス",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.reblog": "ブースト:",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.sound": "通知音を再生",
"onboarding.done": "完了",
@ -176,7 +176,7 @@
"onboarding.page_four.home": "「ホーム」タイムラインではあなたがフォローしている人の投稿を表示します。",
"onboarding.page_four.notifications": "「通知」ではあなたへの他の人からの関わりを表示します。",
"onboarding.page_one.federation": "Mastodonは誰でも参加できるSNSです。",
"onboarding.page_one.handle": "あなたは数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です",
"onboarding.page_one.handle": "あなたは数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です",
"onboarding.page_one.welcome": "Mastodonへようこそ",
"onboarding.page_six.admin": "あなたのインスタンスの管理者は{admin}です。",
"onboarding.page_six.almost_done": "以上です。",
@ -184,7 +184,7 @@
"onboarding.page_six.apps_available": "iOS、Androidあるいは他のプラットフォームで使える{apps}があります。",
"onboarding.page_six.github": "MastodonはOSSです。バグ報告や機能要望あるいは貢献を{github}から行なえます。",
"onboarding.page_six.guidelines": "コミュニティガイドライン",
"onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください",
"onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください",
"onboarding.page_six.various_app": "様々なモバイルアプリ",
"onboarding.page_three.profile": "「プロフィールを編集」から、あなたの自己紹介や表示名を変更できます。またそこでは他の設定ができます。",
"onboarding.page_three.search": "検索バーで、{illustration}や{introductions}のように特定のハッシュタグの投稿を見たり、ユーザーを探したりできます。",
@ -215,7 +215,7 @@
"search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
"search_popout.tips.user": "ユーザー",
"search_results.total": "{count, number}件の結果",
"standalone.public_title": "今こんな話をしています",
"standalone.public_title": "今こんな話をしています...",
"status.cannot_reblog": "この投稿はブーストできません",
"status.delete": "削除",
"status.embed": "埋め込み",

@ -36,7 +36,7 @@
"column.favourites": "Favorieten",
"column.follow_requests": "Volgverzoeken",
"column.home": "Start",
"column.lists": "Lists",
"column.lists": "Lijsten",
"column.mutes": "Genegeerde gebruikers",
"column.notifications": "Meldingen",
"column.pins": "Vastgezette toots",
@ -64,7 +64,7 @@
"confirmations.delete.confirm": "Verwijderen",
"confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.message": "Weet je zeker dat je deze lijst permanent wilt verwijderen?",
"confirmations.domain_block.confirm": "Negeer alles van deze server",
"confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
"confirmations.mute.confirm": "Negeren",
@ -127,14 +127,14 @@
"lightbox.close": "Sluiten",
"lightbox.next": "Volgende",
"lightbox.previous": "Vorige",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.account.add": "Aan lijst toevoegen",
"lists.account.remove": "Uit lijst verwijderen",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"lists.edit": "Lijst bewerken",
"lists.new.create": "Lijst toevoegen",
"lists.new.title_placeholder": "Naam nieuwe lijst",
"lists.search": "Zoek naar mensen die je volgt",
"lists.subheading": "Jouw lijsten",
"loading_indicator.label": "Laden…",
"media_gallery.toggle_visible": "Media wel/niet tonen",
"missing_indicator.label": "Niet gevonden",
@ -146,7 +146,7 @@
"navigation_bar.follow_requests": "Volgverzoeken",
"navigation_bar.info": "Uitgebreide informatie",
"navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen",
"navigation_bar.lists": "Lists",
"navigation_bar.lists": "Lijsten",
"navigation_bar.logout": "Afmelden",
"navigation_bar.mutes": "Genegeerde gebruikers",
"navigation_bar.pins": "Vastgezette toots",

@ -91,7 +91,7 @@
"empty_column.hashtag": "I a pas encara de contengut ligat a aquesta etiqueta.",
"empty_column.home": "Vòstre flux dacuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.",
"empty_column.home.public_timeline": "lo flux public",
"empty_column.list": "I a pas res dins la lista pel moment.",
"empty_column.list": "I a pas res dins la lista pel moment. Quand de membres daquesta lista publiquen de novèls estatuts los veiretz aquí.",
"empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.",
"empty_column.public": "I a pas res aquí! Escrivètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public",
"follow_request.authorize": "Autorizar",

@ -36,7 +36,7 @@
"column.favourites": "Favoritos",
"column.follow_requests": "Seguidores pendentes",
"column.home": "Página inicial",
"column.lists": "Lists",
"column.lists": "Listas",
"column.mutes": "Usuários silenciados",
"column.notifications": "Notificações",
"column.pins": "Postagens fixadas",
@ -64,7 +64,7 @@
"confirmations.delete.confirm": "Excluir",
"confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?",
"confirmations.domain_block.confirm": "Esconder o domínio inteiro",
"confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
"confirmations.mute.confirm": "Silenciar",
@ -110,7 +110,7 @@
"keyboard_shortcuts.back": "para navegar de volta",
"keyboard_shortcuts.boost": "para compartilhar",
"keyboard_shortcuts.column": "Focar um status em uma das colunas",
"keyboard_shortcuts.compose": "to focus the compose textarea",
"keyboard_shortcuts.compose": "para focar a área de redação",
"keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.down": "para mover para baixo na lista",
"keyboard_shortcuts.enter": "to open status",
@ -127,14 +127,14 @@
"lightbox.close": "Fechar",
"lightbox.next": "Próximo",
"lightbox.previous": "Anterior",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.account.add": "Adicionar a listas",
"lists.account.remove": "Remover da lista",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"lists.edit": "Editar lista",
"lists.new.create": "Adicionar lista",
"lists.new.title_placeholder": "Novo título da lista",
"lists.search": "Procurar entre as pessoas que você segue",
"lists.subheading": "Suas listas",
"loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",
@ -146,7 +146,7 @@
"navigation_bar.follow_requests": "Seguidores pendentes",
"navigation_bar.info": "Mais informações",
"navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
"navigation_bar.lists": "Lists",
"navigation_bar.lists": "Listas",
"navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Usuários silenciados",
"navigation_bar.pins": "Postagens fixadas",
@ -177,7 +177,7 @@
"onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
"onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
"onboarding.page_six.almost_done": "Quase acabando...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.appetoot": "Bom Apetoot!",
"onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.",
"onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.",
"onboarding.page_six.guidelines": "diretrizes da comunidade",

@ -1,7 +1,7 @@
{
"account.block": "Bloquear @{name}",
"account.block_domain": "Esconder tudo do domínio {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.",
"account.edit_profile": "Editar perfil",
"account.follow": "Seguir",
"account.followers": "Seguidores",
@ -19,7 +19,7 @@
"account.share": "Partilhar o perfil @{name}",
"account.show_reblogs": "Mostrar partilhas de @{name}",
"account.unblock": "Não bloquear @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "Mostrar {domain}",
"account.unfollow": "Deixar de seguir",
"account.unmute": "Não silenciar @{name}",
"account.unmute_notifications": "Deixar de silenciar @{name}",
@ -36,7 +36,7 @@
"column.favourites": "Favoritos",
"column.follow_requests": "Seguidores Pendentes",
"column.home": "Home",
"column.lists": "Lists",
"column.lists": "Listas",
"column.mutes": "Utilizadores silenciados",
"column.notifications": "Notificações",
"column.pins": "Pinned toot",
@ -64,7 +64,7 @@
"confirmations.delete.confirm": "Eliminar",
"confirmations.delete.message": "De certeza que queres eliminar esta publicação?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.message": "Tens a certeza de que desejas apagar permanentemente esta lista?",
"confirmations.domain_block.confirm": "Esconder tudo deste domínio",
"confirmations.domain_block.message": "De certeza que queres bloquear por completo o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado.",
"confirmations.mute.confirm": "Silenciar",
@ -88,12 +88,12 @@
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viagens & Lugares",
"empty_column.community": "Ainda não existe conteúdo local para mostrar!",
"empty_column.hashtag": "Não foram encontradas publicações com essa hashtag",
"empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.",
"empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
"empty_column.home.public_timeline": "global",
"empty_column.list": "Ainda não existem publicações nesta lista.",
"empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
"empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
"empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Rejeitar",
"getting_started.appsshort": "Aplicações",
@ -116,7 +116,7 @@
"keyboard_shortcuts.enter": "para expandir uma publicação",
"keyboard_shortcuts.favourite": "para adicionar aos favoritos",
"keyboard_shortcuts.heading": "Atalhos do teclado",
"keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.hotkey": "Atalho",
"keyboard_shortcuts.legend": "para mostrar esta legenda",
"keyboard_shortcuts.mention": "para mencionar o autor",
"keyboard_shortcuts.reply": "para responder",
@ -127,14 +127,14 @@
"lightbox.close": "Fechar",
"lightbox.next": "Próximo",
"lightbox.previous": "Anterior",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.account.add": "Adicionar à lista",
"lists.account.remove": "Remover da lista",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"lists.edit": "Editar lista",
"lists.new.create": "Adicionar lista",
"lists.new.title_placeholder": "Novo título da lista",
"lists.search": "Pesquisa entre as pessoas que segues",
"lists.subheading": "As tuas listas",
"loading_indicator.label": "A carregar...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",
@ -146,7 +146,7 @@
"navigation_bar.follow_requests": "Seguidores pendentes",
"navigation_bar.info": "Mais informações",
"navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
"navigation_bar.lists": "Lists",
"navigation_bar.lists": "Listas",
"navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Utilizadores silenciados",
"navigation_bar.pins": "Posts fixos",
@ -209,13 +209,13 @@
"search_popout.search_format": "Formato avançado de pesquisa",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags",
"search_popout.tips.user": "utilizador",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "Espreitar lá dentro...",
"status.cannot_reblog": "Este post não pode ser partilhado",
"status.delete": "Eliminar",
"status.embed": "Embed",
"status.embed": "Incorporar",
"status.favourite": "Adicionar aos favoritos",
"status.load_more": "Carregar mais",
"status.media_hidden": "Media escondida",

@ -91,7 +91,7 @@
"empty_column.hashtag": "这个话题标签下暂时没有内容。",
"empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
"empty_column.home.public_timeline": "公共时间轴",
"empty_column.list": "这个列表中暂时没有内容。",
"empty_column.list": "这个列表中暂时没有内容。列表中用户所发送的的新嘟文将会在这里显示。",
"empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。",
"empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!",
"follow_request.authorize": "同意",

@ -62,7 +62,13 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
const params = [ `stream=${stream}` ];
if (accessToken !== null) {
params.push(`access_token=${accessToken}`);
}
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`);
ws.onopen = connected;
ws.onmessage = e => received(JSON.parse(e.data));

@ -265,198 +265,286 @@
.compose-form {
padding: 10px;
}
.compose-form__warning {
color: darken($ui-secondary-color, 65%);
margin-bottom: 15px;
background: $ui-primary-color;
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
padding: 8px 10px;
border-radius: 4px;
font-size: 13px;
font-weight: 400;
strong {
.compose-form__warning {
color: darken($ui-secondary-color, 65%);
font-weight: 500;
margin-bottom: 15px;
background: $ui-primary-color;
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
padding: 8px 10px;
border-radius: 4px;
font-size: 13px;
font-weight: 400;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
strong {
color: darken($ui-secondary-color, 65%);
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
color: darken($ui-primary-color, 33%);
font-weight: 500;
text-decoration: underline;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
}
}
a {
color: darken($ui-primary-color, 33%);
font-weight: 500;
text-decoration: underline;
.compose-form__autosuggest-wrapper {
position: relative;
.emoji-picker-dropdown {
position: absolute;
right: 5px;
top: 5px;
}
}
.autosuggest-textarea,
.spoiler-input {
position: relative;
}
.autosuggest-textarea__textarea,
.spoiler-input__input {
display: block;
box-sizing: border-box;
width: 100%;
margin: 0;
color: $ui-base-color;
background: $simple-background-color;
padding: 10px;
font-family: inherit;
font-size: 14px;
resize: vertical;
border: 0;
outline: 0;
&:hover,
&:active,
&:focus {
text-decoration: none;
outline: 0;
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
}
.compose-form__modifiers {
color: $ui-base-color;
font-family: inherit;
font-size: 14px;
background: $simple-background-color;
border-radius: 0 0 4px;
}
.spoiler-input__input {
border-radius: 4px;
}
.compose-form__buttons-wrapper {
display: flex;
justify-content: space-between;
}
.autosuggest-textarea__textarea {
min-height: 100px;
border-radius: 4px 4px 0 0;
padding-bottom: 0;
padding-right: 10px + 22px;
resize: none;
.compose-form__buttons {
padding: 10px;
background: darken($simple-background-color, 8%);
box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05);
border-radius: 0 0 4px 4px;
display: flex;
@media screen and (max-width: 600px) {
height: 100px !important; // prevent auto-resize textarea
resize: vertical;
}
}
.icon-button {
box-sizing: content-box;
padding: 0 3px;
.autosuggest-textarea__suggestions {
box-sizing: border-box;
display: none;
position: absolute;
top: 100%;
width: 100%;
z-index: 99;
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
background: $ui-secondary-color;
border-radius: 0 0 4px 4px;
color: $ui-base-color;
font-size: 14px;
padding: 6px;
&.autosuggest-textarea__suggestions--visible {
display: block;
}
}
}
.compose-form__upload-button-icon {
line-height: 27px;
}
.autosuggest-textarea__suggestions__item {
padding: 10px;
cursor: pointer;
border-radius: 4px;
.compose-form__sensitive-button {
display: none;
&:hover,
&:focus,
&:active,
&.selected {
background: darken($ui-secondary-color, 10%);
}
}
.autosuggest-account,
.autosuggest-emoji {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
line-height: 18px;
font-size: 14px;
}
&.compose-form__sensitive-button--visible {
.autosuggest-account-icon,
.autosuggest-emoji img {
display: block;
margin-right: 8px;
width: 16px;
height: 16px;
}
.compose-form__sensitive-button__icon {
line-height: 27px;
.autosuggest-account .display-name__account {
color: lighten($ui-base-color, 36%);
}
}
.compose-form__upload-wrapper {
overflow: hidden;
}
.compose-form__modifiers {
color: $ui-base-color;
font-family: inherit;
font-size: 14px;
background: $simple-background-color;
.compose-form__uploads-wrapper {
display: flex;
flex-direction: row;
padding: 5px;
flex-wrap: wrap;
}
.compose-form__upload-wrapper {
overflow: hidden;
}
.compose-form__upload {
flex: 1 1 0;
min-width: 40%;
margin: 5px;
.compose-form__uploads-wrapper {
display: flex;
flex-direction: row;
padding: 5px;
flex-wrap: wrap;
}
&-description {
position: absolute;
z-index: 2;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
padding: 10px;
opacity: 0;
transition: opacity .1s ease;
.compose-form__upload {
flex: 1 1 0;
min-width: 40%;
margin: 5px;
input {
background: transparent;
color: $ui-secondary-color;
border: 0;
padding: 0;
margin: 0;
width: 100%;
font-family: inherit;
font-size: 14px;
font-weight: 500;
&-description {
position: absolute;
z-index: 2;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
padding: 10px;
opacity: 0;
transition: opacity .1s ease;
input {
background: transparent;
color: $ui-secondary-color;
border: 0;
padding: 0;
margin: 0;
width: 100%;
font-family: inherit;
font-size: 14px;
font-weight: 500;
&:focus {
color: $white;
}
&:focus {
color: $white;
&::placeholder {
opacity: 0.54;
color: $ui-secondary-color;
}
}
&.active {
opacity: 1;
}
}
&::placeholder {
opacity: 0.54;
color: $ui-secondary-color;
.icon-button {
mix-blend-mode: difference;
}
}
&.active {
opacity: 1;
.compose-form__upload-thumbnail {
border-radius: 4px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
height: 100px;
width: 100%;
}
}
.icon-button {
mix-blend-mode: difference;
}
}
.compose-form__buttons-wrapper {
padding: 10px;
background: darken($simple-background-color, 8%);
border-radius: 0 0 4px 4px;
display: flex;
justify-content: space-between;
.compose-form__upload-thumbnail {
border-radius: 4px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
height: 100px;
width: 100%;
}
.compose-form__buttons {
display: flex;
.compose-form__label {
display: block;
line-height: 24px;
vertical-align: middle;
.compose-form__upload-button-icon {
line-height: 27px;
}
&.with-border {
border-top: 1px solid $ui-base-color;
padding-top: 10px;
}
.compose-form__sensitive-button {
display: none;
.compose-form__label__text {
display: inline-block;
vertical-align: middle;
margin-bottom: 14px;
margin-left: 8px;
color: $ui-primary-color;
}
}
&.compose-form__sensitive-button--visible {
display: block;
}
.compose-form__textarea,
.follow-form__input {
background: $simple-background-color;
.compose-form__sensitive-button__icon {
line-height: 27px;
}
}
}
&:disabled {
background: $ui-secondary-color;
}
}
.icon-button {
box-sizing: content-box;
padding: 0 3px;
}
.compose-form__autosuggest-wrapper {
position: relative;
.character-counter__wrapper {
align-self: center;
margin-right: 4px;
.emoji-picker-dropdown {
position: absolute;
right: 5px;
top: 5px;
.character-counter {
cursor: default;
font-family: 'mastodon-font-sans-serif', sans-serif;
font-size: 14px;
font-weight: 600;
color: lighten($ui-base-color, 12%);
&.character-counter--over {
color: $warning-red;
}
}
}
}
}
.compose-form__publish {
display: flex;
min-width: 0;
}
.compose-form__publish {
display: flex;
justify-content: flex-end;
min-width: 0;
.compose-form__publish-button-wrapper {
overflow: hidden;
padding-top: 10px;
.compose-form__publish-button-wrapper {
overflow: hidden;
padding-top: 10px;
}
}
}
.emojione {
@ -1973,121 +2061,6 @@
cursor: default;
}
.autosuggest-textarea,
.spoiler-input {
position: relative;
}
.autosuggest-textarea__textarea,
.spoiler-input__input {
display: block;
box-sizing: border-box;
width: 100%;
margin: 0;
color: $ui-base-color;
background: $simple-background-color;
padding: 10px;
font-family: inherit;
font-size: 14px;
resize: vertical;
border: 0;
outline: 0;
&:focus {
outline: 0;
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.spoiler-input__input {
border-radius: 4px;
}
.autosuggest-textarea__textarea {
min-height: 100px;
border-radius: 4px 4px 0 0;
padding-bottom: 0;
padding-right: 10px + 22px;
resize: none;
@media screen and (max-width: 600px) {
height: 100px !important; // prevent auto-resize textarea
resize: vertical;
}
}
.autosuggest-textarea__suggestions {
box-sizing: border-box;
display: none;
position: absolute;
top: 100%;
width: 100%;
z-index: 99;
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
background: $ui-secondary-color;
border-radius: 0 0 4px 4px;
color: $ui-base-color;
font-size: 14px;
padding: 6px;
&.autosuggest-textarea__suggestions--visible {
display: block;
}
}
.autosuggest-textarea__suggestions__item {
padding: 10px;
cursor: pointer;
border-radius: 4px;
&:hover,
&:focus,
&:active,
&.selected {
background: darken($ui-secondary-color, 10%);
}
}
.autosuggest-account,
.autosuggest-emoji {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
line-height: 18px;
font-size: 14px;
}
.autosuggest-account-icon,
.autosuggest-emoji img {
display: block;
margin-right: 8px;
width: 16px;
height: 16px;
}
.autosuggest-account .display-name__account {
color: lighten($ui-base-color, 36%);
}
.character-counter__wrapper {
line-height: 36px;
margin: 0 16px 0 8px;
padding-top: 10px;
}
.character-counter {
cursor: default;
font-size: 16px;
}
.character-counter--over {
color: $warning-red;
}
.getting-started__wrapper {
position: relative;
overflow-y: auto;

@ -7,9 +7,9 @@ body.rtl {
margin-left: 5px;
}
.character-counter__wrapper {
margin-right: 8px;
margin-left: 16px;
.compose-form .compose-form__buttons-wrapper .character-counter__wrapper {
margin-right: 0;
margin-left: 4px;
}
.navigation-bar__profile {
@ -30,6 +30,22 @@ body.rtl {
.column-header__buttons {
left: 0;
right: auto;
margin-left: -15px;
margin-right: 0;
}
.column-inline-form .icon-button {
margin-left: 0;
margin-right: 5px;
}
.column-header__links .text-btn {
margin-left: 10px;
margin-right: 0;
}
.account__avatar-wrapper {
float: right;
}
.column-header__back-button {
@ -41,10 +57,6 @@ body.rtl {
float: left;
}
.compose-form__modifiers {
border-radius: 0 0 0 4px;
}
.setting-toggle {
margin-left: 0;
margin-right: 8px;

@ -2,13 +2,26 @@
class ProviderDiscovery < OEmbed::ProviderDiscovery
class << self
def get(url, **options)
provider = discover_provider(url, options)
options.delete(:html)
provider.get(url, options)
end
def discover_provider(url, **options)
res = Request.new(:get, url).perform
format = options[:format]
raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
if options[:html]
html = Nokogiri::HTML(options[:html])
else
res = Request.new(:get, url).perform
raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
html = Nokogiri::HTML(res.to_s)
html = Nokogiri::HTML(res.to_s)
end
if format.nil? || format == :json
provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value

@ -45,6 +45,8 @@ class AccountFilter
else
Account.default_scoped
end
when 'staff'
accounts_with_users.merge User.staff
else
raise "Unknown filter: #{key}"
end

@ -27,6 +27,8 @@ class CustomEmojiFilter
CustomEmoji.remote
when 'by_domain'
CustomEmoji.where(domain: value)
when 'shortcode'
CustomEmoji.where(shortcode: value)
else
raise "Unknown filter: #{key}"
end

@ -40,6 +40,12 @@ class FetchLinkCardService < BaseService
return if res.code != 405 && (res.code != 200 || res.mime_type != 'text/html')
@response = Request.new(:get, @url).perform
return if @response.code != 200 || @response.mime_type != 'text/html'
@html = @response.to_s
attempt_oembed || attempt_opengraph
end
@ -70,30 +76,32 @@ class FetchLinkCardService < BaseService
end
def attempt_oembed
response = OEmbed::Providers.get(@url)
embed = OEmbed::Providers.get(@url, html: @html)
return false unless response.respond_to?(:type)
return false unless embed.respond_to?(:type)
@card.type = response.type
@card.title = response.respond_to?(:title) ? response.title : ''
@card.author_name = response.respond_to?(:author_name) ? response.author_name : ''
@card.author_url = response.respond_to?(:author_url) ? response.author_url : ''
@card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : ''
@card.provider_url = response.respond_to?(:provider_url) ? response.provider_url : ''
@card.type = embed.type
@card.title = embed.respond_to?(:title) ? embed.title : ''
@card.author_name = embed.respond_to?(:author_name) ? embed.author_name : ''
@card.author_url = embed.respond_to?(:author_url) ? embed.author_url : ''
@card.provider_name = embed.respond_to?(:provider_name) ? embed.provider_name : ''
@card.provider_url = embed.respond_to?(:provider_url) ? embed.provider_url : ''
@card.width = 0
@card.height = 0
case @card.type
when 'link'
@card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
@card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url)
when 'photo'
@card.embed_url = response.url
@card.width = response.width.presence || 0
@card.height = response.height.presence || 0
return false unless embed.respond_to?(:url)
@card.embed_url = embed.url
@card.image = URI.parse(embed.url)
@card.width = embed.width.presence || 0
@card.height = embed.height.presence || 0
when 'video'
@card.width = response.width.presence || 0
@card.height = response.height.presence || 0
@card.html = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED)
@card.width = embed.width.presence || 0
@card.height = embed.height.presence || 0
@card.html = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED)
when 'rich'
# Most providers rely on <script> tags, which is a no-no
return false
@ -105,17 +113,11 @@ class FetchLinkCardService < BaseService
end
def attempt_opengraph
response = Request.new(:get, @url).perform
return if response.code != 200 || response.mime_type != 'text/html'
html = response.to_s
detector = CharlockHolmes::EncodingDetector.new
detector.strip_tags = true
guess = detector.detect(html, response.charset)
page = Nokogiri::HTML(html, nil, guess&.fetch(:encoding, nil))
guess = detector.detect(@html, @response.charset)
page = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
if meta_property(page, 'twitter:player')
@card.type = :video
@ -132,16 +134,16 @@ class FetchLinkCardService < BaseService
@card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image')
end
@card.title = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
@card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
@card.title = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
@card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
return if @card.title.blank? && @card.html.blank?
@card.save_with_optional_image!
end
def meta_property(html, property)
html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
def meta_property(page, property)
page.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || page.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
end
def lock_options

@ -40,6 +40,6 @@ class FetchRemoteStatusService < BaseService
end
def confirmed_domain?(domain, account)
account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url || account.uri).normalized_host).zero?
account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero?
end
end

@ -22,7 +22,7 @@ class FollowService < BaseService
elsif source_account.requested?(target_account)
# This isn't managed by a method in AccountInteractions, so we modify it
# ourselves if necessary.
req = follow_requests.find_by(target_account: other_account)
req = source_account.follow_requests.find_by(target_account: target_account)
req.update!(show_reblogs: reblogs)
return
end

@ -4,22 +4,11 @@
%td.domain
- unless account.local?
= link_to account.domain, admin_accounts_path(by_domain: account.domain)
%td.protocol
- unless account.local?
= account.protocol.humanize
%td.confirmed
- if account.local?
- if account.user_confirmed?
%i.fa.fa-check
- else
%i.fa.fa-times
%td.subscribed
%td
- if account.local?
= t('admin.accounts.location.local')
- elsif account.subscribed?
%i.fa.fa-check
= t("admin.accounts.roles.#{account.user&.role}")
- else
%i.fa.fa-times
= account.protocol.humanize
%td
= table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
= table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account)

@ -30,6 +30,11 @@
= filter_link_to t('admin.accounts.moderation.suspended'), {suspended: nil}, {suspended: '1'}
- else
= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1'
.filter-subset
%strong= t('admin.accounts.role')
%ul
%li= filter_link_to t('admin.accounts.moderation.all'), staff: nil
%li= filter_link_to t('admin.accounts.roles.staff'), staff: '1'
.filter-subset
%strong= t('admin.accounts.order.title')
%ul
@ -56,9 +61,7 @@
%tr
%th= t('admin.accounts.username')
%th= t('admin.accounts.domain')
%th= t('admin.accounts.protocol')
%th= t('admin.accounts.confirmed')
%th= fa_icon 'paper-plane-o'
%th
%th
%tbody
= render @accounts

@ -104,7 +104,7 @@
- else
= link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account)
- unless @account.local?
- if !@account.local? && @account.hub_url.present?
%hr
%h3 OStatus
@ -132,6 +132,7 @@
- if @account.subscribed?
= link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account)
- if !@account.local? && @account.inbox_url.present?
%hr
%h3 ActivityPub

@ -7,7 +7,7 @@
- if custom_emoji.local?
= t('admin.accounts.location.local')
- else
= custom_emoji.domain
= link_to custom_emoji.domain, admin_custom_emojis_path(by_domain: custom_emoji.domain)
%td
- if custom_emoji.local?
- if custom_emoji.visible_in_picker

@ -17,6 +17,20 @@
- else
= filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil
= form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do
.fields-group
- Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
- if params[key].present?
= hidden_field_tag key, params[key]
- %i(shortcode by_domain).each do |key|
.input.string.optional
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.custom_emojis.#{key}")
.actions
%button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
.table-wrapper
%table.table
%thead

@ -17,9 +17,12 @@ Rails.application.configure do
config.x.alternate_domains = alternate_domains.split(/\s*,\s*/)
config.action_mailer.default_url_options = { host: web_host, protocol: https ? 'https://' : 'http://', trailing_slash: false }
config.x.streaming_api_base_url = 'ws://localhost:4000'
if Rails.env.production?
config.x.streaming_api_base_url = ENV.fetch('STREAMING_API_BASE_URL') { "ws#{https ? 's' : ''}://#{web_host}" }
config.x.streaming_api_base_url = ENV.fetch('STREAMING_API_BASE_URL') do
if Rails.env.production?
"ws#{https ? 's' : ''}://#{web_host}"
else
"ws://#{ENV['REMOTE_DEV'] == 'true' ? host.split(':').first : 'localhost'}:4000"
end
end
end

@ -57,20 +57,65 @@ ar:
order:
title: الترتيب
profile_url: رابط الملف الشخصي
role: التصريحات
roles:
admin: مدير
user: مستخدِم
search: البحث
statuses: المنشورات
title: الحسابات
username: إسم المستخدم
web: الويب
custom_emojis:
copy: نسخ
delete: حذف
emoji: إيموجي
enable: تفعيل
upload: رفع
domain_blocks:
domain: النطاق
show:
undo: إلغاء
undo: إلغاء
email_domain_blocks:
delete: حذف
domain: النطاق
new:
create: إضافة نطاق
instances:
domain_name: النطاق
search: البحث
reports:
are_you_sure: هل أنت متأكد ؟
comment:
label: تعليق
delete: حذف
report_contents: المحتويات
reported_by: أبلغ عنه من طرف
status: الحالة
title: التقارير
view: عرض
settings:
contact_information:
email: البريد الإلكتروني المهني
registrations:
deletion:
desc_html: السماح لأي مستخدم إغلاق حسابه
open:
title: فتح التسجيل
site_terms:
title: شروط الخدمة المخصصة
site_title: إسم مثيل الخادم
title: إعدادات الموقع
statuses:
back_to_account: العودة إلى صفحة الحساب
batch:
delete: حذف
media:
title: الوسائط
title: الإدارة
application_mailer:
salutation: "%{name},"
settings: 'تغيير تفضيلات البريد الإلكتروني : %{link}'
signature: إشعارات ماستدون من %{instance}
view: 'View:'
@ -83,6 +128,7 @@ ar:
forgot_password: نسيت كلمة المرور ؟
login: تسجيل الدخول
logout: خروج
migrate_account: الإنتقال إلى حساب آخر
register: إنشاء حساب
resend_confirmation: إعادة إرسال تعليمات التأكيد
reset_password: إعادة تعيين كلمة المرور
@ -106,15 +152,20 @@ ar:
x_months: "%{count} شه"
x_seconds: "%{count}ث"
deletes:
bad_password_msg: محاولة جيدة يا هاكرز ! كلمة السر خاطئة
proceed: حذف حساب
success_msg: تم حذف حسابك بنجاح
exports:
blocks: قمت بحظر
csv: CSV
follows: أنت تتبع
storage: ذاكرة التخزين
followers:
domain: النطاق
followers_count: عدد المتابِعين
generic:
changes_saved_msg: تم حفظ التعديلات بنجاح !
powered_by: powered by %{link}
powered_by: مدعوم بـ %{link}
save_changes: حفظ التغييرات
validation_errors:
one: Something isn't quite right yet! Please review the error below
@ -128,14 +179,19 @@ ar:
upload: تحميل
landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse.."
landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
lists:
errors:
limit: لقد بلغت الحد الأقصى للقوائم
media_attachments:
validations:
images_and_video: ليس بالإمكان إرفاق فيديو في منشور يحتوي مسبقا على صور
too_many: لا يمكن إرفاق أكثر من 4 ملفات
migrations:
acct: username@domain للحساب الجديد
notification_mailer:
digest:
body: 'Here is a brief summary of what you missed on %{instance} since your last visit on %{since}:'
mention: "%{name} mentioned you in:"
mention: "%{name} أشار إليك في :"
new_followers_summary:
one: لقد حصلت على متابع جديد !
other: لقد تحصلت على %{count} متتبعين جدد ! رائع !
@ -143,11 +199,11 @@ ar:
one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418"
other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418"
favourite:
body: أُعجب %{name} بمنشورك
body: 'أُعجب %{name} بمنشورك :'
subject: "%{name} favourited your status"
follow:
body: "%{name} من متتبعيك الآن !"
subject: "%{name} من متتبعيك الآن !"
subject: "%{name} من متتبعيك الآن"
follow_request:
body: "%{name} has requested to follow you"
subject: 'Pending follower: %{name}'
@ -171,16 +227,21 @@ ar:
pagination:
next: التالي
prev: السابق
preferences:
languages: اللغات
other: إعدادات أخرى
publishing: النشر
remote_follow:
acct: Enter your username@domain you want to follow from
acct: قم بإدخال عنوان حسابك username@domain الذي من خلاله تود المتابعة
missing_resource: Could not find the required redirect URL for your account
proceed: Proceed to follow
proceed: أكمل المتابعة
prompt: 'إنك بصدد متابعة :'
settings:
authorized_apps: التطبيقات المرخص لها
back: عودة إلى ماستدون
edit_profile: تعديل الملف الشخصي
export: تصدير البيانات
followers: المتابِعون المُرَخّصون
import: إستيراد
preferences: التفضيلات
settings: الإعدادات

@ -286,7 +286,7 @@ ca:
desc_html: Mostra una insígnia de personal en una pàgina d'usuari
title: Mostra insígnia de personal
site_description:
desc_html: Paràgraf introductori a la pàgina principal i en etiquetes meta.<br>Pots utilitzar etiquetes HTML, en particular <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
desc_html: Paràgraf introductori a la pàgina principal i en etiquetes meta. Pots utilitzar etiquetes HTML, en particular <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
title: Descripció del lloc
site_description_extended:
desc_html: Un bon lloc per al codi de conducta, regles, directrius i altres coses que distingeixen la vostra instància. Pots utilitzar etiquetes HTML
@ -410,7 +410,7 @@ ca:
storage: Emmagatzematge
followers:
domain: Domini
explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. <strong> Les publicacions privades es lliuren a totes les instàncies on tens seguidors </ strong>. És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquestes instàncies.
explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. <strong> Les publicacions privades es lliuren a totes les instàncies on tens seguidors </strong>. És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquestes instàncies.
followers_count: Nombre de seguidors
lock_link: Bloca el teu compte
purge: Elimina dels seguidors

@ -8,8 +8,11 @@ ar:
inactive: لم يتم تنشيط حسابك بعد.
last_attempt: بإمكانك إعادة المحاولة مرة واحدة قبل أن يتم قفل حسابك.
locked: إن حسابك مقفل.
unauthenticated: يجب عليك تسجيل الدخول أو إنشاء حساب قبل المواصلة.
unconfirmed: يجب عليك تأكيد عنوان بريدك الإلكتروني قبل المواصلة.
mailer:
confirmation_instructions:
subject: 'ماستدون : تعليمات التأكيد لمثيل الخادوم %{instance}'
password_change:
subject: 'ماستدون : تم تغيير كلمة المرور'
reset_password_instructions:

@ -35,7 +35,7 @@ ja:
updated_not_active: パスワードは正常に更新されました。
registrations:
destroyed: アカウントの作成はキャンセルされました。またのご利用をお待ちしています。
signed_up: アカウントの作成が完了しました。Mastodonへようこそ
signed_up: アカウントの作成が完了しました。Mastodonへようこそ
signed_up_but_inactive: アカウントの作成が完了しました。しかし、アカウントが有効化されていないためログインできませんでした。
signed_up_but_locked: アカウントの作成が完了しました。しかし、アカウントがロックされているためログインできませんでした。
signed_up_but_unconfirmed: メールアドレスの確認用のリンクが入力したメールアドレスに送信されました。メール内のリンクをクリックしてアカウントを有効化してください。
@ -58,4 +58,4 @@ ja:
not_locked: ロックされていません
not_saved:
one: エラーが発生したため、%{resource}の保存に失敗しました。
other: "%{count}個のエラーが発生したため、保存に失敗しました。 %{resource}"
other: "%{count}個のエラーが発生したため、%{resource}の保存に失敗しました:"

@ -10,7 +10,7 @@ pt:
inactive: A tua conta ainda não está ativada.
invalid: "%{authentication_keys} ou palavra-passe não válida."
last_attempt: Tens mais uma tentativa antes de a tua conta ficar bloqueada.
locked: A tua conta está bloqueada
locked: A tua conta está bloqueada.
not_found_in_database: "%{authentication_keys} ou palavra-passe não válida."
timeout: A tua sessão expirou. Por favor, entra de novo para continuares.
unauthenticated: Precisas de entrar na tua conta ou registares-te antes de continuar.

@ -0,0 +1,33 @@
gl:
activerecord:
attributes:
doorkeeper/application:
name: Nome do aplicativo
redirect_uri: URI a redireccionar
website: Sitio web do aplicativo
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: non pode conter un fragmento.
invalid_uri: debe ser un URI válido.
relative_uri: debe ser un URI absoluto.
secured_uri: debe ser un URI HTTPS/SSL.
doorkeeper:
applications:
buttons:
authorize: Autorizar
cancel: Cancelar
destroy: Destruír
edit: Editar
submit: Enviar
confirmations:
destroy: Está segura?
edit:
title: Editar aplicativo
form:
error: Eeeeepa! Comprobe os posibles erros no formulario
help:
native_redirect_uri: Utilice %{native_redirect_uri} para probas locais
redirect_uri: Utilice unha liña por URI

@ -116,6 +116,7 @@ en:
roles:
admin: Administrator
moderator: Moderator
staff: Staff
user: User
salmon_url: Salmon URL
search: Search
@ -160,6 +161,7 @@ en:
update_status: "%{name} updated status by %{target}"
title: Audit log
custom_emojis:
by_domain: Domain
copied_msg: Successfully created local copy of the emoji
copy: Copy
copy_failed_msg: Could not make a local copy of that emoji
@ -599,7 +601,7 @@ en:
notifications: Notifications
preferences: Preferences
settings: Settings
two_factor_authentication: Two-factor Authentication
two_factor_authentication: Two-factor Auth
your_apps: Your applications
statuses:
open_in_web: Open in web

@ -116,6 +116,7 @@ fr:
roles:
admin: Administrateur
moderator: Modérateur
staff: Personnel
user: Utilisateur
salmon_url: URL Salmon
search: Rechercher
@ -135,12 +136,13 @@ fr:
web: Web
action_logs:
actions:
confirm_user: "%{name} adresse e-mail confirmée de l'utilisateur %{target}"
confirm_user: "%{name} adresse courriel confirmée de l'utilisateur %{target}"
create_custom_emoji: "%{name} a importé de nouveaux emoji %{target}"
create_domain_block: "%{name} a bloqué le domaine %{target}"
create_email_domain_block: "%{name} a blacklisté le domaine de l'e-mail %{target}"
create_email_domain_block: "%{name} a mis le domaine du courriel %{target} sur liste noire"
demote_user: "%{name} a rétrogradé l'utilisateur %{target}"
destroy_domain_block: "%{name} a débloqué le domaine %{target}"
destroy_email_domain_block: "%{name} a mis le domaine de l'e-mail %{target} sur liste blanche"
destroy_email_domain_block: "%{name} a mis le domaine du courriel %{target} sur liste blanche"
destroy_status: "%{name} a enlevé le statut de %{target}"
disable_2fa_user: "%{name} a désactivé l'authentification à deux facteurs pour l'utilisateur %{target}"
disable_custom_emoji: "%{name} a désactivé l'emoji %{target}"
@ -159,6 +161,7 @@ fr:
update_status: "%{name} a mis à jour le statut de %{target}"
title: Journal d'audit
custom_emojis:
by_domain: Domaine
copied_msg: Copie locale de lémoji créée avec succès!
copy: Copier
copy_failed_msg: Impossible de faire une copie locale de cet émoji
@ -191,7 +194,7 @@ fr:
create: Créer le blocage
hint: Le blocage de domaine nempêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes.
severity:
desc_html: "<strong>Silence</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspendre</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil."
desc_html: "<strong>Silence</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspendre</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil. Utilisez <strong>Aucun</strong> si vous voulez simplement rejeter les fichiers multimédia."
noop: Aucune
silence: Masqué
suspend: Suspendre
@ -285,7 +288,7 @@ fr:
desc_html: Montrer un badge de responsable sur une page utilisateur
title: Montrer un badge de responsable
site_description:
desc_html: Affichée sous la forme dun paragraphe sur la page daccueil et utilisée comme balise meta.<br/>Vous pouvez utiliser des balises HTML, en particulier <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
desc_html: Paragraphe introductif sur la page d'accueil et dans les balises meta. Vous pouvez utiliser des balises HTML, en particulier <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
title: Description du site
site_description_extended:
desc_html: Affichée sur la page dinformations complémentaires du site<br>Vous pouvez utiliser des balises HTML
@ -457,6 +460,9 @@ fr:
title: Inviter des gens
landing_strip_html: <strong>%{name}</strong> utilise %{link_to_root_path}. Vous pouvez læ suivre et interagir si vous possédez un compte quelque part dans le "fediverse".
landing_strip_signup_html: Si ce nest pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>.
lists:
errors:
limit: Vous avez atteint le nombre maximum de listes
media_attachments:
validations:
images_and_video: Impossible de joindre une vidéo à un statut contenant déjà des images
@ -590,11 +596,12 @@ fr:
open_in_web: Ouvrir sur le web
over_character_limit: limite de caractères dépassée de %{max} caractères
pin_errors:
limit: Trop de pouets épinglés
limit: Vous avez déjà épinglé le nombre maximum de pouets
ownership: Vous ne pouvez pas épingler un statut ne vous appartenant pas
private: Les statuts non-publics ne peuvent pas être épinglés
reblog: Un partage ne peut pas être épinglé
show_more: Afficher plus
title: '%{name}: "%{quote}"'
visibilities:
private: Abonné⋅e⋅s uniquement
private_long: Seul⋅e⋅s vos abonné⋅e⋅s verront vos statuts
@ -693,7 +700,7 @@ fr:
manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez lentrer manuellement, voici le secret en clair :'
recovery_codes: Codes de récupération
recovery_codes_regenerated: Codes de récupération régénérés avec succès
recovery_instructions_html: Si vous perdez laccès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer laccès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants.
recovery_instructions_html: Si vous perdez laccès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour retrouver laccès à votre compte. <strong>Conservez les codes de récupération en sécurité</strong>. Par exemple, en les imprimant et en les stockant avec vos autres documents importants.
setup: Installer
wrong_code: Les codes entrés sont incorrects! Lheure du serveur et celle de votre appareil sont-elles correctes?
users:

@ -151,7 +151,7 @@ ja:
memorialize_account: "%{name} さんが %{target} さんを追悼アカウントページに登録しました"
promote_user: "%{name} さんが %{target} さんを昇格しました"
reset_password_user: "%{name} さんが %{target} さんのパスワードをリセットしました"
resolve_report: "%{name} さんがレポート %{target} を棄却しました"
resolve_report: "%{name} さんがレポート %{target} を解決済みにしました"
silence_account: "%{name} さんが %{target} さんをサイレンスにしました"
suspend_account: "%{name} さんが %{target} さんを停止しました"
unsilence_account: "%{name} さんが %{target} さんのサイレンスを解除しました"
@ -192,13 +192,13 @@ ja:
create: ブロックを作成
hint: ドメインブロックはデータベース中のアカウント項目の作成を妨げませんが、遡って自動的に指定されたモデレーションをそれらのアカウントに適用します。
severity:
desc_html: "<strong>サイレンス</strong>はアカウントのトゥートをフォローしていない人から隠します。<strong>停止</strong>はそのアカウントのコンテンツ、メディア、プロフィールデータをすべて削除します。"
desc_html: "<strong>サイレンス</strong>はアカウントのトゥートをフォローしていない人から隠します。<strong>停止</strong>はそのアカウントのコンテンツ、メディア、プロフィールデータをすべて削除します。メディアファイルの拒否は<strong>なし</strong>を使います。"
noop: なし
silence: サイレンス
suspend: 停止
title: 新規ドメインブロック
reject_media: メディアファイルを拒否
reject_media_hint: ローカルに保存されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です
reject_media_hint: ローカルに保存されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です
severities:
noop: なし
silence: サイレンス
@ -271,7 +271,7 @@ ja:
username: 連絡先のユーザー名
registrations:
closed_message:
desc_html: 新規登録を停止しているときにフロントページに表示されます。HTMLタグが使えます
desc_html: 新規登録を停止しているときにフロントページに表示されます。HTMLタグが使えます
title: 新規登録停止時のメッセージ
deletion:
desc_html: 誰でも自分のアカウントを削除できるようにします
@ -289,14 +289,14 @@ ja:
desc_html: フロントページへの表示と meta タグに使用される紹介文です。HTMLタグ、特に<code>&lt;a&gt;</code> と <code>&lt;em&gt;</code>が使えます。
title: インスタンスの説明
site_description_extended:
desc_html: あなたのインスタンスにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます
desc_html: あなたのインスタンスにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます
title: カスタム詳細説明
site_terms:
desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます
desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます
title: カスタム利用規約
site_title: インスタンスの名前
thumbnail:
desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です
desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です
title: インスタンスのサムネイル
timeline_preview:
desc_html: ランディングページに公開タイムラインを表示します
@ -333,7 +333,7 @@ ja:
salutation: "%{name} さん"
settings: 'メール設定の変更: %{link}'
signature: Mastodon %{instance} インスタンスからの通知
view: リンク
view: 'リンク:'
applications:
created: アプリが作成されました
destroyed: アプリが削除されました
@ -359,12 +359,12 @@ ja:
reset_password: パスワードを再発行
set_new_password: 新しいパスワード
authorize_follow:
error: 残念ながら、リモートアカウント情報の取得中にエラーが発生しました
error: 残念ながら、リモートアカウント情報の取得中にエラーが発生しました
follow: フォロー
follow_request: 'あなたは以下のアカウントにフォローリクエストを送信しました:'
following: '成功! あなたは現在以下のアカウントをフォローしています:'
post_follow:
close: またはこのウィンドウを閉じます
close: またはこのウィンドウを閉じます
return: ユーザーのプロフィールに戻る
web: Web を開く
title: "%{acct} をフォロー"
@ -384,7 +384,7 @@ ja:
x_seconds: "%{count}秒"
deletes:
bad_password_msg: パスワードが違います
confirm_password: 本人確認のため、現在のパスワードを入力してください
confirm_password: 本人確認のため、現在のパスワードを入力してください
description_html: あなたのアカウントに含まれるコンテンツは全て削除され、アカウントは無効化されます。これは恒久的なもので、<strong>取り消すことはできません</strong>。なりすましを防ぐために、同じユーザー名で再度登録することはできなくなります。
proceed: アカウントを削除する
success_msg: アカウントは正常に削除されました
@ -397,7 +397,7 @@ ja:
'422':
content: セキュリティ認証に失敗しました。Cookieをブロックしていませんか
title: セキュリティ認証に失敗
'429': リクエストの制限に達しました
'429': リクエストの制限に達しました
'500':
content: もうしわけありませんが、なにかが間違っています。
title: このページは正しくありません
@ -419,24 +419,24 @@ ja:
other: "%{count} 個のドメインからソフトブロックするフォロワーを処理中..."
true_privacy_html: "<strong>プライバシーの保護はエンドツーエンドの暗号化でのみ実現可能</strong>であることに留意ください。"
unlocked_warning_html: 誰でもあなたをフォローすることができ、あなたのプライベート投稿をすぐに見ることができます。フォローする人を限定したい場合は%{lock_link}に設定してください。
unlocked_warning_title: このアカウントは非公開アカウントに設定されていません
unlocked_warning_title: このアカウントは非公開アカウントに設定されていません
generic:
changes_saved_msg: 正常に変更されました
changes_saved_msg: 正常に変更されました
powered_by: powered by %{link}
save_changes: 変更を保存
use_this: これを使う
validation_errors:
one: エラーが発生しました。以下のエラーを確認してください。
other: エラーが発生しました以下の%{count}個のエラーを確認してください
one: エラーが発生しました 以下のエラーを確認してください
other: エラーが発生しました 以下の%{count}個のエラーを確認してください
imports:
preface: 他のインスタンスでエクスポートされたファイルから、フォロー/ブロックした情報をこのインスタンス上のアカウントにインポートできます。
success: ファイルは正常にアップロードされ、現在処理中です。しばらくしてから確認してください
success: ファイルは正常にアップロードされ、現在処理中です。しばらくしてから確認してください
types:
blocking: ブロックしたアカウントリスト
following: フォロー中のアカウントリスト
muting: ミュートしたアカウントリスト
upload: アップロード
in_memoriam_html: 故人を偲んで
in_memoriam_html: 故人を偲んで
invites:
delete: 無効化
expired: 期限切れ
@ -452,7 +452,7 @@ ja:
one: '1'
other: "%{count}"
max_uses_prompt: 無制限
prompt: リンクを生成・共有してこのインスタンスへの新規登録を受け付けることができます
prompt: リンクを生成・共有してこのインスタンスへの新規登録を受け付けることができます
table:
expires_at: 有効期限
uses: 使用
@ -467,15 +467,18 @@ ja:
remove_all: すべて削除
landing_strip_html: "<strong>%{name}</strong> さんはインスタンス %{link_to_root_path} のユーザーです。アカウントさえ持っていればフォローしたり会話したりできます。"
landing_strip_signup_html: もしお持ちでないなら <a href="%{sign_up_path}">こちら</a> からサインアップできます。
lists:
errors:
limit: リストの上限に達しました
media_attachments:
validations:
images_and_video: 既に画像が追加されているため、動画を追加することはできません。
too_many: 追加できるファイルは4つまでです
images_and_video: 既に画像が追加されているため、動画を追加することはできません
too_many: 追加できるファイルは4つまでです
migrations:
acct: 引っ越し先の ユーザー名@ドメイン
currently_redirecting: 'あなたのプロフィールは引っ越し先が設定されています:'
proceed: 保存
updated_msg: アカウントの引っ越し設定を更新しました
updated_msg: アカウントの引っ越し設定を更新しました
moderation:
title: モデレーション
notification_mailer:
@ -492,7 +495,7 @@ ja:
body: "%{name} さんにお気に入り登録された、あなたのトゥートがあります:"
subject: "%{name} さんにお気に入りに登録されました"
follow:
body: "%{name} さんにフォローされています"
body: "%{name} さんにフォローされています"
subject: "%{name} さんにフォローされています"
follow_request:
body: "%{name} さんがあなたにフォローをリクエストしました"
@ -601,7 +604,7 @@ ja:
open_in_web: Webで開く
over_character_limit: 上限は %{max}文字までです
pin_errors:
limit: 固定されているトゥートが多すぎます
limit: 固定されているトゥートの上限に達しました
ownership: 他人のトゥートを固定することはできません
private: 非公開のトゥートを固定することはできません
reblog: ブーストされたトゥートを固定することはできません
@ -692,7 +695,7 @@ ja:
formats:
default: "%Y年%m月%d日 %H:%M"
two_factor_authentication:
code_hint: 確認するには認証アプリで表示されたコードを入力してください
code_hint: 確認するには認証アプリで表示されたコードを入力してください
description_html: "<strong>二段階認証</strong>を有効にするとログイン時、電話でコードを受け取る必要があります。"
disable: 無効
enable: 有効
@ -703,7 +706,7 @@ ja:
lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。
manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
recovery_codes: リカバリーコード
recovery_codes_regenerated: リカバリーコードが再生成されました
recovery_codes_regenerated: リカバリーコードが再生成されました
recovery_instructions_html: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。<strong>リカバリーコードは大切に保全してください。</strong>たとえば印刷してほかの重要な書類と一緒に保管することができます。
setup: 初期設定
wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。

@ -458,6 +458,9 @@ nl:
title: Mensen uitnodigen
landing_strip_html: "<strong>%{name}</strong> is een gebruiker op %{link_to_root_path}. Je kunt deze volgen en ermee communiceren als je op Mastodon (of ergens anders in de fediverse) een account hebt."
landing_strip_signup_html: Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier registreren</a>.
lists:
errors:
limit: Je hebt het maximaal aantal lijsten bereikt
media_attachments:
validations:
images_and_video: Een video kan niet aan een toot met afbeeldingen worden gekoppeld
@ -591,7 +594,7 @@ nl:
open_in_web: In de webapp openen
over_character_limit: Limiet van %{max} tekens overschreden
pin_errors:
limit: Te veel toots vastgezet
limit: Je hebt het maximaal aantal toots al vastgezet
ownership: Een toot van iemand anders kan niet worden vastgezet
private: Alleen openbare toots kunnen worden vastgezet
reblog: Een boost kan niet worden vastgezet

@ -114,8 +114,9 @@ oc:
resubscribe: Se tornar abonar
role: Permissions
roles:
admin: Admin
moderator: Mod
admin: Administrator
moderator: Moderator
staff: Personnal
user: Uitlizaire
salmon_url: URL Salmon
search: Cercar
@ -160,6 +161,7 @@ oc:
update_status: "%{name} metèt a jorn lestatut a %{target}"
title: Audit log
custom_emojis:
by_domain: Domeni
copied_msg: Còpia locala de lemoji ben creada
copy: Copiar
copy_failed_msg: Fracàs de la còpia locala de lemoji
@ -343,7 +345,7 @@ oc:
warning: Mèfi! Agachatz de partejar aquela donada amb degun!
your_token: Vòstre geton daccès
auth:
agreement_html: En vos marcar acceptatz <a href="%{rules_path}">nòstres tèrmes de servici</a> e <a href="%{terms_path}">politica de confidencialitat</a>.
agreement_html: En vos marcar acceptatz <a href="%{rules_path}">las règlas de linstància</a> e <a href="%{terms_path}">politica de confidencialitat</a>.
change_password: Seguretat
delete_account: Suprimir lo compte
delete_account_html: Se volètz suprimir vòstre compte, podètz <a href="%{path}">o far aquí</a>. Vos demandarem que confirmetz.
@ -677,6 +679,7 @@ oc:
private: Se pòt pas penjar los tuts pas publics
reblog: Se pòt pas penjar un tut partejat
show_more: Ne veire mai
title: '%{name}: "%{quote}"'
visibilities:
private: Seguidors solament
private_long: Mostrar pas quals seguidors

@ -469,6 +469,9 @@ pl:
remove_all: Usuń wszystkie
landing_strip_html: "<strong>%{name}</strong> ma konto na %{link_to_root_path}. Możesz je śledzić i wejść z nim w interakcję jeśli masz konto gdziekolwiek w Fediwersum."
landing_strip_signup_html: Jeśli jeszcze go nie masz, możesz <a href="%{sign_up_path}">stworzyć konto</a>.
lists:
errors:
limit: Przekroczyłeś maksymalną liczbę utworzonych list
media_attachments:
validations:
images_and_video: Nie możesz załączyć pliku wideo do wpisu, który zawiera już zdjęcia
@ -606,7 +609,7 @@ pl:
open_in_web: Otwórz w przeglądarce
over_character_limit: limit %{max} znaków przekroczony
pin_errors:
limit: Nie możesz przypiąć więcej wpisów
limit: Przekroczyłeś maksymalną liczbę przypiętych wpisów
ownership: Nie możesz przypiąć cudzego wpisu
private: Nie możesz przypiąć niepublicznego wpisu
reblog: Nie możesz przypiąć podbicia wpisu

@ -282,6 +282,9 @@ pt-BR:
open:
desc_html: Permitir que qualquer um crie uma conta
title: Cadastro aberto
show_staff_badge:
desc_html: Mostrar uma insígnia de equipe na página de usuário
title: Mostrar insígnia de equipe
site_description:
desc_html: Parágrafo introdutório na página inicial e em meta tags. Você pode usar tags HTML, em especial <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
title: Descrição da instância
@ -350,7 +353,7 @@ pt-BR:
login: Entrar
logout: Sair
migrate_account: Mudar para uma conta diferente
migrate_account_html: Se você quer redirecionar essa conta para uma outra você pode <a href="%{path}">configura isso aqui</a>.
migrate_account_html: Se você quer redirecionar essa conta para uma outra você pode <a href="%{path}">configurar isso aqui</a>.
register: Cadastrar-se
resend_confirmation: Reenviar instruções de confirmação
reset_password: Redefinir senha
@ -455,6 +458,9 @@ pt-BR:
title: Convidar pessoas
landing_strip_html: "<strong>%{name}</strong> é um usuário no %{link_to_root_path}. Você pode segui-lo ou interagir com ele se você tiver uma conta em qualquer lugar no fediverso."
landing_strip_signup_html: Se não, você pode <a href="%{sign_up_path}">se cadastrar aqui</a>.
lists:
errors:
limit: Você alcançou o número máximo de listas
media_attachments:
validations:
images_and_video: Não é possível anexar um vídeo a uma postagem que já contém imagens

@ -0,0 +1,75 @@
gl:
simple_form:
hints:
defaults:
avatar: PNG, GIF ou JPG. Como moito 2MB. Será reducida ate 120x120px
digest: Enviar despois de un período longo de inactividade con un resumo das
mencións que recibeu na súa ausencia
display_name:
one: <span class="name-counter">1</span> caracter restante
other: <span class="name-counter">%{count}</span> caracteres restantes
header: PNG, GIF ou JPG. Como moito 2MB. Será reducida a 700x335px
locked: Require que vostede aprove as seguidoras de xeito manual
note:
one: <span class="note-counter">1</span> caracter restante
other: <span class="note-counter">%{count}</span> caracteres restantes
setting_noindex: Afecta ao seu perfil público e páxinas de estado
setting_theme: Afecta ao aspecto de Mastodon en calquer dispositivo cando
está conectada.
imports:
data: Ficheiro CSV exportado desde outra instancia Mastodon
sessions:
otp: Introduza o código de Doble-Factor desde o seu teléfono ou utilice un
dos seus códigos de recuperación.
user:
filtered_languages: Os idiomas marcados filtraranse das liñas temporais públicas
para vostede
labels:
defaults:
avatar: Avatar
confirm_new_password: Confirme o novo contrasinal
confirm_password: Confirme o contrasinal
current_password: Contrasinal actual
data: Data
display_name: Nome mostrado
email: enderezo correo electrónico
expires_in: Caducidade despois de
filtered_languages: Idiomas filtrados
header: Cabezallo
locale: Idioma
locked: Protexer conta
max_uses: Número máximo de usos
new_password: Novo contrasinal
note: Sobre vostede
otp_attempt: Código de Doble-Factor
password: Contrasinal
setting_auto_play_gif: Reprodución automática de GIFs animados
setting_boost_modal: Pedir confirmación antes de promocionar
setting_default_privacy: Intimidade da publicación
setting_default_sensitive: Marcar sempre multimedia como sensible
setting_delete_modal: Solicitar confirmación antes de eliminar unha mensaxe
setting_noindex: Pedir non aparecer nas buscas dos motores de busca
setting_reduce_motion: Reducir o movemento nas animacións
setting_system_font_ui: Utilizar a tipografía por defecto do sistema
setting_theme: Decorado da instancia
setting_unfollow_modal: Solicitar confirmación antes de deixar de seguir alguén
severity: Severidade
type: Tipo de importación
username: Nome de usuaria
interactions:
must_be_follower: Bloquear as notificacións de non-seguidoras
must_be_following: Bloquea as notificacións de personas que non segue
must_be_following_dm: Bloquea as mensaxes directas de personas que non segue
notification_emails:
digest: Enviar correos con resumos
favourite: Enviar un correo cando alguén marca como favorita unha das súas
publicacións
follow: Enviar un correo cando alguén a segue
follow_request: Enviar un correo cando alguén solicita seguila
mention: Enviar un correo cando alguén a menciona
reblog: Enviar un correo cando alguén promociona a súa mensaxe
'no': Non
required:
mark: '*'
text: requerido
'yes': Si

@ -116,6 +116,7 @@ zh-CN:
roles:
admin: 管理员
moderator: 协管
staff: 工作人员
user: 普通用户
salmon_url: Salmon URL
search: 搜索
@ -160,6 +161,7 @@ zh-CN:
update_status: "%{name} 刷新了 %{target} 的嘟文"
title: 运营日志
custom_emojis:
by_domain: 域名
copied_msg: 成功将表情复制到本地
copy: 复制
copy_failed_msg: 无法将表情复制到本地
@ -281,8 +283,8 @@ zh-CN:
desc_html: 允许任何人建立一个帐户
title: 开放注册
show_staff_badge:
desc_html: 在个人资料页上显示管理员标志
title: 显示管理员标志
desc_html: 在个人资料页上显示工作人员标志
title: 显示工作人员标志
site_description:
desc_html: 展示在首页以及 meta 标签中的网站简介。可以使用 HTML 标签,包括 <code>&lt;a&gt;</code> 和 <code>&lt;em&gt;</code>。
title: 本站简介
@ -341,7 +343,7 @@ zh-CN:
warning: 一定小心,千万不要把它分享给任何人!
your_token: 你的访问令牌
auth:
agreement_html: 注册即表示你同意<a href="%{rules_path}">我们的使用条款</a>和<a href="%{terms_path}">隐私权政策</a>。
agreement_html: 注册即表示你同意遵守<a href="%{rules_path}">本实例的相关规定</a>和<a href="%{terms_path}">我们的使用条款</a>。
change_password: 帐户安全
delete_account: 删除帐户
delete_account_html: 如果你想删除你的帐户,请<a href="%{path}">点击这里继续</a>。你需要确认你的操作。
@ -368,18 +370,18 @@ zh-CN:
title: 关注 %{acct}
datetime:
distance_in_words:
about_x_hours: "%{count} 时"
about_x_months: "%{count} 个月"
about_x_years: "%{count} 年"
almost_x_years: "%{count} 年"
about_x_hours: "%{count}时"
about_x_months: "%{count}个月"
about_x_years: "%{count}年"
almost_x_years: "%{count}年"
half_a_minute: 刚刚
less_than_x_minutes: "%{count} 分"
less_than_x_minutes: "%{count}分"
less_than_x_seconds: 刚刚
over_x_years: "%{count} 年"
x_days: "%{count} 天"
x_minutes: "%{count} 分"
x_months: "%{count} 个月"
x_seconds: "%{count} 秒"
over_x_years: "%{count}年"
x_days: "%{count}天"
x_minutes: "%{count}分"
x_months: "%{count}个月"
x_seconds: "%{count}秒"
deletes:
bad_password_msg: 想得美,黑客!密码输入错误
confirm_password: 输入你当前的密码来验证身份
@ -591,6 +593,7 @@ zh-CN:
private: 不能置顶非公开的嘟文
reblog: 不能置顶转嘟
show_more: 显示更多
title: "%{name}:“%{quote}”"
visibilities:
private: 仅关注者
private_long: 只有关注你的用户能看到

@ -0,0 +1,6 @@
class RemoveDuplicateIndexesInLists < ActiveRecord::Migration[5.1]
def change
remove_index :list_accounts, name: "index_list_accounts_on_account_id"
remove_index :list_accounts, name: "index_list_accounts_on_list_id"
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171201000000) do
ActiveRecord::Schema.define(version: 20171212195226) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -211,10 +211,8 @@ ActiveRecord::Schema.define(version: 20171201000000) do
t.bigint "account_id", null: false
t.bigint "follow_id", null: false
t.index ["account_id", "list_id"], name: "index_list_accounts_on_account_id_and_list_id", unique: true
t.index ["account_id"], name: "index_list_accounts_on_account_id"
t.index ["follow_id"], name: "index_list_accounts_on_follow_id"
t.index ["list_id", "account_id"], name: "index_list_accounts_on_list_id_and_account_id"
t.index ["list_id"], name: "index_list_accounts_on_list_id"
end
create_table "lists", force: :cascade do |t|

@ -17,7 +17,7 @@ module Mastodon
end
def pre
'rc3'
'rc4'
end
def flags

@ -97,6 +97,8 @@ const startWorker = (workerId) => {
};
const app = express();
app.set('trusted proxy', process.env.TRUSTED_PROXY_IP || 'loopback,uniquelocal');
const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL)));
const server = http.createServer(app);
const redisNamespace = process.env.REDIS_NAMESPACE || null;
@ -177,6 +179,12 @@ const startWorker = (workerId) => {
next();
};
const setRemoteAddress = (req, res, next) => {
req.remoteAddress = req.connection.remoteAddress;
next();
};
const accountFromToken = (token, req, next) => {
pgPool.connect((err, client, done) => {
if (err) {
@ -208,17 +216,22 @@ const startWorker = (workerId) => {
});
};
const accountFromRequest = (req, next) => {
const accountFromRequest = (req, next, required = true) => {
const authorization = req.headers.authorization;
const location = url.parse(req.url, true);
const accessToken = location.query.access_token;
if (!authorization && !accessToken) {
const err = new Error('Missing access token');
err.statusCode = 401;
if (required) {
const err = new Error('Missing access token');
err.statusCode = 401;
next(err);
return;
next(err);
return;
} else {
next();
return;
}
}
const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken;
@ -226,7 +239,17 @@ const startWorker = (workerId) => {
accountFromToken(token, req, next);
};
const PUBLIC_STREAMS = [
'public',
'public:local',
'hashtag',
'hashtag:local',
];
const wsVerifyClient = (info, cb) => {
const location = url.parse(info.req.url, true);
const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream);
accountFromRequest(info.req, err => {
if (!err) {
cb(true, undefined, undefined);
@ -234,16 +257,24 @@ const startWorker = (workerId) => {
log.error(info.req.requestId, err.toString());
cb(false, 401, 'Unauthorized');
}
});
}, authRequired);
};
const PUBLIC_ENDPOINTS = [
'/api/v1/streaming/public',
'/api/v1/streaming/public/local',
'/api/v1/streaming/hashtag',
'/api/v1/streaming/hashtag/local',
];
const authenticationMiddleware = (req, res, next) => {
if (req.method === 'OPTIONS') {
next();
return;
}
accountFromRequest(req, next);
const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path);
accountFromRequest(req, next, authRequired);
};
const errorMiddleware = (err, req, res, {}) => {
@ -275,8 +306,10 @@ const startWorker = (workerId) => {
};
const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false) => {
const accountId = req.accountId || req.remoteAddress;
const streamType = notificationOnly ? ' (notification)' : '';
log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}${streamType}`);
log.verbose(req.requestId, `Starting stream from ${id} for ${accountId}${streamType}`);
const listener = message => {
const { event, payload, queued_at } = JSON.parse(message);
@ -286,7 +319,7 @@ const startWorker = (workerId) => {
const delta = now - queued_at;
const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`);
log.silly(req.requestId, `Transmitting for ${accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`);
output(event, encodedPayload);
};
@ -313,26 +346,31 @@ const startWorker = (workerId) => {
return;
}
const queries = [
client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)),
];
if (req.accountId) {
const queries = [
client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)),
];
if (accountDomain) {
queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain]));
}
if (accountDomain) {
queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain]));
}
Promise.all(queries).then(values => {
done();
Promise.all(queries).then(values => {
done();
if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) {
return;
}
if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) {
return;
}
transmit();
}).catch(err => {
transmit();
}).catch(err => {
done();
log.error(err);
});
} else {
done();
log.error(err);
});
transmit();
}
});
} else {
transmit();
@ -345,13 +383,15 @@ const startWorker = (workerId) => {
// Setup stream output to HTTP
const streamToHttp = (req, res) => {
const accountId = req.accountId || req.remoteAddress;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Transfer-Encoding', 'chunked');
const heartbeat = setInterval(() => res.write(':thump\n'), 15000);
req.on('close', () => {
log.verbose(req.requestId, `Ending stream for ${req.accountId}`);
log.verbose(req.requestId, `Ending stream for ${accountId}`);
clearInterval(heartbeat);
});
@ -383,8 +423,10 @@ const startWorker = (workerId) => {
// Setup stream end for WebSockets
const streamWsEnd = (req, ws, closeHandler = false) => (id, listener) => {
const accountId = req.accountId || req.remoteAddress;
ws.on('close', () => {
log.verbose(req.requestId, `Ending stream for ${req.accountId}`);
log.verbose(req.requestId, `Ending stream for ${accountId}`);
unsubscribe(id, listener);
if (closeHandler) {
closeHandler();
@ -392,7 +434,7 @@ const startWorker = (workerId) => {
});
ws.on('error', () => {
log.verbose(req.requestId, `Ending stream for ${req.accountId}`);
log.verbose(req.requestId, `Ending stream for ${accountId}`);
unsubscribe(id, listener);
if (closeHandler) {
closeHandler();
@ -401,6 +443,7 @@ const startWorker = (workerId) => {
};
app.use(setRequestId);
app.use(setRemoteAddress);
app.use(allowCrossDomain);
app.use(authenticationMiddleware);
app.use(errorMiddleware);
@ -455,6 +498,7 @@ const startWorker = (workerId) => {
const req = ws.upgradeReq;
const location = url.parse(req.url, true);
req.requestId = uuid.v4();
req.remoteAddress = ws._socket.remoteAddress;
ws.isAlive = true;
@ -527,12 +571,14 @@ const startWorker = (workerId) => {
const onError = (err) => {
log.error(err);
server.close();
process.exit(0);
};
process.on('SIGINT', onExit);
process.on('SIGTERM', onExit);
process.on('exit', onExit);
process.on('error', onError);
process.on('uncaughtException', onError);
};
throng({

Loading…
Cancel
Save