Merge remote-tracking branch 'upstream/master'

update master
pull/1623/head
pinfort 2017-04-12 18:43:45 +09:00
commit cf4d23284e
107 changed files with 1249 additions and 2608 deletions

View File

@ -25,11 +25,11 @@ If you would like, you can [support the development of this project on Patreon][
## Resources
- [List of Mastodon instances](docs/Using-Mastodon/List-of-Mastodon-instances.md)
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
- [API overview](docs/Using-the-API/API.md)
- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md)
- [List of apps](docs/Using-Mastodon/Apps.md)
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
## Features
@ -67,23 +67,53 @@ Consult the example configuration file, `.env.production.sample` for the full li
[![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com")
The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`). You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`).
Review the settings in docker-compose.yml. Note that it is not default to store the postgresql database and redis databases in a persistent storage location,
so you may need or want to adjust the settings there.
Before running the first time, you need to build the images:
docker-compose build
And finally
Then, you need to fill in the .env.production file:
cp .env.production.sample .env.production
vi .env.production
docker-compose up -d
Do NOT change the REDIS_* or DB_* settings when running with the default docker configurations.
As usual, the first thing you would need to do would be to run migrations:
You will need to fill in, at least:
LOCAL_DOMAIN, LOCAL_HTTPS, PAPERCLIP_SECRET, SECRET_KEY_BASE, OTP_SECRET, and the SMTP_*
settings. To generate the PAPERCLIP_SECRET, SECRET_KEY_BASE, and OTP_SECRET, you may use:
docker-compose run --rm web rake secret
Do this once for each of those keys, and copy the result into the .env.production file in
the appropriate field.
Then you should run the db:migrate command to create the database, or migrate it from an older release:
docker-compose run --rm web rails db:migrate
And since the instance running in the container will be running in production mode, you need to pre-compile assets:
Then, you will also need to precompile the assets:
docker-compose run --rm web rails assets:precompile
The container has two volumes, for the assets and for user uploads. The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
before you can launch the docker image with:
docker-compose up
If you wish to run this as a daemon process instead of monitoring it on console, use instead:
docker-compose up -d
Then you may login to your new Mastodon instance by browsing to http(s)://(yourhost):3000/
Following that, make sure that you read the [production guide](docs/Running-Mastodon/Production-guide.md). You are probably going to want to understand how
to configure NGINX to make your Mastodon instance available to the rest of the world.
The container has two volumes, for the assets and for user uploads, and optionally two more, for the postgresql and redis databases.
The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
**Note**: The `--rm` option for docker-compose will remove the container that is created to run a one-off command after it completes. As data is stored in volumes it is not affected by that container clean-up.
@ -117,25 +147,25 @@ Which will re-create the updated containers, leaving databases and data as is. D
## Deployment without Docker
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
## Deployment on Scalingo
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
[You can view a guide for deployment on Scalingo here.](docs/Running-Mastodon/Scalingo-guide.md)
[You can view a guide for deployment on Scalingo here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md)
## Deployment on Heroku (experimental)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](docs/Running-Mastodon/Heroku-guide.md)
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md)
## Development with Vagrant
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
[You can find the guide for setting up a Vagrant development environment here.](docs/Running-Mastodon/Vagrant-guide.md)
[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md)
## Contributing

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 258 KiB

View File

@ -50,6 +50,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
};
};
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
export function refreshNotifications() {
return (dispatch, getState) => {
dispatch(refreshNotificationsRequest());
@ -61,7 +63,7 @@ export function refreshNotifications() {
params.since_id = ids.first().get('id');
}
params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
params.exclude_types = excludeTypesFromSettings(getState());
api(getState).get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
@ -109,7 +111,7 @@ export function expandNotifications() {
const params = {};
params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
params.exclude_types = excludeTypesFromSettings(getState());
api(getState).get(url, params).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');

View File

@ -1,5 +1,5 @@
import axios from 'axios';
import LinkHeader from 'http-link-header';
import LinkHeader from './link_header';
export const getLinks = response => {
const value = response.headers.link;

View File

@ -65,7 +65,7 @@ const Account = React.createClass({
<div className='account'>
<div style={{ display: 'flex' }}>
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={36} /></div>
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
<DisplayName account={account} />
</Permalink>

View File

@ -31,7 +31,7 @@ const IconButton = React.createClass({
e.preventDefault();
if (!this.props.disabled) {
this.props.onClick();
this.props.onClick(e);
}
},

View File

@ -27,6 +27,7 @@ const Status = React.createClass({
onOpenMedia: React.PropTypes.func,
onBlock: React.PropTypes.func,
me: React.PropTypes.number,
boostModal: React.PropTypes.bool,
muted: React.PropTypes.bool
},

View File

@ -46,8 +46,8 @@ const StatusActionBar = React.createClass({
this.props.onFavourite(this.props.status);
},
handleReblogClick () {
this.props.onReblog(this.props.status);
handleReblogClick (e) {
this.props.onReblog(this.props.status, e);
},
handleDeleteClick () {

View File

@ -41,15 +41,17 @@ import Report from '../features/report';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import de from 'react-intl/locale-data/de';
import es from 'react-intl/locale-data/es';
import fr from 'react-intl/locale-data/fr';
import pt from 'react-intl/locale-data/pt';
import hu from 'react-intl/locale-data/hu';
import uk from 'react-intl/locale-data/uk';
import fi from 'react-intl/locale-data/fi';
import eo from 'react-intl/locale-data/eo';
import ru from 'react-intl/locale-data/ru';
import es from 'react-intl/locale-data/es';
import fi from 'react-intl/locale-data/fi';
import fr from 'react-intl/locale-data/fr';
import hu from 'react-intl/locale-data/hu';
import ja from 'react-intl/locale-data/ja';
import pt from 'react-intl/locale-data/pt';
import ru from 'react-intl/locale-data/ru';
import uk from 'react-intl/locale-data/uk';
import zh from 'react-intl/locale-data/zh';
import { localeData as zh_hk } from '../locales/zh-hk';
import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store';
@ -64,7 +66,21 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
});
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo, ...ru, ...ja]);
addLocaleData([
...en,
...de,
...eo,
...es,
...fi,
...fr,
...hu,
...ja,
...pt,
...ru,
...uk,
...zh,
...zh_hk,
]);
const Mastodon = React.createClass({

View File

@ -26,7 +26,8 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
status: getStatus(state, props.id),
me: state.getIn(['meta', 'me'])
me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal'])
});
return mapStateToProps;
@ -38,11 +39,19 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(replyCompose(status, router));
},
onReblog (status) {
onModalReblog (status) {
dispatch(reblog(status));
},
onReblog (status, e) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status));
if (e.altKey || !this.boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
}
}
},

View File

@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
const AutosuggestAccount = ({ account }) => (
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={18} /></div>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
<DisplayName account={account} />
</div>
);

View File

@ -46,7 +46,7 @@ const EmojiPickerDropdown = React.createClass({
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
</DropdownTrigger>
<DropdownContent className='dropdown__left'>
<DropdownContent className='dropdown__left light'>
<EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
</DropdownContent>
</Dropdown>

View File

@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
<div>
<div style={outerStyle}>
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
<DisplayName account={account} />
</Permalink>

View File

@ -79,14 +79,14 @@ const Notification = React.createClass({
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
switch(notification.get('type')) {
case 'follow':
return this.renderFollow(account, link);
case 'mention':
return this.renderMention(notification);
case 'favourite':
return this.renderFavourite(notification, link);
case 'reblog':
return this.renderReblog(notification, link);
case 'follow':
return this.renderFollow(account, link);
case 'mention':
return this.renderMention(notification);
case 'favourite':
return this.renderFavourite(notification, link);
case 'reblog':
return this.renderReblog(notification, link);
}
}

View File

@ -37,8 +37,8 @@ const ActionBar = React.createClass({
this.props.onReply(this.props.status);
},
handleReblogClick () {
this.props.onReblog(this.props.status);
handleReblogClick (e) {
this.props.onReblog(this.props.status, e);
},
handleFavouriteClick () {

View File

@ -38,7 +38,8 @@ const makeMapStateToProps = () => {
status: getStatus(state, Number(props.params.statusId)),
ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
me: state.getIn(['meta', 'me'])
me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal'])
});
return mapStateToProps;
@ -55,7 +56,8 @@ const Status = React.createClass({
status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
me: React.PropTypes.number
me: React.PropTypes.number,
boostModal: React.PropTypes.bool
},
mixins: [PureRenderMixin],
@ -82,11 +84,19 @@ const Status = React.createClass({
this.props.dispatch(replyCompose(status, this.context.router));
},
handleReblogClick (status) {
handleModalReblog (status) {
this.props.dispatch(reblog(status));
},
handleReblogClick (status, e) {
if (status.get('reblogged')) {
this.props.dispatch(unreblog(status));
} else {
this.props.dispatch(reblog(status));
if (e.altKey || !this.props.boostModal) {
this.handleModalReblog(status);
} else {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
}
}
},

View File

@ -0,0 +1,77 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Button from '../../../components/button';
import StatusContent from '../../../components/status_content';
import Avatar from '../../../components/avatar';
import RelativeTimestamp from '../../../components/relative_timestamp';
import DisplayName from '../../../components/display_name';
const messages = defineMessages({
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }
});
const BoostModal = React.createClass({
contextTypes: {
router: React.PropTypes.object
},
propTypes: {
status: ImmutablePropTypes.map.isRequired,
onReblog: React.PropTypes.func.isRequired,
onClose: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
handleReblog() {
this.props.onReblog(this.props.status);
this.props.onClose();
},
handleAccountClick (e) {
if (e.button === 0) {
e.preventDefault();
this.props.onClose();
this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
}
},
render () {
const { status, intl, onClose } = this.props;
return (
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div className='status light'>
<div style={{ fontSize: '15px' }}>
<div style={{ float: 'right', fontSize: '14px' }}>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
</div>
<DisplayName account={status.get('account')} />
</a>
</div>
<StatusContent status={status} />
</div>
</div>
<div className='boost-modal__action-bar'>
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Alt + <i className='fa fa-retweet' /></span> }} /></div>
<Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} />
</div>
</div>
);
}
});
export default injectIntl(BoostModal);

View File

@ -41,8 +41,11 @@ const Column = React.createClass({
mixins: [PureRenderMixin],
handleHeaderClick () {
let node = ReactDOM.findDOMNode(this);
this._interruptScrollAnimation = scrollTop(node.querySelector('.scrollable'));
const scrollable = ReactDOM.findDOMNode(this).querySelector('.scrollable');
if (!scrollable) {
return;
}
this._interruptScrollAnimation = scrollTop(scrollable);
},
handleWheel () {

View File

@ -1,9 +1,11 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import MediaModal from './media_modal';
import BoostModal from './boost_modal';
import { TransitionMotion, spring } from 'react-motion';
const MODAL_COMPONENTS = {
'MEDIA': MediaModal
'MEDIA': MediaModal,
'BOOST': BoostModal
};
const ModalRoot = React.createClass({

View File

@ -0,0 +1,33 @@
import Link from 'http-link-header';
import querystring from 'querystring';
Link.parseAttrs = (link, parts) => {
let match = null
let attr = ''
let value = ''
let attrs = ''
let uriAttrs = /<(.*)>;\s*(.*)/gi.exec(parts)
if(uriAttrs) {
attrs = uriAttrs[2]
link = Link.parseParams(link, uriAttrs[1])
}
while(match = Link.attrPattern.exec(attrs)) {
attr = match[1].toLowerCase()
value = match[4] || match[3] || match[2]
if( /\*$/.test(attr)) {
Link.setAttr(link, attr, Link.parseExtendedValue(value))
} else if(/%/.test(value)) {
Link.setAttr(link, attr, querystring.decode(value))
} else {
Link.setAttr(link, attr, value)
}
}
return link
};
export default Link;

View File

@ -12,10 +12,12 @@ const fr = {
"status.sensitive_toggle": "Cliquer pour dévoiler",
"status.show_more": "Déplier",
"status.show_less": "Replier",
"status.open": "Déplier ce status",
"status.open": "Déplier ce statut",
"status.report": "Signaler @{name}",
"status.load_more": "Charger plus",
"status.media_hidden": "Média caché",
"video_player.toggle_sound": "Mettre/Couper le son",
"video_player.toggle_visible": "Afficher/Cacher la vidéo",
"account.mention": "Mentionner",
"account.edit_profile": "Modifier le profil",
"account.unblock": "Débloquer",
@ -42,16 +44,25 @@ const fr = {
"column.notifications": "Notifications",
"column.blocks": "Utilisateurs bloqués",
"column.favourites": "Favoris",
"column.follow_requests": "Demandes de suivi",
"empty_column.notifications": "Vous navez pas encore de notification. Interagissez avec dautres utilisateurs⋅trices pour débuter la conversation.",
"empty_column.public": "Il n'y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs d'autres instances pour remplir le fil public.",
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d'autres utilisateurs.",
"empty_column.home.public_timeline": "le fil public",
"empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
"empty_column.hashtag": "Il n'y a encore aucun contenu relatif à ce hashtag",
"tabs_bar.compose": "Composer",
"tabs_bar.home": "Accueil",
"tabs_bar.mentions": "Mentions",
"tabs_bar.public": "Fil public global",
"tabs_bar.notifications": "Notifications",
"tabs_bar.local_timeline": "Fil public local",
"tabs_bar.federated_timeline": "Fil public global",
"compose_form.placeholder": "Quavez-vous en tête ?",
"compose_form.publish": "Pouet",
"compose_form.sensitive": "Marquer le média comme délicat",
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
"compose_form.spoiler_placeholder": "Avertissement",
"compose_form.private": "Rendre privé",
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
"compose_form.unlisted": "Ne pas afficher dans les fils publics",
@ -64,23 +75,31 @@ const fr = {
"navigation_bar.favourites": "Favoris",
"navigation_bar.info": "Plus d'informations",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.follow_requests": "Demandes de suivi",
"reply_indicator.cancel": "Annuler",
"search.placeholder": "Chercher",
"search.placeholder": "Rechercher",
"search.account": "Compte",
"search.hashtag": "Mot-clé",
"search_results.total": "{count} {count, plural, one {résultat} other {résultats}}",
"search.status_by": "Statuts de {name}",
"upload_button.label": "Joindre un média",
"upload_form.undo": "Annuler",
"upload_progress.label": "Envoi en cours…",
"upload_area.title": "Glissez et déposez pour envoyer",
"notification.follow": "{name} vous suit.",
"notification.favourite": "{name} a ajouté à ses favoris :",
"notification.reblog": "{name} a partagé votre statut :",
"notification.mention": "{name} vous a mentionné⋅e :",
"notifications.column_settings.alert": "Notifications locales",
"notifications.column_settings.show": "Afficher dans la colonne",
"notifications.column_settings.sound": "Émettre un son",
"notifications.column_settings.follow": "Nouveaux abonnés :",
"notifications.column_settings.favourite": "Favoris :",
"notifications.column_settings.mention": "Mentions :",
"notifications.column_settings.reblog": "Partages :",
"notifications.clear": "Nettoyer",
"notifications.clear_confirmation": "Voulez-vous vraiment nettoyer toutes vos notifications ?",
"notifications.settings": "Paramètres de la colonne",
"privacy.public.short": "Public",
"privacy.public.long": "Afficher dans les fils publics",
"privacy.unlisted.short": "Non-listé",
@ -90,6 +109,20 @@ const fr = {
"privacy.direct.short": "Direct",
"privacy.direct.long": "Nafficher que pour les personnes mentionné⋅e⋅s",
"privacy.change": "Ajuster la confidentialité du message",
"media_gallery.toggle_visible": "Modifier la visibilité",
"missing_indicator.label": "Non trouvé",
"follow_request.authorize": "Autoriser",
"follow_request.reject": "Rejeter",
"home.settings": "Paramètres de la colonne",
"home.column_settings.basic": "Basique",
"home.column_settings.show_reblogs": "Afficher les partages",
"home.column_settings.show_replies": "Afficher les réponses",
"home.column_settings.advanced": "Avancé",
"home.column_settings.filter_regex": "Filtrer avec une expression rationnelle",
"report.heading": "Nouveau signalement",
"report.placeholder": "Commentaires additionnels",
"report.submit": "Envoyer",
"report.target": "Signalement"
};
export default fr;

View File

@ -9,6 +9,7 @@ import fi from './fi';
import eo from './eo';
import ru from './ru';
import ja from './ja';
import zh_hk from './zh-hk';
const locales = {
@ -22,8 +23,8 @@ const locales = {
fi,
eo,
ru,
ja
ja,
'zh-HK': zh_hk,
};
export default function getMessagesForLocale (locale) {

View File

@ -0,0 +1,113 @@
import zh from 'react-intl/locale-data/zh';
const localeData = zh.reduce(function (acc, localeData) {
if (localeData.locale === "zh-Hant-HK") {
// rename the locale "zh-Hant-HK" as "zh-HK"
// (match the code usually used in Accepted-Language header)
acc.push(Object.assign({},
localeData,
{
"locale": "zh-HK",
"parentLocale": "zh-Hant-HK",
}
));
}
return acc;
}, []);
export { localeData as localeData };
const zh_hk = {
"account.block": "封鎖 @{name}",
"account.edit_profile": "修改個人資料",
"account.follow": "關注",
"account.followers": "關注的人",
"account.follows_you": "關注你",
"account.follows": "正在關注",
"account.mention": "提及 @{name}",
"account.posts": "文章",
"account.requested": "等候審批",
"account.unblock": "解除對 @{name} 的封鎖",
"account.unfollow": "取消關注",
"column_back_button.label": "先前顯示",
"column.community": "本站時間軸",
"column.home": "家",
"column.notifications": "通知",
"column.public": "跨站公共時間軸",
"compose_form.placeholder": "你在想甚麼?",
"compose_form.privacy_disclaimer": "你的私人文章,將被遞送至你所提及的 {domains} 用戶。你是否信任 {domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於各 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將無法收到這篇文章的私隱設定,然後可能被轉推給不能預知的用戶閱讀。",
"compose_form.private": "標示為「只有關注你的人能看」",
"compose_form.publish": "發文",
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
"compose_form.unlisted": "請勿在公共時間軸顯示",
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
"empty_column.hashtag": "這個標籤暫時未有內容。",
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
"empty_column.home.public_timeline": "公共時間軸",
"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.",
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
"getting_started.apps": "手機或桌面應用程式",
"getting_started.heading": "開始使用",
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
"home.column_settings.basic": "基本",
"home.column_settings.show_reblogs": "顯示被轉推的文章",
"home.column_settings.show_replies": "顯示回應文章",
"home.column_settings.advanced": "進階",
"lightbox.close": "關閉",
"loading_indicator.label": "載入中...",
"missing_indicator.label": "找不到內容",
"navigation_bar.community_timeline": "本站時間軸",
"navigation_bar.edit_profile": "修改個人資料",
"navigation_bar.logout": "登出",
"navigation_bar.preferences": "個人設定",
"navigation_bar.public_timeline": "跨站公共時間軸",
"notification.favourite": "{name} 喜歡你的文章",
"notification.follow": "{name} 開始開始你",
"notification.mention": "{name} 提及你",
"notification.reblog": "{name} 轉推你的文章",
"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": "播放音效",
"reply_indicator.cancel": "取消",
"report.target": "Reporting",
"search.account": "用戶",
"search.hashtag": "標籤",
"search.placeholder": "搜尋",
"search_results.total": "{count} 項結果",
"search.status_by": "按用戶名稱搜尋文章",
"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.home": "家",
"tabs_bar.local_timeline": "本站",
"tabs_bar.mentions": "提及",
"tabs_bar.notifications": "通知",
"tabs_bar.public": "跨站公共時間軸",
"tabs_bar.federated_timeline": "跨站",
"upload_area.title": "將檔案拖放至此上載",
"upload_button.label": "上載媒體檔案",
"upload_progress.label": "上載中……",
"upload_form.undo": "還原",
"video_player.toggle_sound": "開關音效",
};
export default zh_hk;

View File

@ -149,7 +149,7 @@
order: 1;
}
@media screen and (max-width: 360px) {
@media screen and (max-width: 480px) {
.details {
display: block;
}

File diff suppressed because one or more lines are too long

View File

@ -349,6 +349,43 @@ a.status__content__spoiler-link {
.status__display-name {
color: lighten($color1, 26%);
}
&.light {
.status__relative-time {
color: $color3;
}
.status__display-name {
color: $color1;
}
.display-name {
strong {
color: $color1;
}
span {
color: $color3;
}
}
.status__content {
color: $color1;
a {
color: $color4;
}
a.status__content__spoiler-link {
color: $color5;
background: $color3;
&:hover {
background: lighten($color3, 8%);
}
}
}
}
}
.status-check-box {
@ -667,6 +704,12 @@ a.status__content__spoiler-link {
left: 8px;
}
&.light {
&:before {
border-color: transparent transparent $color5 transparent;
}
}
& > ul {
list-style: none;
background: $color2;
@ -684,7 +727,7 @@ a.status__content__spoiler-link {
}
& > .emoji-dialog {
left: -249px;
left: -210px;
}
}
@ -1215,7 +1258,7 @@ a.status__content__spoiler-link {
@import 'boost';
button i.fa-retweet {
button.icon-button i.fa-retweet {
height: 19px;
width: 22px;
background-position: 0 0;
@ -1227,7 +1270,7 @@ button i.fa-retweet {
}
}
button.active i.fa-retweet {
button.icon-button.active i.fa-retweet {
transition-duration: 0.9s;
background-position: 0 100%;
}
@ -1431,14 +1474,14 @@ button.active i.fa-retweet {
}
.emoji-dialog {
width: 280px;
height: 220px;
background: darken($color3, 10%);
width: 245px;
height: 270px;
background: $color5;
box-sizing: border-box;
border-radius: 2px;
border-radius: 4px;
overflow: hidden;
position: relative;
box-shadow: 0 0 15px rgba($color8, 0.4);
box-shadow: 0 0 8px rgba($color8, 0.2);
.emojione {
margin: 0;
@ -1448,7 +1491,6 @@ button.active i.fa-retweet {
.emoji-dialog-header {
padding: 0 10px;
background-color: $color3;
ul {
padding: 0;
@ -1459,18 +1501,29 @@ button.active i.fa-retweet {
li {
display: inline-block;
box-sizing: border-box;
height: 42px;
padding: 9px 5px;
padding: 10px 5px;
cursor: pointer;
border-bottom: 2px solid transparent;
.emoji {
width: 18px;
height: 18px;
}
img, svg {
width: 22px;
height: 22px;
width: 18px;
height: 18px;
filter: grayscale(100%);
}
&:hover {
img, svg {
filter: grayscale(0);
}
}
&.active {
background: lighten($color3, 6%);
border-bottom-color: $color4;
img, svg {
filter: grayscale(0);
@ -1494,7 +1547,7 @@ button.active i.fa-retweet {
.emoji-category-header {
box-sizing: border-box;
overflow-y: hidden;
padding: 8px 16px 0;
padding: 10px 8px 10px 16px;
display: table;
> * {
@ -1504,10 +1557,10 @@ button.active i.fa-retweet {
}
.emoji-category-title {
font-size: 14px;
font-family: sans-serif;
font-weight: normal;
color: $color1;
font-size: 12px;
text-transform: uppercase;
font-weight: 500;
color: darken($color2, 18%);
cursor: default;
}
@ -1547,7 +1600,7 @@ button.active i.fa-retweet {
width: 7px;
height: 7px;
border-radius: 10px;
border: 2px solid $color1;
border: 2px solid $color5;
top: 2px;
left: 2px;
}
@ -1555,14 +1608,20 @@ button.active i.fa-retweet {
}
.emoji-search-wrapper {
padding: 6px 16px;
padding: 10px;
border-bottom: 1px solid lighten($color2, 4%);
}
.emoji-search {
font-size: 12px;
padding: 6px 4px;
font-size: 14px;
font-weight: 400;
padding: 7px 9px;
font-family: inherit;
display: block;
width: 100%;
border: 1px solid #ddd;
background: rgba($color2, 0.3);
color: darken($color2, 18%);
border: 1px solid $color2;
border-radius: 4px;
}
@ -1575,11 +1634,21 @@ button.active i.fa-retweet {
}
.emoji-search-wrapper + .emoji-categories-wrapper {
top: 83px;
top: 93px;
}
.emoji-row .emoji:hover {
background: lighten($color2, 3%);
.emoji-row .emoji {
img, svg {
transition: transform 60ms ease-in-out;
}
&:hover {
background: lighten($color2, 3%);
img, svg {
transform: translateZ(0) scale(1.2);
}
}
}
.emoji {
@ -1936,3 +2005,41 @@ button.active i.fa-retweet {
max-height: 80vh;
}
}
.boost-modal {
background: lighten($color2, 8%);
color: $color1;
border-radius: 8px;
overflow: hidden;
max-width: 90vw;
width: 480px;
position: relative;
flex-direction: column;
}
.boost-modal__container {
padding: 10px;
.status {
user-select: text;
border-bottom: 0;
}
}
.boost-modal__action-bar {
display: flex;
background: $color2;
padding: 10px;
line-height: 36px;
& > div {
flex: 1 1 auto;
text-align: right;
color: lighten($color1, 33%);
padding-right: 10px;
}
.button {
flex: 0 0 auto;
}
}

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Settings
module Exports
class BlockedAccountsController < ApplicationController
before_action :authenticate_user!
def index
export_data = Export.new(current_account.blocking).to_csv
respond_to do |format|
format.csv { send_data export_data, filename: 'blocking.csv' }
end
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Settings
module Exports
class FollowingAccountsController < ApplicationController
before_action :authenticate_user!
def index
export_data = Export.new(current_account.following).to_csv
respond_to do |format|
format.csv { send_data export_data, filename: 'following.csv' }
end
end
end
end
end

View File

@ -1,46 +1,13 @@
# frozen_string_literal: true
require 'csv'
class Settings::ExportsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_account
def show
@total_storage = current_account.media_attachments.sum(:file_file_size)
@total_follows = current_account.following.count
@total_blocks = current_account.blocking.count
end
def download_following_list
@accounts = current_account.following
respond_to do |format|
format.csv { render text: accounts_list_to_csv(@accounts) }
end
end
def download_blocking_list
@accounts = current_account.blocking
respond_to do |format|
format.csv { render text: accounts_list_to_csv(@accounts) }
end
end
private
def set_account
@account = current_user.account
end
def accounts_list_to_csv(list)
CSV.generate do |csv|
list.each do |account|
csv << [(account.local? ? account.local_username_and_domain : account.acct)]
end
end
end
end

View File

@ -23,8 +23,9 @@ class Settings::PreferencesController < ApplicationController
}
current_user.settings['default_privacy'] = user_params[:setting_default_privacy]
current_user.settings['boost_modal'] = user_params[:setting_boost_modal] == '1'
if current_user.update(user_params.except(:notification_emails, :interactions, :setting_default_privacy))
if current_user.update(user_params.except(:notification_emails, :interactions, :setting_default_privacy, :setting_boost_modal))
redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
else
render action: :show
@ -34,6 +35,6 @@ class Settings::PreferencesController < ApplicationController
private
def user_params
params.require(:user).permit(:locale, :setting_default_privacy, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following])
params.require(:user).permit(:locale, :setting_default_privacy, :setting_boost_modal, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following])
end
end

View File

@ -1,285 +0,0 @@
# frozen_string_literal: true
module AtomBuilderHelper
def stream_updated_at
if @account.stream_entries.last
(@account.updated_at > @account.stream_entries.last.created_at ? @account.updated_at : @account.stream_entries.last.created_at)
else
@account.updated_at
end
end
def entry(xml, is_root = false, &block)
if is_root
root_tag(xml, :entry, &block)
else
xml.entry(&block)
end
end
def feed(xml, &block)
root_tag(xml, :feed, &block)
end
def unique_id(xml, date, id, type)
xml.id_ TagManager.instance.unique_tag(date, id, type)
end
def simple_id(xml, id)
xml.id_ id
end
def published_at(xml, date)
xml.published date.iso8601
end
def updated_at(xml, date)
xml.updated date.iso8601
end
def verb(xml, verb)
xml['activity'].send('verb', TagManager::VERBS[verb])
end
def content(xml, content, warning = nil)
xml.summary(warning) unless warning.blank?
xml.content({ type: 'html' }, content) unless content.blank?
end
def title(xml, title)
xml.title strip_tags(title || '').truncate(80)
end
def author(xml, &block)
xml.author(&block)
end
def category(xml, term)
xml.category(term: term)
end
def target(xml, &block)
xml['activity'].object(&block)
end
def object_type(xml, type)
xml['activity'].send('object-type', TagManager::TYPES[type])
end
def uri(xml, uri)
xml.uri uri
end
def name(xml, name)
xml.name name
end
def summary(xml, summary)
xml.summary(summary) unless summary.blank?
end
def subtitle(xml, subtitle)
xml.subtitle(subtitle) unless subtitle.blank?
end
def link_alternate(xml, url)
xml.link(rel: 'alternate', type: 'text/html', href: url)
end
def link_self(xml, url)
xml.link(rel: 'self', type: 'application/atom+xml', href: url)
end
def link_next(xml, url)
xml.link(rel: 'next', type: 'application/atom+xml', href: url)
end
def link_hub(xml, url)
xml.link(rel: 'hub', href: url)
end
def link_salmon(xml, url)
xml.link(rel: 'salmon', href: url)
end
def portable_contact(xml, account)
xml['poco'].preferredUsername account.username
xml['poco'].displayName(account.display_name) unless account.display_name.blank?
xml['poco'].note(Formatter.instance.simplified_format(account)) unless account.note.blank?
end
def in_reply_to(xml, uri, url)
xml['thr'].send('in-reply-to', ref: uri, href: url, type: 'text/html')
end
def link_mention(xml, account)
xml.link(:rel => 'mentioned', :href => TagManager.instance.uri_for(account), 'ostatus:object-type' => TagManager::TYPES[:person])
end
def link_enclosure(xml, media)
xml.link(rel: 'enclosure', href: full_asset_url(media.file.url(:original, false)), type: media.file_content_type, length: media.file_file_size)
end
def link_avatar(xml, account)
single_link_avatar(xml, account, :original, 120)
end
def link_header(xml, account)
xml.link('rel' => 'header', 'type' => account.header_content_type, 'media:width' => 700, 'media:height' => 335, 'href' => full_asset_url(account.header.url(:original)))
end
def logo(xml, url)
xml.logo url
end
def email(xml, email)
xml.email email
end
def conditionally_formatted(activity)
if activity.is_a?(Status)
Formatter.instance.format(activity.reblog? ? activity.reblog : activity)
elsif activity.nil?
nil
else
activity.content
end
end
def link_visibility(xml, item)
return unless item.respond_to?(:visibility) && item.public_visibility?
xml.link(:rel => 'mentioned', :href => TagManager::COLLECTIONS[:public], 'ostatus:object-type' => TagManager::TYPES[:collection])
end
def privacy_scope(xml, level)
xml['mastodon'].scope(level)
end
def include_author(xml, account)
simple_id xml, TagManager.instance.uri_for(account)
object_type xml, :person
uri xml, TagManager.instance.uri_for(account)
name xml, account.username
email xml, account.local? ? account.local_username_and_domain : account.acct
summary xml, account.note
link_alternate xml, TagManager.instance.url_for(account)
link_avatar xml, account
link_header xml, account
portable_contact xml, account
privacy_scope xml, account.locked? ? :private : :public
end
def rich_content(xml, activity)
if activity.is_a?(Status)
content xml, conditionally_formatted(activity), activity.spoiler_text
else
content xml, conditionally_formatted(activity)
end
end
def include_target(xml, target)
simple_id xml, TagManager.instance.uri_for(target)
if target.object_type == :person
include_author xml, target
else
object_type xml, target.object_type
verb xml, target.verb
title xml, target.title
link_alternate xml, TagManager.instance.url_for(target)
end
# Statuses have content and author
return unless target.is_a?(Status)
rich_content xml, target
verb xml, target.verb
published_at xml, target.created_at
updated_at xml, target.updated_at
author(xml) do
include_author xml, target.account
end
if target.reply?
in_reply_to xml, TagManager.instance.uri_for(target.thread), TagManager.instance.url_for(target.thread)
end
link_visibility xml, target
target.mentions.each do |mention|
link_mention xml, mention.account
end
target.media_attachments.each do |media|
link_enclosure xml, media
end
target.tags.each do |tag|
category xml, tag.name
end
category(xml, 'nsfw') if target.sensitive?
privacy_scope(xml, target.visibility)
end
def include_entry(xml, stream_entry)
unique_id xml, stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type
published_at xml, stream_entry.created_at
updated_at xml, stream_entry.updated_at
title xml, stream_entry.title
rich_content xml, stream_entry.activity
verb xml, stream_entry.verb
link_self xml, account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')
link_alternate xml, account_stream_entry_url(stream_entry.account, stream_entry)
object_type xml, stream_entry.object_type
# Comments need thread element
if stream_entry.threaded?
in_reply_to xml, TagManager.instance.uri_for(stream_entry.thread), TagManager.instance.url_for(stream_entry.thread)
end
if stream_entry.targeted?
target(xml) do
include_target(xml, stream_entry.target)
end
end
link_visibility xml, stream_entry.activity
stream_entry.mentions.each do |mentioned|
link_mention xml, mentioned
end
return unless stream_entry.activity.is_a?(Status)
stream_entry.activity.media_attachments.each do |media|
link_enclosure xml, media
end
stream_entry.activity.tags.each do |tag|
category xml, tag.name
end
category(xml, 'nsfw') if stream_entry.activity.sensitive?
privacy_scope(xml, stream_entry.activity.visibility)
end
private
def root_tag(xml, tag, &block)
xml.send(tag, {
'xmlns' => TagManager::XMLNS,
'xmlns:thr' => TagManager::THR_XMLNS,
'xmlns:activity' => TagManager::AS_XMLNS,
'xmlns:poco' => TagManager::POCO_XMLNS,
'xmlns:media' => TagManager::MEDIA_XMLNS,
'xmlns:ostatus' => TagManager::OS_XMLNS,
'xmlns:mastodon' => TagManager::MTDN_XMLNS,
}, &block)
end
def single_link_avatar(xml, account, size, px)
xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => px, 'media:height' => px, 'href' => full_asset_url(account.avatar.url(size)))
end
end

View File

@ -6,15 +6,15 @@ module SettingsHelper
de: 'Deutsch',
es: 'Español',
eo: 'Esperanto',
pt: 'Português',
fr: 'Français',
hu: 'Magyar',
uk: 'Українська',
'zh-CN': '简体中文',
pt: 'Portuguテェs',
fi: 'Suomi',
ru: 'Русский',
ja: '日本語',
ru: 'ミムτコミクミケ',
uk: 'ミ」ミコム€ミーム厘スム糊コミー',
ja: ',
'zh-CN': '€,
'zh-HK': ',
}.freeze
def human_locale(locale)

View File

@ -9,10 +9,6 @@ module StreamEntriesHelper
"@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}"
end
def avatar_for_status_url(status)
status.reblog? ? status.reblog.account.avatar.url(:original) : status.account.avatar.url(:original)
end
def entry_classes(status, is_predecessor, is_successor, include_threads)
classes = ['entry']
classes << 'entry-reblog u-repost-of h-cite' if status.reblog?
@ -22,18 +18,6 @@ module StreamEntriesHelper
classes.join(' ')
end
def relative_time(date)
date < 5.days.ago ? date.strftime('%d.%m.%Y') : "#{time_ago_in_words(date)} ago"
end
def reblogged_by_me_class(status)
user_signed_in? && @reblogged.key?(status.id) ? 'reblogged' : ''
end
def favourited_by_me_class(status)
user_signed_in? && @favourited.key?(status.id) ? 'favourited' : ''
end
def rtl?(text)
return false if text.empty?

18
app/models/export.rb Normal file
View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'csv'
class Export
attr_reader :accounts
def initialize(accounts)
@accounts = accounts
end
def to_csv
CSV.generate do |csv|
accounts.each do |account|
csv << [(account.local? ? account.local_username_and_domain : account.acct)]
end
end
end
end

View File

@ -26,4 +26,8 @@ class User < ApplicationRecord
def setting_default_privacy
settings.default_privacy || (account.locked? ? 'private' : 'public')
end
def setting_boost_modal
settings.boost_modal
end
end

View File

@ -5,5 +5,4 @@ class BaseService
include ActionView::Helpers::SanitizeHelper
include RoutingHelper
include AtomBuilderHelper
end

View File

@ -35,7 +35,7 @@
.info
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
·
= link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
·
= link_to t('about.about_this'), about_more_path
@ -79,8 +79,8 @@
.info
= link_to t('about.terms'), terms_path
·
= link_to t('about.apps'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md'
= link_to t('about.apps'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md'
·
= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
·
= link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'

View File

@ -5,6 +5,7 @@ node(:meta) do
access_token: @token,
locale: I18n.locale,
me: current_account.id,
boost_modal: current_account.user.setting_boost_modal,
}
end

View File

@ -10,8 +10,8 @@
%tr
%th= t('exports.follows')
%td= @total_follows
%td= table_link_to 'download', t('exports.csv'), follows_settings_export_path(format: :csv)
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
%tr
%th= t('exports.blocks')
%td= @total_blocks
%td= table_link_to 'download', t('exports.csv'), blocks_settings_export_path(format: :csv)
%td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv)

View File

@ -22,5 +22,8 @@
= ff.input :must_be_follower, as: :boolean, wrapper: :with_label
= ff.input :must_be_following, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View File

@ -4,32 +4,41 @@ require 'csv'
class ImportWorker
include Sidekiq::Worker
sidekiq_options queue: 'pull', retry: false
def perform(import_id)
import = Import.find(import_id)
attr_reader :import
case import.type
def perform(import_id)
@import = Import.find(import_id)
case @import.type
when 'blocking'
process_blocks(import)
process_blocks
when 'following'
process_follows(import)
process_follows
end
import.destroy
@import.destroy
end
private
def process_blocks(import)
from_account = import.account
def from_account
@import.account
end
CSV.new(open(import.data.url)).each do |row|
next if row.size != 1
def import_contents
Paperclip.io_adapters.for(@import.data).read
end
def import_rows
CSV.new(import_contents).reject(&:blank?)
end
def process_blocks
import_rows.each do |row|
begin
target_account = FollowRemoteAccountService.new.call(row[0])
target_account = FollowRemoteAccountService.new.call(row.first)
next if target_account.nil?
BlockService.new.call(from_account, target_account)
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
@ -38,14 +47,10 @@ class ImportWorker
end
end
def process_follows(import)
from_account = import.account
CSV.new(open(import.data.url)).each do |row|
next if row.size != 1
def process_follows
import_rows.each do |row|
begin
FollowService.new.call(from_account, row[0])
FollowService.new.call(from_account, row.first)
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
next
end

View File

@ -25,7 +25,21 @@ module Mastodon
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo, :ru, :ja]
config.i18n.available_locales = [
:en,
:de,
:eo,
:es,
:fi,
:fr,
:hu,
:ja,
:pt,
:ru,
:uk,
'zh-CN',
:'zh-HK',
]
config.i18n.default_locale = :en

View File

@ -40,7 +40,7 @@ Rails.application.configure do
# By default, use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'debug').to_sym
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info').to_sym
# Prepend all log lines with the following tags.
config.log_tags = [:request_id]
@ -64,7 +64,7 @@ Rails.application.configure do
password: ENV.fetch('REDIS_PASSWORD') { false },
db: 0,
namespace: 'cache',
expires_in: 20.minutes,
expires_in: 10.minutes,
}
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
@ -94,13 +94,13 @@ Rails.application.configure do
# E-mails
config.action_mailer.smtp_settings = {
:port => ENV['SMTP_PORT'],
:address => ENV['SMTP_SERVER'],
:user_name => ENV['SMTP_LOGIN'],
:password => ENV['SMTP_PASSWORD'],
:domain => ENV['SMTP_DOMAIN'] || config.x.local_domain,
:authentication => ENV['SMTP_AUTH_METHOD'] || :plain,
:openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'] || 'peer',
:port => ENV['SMTP_PORT'],
:address => ENV['SMTP_SERVER'],
:user_name => ENV['SMTP_LOGIN'],
:password => ENV['SMTP_PASSWORD'],
:domain => ENV['SMTP_DOMAIN'] || config.x.local_domain,
:authentication => ENV['SMTP_AUTH_METHOD'] || :plain,
:openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'],
:enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true,
}

View File

@ -29,7 +29,7 @@ en:
success: Successfully authenticated from %{kind} account.
passwords:
no_token: You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided.
send_instructions: You will receive an email with instructions on how to reset your password in a few minutes.
send_instructions: If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.
send_paranoid_instructions: If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.
updated: Your password has been changed successfully. You are now signed in.
updated_not_active: Your password has been changed successfully.

View File

@ -29,7 +29,7 @@ fi:
success: Onnistuneesti varmennettu %{kind} tilillä.
passwords:
no_token: Et pääse tälle sivulle ilman salasanan vaihto sähköpostia. Jos tulet tämmöisestä postista, varmista että sinulla on täydellinen URL.
send_instructions: Saat sähköpostitse ohjeet salasanan palautukseen muutaman minuutin kuluessa.
send_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen.
send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen.
updated: Salasanasi vaihdettu onnistuneesti. Olet nyt kirjautunut sisään.
updated_not_active: Salasanasi vaihdettu onnistuneesti.

View File

@ -12,7 +12,7 @@ fr:
last_attempt: Vous avez droit à une tentative avant que votre compte ne soit verrouillé.
locked: Votre compte est verrouillé.
not_found_in_database: Email ou mot de passe invalide.
timeout: Votre session est expirée. Veuillez vous reconnecter pour continuer.
timeout: Votre session a expiré. Veuillez vous reconnecter pour continuer.
unauthenticated: Vous devez vous connecter ou vous inscrire pour continuer.
unconfirmed: Vous devez valider votre compte pour continuer.
mailer:
@ -21,23 +21,23 @@ fr:
password_change:
subject: Votre mot de passe a été modifié avec succés.
reset_password_instructions:
subject: Instructions pour changer le mot de passe
subject: Instructions pour modifier le mot de passe
unlock_instructions:
subject: Instructions pour déverrouiller le compte
omniauth_callbacks:
failure: 'Nous n''avons pas pu vous authentifier via %{kind} : ''%{reason}''.'
success: Authentifié avec succès via %{kind}.
passwords:
no_token: Vous ne pouvez accéder à cette page sans passer par un e-mail de réinitialisation de mot de passe. Si vous êtes passé par un e-mail de ce type, assurez-vous d'utiliser l'URL complète.
no_token: Vous ne pouvez accéder à cette page sans passer par un e-mail de réinitialisation de mot de passe. Si vous êtes passé⋅e par un e-mail de ce type, assurez-vous d'utiliser l'URL complète.
send_instructions: Vous allez recevoir les instructions de réinitialisation du mot de passe dans quelques instants
send_paranoid_instructions: Si votre e-mail existe dans notre base de données, vous allez recevoir un lien de réinitialisation par e-mail
updated: Votre mot de passe a été édité avec succès, vous êtes maintenant connecté
updated_not_active: Votre mot de passe a été changé avec succès.
updated: Votre mot de passe a été modifié avec succès, vous êtes maintenant connecté⋅e
updated_not_active: Votre mot de passe a été modifié avec succès.
registrations:
destroyed: Votre compte a été supprimé avec succès. Nous espérons vous revoir bientôt.
signed_up: Bienvenue, vous êtes connecté.
signed_up_but_inactive: Vous êtes bien enregistré. Vous ne pouvez cependant pas vous connecter car votre compte n'est pas encore activé.
signed_up_but_locked: Vous êtes bien enregistré. Vous ne pouvez cependant pas vous connecter car votre compte est verrouillé.
signed_up: Bienvenue, vous êtes connecté⋅e.
signed_up_but_inactive: Vous êtes bien enregistré⋅e. Vous ne pouvez cependant pas vous connecter car votre compte n'est pas encore activé.
signed_up_but_locked: Vous êtes bien enregistré⋅e. Vous ne pouvez cependant pas vous connecter car votre compte est verrouillé.
signed_up_but_unconfirmed: Un message contenant un lien de confirmation a été envoyé à votre adresse email. Ouvrez ce lien pour activer votre compte.
update_needs_confirmation: Votre compte a bien été mis à jour mais nous devons vérifier votre nouvelle adresse email. Merci de vérifier vos emails et de cliquer sur le lien de confirmation pour finaliser la validation de votre nouvelle adresse.
updated: Votre compte a été modifié avec succès.
@ -48,14 +48,14 @@ fr:
unlocks:
send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants
send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un email contenant les instructions pour le déverrouiller.
unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté.
unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté⋅e.
errors:
messages:
already_confirmed: a déjà été validé(e), veuillez essayer de vous connecter
already_confirmed: a déjà été validé⋅e, veuillez essayer de vous connecter
confirmation_period_expired: à confirmer dans les %{period}, merci de faire une nouvelle demande
expired: a expiré, merci d'en faire une nouvelle demande
not_found: n'a pas été trouvé(e)
not_locked: n'était pas verrouillé(e)
not_found: n'a pas été trouvé⋅e
not_locked: n'était pas verrouillé⋅e
not_saved:
one: '1 erreur a empêché ce(tte) %{resource} d''être sauvegardé(e) :'
other: '%{count} erreurs ont empêché ce(tte) %{resource} d''être sauvegardé(e) : '
one: '1 erreur a empêché ce(tte) %{resource} dêtre sauvegardé⋅e :'
other: '%{count} erreurs ont empêché %{resource} dêtre sauvegardé⋅e :'

View File

@ -0,0 +1,61 @@
---
zh-HK:
devise:
confirmations:
confirmed: 你的電郵地址確認成功
send_instructions: 你將會在幾分鐘內收到確認指示電郵,上面有確認你電郵地址的指示。
send_paranoid_instructions: 如果你的電郵地址已經存在於我們的資料庫,你將會在幾分鐘內收到電郵,確認你電郵地址的指示。
failure:
already_authenticated: 你之前已經登入了。
inactive: 你的用戶並未啟用。
invalid: 不正確的 %{authentication_keys} 或密碼。
last_attempt: 若你再一次嘗試失敗,我們將鎖定你的用戶,以察安全。
locked: 你的用戶已被鎖定
not_found_in_database: 不正確的 %{authentication_keys} 或密碼。
timeout: 你的登入階段已經過期,請重新登入以繼續使用。
unauthenticated: 你必須先登入或登記,以繼續使用。
unconfirmed: 你必須先確認電郵地址,繼續使用。
mailer:
confirmation_instructions:
subject: 'Mastodon: 確認電郵地址'
password_change:
subject: 'Mastodon: 更改密碼'
reset_password_instructions:
subject: 'Mastodon: 重設密碼'
unlock_instructions:
subject: 'Mastodon: 解除用戶鎖定'
omniauth_callbacks:
failure: 無法以 %{kind} 登入你的用戶,原因是︰「%{reason}」。
success: 成功以 %{kind} 登入你的用戶。
passwords:
no_token: 你必須使用重設密碼電郵內的網址進入本頁。如果你確是使用電郵內的網址,請確認你用了完整的網址。
send_instructions: 你將在幾分鐘內收到重設密碼的電郵指示。
send_paranoid_instructions: 如果你的電郵地址已經存在於我們的資料庫,你將會在幾分鐘內收到重設密碼的電郵指示。
updated: 你的密碼已經更新,你現在正登入本站。
updated_not_active: 你的密碼已經更新。
registrations:
destroyed: 再見了!你的用戶已被取消,希望我們相有相見的機會吧。
signed_up: 歡迎你!你的登記已經成功。
signed_up_but_inactive: 你的登記已經成功,可是由於你的用戶還被被啟用,暫時還不能讓你登入。
signed_up_but_locked: 你的登記已經成功,可是由於你的用戶已被鎖定,我們無法讓你登入。
signed_up_but_unconfirmed: 一條確認連結已經電郵到你的郵址。請使用讓連結啟用你的用戶。
update_needs_confirmation: 你的用戶已經更新,但我們需要確認你的電郵地址。請打開你的郵箱,使用確認電郵的連結來確認的地郵址。
updated: 你的用戶已經成功更新。
sessions:
already_signed_out: 成功登出。
signed_in: 成功登入。
signed_out: 成功登出。
unlocks:
send_instructions: 你將在幾分鐘內收到解除用戶鎖定的電郵指示。
send_paranoid_instructions: 如果你的電郵地址已經存在於我們的資料庫,你將在幾分鐘內收到解除用戶鎖定的電郵指示。
unlocked: 你的用戶已經解鎖,請登入以繼續。
errors:
messages:
already_confirmed: 先前已經確認,請嘗試登入
confirmation_period_expired: 需要在 %{period} 之內確認。請重新申請
expired: 已經過期,請重新申請
not_found: 找不到
not_locked: 並未被鎖定
not_saved:
one: '1 個錯誤令 %{resource} 被法被儲存︰'
other: "%{count} 個錯誤令 %{resource} 被法被儲存︰"

View File

@ -1,5 +1,12 @@
---
fr:
activemodel:
errors:
models:
remote_follow:
attributes:
acct:
blank: Le nom d'utilisateur ne doit pas être vide
activerecord:
attributes:
doorkeeper/application:
@ -14,6 +21,23 @@ fr:
invalid_uri: doit être une URL valide.
relative_uri: doit être une URL absolue.
secured_uri: doit être une URL HTTP/SSL.
account:
attributes:
username:
blank: Identifiant vide
user:
attributes:
email:
taken: Email pris
invalid: Email invalide
blank: Email vide
password:
blank: Mot de passe vide
too_short: Mot de passe trop court
password_confirmation:
confirmation: Le mot de passe ne correspond pas
messages:
record_invalid: Données invalides
doorkeeper:
applications:
buttons:

View File

@ -0,0 +1,113 @@
---
zh-HK:
activerecord:
attributes:
doorkeeper/application:
name: 名稱
redirect_uri: 轉接 URI
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: 'URI 不可包含 "#fragment" 部份'
invalid_uri: 必需有正確的 URI.
relative_uri: 必需為絕對 URI.
secured_uri: 必需使用有 HTTPS/SSL 加密的 URI.
doorkeeper:
applications:
buttons:
authorize: 認證
cancel: 取消
destroy: 移除
edit: 編輯
submit: 提交
confirmations:
destroy: 是否確定?
edit:
title: 編輯應用程式
form:
error: 噢!請檢查你表格的錯誤訊息
help:
native_redirect_uri: 使用 %{native_redirect_uri} 作局部測試
redirect_uri: 每行輸入一個 URI
scopes: 請用半形空格分開權限範圍 (scope)。留空表示使用預設的權限範圍
index:
callback_url: 回傳網址
name: 名稱
new: 新增應用程式
title: 你的應用程式
new:
title: 新增應用程式
show:
actions: 操作
application_id: 應用程式 ID
callback_urls: 回傳網址
scopes: 權限範圍
secret: 密碼
title: '應用程式︰ %{name}'
authorizations:
buttons:
authorize: 批准
deny: 拒絕
error:
title: 發生錯誤
new:
able_to: 要求獲取權限
prompt: 應用程式 %{client_name} 要求得到你用戶的部份權限
title: 需要用戶授權
show:
title: 授權代碼
authorized_applications:
buttons:
revoke: 取消授權
confirmations:
revoke: 是否確定要取消授權?
index:
application: 應用程式
created_at: 授權於
date_format: "%Y-%m-%d %H:%M:%S"
scopes: 權限範圍
title: 已獲你授權的程用程式
errors:
messages:
access_denied: 資源擁有者或授權伺服器不接受請求。
credential_flow_not_configured: 資源擁有者密碼認證程序 (Resource Owner Password Credentials flow) 失敗,原因是 Doorkeeper.configure.resource_owner_from_credentials 沒有設定。
invalid_client: 用戶程式認證 (Client authentication) 失敗,原因是用戶程式未有登記、沒有指定用戶程式 (client)、或者使用了不支援的認證方法 (method)。
invalid_grant: 授權申請 (authorization grant) 不正確、過期、已被取消,或者無法對應授權請求 (authorization request) 內的轉接 URI或者屬於別的用戶程式。
invalid_redirect_uri: 不正確的轉接網址。
invalid_request: 請求缺少了必要的參數、包含了不支援的參數、或者其他輸入錯誤。
invalid_resource_owner: 資源擁有者的登入資訊錯誤、或者無法找到該資源擁有者。
invalid_scope: 請求的權限範圍 (scope) 不正確、未有定義、或者輸入錯誤。
invalid_token:
expired: access token 已經過期
revoked: access token 已被取消
unknown: access token 不正確
resource_owner_authenticator_not_configured: 無法找到資源擁有者,原因是 Doorkeeper.configure.resource_owner_authenticator 沒有設定。
server_error: 認證伺服器遇上未知狀況,令請求無法通過。
temporarily_unavailable: 認證伺服器由於臨時負荷過重或者維護,目前未能處理請求。
unauthorized_client: 用戶程式無權用此方法 (method) 請行這個請求。
unsupported_grant_type: 授權伺服器不支援這個授權類型 (grant type)。
unsupported_response_type: 授權伺服器不支援這個回應類型 (response type).
flash:
applications:
create:
notice: 已新增應用程式。
destroy:
notice: 已刪除應用程式。
update:
notice: 已更新應用程式。
authorized_applications:
destroy:
notice: 已取消應用程式授權。
layouts:
admin:
nav:
applications: 應用程式
oauth2_provider: OAuth2 供應者
application:
title: 需要 OAuth 授權
scopes:
follow: 關注、封鎖、解除封鎖及取消關注用戶
read: 閱讀你的用戶資料
write: 以你的名義發佈文章

View File

@ -11,12 +11,12 @@ fi:
domain_count_before: Yhdistyneenä
features:
api: Avoin API ohjelmille ja palveluille
blocks: Rikkaat esto ja hiljennys työkalut
blocks: Rikkaat esto- ja hiljennystyökalut
characters: 500 kirjainta per viesti
chronology: Aikajana on kronologisessa järjestyksessä
ethics: 'Eettinen suunnittelu: ei mainoksia, no seurantaa'
ethics: 'Eettinen suunnittelu: ei mainoksia, ei seurantaa'
gifv: GIFV settejä ja lyhyitä videoita
privacy: Julkaisu kohtainen yksityisyys asetus
privacy: Julkaisukohtainen yksityisyysasetus
public: Julkiset aikajanat
features_headline: Mikä erottaa Mastodonin muista
get_started: Aloita käyttö
@ -39,23 +39,23 @@ fi:
remote_follow: Etäseuranta
unfollow: Lopeta seuraaminen
application_mailer:
settings: 'Muokkaa sähköposti asetuksia: %{link}'
signature: Mastodon ilmoituksia palvelimelta %{instance}
settings: 'Muokkaa sähköpostiasetuksia: %{link}'
signature: Mastodon-ilmoituksia palvelimelta %{instance}
view: 'Katso:'
applications:
invalid_url: Annettu URL on väärä
auth:
change_password: Tunnukset
didnt_get_confirmation: Etkö saanut varmennus ohjeita?
didnt_get_confirmation: Etkö saanut varmennusohjeita?
forgot_password: Unohditko salasanasi?
login: Kirjaudu sisään
logout: Kirjaudu ulos
register: Rekisteröidy
resend_confirmation: Lähetä varmennus ohjeet uudestaan
reset_password: Palauta Salasana
resend_confirmation: Lähetä varmennusohjeet uudestaan
reset_password: Palauta salasana
set_new_password: Aseta uusi salasana
authorize_follow:
error: Valitettavasti tapahtui virhe etätilin haussa
error: Valitettavasti tapahtui virhe etätilin haussa.
follow: Seuraa
prompt_html: 'Sinä (<strong>%{self}</strong>) olet pyytänyt lupaa seurata:'
title: Seuraa %{acct}
@ -79,12 +79,12 @@ fi:
follows: Seurattavat
storage: Mediasi
generic:
changes_saved_msg: Muutokset onnistuneesti tallenettu!
changes_saved_msg: Muutokset onnistuneesti tallennettu!
powered_by: powered by %{link}
save_changes: Tallenna muutokset
validation_errors:
one: Jokin ei ole viellä oikein! Katso virhe alapuolelta
other: Jokin ei ole viellä oikein! Katso %{count} virhettä alapuolelta
one: Jokin ei ole viellä oikein! Katso virhe alapuolelta.
other: Jokin ei ole viellä oikein! Katso %{count} virhettä alapuolelta.
imports:
preface: Voit tuoda tiettyä dataa kaikista ihmisistä joita seuraat tai estät tilillesi tälle palvelimelle tiedostoista, jotka on luotu toisella palvelimella
success: Datasi on onnistuneesti ladattu ja käsitellään pian
@ -151,12 +151,12 @@ fi:
formats:
default: "%b %d, %Y, %H:%M"
two_factor_auth:
description_html: Jos otat käyttöön <strong>kaksivaiheisen tunnistuksen</stron>, kirjautumiseen vaaditaan puhelin, joka voi generoida tokeneita kirjautumista varten.
description_html: Jos otat käyttöön <strong>kaksivaiheisen tunnistuksen</stron>, kirjautumiseen vaaditaan puhelin, joka voi luoda tokeneita kirjautumista varten.
disable: Poista käytöstä
enable: Ota käyttöön
instructions_html: "<strong>Skannaa tämä QR-koodi Google Authenticator tai samanlaiseen sovellukseen puhelimellasi</strong>. Tästä hetkestä lähtien ohjelma generoi koodin, mikä sinun tarvitsee syöttää sisäänkirjautuessa."
instructions_html: "<strong>Skannaa tämä QR-koodi Google Authenticator- tai vastaavaan sovellukseen puhelimellasi</strong>. Tästä hetkestä lähtien ohjelma luo koodin, mikä sinun tarvitsee syöttää sisäänkirjautuessa."
plaintext_secret_html: 'Plain-text secret: <samp>%{secret}</samp>'
warning: Jos et juuri nyt voi konfiguroida authenticator-applikaatiota juuri nyt, sinun pitäisi klikata "Poista käytöstä" tai et voi kirjautua sisään.
users:
invalid_email: Virheellinen sähköposti
invalid_otp_token: Virheellinen kaksivaihe tunnistus koodi
invalid_otp_token: Virheellinen kaksivaihetunnistuskoodi

View File

@ -29,6 +29,7 @@ en:
setting_default_privacy: Post privacy
type: Import type
username: Username
setting_boost_modal: Show confirmation dialog before boosting
interactions:
must_be_follower: Block notifications from non-followers
must_be_following: Block notifications from people you don't follow

View File

@ -0,0 +1,46 @@
---
zh-HK:
simple_form:
hints:
defaults:
avatar: 支援 PNG, GIF 或 JPG 圖片,檔案大小上限為 2MB會被縮裁成 120x120px
display_name: 最多 30 個字元
header: 支援 PNG, GIF 或 JPG 圖片,檔案大小上限為 2MB會被縮裁成 700x335px
locked: 你必須人手核准每個用戶對你的關注請求,而你的文章私隱會被預設為「只有關注你的人能看」
note: 最多 160 個字元
imports:
data: 自其他服務站匯出的 CSV 檔案
labels:
defaults:
avatar: 個人頭像
confirm_new_password: 確認新密碼
confirm_password: 確認密碼
current_password: 目前密碼
data: 資料
display_name: 顯示名稱
email: 電郵地址
header: 個人頁面頂部
locale: 語言
locked: 將用戶轉為「私人」
new_password: 新密碼
note: 簡介
otp_attempt: 雙重認證碼
password: 密碼
setting_default_privacy: 文章預設私隱度
type: 匯入資料類型
username: 用戶名稱
interactions:
must_be_follower: 隱藏沒有關注你的用戶的通知
must_be_following: 隱藏你不關注的用戶的通知
notification_emails:
digest: 定期電郵摘要
favourite: 當有用戶喜歡你的文章時,發電郵通知
follow: 當有用戶關注你時,發電郵通知
follow_request: 當有用戶要求關注你時,發電郵通知
mention: 當有用戶在文章提及你時,發電郵通知
reblog: 當有用戶轉推你的文章時,發電郵通知
'no': '否'
required:
mark: "*"
text: 必須填寫
'yes': '是'

172
config/locales/zh-HK.yml Normal file
View File

@ -0,0 +1,172 @@
---
zh-HK:
about:
about_mastodon: Mastodon (長毛象)是一個<em>自由、開放源碼</em>的社交網站。它是一個分散式的服務,避免你的通訊被單一商業機構壟斷操控。請你選擇一家你信任的 Mastodon 服務站,在上面建立帳號,然後你就可以和任一 Mastodon 服務站上的用戶互通,享受無縫的<em>社交網絡</em>交流。
about_this: 關於本服務站
apps: 應用程式
business_email: 商業電郵︰
closed_registrations: 本服務站暫時停止接受登記。
contact: 聯絡
description_headline: 關於 %{domain}
domain_count_after: 個其他服務站
domain_count_before: 已連接至
features:
api: 開放 API供各式應用程式及服務連入
blocks: 完善的封鎖用戶、靜音功能
characters: 每篇文章最多 500 字
chronology: 時間軸忠實按時序顯示文章,不作多餘處理
ethics: 良心設計︰沒有廣告,不追蹤你的使用行為
gifv: 支援顯示 GIFV 短片圖組
privacy: 可逐篇文章設定私隱度
public: 公共時間軸
features_headline: 甚麼讓 Mastodon 與眾不同
get_started: 立即登記
links: 連結
other_instances: 其他服務站
source_code: 源代碼
status_count_after: 篇文章
status_count_before: 他們共發佈了
terms: 使用條款
user_count_after: 位使用者
user_count_before: 這裏共註冊有
accounts:
follow: 關注
followers: 關注者
following: 正在關注
nothing_here: 暫時未有內容可以顯示
people_followed_by: '%{name} 關注的人'
people_who_follow: 關注 %{name} 的人
posts: 文章
remote_follow: 跨站關注
unfollow: 取消關注
application_mailer:
settings: '修改電郵設定︰ %{link}'
signature: 來自 %{instance} 的 Mastodon 通知
view: '進入瀏覽︰'
applications:
invalid_url: 所提供的網址不正確
auth:
change_password: 登入資訊
didnt_get_confirmation: 沒有收到確認指示電郵?
forgot_password: 忘記了密碼?
login: 登入
logout: 登出
register: 登記
resend_confirmation: 重發確認指示電郵
reset_password: 重設密碼
set_new_password: 設定新密碼
authorize_follow:
error: 對不起,尋找這個跨站用戶的過程發生錯誤
follow: 關注
prompt_html: '你 (<strong>%{self}</strong>) 正準備關注︰'
title: 關注 %{acct}
datetime:
distance_in_words:
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_seconds: 剛剛
over_x_years: "%{count}y"
x_days: "%{count}日"
x_minutes: "%{count}分鐘"
x_months: "%{count}個月"
x_seconds: "%{count}秒"
exports:
blocks: 被你封鎖的用戶
csv: CSV
follows: 你所關注的用戶
storage: 媒體容量大小
generic:
changes_saved_msg: 已成功儲存修改
powered_by: 網站由 %{link} 開發
save_changes: 儲存修改
validation_errors:
one: 提交的資料有問題
other: 提交的資料有 %{count} 項問題
imports:
preface: 你可以在此匯入你在其他服務站所匯出的資料檔,包括︰你所關注的用戶,被你封鎖的用戶。
success: 你已成功上載資料檔,我們正將資料匯入,請稍候
types:
blocking: 被你封鎖的用戶名單
following: 你所關注的用戶名單
upload: 上載
landing_strip_html: <strong>%{name}</strong> 是一個在 <strong>%{domain}</strong> 的用戶。只要你有任何 Mastodon 服務站、或者聯盟網站的用戶,便可以跨站關注此站用戶,或者與他們互動。如果你沒有這類用戶,歡迎在<a href="%{sign_up_path}">此處登記</a>。
media_attachments:
validations:
images_and_video: 不能在已有圖片的文章上加入影片
too_many: 不可以加入超過 4 個檔案
notification_mailer:
digest:
body: '這是自從你在%{since}使用%{instance}以後,你錯失了的訊息︰'
mention: "%{name} 在此提及了你︰"
new_followers_summary:
one: 你新獲得了 1 位關注者了!恭喜!
other: 你新獲得了 %{count} 位關注者了!好厲害!
subject:
one: "自從上次登入以來,你收到 1 則新的通知 \U0001F418"
other: "自從上次登入以來,你收到 %{count} 則新的通知 \U0001F418"
favourite:
body: '你的文章獲得 %{name} 的喜愛'
subject: "%{name} 喜歡你的文章"
follow:
body: "%{name} 開始關注你!"
subject: "%{name} 現正關注你"
follow_request:
body: "%{name} 要求關注你"
subject: '等候關注你的用戶︰ %{name}'
mention:
body: '%{name} 在文章中提及你︰'
subject: '%{name} 在文章中提及你'
reblog:
body: '你的文章得到 %{name} 的轉推'
subject: "%{name} 轉推了你的文章"
pagination:
next: 下一頁
prev: 上一頁
truncate: "……"
remote_follow:
acct: 請輸入你的︰用戶名稱@服務點域名
missing_resource: 無法找到你用戶的轉接網址
proceed: 下一步
prompt: '你希望關注︰'
settings:
authorized_apps: 授權應用程式
back: 回到 Mastodon
edit_profile: 修改個人資料
export: 匯出
import: 匯入
preferences: 偏好設定
settings: 設定
two_factor_auth: 雙重認證
statuses:
open_in_web: 開啟網頁
over_character_limit: 超過了 %{max} 字的限制
show_more: 顯示更多
visibilities:
private: 只有關注你的人能看
public: 公開
unlisted: 公開,但不在公共時間軸顯示
stream_entries:
click_to_show: 點擊顯示
reblogged: 轉推
sensitive_content: 敏感內容
time:
formats:
default: "%Y年%-m月%d日 %H:%M"
two_factor_auth:
code_hint: 請輸入你認證器產生的代碼,以確認設定
description_html: 當你啟用<strong>雙重認證</strong>後,你登入時將需要使你手機、或其他種類認證器產生的代碼。
disable: 停用
enable: 啟用
enabled_success: 已成功啟用雙重認證
instructions_html: <strong>請用你手機的認證器應用程式(如 Google Authenticator、Authy掃描這裏的 QR 圖形碼</strong>。在雙重認證啟用後,你登入時將須要使用此應用程式產生的認證碼。
manual_instructions: 如果你無法掃描 QR 圖形碼,請手動輸入這個文字密碼︰
setup: 設定
warning: 如果你現在無法正確設定你的應用程式,請即「停用」雙重認證,否則日後可能無法登入本站。
wrong_code: 你輸入的認證碼並不正確!可能伺服器時間和你手機不一致,請檢查你手機的時鐘,或與本站管理員聯絡。
users:
invalid_email: 電郵地址格式不正確
invalid_otp_token: 雙重認證確認碼不正確

View File

@ -53,11 +53,10 @@ Rails.application.routes.draw do
resource :preferences, only: [:show, :update]
resource :import, only: [:show, :create]
resource :export, only: [:show] do
collection do
get :follows, to: 'exports#download_following_list'
get :blocks, to: 'exports#download_blocking_list'
end
resource :export, only: [:show]
namespace :exports, constraints: { format: :csv } do
resources :follows, only: :index, controller: :following_accounts
resources :blocks, only: :index, controller: :blocked_accounts
end
resource :two_factor_auth, only: [:show, :new, :create] do

View File

@ -14,6 +14,7 @@ defaults: &defaults
site_contact_email: ''
open_registrations: true
closed_registrations_message: ''
boost_modal: true
notification_emails:
follow: false
reblog: false

View File

@ -1,46 +1 @@
Sponsors
========
These people make the development of Mastodon possible through [Patreon](https://www.patreon.com/user?u=619786):
**Extra special Patrons**
- [World'sTallestLadder](https://mastodon.social/users/carcinoGeneticist)
- [Jimmy Tidey](https://mastodon.social/users/jimmytidey)
- [Kurtis Rainbolt-Greene](https://mastodon.social/users/krainboltgreene)
- [Kit Redgrave](https://socially.constructed.space/users/KitRedgrave)
- [Zeipher](https://mastodon.social/users/Zeipher)
- [Effy Elden](https://mastodon.social/users/effy)
- [Zoë Quinn](https://mastodon.social/users/zoequinn)
**Thank you to the following people**
- [Harris Bomberguy](https://mastodon.social/users/Hbomberguy)
- [Edward Saperia](https://nwspk.com)
- [Yoz Grahame](http://yoz.com/)
- [Jenn Kaplan](https://gay.crime.team/users/jkap)
- [Natalie Weizenbaum](https://mastodon.social/users/nex3)
- [Matteo De Micheli](http://matteodem.ch/)
- [BirdMachine](https://mastodon.social/users/BirdMachine)
- [Jessica Hayley](https://mastodon.social/users/jayhay)
- [Niels Roesen Abildgaard](http://hypesystem.dk/)
- [Zatnosk](https://github.com/Zatnosk)
- [Spex Bluefox](https://mastodon.social/users/Spex)
- [J. C. Holder](http://jcholder.com/)
- [glocal](https://mastodon.social/users/glocal)
- [jk](https://mastodon.social/users/jk)
- [C418](https://mastodon.social/users/C418)
- [halcy](https://icosahedron.website/users/halcy)
- [Extropic](https://gnusocial.no/extropic)
- [Pat Monaghan](http://iwrite.software/)
- TBD
- TBD
- TBD
- TBD
- TBD
- TBD
- TBD
- TBD
- TBD
- TBD
- TBD
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Contributing-to-Mastodon/Sponsors.md)

View File

@ -1,48 +1 @@
Translating
===========
If you want to localise Mastodon into your language, here is how.
There are two parts to Mastodon, the server and the web client. The translations for the web client are in `app/assets/javascripts/components/locales`. For the server-side, the translations live in `config/locales` and are divided into different files. Here are all the files youll need to translate:
| Original file (English) | Location | Description |
|---|---|---|
| [`en.jsx`](/app/assets/javascripts/components/locales/en.jsx) | `app/assets/javascripts/components/locales/en.jsx` | Strings for the web client |
| [`en.yml`](/config/locales/en.yml) | `config/locales/en.yml` | Strings for general use |
| [`simple_form.en.yml`](/config/locales/simple_form.en.yml) | `config/locales/simple_form.en.yml` | Strings for the settings area |
| [`devise.en.yml`](/config/locales/devise.en.yml) | `config/locales/devise.en.yml` | Generic strings for Devise |
| [`doorkeeper.en.yml`](/config/locales/doorkeeper.en.yml) | `config/locales/doorkeeper.en.yml` | Generic strings for Doorkeeper |
## Translating
If you use Github, first clone the Mastodon repository to your account.
1. Duplicate the files in their folder and replace `en` in the filenames by your languages standard two-letters code ([ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)).
For instance `simple_form.en.yml` becomes `simple_form.es.yml` in the Spanish translation.
2. Also replace the language code in the first lines of all the files, and the last line of the `.jsx` file.
3. Translate the right-side values from English to your language. Keep the indentation and punctuation.
Since Devise and Doorkeeper are popular libraries, there may already be translation files for your language available on the Internet.
## Declaring the language
The locales are mentioned in several other files. To activate your translation, add your language code to the different lists present in these files:
| File | Location | Comment |
|---|---|---|
| [`index.jsx`](/app/assets/javascripts/components/locales/index.jsx) | `app/assets/javascripts/components/locales/index.jsx` | 2 lines to add |
|[`mastodon.jsx`](/app/assets/javascripts/components/containers/mastodon.jsx) | `app/assets/javascripts/components/containers/mastodon.jsx` | 1 line to add + 1 list to complete |
| [`settings_helper.rb`](/app/helpers/settings_helper.rb) | `app/helpers/settings_helper.rb` | 1 line to add + your languages name |
| [`application.rb`](/config/application.rb) | `config/application.rb` | 1 list to complete |
## Sending the translation
You can then push the files to git and submit a pull request.
## Testing the translation
Once the pull request is accepted, wait for the code to be deployed on a Mastodon instance. Log-in with your account there, and change the locale in the settings. Browse and use the website. See if everything makes sense in context and if anything seems out of place or breaks the layout. Invite other Mastodon users speaking your language to try it and give feedback. Make changes accordingly and update the translation.
## Updating the translation
Keep an eye on the original English files in `app/assets/javascripts/components/locales` and `config/locales`. When they are updated, pass on the changes to your language files. For new strings, add the new lines to the same position and translate them. Once youre finished with the updates, you can submit a new pull request.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Contributing-to-Mastodon/Translating.md)

View File

@ -1,50 +1 @@
Protocol extensions
===================
Some functionality in Mastodon required some additions to the protocols to enable seamless federation of those features:
### Federation of blocks/unblocks
ActivityStreams was lacking verbs for block/unblock. Mastodon creates Salmon slaps for block and unblock events, which are not part of a user's public feed, but are nevertheless delivered to the target user. The intent of these Salmon slaps is not to notify the target user, but to notify the target user's server, so that it can perform any number of UX-related tasks such as removing the target user as a follower of the blocker, and/or displaying a message to the target user such as "You can't follow this person because you've been blocked"
The Salmon slaps have the exact same structure as standard follow/unfollow slaps, the verbs are namespaced:
- `http://mastodon.social/schema/1.0/block`
- `http://mastodon.social/schema/1.0/unblock`
### Federation of sensitive material
Statuses can be marked as containing sensitive (or not safe for work) media. This is symbolized by a `<category term="nsfw" />` on the Atom entry
### Federation of privacy features
#### Locked accounts and status privacy levels
Accounts and statuses have an access "scope":
Accounts can be "private" or "public". The former requires a follow request to be approved before a follow relationship can be established, the latter can be followed directly.
Statuses can be "private", "unlisted" or "public". Private must only be shown to the followers of the account or people mentioned in the status; public can be displayed publicly. Unlisted statuses may be displayed publicly but preferably outside of any spotlights e.g. "whole known network" or "public" timelines.
Namespace of the scope element is `http://mastodon.social/schema/1.0`. Example:
```xml
<entry>
<!-- ... -->
<author>
<!-- ... -->
<mastodon:scope>private</mastodon:scope>
</author>
<!-- ... -->
<mastodon:scope>private</mastodon:scope>
</entry>
```
#### Follow requests
Mastodon uses the following Salmon slaps to signal a follow request, a follow request authorization and a follow request rejection:
- `http://activitystrea.ms/schema/1.0/request-friend`
- `http://activitystrea.ms/schema/1.0/authorize`
- `http://activitystrea.ms/schema/1.0/reject`
The activity object of the request-friend slap is the account in question. The activity object of the authorize and reject slaps is the original request-friend activity. Request-friend slap is sent to the locked account, when the end-user of that account decides, the authorize/reject decision slap is sent back to the requester.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Extensions.md)

View File

@ -1,36 +1 @@
Index
=====
**Mastodon** is a free, open-source GNU social-compatible social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
### Using Mastodon
- [Frequently Asked Questions](Using-Mastodon/FAQ.md)
- [List of Mastodon instances](Using-Mastodon/List-of-Mastodon-instances.md)
- [Apps](Using-Mastodon/Apps.md)
- [User Guide](Using-Mastodon/User-guide.md)
### Using the API
- [API documentation](Using-the-API/API.md)
- [Streaming API documentation](Using-the-API/Streaming-API.md)
- [Testing the API with cURL](Using-the-API/Testing-with-cURL.md)
- [OAuth details](Using-the-API/OAuth-details.md)
- [Tips for app developers](Using-the-API/Tips-for-app-developers.md)
- [Push notifications](Using-the-API/Push-notifications.md)
### Running Mastodon
- [Production guide](Running-Mastodon/Production-guide.md)
- [Alternative: Running on Heroku](Running-Mastodon/Heroku-guide.md)
- [Development guide](Running-Mastodon/Development-guide.md)
- [Alternative: Development with Vagrant](Running-Mastodon/Vagrant-guide.md)
- [Administration guide](Running-Mastodon/Administration-guide.md)
- [Tuning Mastodon](Running-Mastodon/Tuning.md)
### Contributing to Mastodon
- [Sponsors](Contributing-to-Mastodon/Sponsors.md)
- [Translate Mastodon in your language](Contributing-to-Mastodon/Translating.md)
- [Report bugs and submit ideas](https://github.com/tootsuite/mastodon/issues)
### Protocols
- [List of used specs and RFCs for the federation](Specs-and-RFCs-used.md)
- [Extensions of the above protocols](Extensions.md)
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/README.md)

View File

@ -1,45 +1 @@
Administration guide
====================
So, you have a working Mastodon instance... now what?
## Turning into an admin
The following rake task:
RAILS_ENV=production bundle exec rails mastodon:make_admin USERNAME=alice
Would turn the local user "alice" into an admin.
## Administration web interface
A user that is designated as `admin = TRUE` in the database is able to access a suite of administration tools:
* View, edit, silence, or suspend users - https://yourmastodon.instance/admin/accounts
* View PubSubHubbub subscriptions - https://yourmastodon.instance/admin/pubsubhubbub
* View domain blocks - https://yourmastodon.instance/admin/domain_blocks
* Sidekiq dashboard - https://yourmastodon.instance/sidekiq
* PGHero dashboard for PostgreSQL - https://yourmastodon.instance/pghero
* Edit site settings - https://yourmastodon.instance/admin/settings
## Site settings
Your site settings are stored in the `settings` database table, and editable through the admin interface at https://yourmastodon.instance/admin/settings.
You are able to set the following settings:
- Site title
- Contact username
- Contact email
- Site description
- Site extended description
You may wish to use the extended description (shown at https://yourmastodon.instance/about/more ) to display content guidelines or a user agreement (see https://mastodon.social/about/more for an example).
## Confirming Users Manually
The following rake task:
RAILS_ENV=production bundle exec rails mastodon:confirm_email USER_EMAIL=alice@alice.com
Will confirm a user manually, in case they don't have access to their confirmation email for whatever reason.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Administration-guide.md)

View File

@ -1,50 +1 @@
Development guide
=================
**Don't use Docker to do development**. It's a quick way to get Mastodon running in production, it's **really really inconvenient for development**. Normally in Rails development environment you get hot reloading of backend code and on-the-fly compilation of assets like JS and CSS, but you lose those benefits by compiling a Docker image. If you want to contribute to Mastodon, it is worth it to simply set up a proper development environment.
In fact, all you need is described in the [production guide](Production-guide.md), **with the following exceptions**. You **don't** need:
- Nginx
- SystemD
- An `.env.production` file. If you need to set any environment variables, you can use an `.env` file
- To prefix any commands with `RAILS_ENV=production` since the default environment is "development" anyway
- Any cronjobs
The command to install project dependencies does not require any flags, i.e. simply
bundle install
By default the development environment wants to connect to a `mastodon_development` database on localhost using your user/ident to login to Postgres (i.e. not a md5 password)
You can run Mastodon with:
rails s
And open `http://localhost:3000` in your browser. Background jobs run inline (aka synchronously) in the development environment, so you don't need to run a Sidekiq process.
By default, your development environment will have an admin account created for you to use - the email address will be `admin@YOURDOMAIN` (e.g. admin@localhost:3000) and the password will be `mastodonadmin`.
You can run tests with:
rspec
You can check localization status with:
i18n-tasks health
You can check code quality with:
rubocop
## Development tips
You can use a localhost->world tunneling service like ngrok if you want to test federation, **however** that should not be your primary mode of operation. If you want to have a permanently federating server, set up a proper instance on a VPS with a domain name, and simply keep it up to date with your own fork of the project while doing development on localhost.
Ngrok and similar services give you a random domain on each start up. This is good enough to test how the code you're working on handles real-world situations. But as soon as your domain changes, for everybody else concerned you're a different instance than before.
Generally, federation bits are tricky to work on for exactly this reason - it's hard to test. And when you are testing with a disposable instance you are polluting the databases of the real servers you're testing against, usually not a big deal but can be annoying. The way I have handled this so far was thus: I have used ngrok for one session, and recorded the exchanges from its web interface to create fixtures and test suites. From then on I've been working with those rather than live servers.
I advise to study the existing code and the RFCs before trying to implement any federation-related changes. It's not *that* difficult, but I think "here be dragons" applies because it's easy to break.
If your development environment is running remotely (e.g. on a VPS or virtual machine), setting the `REMOTE_DEV` environment variable will swap your instance from using "letter opener" (which launches a local browser) to "letter opener web" (which collects emails and displays them at /letter_opener ).
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md)

View File

@ -1,86 +1 @@
Heroku guide
============
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?button-url=https://github.com/tootsuite/mastodon&template=https://github.com/tootsuite/mastodon)
Mastodon can be run on a free [Heroku](https://heroku.com) app. It should be
noted this has limited testing and could have unpredictable results.
## Basic setup
Click the button above to start creating a Heroku app with the Mastodon repo as
the source. This tells Heroku to use the `app.json` file which does things like
prompt for config variables, set up the right buildpacks, run a postdeploy task,
and add the appropriate addons.
If you don't use the deploy button and app.json approach, you will need to do
some of that manually.
## Domain names and SSL
You can add your domain name to the Heroku app's setting, and then also use
Heroku's (free) auto renewal program for Lets Encrypt certificates, by
requesting a cert from the settings screen. You'll have to point your hostname
DNS at Heroku using the values heroku gives you on this screen, using whatever
method is appropriate for your DNS setup.
You should set the Heroku config vars of `LOCAL_DOMAIN` to your hostname, and
`LOCAL_HTTPS` to "true" as well.
## Email
Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans
that should suit your interests. Look in `production.rb` to see which config
variables need to be set on Heroku for outgoing email to work.
## File storage
You will want Amazon S3 for file storage. The only exception is for development
purposes, where you may not care if files are not saved. Follow a guide online
for creating a free Amazon S3 bucket and Access Key, then enter the details.
If you deploy from the web, the format for all the S3 bits use Paperclip conventions:
S3 Bucket is just the name of the bucket, e.g. `bucketname` not the full ARN.
S3 Region is the AWS code for the region e.g. `ap-northeast-1` not the name of the city displayed on the AWS Dashboard.
To protect the privacy of the users of the your instance, you should have permissons on the your S3 bucket set to no-read and no-write for the public and non-application-specific AWS users, with only one authorized IAM user or group set up to be able to upload or display content. This is an example of an IAM policy used for the S3 bucket used Mastadon instance hentai.loan:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets"
],
"Resource": [
"arn:aws:s3:::*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::hentailoan”,
"arn:aws:s3:::hentailoan/*"
]
}
]
}
## Deployment
You can deploy from the Heroku web interface or from the command line. Run:
`heroku run rails db:migrate`
after you first deploy to set up the first database.
To make yourself an admin, you may need to use the `heroku` CLI application after creating an account online:
`heroku rake mastodon:make_admin USERNAME=yourUsername`
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md)

View File

@ -1,261 +1 @@
Production guide
================
## Nginx
Regardless of whether you go with the Docker approach or not, here is an example Nginx server configuration:
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+AES;
ssl_ecdh_curve prime256v1;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
keepalive_timeout 70;
sendfile on;
client_max_body_size 0;
root /home/mastodon/live/public;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
location / {
try_files $uri @proxy;
}
location @proxy {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Proxy "";
proxy_pass_header Server;
proxy_pass http://localhost:3000;
proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
tcp_nodelay on;
}
location /api/v1/streaming {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Proxy "";
proxy_pass http://localhost:4000;
proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
tcp_nodelay on;
}
error_page 500 501 502 503 504 /500.html;
}
```
## Running in production without Docker
It is recommended to create a special user for mastodon on the server (you could call the user `mastodon`), though remember to disable outside login for it. You should only be able to get into that user through `sudo su - mastodon`.
## General dependencies
sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file git curl
curl -sL https://deb.nodesource.com/setup_4.x | sudo bash -
sudo apt-get install nodejs
sudo npm install -g yarn
## Redis
sudo apt-get install redis-server redis-tools
## Postgres
sudo apt-get install postgresql postgresql-contrib
Setup a user and database for Mastodon:
sudo su - postgres
psql
In the prompt:
CREATE USER mastodon CREATEDB;
\q
## Rbenv
It is recommended to use rbenv (exclusively from the `mastodon` user) to install the desired Ruby version. Follow the guides to [install rbenv][1] and [rbenv-build][2] (I recommend checking the [prerequisites][3] for your system on the rbenv-build project and installing them beforehand, obviously outside the unprivileged `mastodon` user)
[1]: https://github.com/rbenv/rbenv#installation
[2]: https://github.com/rbenv/ruby-build#installation
[3]: https://github.com/rbenv/ruby-build/wiki#suggested-build-environment
Then once `rbenv` is ready, run `rbenv install 2.4.1` to install the Ruby version for Mastodon.
## Git
You need the `git-core` package installed on your system. If it is so, from the `mastodon` user:
cd ~
git clone https://github.com/tootsuite/mastodon.git live
cd live
Then you can proceed to install project dependencies:
gem install bundler
bundle install --deployment --without development test
yarn install
## Configuration
Then you have to configure your instance:
cp .env.production.sample .env.production
nano .env.production
Fill in the important data, like host/port of the redis database, host/port/username/password of the postgres database, your domain name, SMTP details (e.g. from Mailgun or equivalent transactional e-mail service, many have free tiers), whether you intend to use SSL, etc. If you need to generate secrets, you can use:
rake secret
To get a random string. If you are setting up on one single server (most likely), then `REDIS_HOST` is localhost and `DB_HOST` is `/var/run/postgresql`, `DB_USER` is `mastodon` and `DB_NAME` is `mastodon_production` while `DB_PASS` is empty because this setup will use the ident authentication method (system user "mastodon" maps to postgres user "mastodon").
## Setup
And setup the database for the first time, this will create the tables and basic data:
RAILS_ENV=production bundle exec rails db:setup
Finally, pre-compile all CSS and JavaScript files:
RAILS_ENV=production bundle exec rails assets:precompile
## Systemd
Example systemd configuration for the web workers, to be placed in `/etc/systemd/system/mastodon-web.service`:
```systemd
[Unit]
Description=mastodon-web
After=network.target
[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="PORT=3000"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb
TimeoutSec=15
Restart=always
[Install]
WantedBy=multi-user.target
```
Example systemd configuration for the background workers, to be placed in `/etc/systemd/system/mastodon-sidekiq.service`:
```systemd
[Unit]
Description=mastodon-sidekiq
After=network.target
[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=5"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push
TimeoutSec=15
Restart=always
[Install]
WantedBy=multi-user.target
```
Example systemd configuration file for the streaming API, to be placed in `/etc/systemd/system/mastodon-streaming.service`:
```systemd
[Unit]
Description=mastodon-streaming
After=network.target
[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="NODE_ENV=production"
Environment="PORT=4000"
ExecStart=/usr/bin/npm run start
TimeoutSec=15
Restart=always
[Install]
WantedBy=multi-user.target
```
This allows you to `sudo systemctl enable /etc/systemd/system/mastodon-*.service` and `sudo systemctl start mastodon-web.service mastodon-sidekiq.service mastodon-streaming.service` to get things going.
## Cronjobs
I recommend creating a couple cronjobs for the following tasks:
- `RAILS_ENV=production bundle exec rake mastodon:media:clear`
- `RAILS_ENV=production bundle exec rake mastodon:push:refresh`
- `RAILS_ENV=production bundle exec rake mastodon:feeds:clear`
You may want to run `which bundle` first and copypaste that full path instead of simply `bundle` in the above commands because cronjobs usually don't have all the paths set. The time and intervals of when to run these jobs are up to you, but once every day should be enough for all.
You can edit the cronjob file for the `mastodon` user by running `sudo crontab -e -u mastodon` (outside of the mastodon user).
## Things to look out for when upgrading Mastodon
You can upgrade Mastodon with a `git pull` from the repository directory. You may need to run:
- `RAILS_ENV=production bundle exec rails db:migrate`
- `RAILS_ENV=production bundle exec rails assets:precompile`
Depending on which files changed, e.g. if anything in the `/db/` or `/app/assets` directory changed, respectively. Also, Mastodon runs in memory, so you need to restart it before you see any changes. If you're using systemd, that would be:
sudo systemctl restart mastodon-*.service
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md)

View File

@ -1,13 +1 @@
Scalingo guide
==============
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
1. Click the above button.
2. Fill in the options requested.
* You can use a .scalingo.io domain, which will be simple to set up, or you can use a custom domain.
* You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details.
* If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests.
3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Scalingo dashboard.
To make yourself an admin, you can use the `scalingo` CLI: `scalingo run -e USERNAME=yourusername rails mastodon:make_admin`.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md)

View File

@ -1,104 +1 @@
Tuning Mastodon
===============
Mastodon has three types of processes:
- web
- streaming API
- background processing
By default, the web type spawns two worker processes with 5 threads each, the streaming API is a single thread/process with 10 database pool connections, and background processing spawns one process with 5 threads.
### Web
The web process serves short-lived HTTP requests for most of the application. The following environment variables control it:
- `WEB_CONCURRENCY` controls the number of worker processes
- `MAX_THREADS` controls the number of threads per process
The default is 2 workers with 5 threads each. Threads share the memory of their parent process. Different processes allocate their own memory each. Threads in Ruby are not native threads, so it's more or less: threads equal concurrency, processes equal parallelism. A larger number of threads maxes out your CPU first, a larger number of processes maxes out your RAM first.
These values affect how many HTTP requests can be served at the same time. When not enough threads are available, requests are queued until they can be answered.
For a single-user instance, 1 process with 5 threads should be more than enough.
### Streaming API
The streaming API handles long-lived HTTP and WebSockets connections, through which clients receive real-time updates. It is a single-threaded process. By default it has a database connection pool of 10, which means 10 different database queries can run *at the same time*. The database is not heavily used in the streaming API, only for initial authentication of the request, and for some special receiver-specific filter queries when receiving new messages. At the time of writing this value cannot be reconfigured, but mostly doesn't need to.
If you need to scale up the streaming API, spawn more separate processes on different ports (e.g. 4000, 4001, 4003, etc) and load-balance between them with nginx.
### Background processing
Many tasks in Mastodon are delegated to background processing to ensure the HTTP requests are fast, and to prevent HTTP request aborts from affecting the execution of those tasks. Sidekiq is a single process, with a configurable numbero of threads. By default, it is 5. That means, 5 different jobs can be executed at the same time. Others will be queued until they can be processed.
While the amount of threads in the web process affects the responsiveness of the Mastodon instance to the end-user, the amount of threads allocated to background processing affects how quickly posts can be delivered from the author to anyone else, how soon e-mails are sent out, etc.
The amount of threads is not controlled by an environment variable in this case, but a command line argument in the invocation of Sidekiq:
bundle exec sidekiq -c 15 -q default -q mailers -q push
Would start the sidekiq process with 15 threads. Please mind that each threads needs to be able to connect to the database, which means that the database pool needs to be large enough to support all the threads. The database pool size is controlled with the `DB_POOL` environment variable, and defaults to the value of `MAX_THREADS` (therefore, is 5 by default).
You might notice that the above command specifies three queues to be processed:
- "default" contains most tasks such as delivering messages to followers and processing incoming notifications from other instances
- "mailers" contains tasks that send e-mails
- "push" contains tasks that deliver messages to other instances
If you wish, you could start three different processes for each queue, to ensure that even when there is a lot of tasks of one type, important tasks of other types still get executed in a timely manner.
___
### How to set environment variables
#### With systemd
In the `.service` file:
```systemd
...
Environment="WEB_CONCURRENCY=1"
Environment="MAX_THREADS=5"
ExecStart="..."
...
```
Don't forget to `sudo systemctl daemon-reload` before restarting the services so that the changes would take effect!
#### With docker-compose
Edit `docker-compose.yml`:
```yml
...
web:
restart: always
build: .
env_file: .env.production
environment:
- WEB_CONCURRENCY=1
- MAX_THREADS=5
...
```
Re-create the containers with `docker-compose up -d` for the changes to take effect.
You can also scale the number of containers per "service" (where service is "web", "sidekiq" and "streaming"):
docker-compose scale web=1 sidekiq=2 streaming=3
Realistically the `docker-compose.yml` file needs to be modified a bit further for the above to work, because by default it wants to bind the web container to host port 3000 and streaming container to host port 4000, of either of which there is only one on the host system. However, if you change:
```yml
ports:
- "3000:3000"
```
to simply:
```yml
ports:
- "3000"
```
for each service respectively, Docker will allocate random host ports of the services, allowing multiple containers to run alongside each other. But it will be on you to look up which host ports those are (e.g. with `docker ps`), and they will be different on each container restart.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Tuning-guide.md)

View File

@ -1,66 +1 @@
Vagrant guide
=============
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
## Basic setup
Install the latest versions of Vagrant and VirtualBox for your operating systems, and then run:
vagrant plugin install vagrant-hostsupdater
This is optional, but will update your 'hosts' file when you start the virtual machine, allowing you to access the site at http://mastodon.dev (instead of http://localhost:3000).
To create and provision a new virtual machine for Mastodon development:
git clone git@github.com:tootsuite/mastodon.git
cd mastodon
vagrant up
**Note:** On Linux hosts, you will need to [enable NFS support](https://www.vagrantup.com/docs/synced-folders/nfs.html).
Running `vagrant up` for the first time will run provisioning, which will:
- Download the Ubuntu 14.04 base image, if there isn't already a copy on your machine
- Create a new VirtualBox virtual machine from that image
- Run the provisioning script (located inside the Vagrantfile), which installs the system packages, Ruby gems, and JS modules required for Mastodon
- Run the startup script
## Starting the server
The Vagrant box will automatically start after provisioning. It can be started in future with `vagrant up` from the mastodon directory.
Once the Ubuntu virtual machine has booted, it will run the startup script, which loads the environment variables from `.env.vagrant` and then runs `rails s -d -b 0.0.0.0`. This will start a Rails server. You can then access your development site at http://mastodon.dev (or at http://localhost:3000 if you haven't installed vagrants-hostupdater). By default, your development environment will have an admin account created for you to use - the email address will be `admin@mastodon.dev` and the password will be `mastodonadmin`.
To stop the server, simply run `vagrant halt`.
## Using the server
You should now have a working Mastodon instance, although it will not federate, as it is not publicly accessible. Should you need temporary federation for development and testing, see the Ngrok information in the [Development Guide](Development-guide.md).
By default, your instance's ActionMailer will use "Letter Opener Web" for email. This means that any email that would normally be sent, will instead be stored, and accessible at http://mastodon.dev/letter_opener - you can use this to verify a registered user account.
## Making changes/developing
You are able to set environment variables, which are used for Mastodon configuration, by editing the `.env.vagrant` file. Any changes you make will take effect after a Vagrant restart.
Vagrant has mounted your mastodon folder inside the virtual machine. This means that any change to the files in the folder(e.g. the Rails controllers or the React components in /app) should immediately take effect on the live server. This allows you to make and test changes, and create new commits, without ever needing to access the virtual machine.
Should you need to access the virtual machine (for example, to manually restart the Rails process without restarting the box), run `vagrant ssh` from the mastodon folder. You will now be logged in as the `vagrant` user on the VirtualBox Ubuntu VM. You will want to `cd /vagrant` to see the app folder.
## Debugging
You can find the Rails server logs in in the `log` folder, which will often have the information you need.
If your Mastodon instance or Vagrant box are really not behaving, you can re-run the provisioning process. Stop the box with `vagrant halt`, and then run `vagrant destroy` - this will delete the virtual machine. You may then run `vagrant up` to create a new box, and re-run provisioning.
## Testing
To run the `rspec` tests and `rubocop` style checker, you may either:
* Install the relevant gems locally, or
* SSH into the virtual machine, `cd /vagrant`, and then run the commands
## Support/help
If you are confused, or having any issues with the above, the Mastodon IRC channel ( irc.freenode.net #mastodon ) is a good place to find assistance.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md)

View File

@ -1,12 +1 @@
Specs and RFCs used
===================
* [OStatus](https://www.w3.org/community/ostatus/wiki/images/9/93/OStatus_1.0_Draft_2.pdf)
* [Salmon](http://www.salmon-protocol.org/salmon-protocol-summary)
* [Portable Contacts](https://web.archive.org/web/20160305010620/http://portablecontacts.net/draft-spec.html)
* [Atom](https://tools.ietf.org/html/rfc4287)
* [Atom ActivityStreams](http://activitystrea.ms/specs/atom/1.0/)
* [Atom Threading](https://tools.ietf.org/html/rfc4685)
* [PubSubHubbub](https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html)
* [Webfinger](https://tools.ietf.org/html/rfc7033)
* [Link-based Resource Descriptor Discovery](https://tools.ietf.org/html/rfc6415)
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Specs-and-RFCs-used.md)

View File

@ -1,44 +1 @@
# 2-Factor Authentication
2-Factor Authentication is a security mechanism that requires you to enter a computer generated code from your phone every time you log into Mastodon.
We highly recommend that you set up 2-factor authentication as it prevents malicious users from logging into your account if they obtain your password.
## Warning
If you lose access to your 2-factor authentication (such as by losing your phone or performing a factory reset) and you do cannot log in, you will not be able to access your account and will need to contact an instance admin to remove 2-factor authentication from your account.
## Setup
1. Open your [settings page](https://mastodon.social/settings/two_factor_auth) and navigate to the Two-factor Authentication page
2. Press the big blue "Enable" button that appears on the right ![screenshot](screenshots/2fa/enable.png)
3. Follow instructions below to install an authenticator for your smartphone
## Android
__Recommended Application:__ [Google
Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2)
4. Download the above application on your phone
5. Open the "Authenticator" app
6. Press the + button in the bottom right-hand corner
7. Press "Scan a barcode"
8. Line up the black and white QR code with the target box that appears on your camera
9. Now, whenever you log in to Mastodon, open the Authenticator app and enter the 6 digit code that appears above the "mastodon.social (email address)" text
## iPhone
__Recommended Application:__ iPhone: [Authenticator by Matt
Ruben](https://itunes.apple.com/us/app/authenticator/id766157276?mt=8)
4. Download the above application on your phone
5. Open the "Authenticator" app
6. Press the + button in the bottom right-hand corner
7. Authenticator should prompt you for access to your camera- hit "OK"
8. Line up the black and white QR code with the target box that appears on your camera
9. Now, whenever you log in to Mastodon, open the Authenticator app and enter the 6 digit code that appears above the "mastodon.social (email address)" text
# Disabling 2-factor Authentication
1. Go to [the 2-factor authentication settings page](https://mastodon.social/settings/two_factor_auth)
2. Press the big blue "Disable" button underneath your QR code ![disable button screenshot](screenshots/2fa/disable.png)
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/2FA.md)

View File

@ -1,19 +1 @@
List of apps
============
Some people have started working on apps for the Mastodon API. Here is a list of them:
|App|Platform|Link|Developer(s)|
|---|--------|----|------------|
|[Tusky](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky)|Android|<https://github.com/Vavassor/Tusky>|[@Vavassor@mastodon.social](https://mastodon.social/users/Vavassor)|
|mastodroid|Android|<https://github.com/alin-rautoiu/mastodroid>|[@charlag@mastodon.social](https://mastodon.social/users/charlag)|
|TootyFruity|Android|<https://play.google.com/store/apps/details?id=ch.kevinegli.tootyfruity221258>|[@eggplant@mastodon.social](https://mastodon.social/users/eggplant)|
|11t|iOS/Android|<https://github.com/jeroensmeets/mastodon-app>|[@jeroensmeets@mastodon.social](https://mastodon.social/users/jeroensmeets)|
|[Amaroq](https://itunes.apple.com/us/app/amarok-for-mastodon/id1214116200?ls=1&mt=8)|iOS|<https://itunes.apple.com/us/app/amarok-for-mastodon/id1214116200?ls=1&mt=8>|[@eurasierboy@mastodon.social](https://mastodon.social/users/eurasierboy)|
|Albatross|iOS||[@goldie_ice@mastodon.social](https://mastodon.social/users/goldie_ice)|
|Tooter|Chrome|<https://github.com/ineffyble/tooter>|[@effy@mastodon.social](https://mastodon.social/users/effy)|
|tootstream|CLI|<https://github.com/magicalraccoon/tootstream>|[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)|
|HackerNewsBot|CLI|<https://github.com/raymestalez/mastodon-hnbot>|[@rayalez@hackertribe.io](https://hackertribe.io/users/rayalez)|
|Mastodon.tools|Wordpress, web browser, social network|<https://github.com/davidlibeau/mastodon-tools>|[@David@mastodon.xyz](https://mastodon.xyz/users/David)|
If you have a project like this, let me know so I can add it to the list!
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)

View File

@ -1,44 +1 @@
Frequently Asked Questions
==========================
#### What is a Mastodon?
A prehistoric animal, predecessor of the mammoth.
#### Why the name Mastodon?
There's a progressive metal band with the same name that I'm a fan of that brought the animal to my attention. I thought it's a pretty cool name/animal.
#### How exactly is it decentralized?
There are different ways in which something can be decentralized; in this case, Mastodon is the "federated" kind. Think e-mail, not BitTorrent. There are different servers (instances), users have an account on one of them, but can interact and follow each other regardless of where their account is.
#### Technically, how does the federation work?
We are using the OStatus suite of protocols:
1. Webfinger for user-on-domain lookup
2. Atom feeds with ActivityStreams, Portable Contacts, Threads extensions for the actual content
3. PubSubHubbub for subscribing to Atom feeds
4. Salmon for delivering certain items from the Atom feeds to interested parties such as the mentioned user, author of the status being replied to, person being followed, etc
#### What is mastodon.social?
The "flagship" instance of Mastodon, aka the server I run myself with the latest code. It's not supposed to be the only instance in the end.
#### What else is part of the federated network?
Let's call it the "fediverse". It has existed for a longer while, populated by GNU social servers, Friendica, Hubzilla, Diaspora etc. Not every one of those servers is fully compatible with every other. Mastodon strives to be fully standards-compliant and compatibility with GNU social is higher in priority than the others.
#### I tried logging into a GNU social client app with Mastodon and it didn't work, why?
While Mastodon is compatible with GNU social in terms of server to server communication, the client to server API (aka how you access Mastodon) is different. Therefore, client apps that were made for specifically GNU social will not work with Mastodon. The reason for this is half technical, half ideological.
Because Mastodon has been created from a blank slate, it is much simpler to have the API mirror internal structures as closely as possible, rather than build an emulation layer. Secondly, the GNU social client API is actually a half-way implementation of the legacy Twitter API - that's the reason why it works with some older Twitter client apps. However, many of those apps are not maintained anymore, the GNU social API does not actually keep up with the real Twitter API and never fully implemented all its features; at the same time, the Twitter API was never meant for a federated service and so obscures some of the functionality.
#### How is Mastodon funded?
Development of Mastodon and hosting of mastodon.social is funded through my [Patreon (also BTC/PayPal donations)](https://www.patreon.com/user?u=619786). Beyond that, I am not interested in VC funding, monetizing, advertising, or anything of that sort. I could offer setup/maintenance services on demand.
The software is free and open source and communities should host their own servers if they can, that way the costs are more or less distributed. Obviously it'd be hard for me to pay the bills if literally everyone decided to use the mastodon.social instance only.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)

View File

@ -1,82 +1 @@
List of Known Mastodon instances
==========================
There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz) showing realtime information about instances.
| Name | Theme/Notes, if applicable | Open Registrations | IPv6 |
| -------------|-------------|---|---|
| [mastodon.social](https://mastodon.social) |Flagship, quick updates|No|No|
| [securitymastod.one](https://securitymastod.one/) |Information security enthusiasts and pros|Yes|Yes|
| [mastodon.nuzgo.net](https://mastodon.nuzgo.net/) |Mastodon instance hosted in Paris |Yes|Yes|
| [mastodon.cx](https://mastodon.cx/) |Alternative Mastodon instance hosted in France|Yes|Yes|
| [mastodon.network](https://mastodon.network) |N/A|Yes|Yes|
| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No|
| [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|No|
| [socially.constructed.space](https://socially.constructed.space) |Single user|No|No|
| [epiktistes.com](https://epiktistes.com) |N/A|Yes|No|
| [fern.surgeplay.com](https://fern.surgeplay.com) |Federates everywhere, Minecraft-focused|Yes|No
| [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|No|No|
| [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No|
| [memetastic.space](https://memetastic.space) |Memes|Yes|No|
| [masto.razrnet.fr](https://masto.razrnet.fr) |Instance Française pour tout le monde ! Développeurs, gamers, etc...|Yes|No|
| [social.diskseven.com](https://social.diskseven.com) |Single user|No|Yes|
| [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No|
| [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes|
| [mastodon.land](https://mastodon.land) |N/A|Yes|Yes|
| [mastodon.partipirate.org](https://mastodon.partipirate.org) |French Pirate Party Instance - Politics and stuff|Yes|No|
| [social.targaryen.house](https://social.targaryen.house) |Federates everywhere, quick updates.|Yes|Yes|
| [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|Yes|Yes|
| [mstdn.io](https://mstdn.io) |N/A|Yes|Yes|
| [social.imirhil.fr](https://social.imirhil.fr) |N/A|No|Yes|
| [social.wxcafe.net](https://social.wxcafe.net) |Open registrations, queer people, activists, safe as much as possible |Yes|Yes|
| [octodon.social](https://octodon.social) |Open registrations, federates everywhere, cutest instance yet|Yes|Yes|
| [mastodon.club](https://mastodon.club)|Open Registration, Open Federation, Mostly Canadians|Yes|No|
| [mastodon.irish](https://mastodon.irish)|Open Registration|Yes|No|
| [hostux.social](https://hostux.social) |N/A|Yes|Yes|
| [social.alex73630.xyz](https://social.alex73630.xyz) |Francophones|Yes|Yes|
| [oc.todon.fr](https://oc.todon.fr) |Modérée et principalement francophone, pas de tolérances pour misogynie/LGBTphobies/validisme/etc.|Yes|Yes|
| [maly.io](https://maly.io) |N/A|Yes|No|
| [social.lou.lt](https://social.lou.lt) |Francophones|Yes|No|
| [mastodon.ninetailed.uk](https://mastodon.ninetailed.uk) |Open registrations, furry-friendly, UK-based|Yes|No|
| [soc.louiz.org](https://soc.louiz.org) |"Coucou"|Yes|No|
| [7nw.eu](https://7nw.eu) |N/A|Yes|No|
| [mastodon.gougere.fr](https://mastodon.gougere.fr)|N/A|Yes|No|
| [aleph.land](https://aleph.land)|N/A|Yes|No|
| [share.elouworld.org](https://share.elouworld.org)|N/A|No|No|
| [social.lkw.tf](https://social.lkw.tf)|N/A|No|No|
| [manowar.social](https://manowar.social)|N/A|No|No|
| [social.ballpointcarrot.net](https://social.ballpointcarrot.net)|N/A|No|No|
| [social.nasqueron.org](https://social.nasqueron.org) |Dreamers, open source developers, free culture|Yes|Yes|
| [status.dissidence.ovh](https://status.dissidence.ovh)|N/A|Yes|Yes|
| [mastodon.cc](https://mastodon.cc)|Art|Yes|No|
| [mastodon.technology](https://mastodon.technology)|Open registrations, federates everywhere, for tech folks|Yes|No|
| [mastodon.systemlab.fr](https://mastodon.systemlab.fr/)|Le mastodon Français, informatique, jeux-vidéos, gaming et hébergement.|Yes|
| [mastodon.top](https://mastodon.top) |N/A|Yes|Yes|
| [niu.moe](https://niu.moe/)|:dolls: The most cutest node ever, FR/EN, anime and computer :balloon:|Yes|Yes|
| [im-in.space](https://im-in.space/)|SPAAAAACE! Probably with a lot of French people. (Invite-only, might randomly open registrations)|No|Yes|
| [social.bytestemplar.com](https://social.bytestemplar.com)|N/A|Yes|No|
| [digitalhumanities.club](http://www.digitalhumanities.club)|[Digital humanities](http://whatisdigitalhumanities.com) community; invitations will open once code of conduct drafted.|No|No
| [design.vu](https://design.vu)|— what's your design view‽|Yes|No|
| [masto.raildecake.fr](https://masto.raildecake.fr)|Hebergé chez un FAI associatif dans le sud de la france, grillons & pins en options|Yes|No|
| [good-dragon.com](https://good-dragon.com/)|Quick updates, Relaxed Moderation, Federates Everywhere, Furries|Yes|No|
| [rich.gop](https://rich.gop/)|Federates everywhere, Open registration, Privacy respected|Yes|Yes|
| [social.nowa.re](https://social.nowa.re)|Open Registration|Yes|No|
| [mastodon.ml](http://mastodon.ml) |A chill place to hangout and chat about anime, programming and movies.|Yes|Yes|
| [off-the-clock.us](https://off-the-clock.us/)|The work day is over.|Yes|No|
| [infinimatix.net](https://infinimatix.net)|Informatics|Yes|Yes|
| [social.0day.agency](https://social.0day.agency)|Infosec, Hacking, Fun (only protonmail)|Yes|Yes|
| [kagrumez.lerk.io](https://kagrumez.lerk.io)|Open registration. German end english.|Yes|No|
| [meow.social](https://meow.social)|A furry fandom focused instance|Yes|No|
| [neumastodon.com](https://neumastodon.com/)|Northeastern University Mastodon |Yes|No|
| [dancingbanana.party](https://dancingbanana.party)|La banane qui danse.|Yes|No|
| [mastodon.brussels](https://mastodon.brussels/)|Le mastodon pour les belges, si vous aimez la bonne ambiance venez nous rejoindre !|Yes|Yes|
| [mastodon.llamasweet.tech](https://mastodon.llamasweet.tech/)|Mastodon about Android developement|Yes|No|
| [manx.social](https://manx.social/)|Instance for the Isle of Man|Yes|Yes|
| [mastodon.host](https://mastodon.host/)|Lightly moderated, federates everywhere and has a follow bot ( Huge federated timeline )|Yes|No|
| [mastodon.fun](https://mastodon.fun/)|Mastodon for everyone ! |Yes|Yes|
| [oulipo.social](https://oulipo.social/)|An Oulipo Mastodon in which that fifth symbol in Latin script is taboo|Yes|No|
| [indigo.zone](https://indigo.zone)|Open Registrations, General Purpose|Yes|No|
| [mastodon.cloud](https://mastodon.cloud)|An open Mastodon instance with people from all around the world|Yes|Yes|
| [mst3k.interlinked.me](https://mst3k.interlinked.me)|Open registrations, general purpose|Yes|Yes|
We are no longer maintaining this list as instances are popping up too quickly for using GitHub to be a tenable system for tracking them. Please standby while we work on another solution
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)

View File

@ -1,206 +1 @@
Mastodon User's Guide
=====================
* [Intro](User-guide.md#intro)
* [Decentralization and Federation](User-guide.md#decentralization-and-federation)
* [Getting Started](User-guide.md#getting-started)
* [Setting Up Your Profile](User-guide.md#setting-up-your-profile)
* [E-Mail Notifications](User-guide.md#e-mail-notifications)
* [Text Posts](User-guide.md#text-posts)
* [Content Warnings](User-guide.md#content-warnings)
* [Hashtags](User-guide.md#hashtags)
* [Boosts and Favourites](User-guide.md#boosts-and-favourites)
* [Posting Images](User-guide.md#posting-images)
* [Following Other Users](User-guide.md#following-other-users)
* [Notifications](User-guide.md#notifications)
* [Mobile Apps](User-guide.md#mobile-apps)
* [The Federated Timeline](User-guide.md#the-federated-timeline)
* [The Local Timeline](User-guide.md#the-local-timeline)
* [Searching](User-guide.md#searching)
* [Privacy, Safety and Security](User-guide.md#privacy-safety-and-security)
* [Two-Factor Authentication](User-guide.md#two-factor-authentication)
* [Account Privacy](User-guide.md#account-privacy)
* [Toot Privacy](User-guide.md#toot-privacy)
* [Blocking](User-guide.md#blocking)
* [Reporting Toots or Users](User-guide.md#reporting-toots-or-users)
## Intro
Mastodon is a social network application based on the OStatus protocol. It behaves a lot like other social networks, especially Twitter, with one key difference - it is open-source and anyone can start their own server (also called an "*instance*"), and users of any instance can interact freely with those of other instances (called "*federation*"). Thus, it is possible for small communities to set up their own servers to use amongst themselves while also allowing interaction with other communities.
#### Decentralization and Federation
Mastodon is a system decentralized through a concept called "*federation*" - rather than depending on a single person or organization to run its infrastructure, anyone can download and run the software and run their own server. Federation means different Mastodon servers can interact with each other seamlessly, similar to e.g. e-mail.
As such, anyone can download Mastodon and e.g. run it for a small community of people, but any user registered on that instance can follow, send, and read posts from other Mastodon instances (as well as servers running other OStatus-compatible services, such as GNU Social and postActiv). This means that not only is users' data not inherently owned by a company with an interest in selling it to advertisers, but also that if any given server shuts down its users can set up a new one or migrate to another instance, rather than the entire service being lost.
Within each Mastodon instance, usernames just appear as `@username`, similar to other services such as Twitter. Users from other instances appear, and can be searched for and followed, as `@user@servername.ext` - so e.g. `@gargron` on the `mastodon.social` instance can be followed from other instances as `@gargron@mastodon.social`).
Posts from users on external instances are "*federated*" into the local one, i.e. if `user1@mastodon1` follows `user2@gnusocial2`, any posts `user2@gnusocial2` makes appear in both `user1@mastodon1`'s Home feed and the public timeline on the `mastodon1` server. Mastodon server administrators have some control over this and can exclude users' posts from appearing on the public timeline; post privacy settings from users on Mastodon instances also affect this, see below in the [Toot Privacy](User-guide.md#toot-privacy) section.
## Getting Started
#### Setting Up Your Profile
You can customise your Mastodon profile in a number of ways - you can set a custom "display" name, a profile "avatar" picture, a background image for your profile page header, and a short "bio" that summarises you or your account.
![Preferences icon](screenshots/preferences.png) To edit your profile, click the Preferences icon in the Compose column and select "Edit Profile" on the left-hand menu on the Preferences page. Your display name is limited to 30 characters, your bio to 160. Avatars and header pictures can be uploaded as png, gif or jpg images and cannot be larger than 2MB. They will be resized to standard sizes - 120x120 pixels for avatars, 700x335 pixels for header pictures.
#### E-Mail Notifications
![Preferences icon](screenshots/preferences.png) Mastodon can notify you of activity via e-mail if you so choose. To adjust your settings for receiving e-mail notifications, click the Preferences icon in the Compose column and select the "Preferences" page from the left-hand menu. Here you will find a number of checkboxes to enable or disable e-mail notifications for various types of activity.
#### Text Posts
The most basic way to interact with Mastodon is to make a text post, also called a *Toot*. In order to toot, simply enter the message you want to post into the "What is on your mind?" text box in the Compose column and click "TOOT". There is a limit of up to 500 characters per toot; if you really do need more than this you can reply to your own toots so they will appear like a conversation.
If you want to reply to another user's toot, you can click the "Reply" icon on it. This will add their username to your input box along with a preview of the message you're replying to, and the user will receive a notification of your response.
Similarly, in order to start a conversation with another user, just mention their user name in your toot. When you type the @ symbol followed directly (without a space) by any character in a message, Mastodon will automatically start suggesting users that match the username you're typing. Like with replies, mentioning a user like this will send them a notification. If the post starts with a mention, it will be treated as a reply and will only appear in the Home timelines of users who follow both you and the user you are mentioning. It will still be visible on your profile depending on privacy settings.
##### Content Warnings
When you want to post something that you don't want to be immediately visible - for example, spoilers for that film that's just come out, or some personal thoughts that mention potentially upsetting topics, you can "hide" it behind a Content Warning.
To do this, click the ![CW icon](screenshots/compose-cw.png) "CW" switch under the Compose box. This will add another text box labeled "Content warning"; you should enter a short summary of what the "body" of your post contains here while your actual post goes into the "What is on your mind?" box as normal.
![animation showing how to enable content warnings](screenshots/content-warning.gif)
This will cause the body of your post to be hidden behind a "Show More" button in the timeline, with only the content warning and any mentioned users visible by default:
![animation showing content warnings in the timeline](screenshots/cw-toot.gif)
**NOTE** that this will not hide images included in your post - images can be marked as "sensitive" separately to hide them from view until clicked on. To find out how to do this, see the [Posting Images](User-guide.md#posting-images) section of this user guide.
##### Hashtags
If you're making a post belonging to a wider subject, it might be worth adding a "hashtag" to it. This can be done simply by writing in the post a # sign followed by a phrase, e.g. #introductions (which is popular on mastodon.social for new users to introduce themselves to the community), or #politics for political discussions, etc. Clicking on a hashtag in a toot will show a timeline consisting only of public posts that include this hashtag (i.e. it's a shortcut to searching for it). This allows users to group messages of similar subjects together, forming a separate "timeline" for people interested in that subject. Hashtags can also be searched for from the search bar above the compose box.
##### Boosts and Favourites
You can *favourite* another user's toot by clicking the star icon underneath. This will send the user a notification that you have marked their post as a favourite; the meaning of this varies widely by context from a general "I'm listening" to signalling agreement or offering support for the ideas expressed.
Additionally you can *boost* toots by clicking the "circular arrows" icon. Boosting a toot will show it on your profile timeline and make it appear to all your followers, even if they aren't following the user who made the original post. This is helpful if someone posts a message you think others should see, as it increases the message's reach while keeping the author information intact.
#### Posting Images
![Image icon](screenshots/compose-media.png) In order to post an image, simply click or tap the "image" icon in your Compose column and select a file to upload.
If the image is "not safe for work" or has otherwise sensitive content, you can select the ![NSFW toggle](screenshots/compose-nsfw.png) "NSFW" button which appears once you have added an image. This will hide the image in your post by default, making it clickable to show the preview. This is the "visual" version of [content warnings](User-guide.md#content-warnings) and could be combined with them if there is text to accompany the image - otherwise it's fine to just mark the image as sensitive and make the body of your post the content warning.
You can also attach video files or GIF animations to Toots. However, there is a 4MB file size limit for these files and videos must be in .webm or .mp4 format.
#### Following Other Users
Following another user will make all of their toots as well as other users' toots which they [boost](User-guide.md#boosts-and-favourites) appear in your Home column. This gives you a separate timeline from the [public timelines](User-guide.md#the-public-timelines) in which you can read what particular people are up to without the noise of general conversation.
![Follow icon](screenshots/follow.png) In order to follow a user, click their name or avatar to open their profile, then click the Follow icon in the top left of their profile view.
If their account has a padlock icon ![Padlock icon](screenshots/locked-icon.png) next to their user name, they will receive a notification of your request to follow them and they will need to approve this before you are added to their follower list (and thus see their toots). To show you that you are waiting for someone to approve your follow request, the Follow icon ![Follow icon](screenshots/follow-icon.png) on their profile will be replaced with an hourglass icon ![Pending icon](screenshots/pending-icon.png). The requirement for new followers to be approved is something you can enable for your own profile under preferences.
Once you follow a user, the Follow icon will be highlighted in blue on their profile ![Following icon](screenshots/following-icon.png); you can unfollow them again by clicking this.
If you know someone's user name you can also open their profile for following by entering it in the [Search box](User-guide.md#searching) in the Compose column. This also works for remote users, though depending on whether they are known to your home instance you might have to enter their full name including the domain (e.g. `gargron@mastodon.social`) into the search box before their profile will appear in the suggestions.
Alternately, if you already have a user's profile open in a separate browser tab, most OStatus-related networks should have a "Follow" or "Subscribe" button on their profile page. This will ask you to enter the full user name to follow **from** (ie. if your account is on mastodon.social you would want to enter this as `myaccount@mastodon.social`)
#### Notifications
When someone follows your account or requests to follow you, mentions your user name, or boosts or favourites one of your toots, you will receive a notification for this. These will appear as desktop notifications on your computer (if your web browser supports this and you've enabled them) as well as in your "Notifications" column.
![Notification Settings icon](screenshots/notifications-settings.png) You can filter what kind of notifications you see in the Notifications column by clicking the Notification Settings icon at the top of the column and ticking or un-ticking what you do or don't want to see notifications for.
![Clear icon](screenshots/notifications-clear.png) If your notifications become cluttered, you can clear the column by clicking the Clear icon at the top of the column; this will wipe its contents.
![Preferences icon](screenshots/preferences.png) You can also disable notifications from people you don't follow or who don't follow you entirely - to do this, click the Preferences icon in the Compose column, select "Preferences" on the left-hand menu and check either of the respective "Block notifications" options.
#### Mobile Apps
Mastodon has an open API, so anyone can develop a client or app to use Mastodon from anything. Many people have already developed mobile apps for iOS and Android. You can find a list of these [here](Apps.md). Many of these projects are also open source and welcome collaborators.
#### The Public Timelines
In addition to your Home timeline, there are two public timelines available. The Federated Timeline and the Local Timeline. These are both a good way to meet new people to follow or interact with.
##### The Federated Timeline
The Federated Timeline shows all public posts from all users "known" to your instance. This means the user is either on the same instance as you, or somebody on your instance follows that user. The Federated Timeline is a great way to engage in the broad chatter of the world. Following users on remote instances who you meet on the Federated Timeline can lead to meeting more users on more instances and further connecting your instance to more and more of the entire Mastodon and OStatus network.
![Federated Timeline icon](screenshots/federated-timeline.png) To view the federated timeline, click the "Federated Timeline" icon in your Compose column or the respective button on the Getting Started panel. To hide the federated timeline again, simply click the "Back" link at the top of the column while you're viewing it.
#### The Local Timeline
The Local Timeline only shows public posts made by users on your home instance. This can be useful if your instance has particular community norms that users on other instances may not have, such as particular topics that get put under content warnings; or particular in-jokes and shared interests. To view the Local Timeline, click the ![Menu icon](screenshots/compose-menu.png) Menu icon on the Compose pane and then select "Local Timeline" on the rightmost column.
#### Searching
Mastodon has a search function - you can use it to search for users and [hashtags](User-guide.md#hashtags). The search does not look through the entire text of posts, only hashtags. In order to start a search, just type into the search box in the Compose column and hit *enter*; This will open the search pane. The search pane will show suggestions as you type. Selecting any of these will open the user's profile or a view of all toots on the hashtag.
## Privacy, Safety and Security
Mastodon has a number of advanced security, privacy and safety features over more public networks such as Twitter. Particularly the privacy controls are fairly granular; this section will explain how these features work.
#### Two-Factor Authentication
Two-Factor Authentication (2FA) is a mechanism that improves the security of your Mastodon account by requiring a numeric code from another device (most commonly mobile phones) linked to your Mastodon account when you log in - this means that even if someone gets hold of both your e-mail address and your password, they cannot take over your Mastodon account as they would need a physical device you own to log in.
Mastodon's 2FA uses Google Authenticator (or compatible apps, such as Authy). You can install this for free to your [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2) or [iOS](https://itunes.apple.com/gb/app/google-authenticator/id388497605) device; [this Wikipedia page](https://en.wikipedia.org/wiki/Google_Authenticator#Implementations) lists further versions of the app for other systems.
![Preferences icon](screenshots/preferences.png) In order to enable 2FA for your Mastodon account, click the Preferences icon in the Compose column, click "Two-factor Authentication" in the left menu on the settings page and follow the instructions. Once activated, every time you log in you will need a one-time code generated by the Authenticator app on the device you've linked to your account.
#### Account Privacy
To allow you more control over who can see your toots, Mastodon supports "private" or "locked" accounts. If your account is set to private, you will be notified every time someone tries to follow you, and you will be able to allow or deny the follow request. Additionally, if your account is private, any new toots you compose will default to being private (see the [Toot Privacy](User-guide.md#toot-privacy) section below).
![Preferences icon](screenshots/preferences.png) To make your account private, click the Preferences icon in the Compose pane, select "Edit Profile" and tick the "Make account private" checkbox, then click "Save Changes".
![Screenshot of the "Private Account" setting](screenshots/private.png)
#### Toot Privacy
Toot privacy is handled independently of account privacy, and individually for each toot. The four tiers of visibility for toots are Public (default), Unlisted, Private, and Direct. In order to select your privacy level, click the ![Globe icon](screenshots/compose-privacy.png) globe icon. Changes to this setting are remembered between posts, i.e. if you make one private toot, each toot you make will be private until you change it back to public. You can change your default post privacy under preferences.
**Public** is the default status of toots on most accounts. Public toots are visible to any other user on the public timelines, federate to other Mastodon and OStatus instances without restriction, and appear on your user profile page to anyone including search engine bots and visitors who aren't logged into a Mastodon account.
**Unlisted** toots are public, except that they do not appear in the public timelines or search results. They are visible to anyone following you and appear on your profile page to the public even without a Mastodon login. Other than not appearing in the public timelines or search results, they function identically to public posts.
**Private** toots do not appear in the public timeline nor on your profile page to anyone viewing it unless they are on your Followers list. The option is of limited use if your account is not also set to require approval of new followers (as anyone can follow you without confirmation and thus see your private toots). However the separation of this means that if you *do* set your entire account to private, you can switch this option off on a toot to make unlisted or even public toots from your otherwise private account.
Private toots cannot be boosted. If someone you follow makes a private toot, it will appear in your timeline with a padlock icon in place of the Boost icon. **NOTE** that remote instances may not respect this.
Private toots do not federate to other instances, unless you @mention a remote user. In this case, they will federate to their instance, and users on that instance who follow both you and the @mentioned user will see it in their Home timelines. There is no reliable way to check if an instance will actually respect post privacy. Non-Mastodon servers, such as a GNU Social server, do not support Mastodon privacy settings. A user on GNU Social who you @mention in a private post would not even be aware that the post is intended to be private and would be able to boost it, which would undo the privacy setting. There is also no way to guarantee that someone could not just modify the code on their particular Mastodon instance to not respect private post restrictions. A warning will be displayed if you're composing a private toot that will federate to another instance. You should thus think through how much you trust the user you are @mentioning and the instance they are on.
Private posts are not encrypted. Make sure you trust your instance admin not to just read your private posts on the back-end. Do not say anything you would not want potentially intercepted.
**Direct** posts are only visible to users you have @mentioned in them and cannot be boosted. Like with private posts, you should be mindful that the remote instance may not respect this protocol. If you are discussing a sensitive matter you should move the conversation off of Mastodon.
To summarise:
Toot Privacy | Visible on Profile | Visible on Public Timeline | Federates to other instances
------------ | ------------------ | -------------------------- | ---------------------------
Public | Anyone incl. anonymous viewers | Yes | Yes
Unlisted | Anyone incl. anonymous viewers | No | Yes
Private | Followers only | No | Only remote @mentions
Direct | No | No | Only remote @mentions
#### Blocking
You can block a user to stop them contacting you. To do this, you can click or tap the Menu icon on either a toot of theirs or their profile view and select "Block".
**NOTE** that this will stop them from seeing your public toots while they are logged in, but they *will* be able to see your public toots by simply opening your profile in another browser that isn't logged into Mastodon (or logged into a different account that you have not blocked).
Mentions, favourites, boosts or any other interaction with you from a blocked user will be hidden from your view. You will not see replies to a blocked person, even if the reply mentions you, nor will you see their toots if someone boosts them.
The blocked user will not be notified of your blocking them. They will be removed from your followers.
#### Muting
If you do not wish to see posts from a particular user, but do not care about if they see your posts, you may choose to *mute* them. You can mute a user from the same menu on their profile page that you would block them from. You will not see posts from a muted user unless they @mention you. A muted user will have no way to know that you have them muted.
#### Reporting Toots or Users
If you encounter a toot or a user that is breaking the rules of your instance or that you otherwise want to draw the instance administrators' attention to (e.g. if someone is harassing another user, spamming pornography or posting illegal content), you can click the "..." menu button on the toot or the "hamburger" menu on the profile and select to report this. The rightmost column will then switch over to the following form:
![Report form](screenshots/report.png)
In this form, you can select any toots you would like to report to the instance administrators and fill in any comment that might be helpful in identifying or handling the issue (from "is a spammer" to "this post contains untagged pornography"). The report will be visible to server administrators once it is sent so they can take appropriate action, for example hiding the user's posts from the public timeline or banning their account.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -1,535 +1 @@
API overview
============
## Contents
- [Available libraries](#available-libraries)
- [Notes](#notes)
- [Methods](#methods)
- [Accounts](#accounts)
- [Apps](#apps)
- [Blocks](#blocks)
- [Favourites](#favourites)
- [Follow Requests](#follow-requests)
- [Follows](#follows)
- [Instances](#instances)
- [Media](#media)
- [Mutes](#mutes)
- [Notifications](#notifications)
- [Reports](#reports)
- [Search](#search)
- [Statuses](#statuses)
- [Timelines](#timelines)
- [Entities](#entities)
- [Account](#account)
- [Application](#application)
- [Attachment](#attachment)
- [Card](#card)
- [Context](#context)
- [Error](#error)
- [Instance](#instance)
- [Mention](#mention)
- [Notification](#notification)
- [Relationship](#relationship)
- [Results](#results)
- [Status](#status)
- [Tag](#tag)
___
## Available libraries
- [For Ruby](https://github.com/tootsuite/mastodon-api)
- [For Python](https://github.com/halcy/Mastodon.py)
- [For JavaScript](https://github.com/Zatnosk/libodonjs)
- [For JavaScript (Node.js)](https://github.com/jessicahayley/node-mastodon)
- [For Elixir](https://github.com/milmazz/hunter)
___
## Notes
### Parameter types
When an array parameter is mentioned, the Rails convention of specifying array parameters in query strings is meant.
For example, a ruby array like `foo = [1, 2, 3]` can be encoded in the params as `foo[]=1&foo[]=2&foo[]=3`.
Square brackets can be indexed but can also be empty.
When a file parameter is mentioned, a form-encoded upload is expected.
### Selecting ranges
For most `GET` operations that return arrays, the query parameters `max_id` and `since_id` can be used to specify the range of IDs to return.
API methods that return collections of items can return a `Link` header containing URLs for the `next` and `prev` pages.
See the [Link header RFC](https://tools.ietf.org/html/rfc5988) for more information.
### Errors
If the request you make doesn't go through, Mastodon will usually respond with an [Error](#error).
___
## Methods
### Accounts
#### Fetching an account:
GET /api/v1/accounts/:id
Returns an [Account](#account).
#### Getting the current user:
GET /api/v1/accounts/verify_credentials
Returns the authenticated user's [Account](#account).
#### Updating the current user:
PATCH /api/v1/accounts/update_credentials
Form data:
- `display_name`: The name to display in the user's profile
- `note`: A new biography for the user
- `avatar`: A base64 encoded image to display as the user's avatar (e.g. `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...`)
- `header`: A base64 encoded image to display as the user's header image (e.g. `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...`)
#### Getting an account's followers:
GET /api/v1/accounts/:id/followers
Returns an array of [Accounts](#account).
#### Getting who account is following:
GET /api/v1/accounts/:id/following
Returns an array of [Accounts](#account).
#### Getting an account's statuses:
GET /api/v1/accounts/:id/statuses
Query parameters:
- `only_media` (optional): Only return statuses that have media attachments
- `exclude_replies` (optional): Skip statuses that reply to other statuses
Returns an array of [Statuses](#status).
#### Following/unfollowing an account:
GET /api/v1/accounts/:id/follow
GET /api/v1/accounts/:id/unfollow
Returns the target [Account](#account).
#### Blocking/unblocking an account:
GET /api/v1/accounts/:id/block
GET /api/v1/accounts/:id/unblock
Returns the target [Account](#account).
#### Muting/unmuting an account:
GET /api/v1/accounts/:id/mute
GET /api/v1/accounts/:id/unmute
Returns the target [Account](#account).
#### Getting an account's relationships:
GET /api/v1/accounts/relationships
Query parameters:
- `id` (can be array): Account IDs
Returns an array of [Relationships](#relationships) of the current user to a list of given accounts.
#### Searching for accounts:
GET /api/v1/accounts/search
Query parameters:
- `q`: What to search for
- `limit`: Maximum number of matching accounts to return (default: `40`)
Returns an array of matching [Accounts](#accounts).
Will lookup an account remotely if the search term is in the `username@domain` format and not yet in the database.
### Apps
#### Registering an application:
POST /api/v1/apps
Form data:
- `client_name`: Name of your application
- `redirect_uris`: Where the user should be redirected after authorization (for no redirect, use `urn:ietf:wg:oauth:2.0:oob`)
- `scopes`: This can be a space-separated list of the following items: "read", "write" and "follow" (see [this page](OAuth-details.md) for details on what the scopes do)
- `website`: (optional) URL to the homepage of your app
Creates a new OAuth app.
Returns `id`, `client_id` and `client_secret` which can be used with [OAuth authentication in your 3rd party app](Testing-with-cURL.md).
These values should be requested in the app itself from the API for each new app install + mastodon domain combo, and stored in the app for future requests.
### Blocks
#### Fetching a user's blocks:
GET /api/v1/blocks
Returns an array of [Accounts](#account) blocked by the authenticated user.
### Favourites
#### Fetching a user's favourites:
GET /api/v1/favourites
Returns an array of [Statuses](#status) favourited by the authenticated user.
### Follow Requests
#### Fetching a list of follow requests:
GET /api/v1/follow_requests
Returns an array of [Accounts](#account) which have requested to follow the authenticated user.
#### Authorizing or rejecting follow requests:
POST /api/v1/follow_requests/authorize
POST /api/v1/follow_requests/reject
Form data:
- `id`: The id of the account to authorize or reject
Returns an empty object.
### Follows
#### Following a remote user:
POST /api/v1/follows
Form data:
- `uri`: `username@domain` of the person you want to follow
Returns the local representation of the followed account, as an [Account](#account).
### Instances
#### Getting instance information:
GET /api/v1/instance
Returns the current [Instance](#instance).
Does not require authentication.
### Media
#### Uploading a media attachment:
POST /api/v1/media
Form data:
- `file`: Media to be uploaded
Returns an [Attachment](#attachment) that can be used when creating a status.
### Mutes
#### Fetching a user's mutes:
GET /api/v1/mutes
Returns an array of [Accounts](#account) muted by the authenticated user.
### Notifications
#### Fetching a user's notifications:
GET /api/v1/notifications
Returns a list of [Notifications](#notification) for the authenticated user.
#### Getting a single notification:
GET /api/v1/notifications/:id
Returns the [Notification](#notification).
#### Clearing notifications:
POST /api/v1/notifications/clear
Deletes all notifications from the Mastodon server for the authenticated user.
Returns an empty object.
### Reports
#### Fetching a user's reports:
GET /api/v1/reports
Returns a list of [Reports](#report) made by the authenticated user.
#### Reporting a user:
POST /api/v1/reports
Form data:
- `account_id`: The ID of the account to report
- `status_ids`: The IDs of statuses to report (can be an array)
- `comment`: A comment to associate with the report.
Returns the finished [Report](#report).
### Search
#### Searching for content:
GET /api/v1/search
Form data:
- `q`: The search query
- `resolve`: Whether to resolve non-local accounts
Returns [Results](#results).
If `q` is a URL, Mastodon will attempt to fetch the provided account or status.
Otherwise, it will do a local account and hashtag search.
### Statuses
#### Fetching a status:
GET /api/v1/statuses/:id
Returns a [Status](#status).
#### Getting status context:
GET /api/v1/statuses/:id/context
Returns a [Context](#context).
#### Getting a card associated with a status:
GET /api/v1/statuses/:id/card
Returns a [Card](#card).
#### Getting who reblogged/favourited a status:
GET /api/v1/statuses/:id/reblogged_by
GET /api/v1/statuses/:id/favourited_by
Returns an array of [Accounts](#account).
#### Posting a new status:
POST /api/v1/statuses
Form data:
- `status`: The text of the status
- `in_reply_to_id` (optional): local ID of the status you want to reply to
- `media_ids` (optional): array of media IDs to attach to the status (maximum 4)
- `sensitive` (optional): set this to mark the media of the status as NSFW
- `spoiler_text` (optional): text to be shown as a warning before the actual content
- `visibility` (optional): either "direct", "private", "unlisted" or "public"
Returns the new [Status](#status).
#### Deleting a status:
DELETE /api/v1/statuses/:id
Returns an empty object.
#### Reblogging/unreblogging a status:
POST /api/v1/statuses/:id/reblog
POST /api/v1/statuses/:id/unreblog
Returns the target [Status](#status).
#### Favouriting/unfavouriting a status:
POST /api/v1/statuses/:id/favourite
POST /api/v1/statuses/:id/unfavourite
Returns the target [Status](#status).
### Timelines
#### Retrieving a timeline:
GET /api/v1/timelines/home
GET /api/v1/timelines/public
GET /api/v1/timelines/tag/:hashtag
Query parameters:
- `local` (optional; public and tag timelines only): Only return statuses originating from this instance
Returns an array of [Statuses](#status), most recent ones first.
___
## Entities
### Account
| Attribute | Description |
| ------------------------ | ----------- |
| `id` | The ID of the account |
| `username` | The username of the account |
| `acct` | Equals `username` for local users, includes `@domain` for remote ones |
| `display_name` | The account's display name |
| `note` | Biography of user |
| `url` | URL of the user's profile page (can be remote) |
| `avatar` | URL to the avatar image |
| `header` | URL to the header image |
| `locked` | Boolean for when the account cannot be followed without waiting for approval first |
| `created_at` | The time the account was created |
| `followers_count` | The number of followers for the account |
| `following_count` | The number of accounts the given account is following |
| `statuses_count` | The number of statuses the account has made |
### Application
| Attribute | Description |
| ------------------------ | ----------- |
| `name` | Name of the app |
| `website` | Homepage URL of the app |
### Attachment
| Attribute | Description |
| ------------------------ | ----------- |
| `id` | ID of the attachment |
| `type` | One of: "image", "video", "gifv" |
| `url` | URL of the locally hosted version of the image |
| `remote_url` | For remote images, the remote URL of the original image |
| `preview_url` | URL of the preview image |
| `text_url` | Shorter URL for the image, for insertion into text (only present on local images) |
### Card
| Attribute | Description |
| ------------------------ | ----------- |
| `url` | The url associated with the card |
| `title` | The title of the card |
| `description` | The card description |
| `image` | The image associated with the card, if any |
### Context
| Attribute | Description |
| ------------------------ | ----------- |
| `ancestors` | The ancestors of the status in the conversation, as a list of [Statuses](#status) |
| `descendants` | The descendants of the status in the conversation, as a list of [Statuses](#status) |
### Error
| Attribute | Description |
| ------------------------ | ----------- |
| `error` | A textual description of the error |
### Instance
| Attribute | Description |
| ------------------------ | ----------- |
| `uri` | URI of the current instance |
| `title` | The instance's title |
| `description` | A description for the instance |
| `email` | An email address which can be used to contact the instance administrator |
### Mention
| Attribute | Description |
| ------------------------ | ----------- |
| `url` | URL of user's profile (can be remote) |
| `username` | The username of the account |
| `acct` | Equals `username` for local users, includes `@domain` for remote ones |
| `id` | Account ID |
### Notification
| Attribute | Description |
| ------------------------ | ----------- |
| `id` | The notification ID |
| `type` | One of: "mention", "reblog", "favourite", "follow" |
| `created_at` | The time the notification was created |
| `account` | The [Account](#account) sending the notification to the user |
| `status` | The [Status](#status) associated with the notification, if applicable |
### Relationship
| Attribute | Description |
| ------------------------ | ----------- |
| `following` | Whether the user is currently following the account |
| `followed_by` | Whether the user is currently being followed by the account |
| `blocking` | Whether the user is currently blocking the account |
| `muting` | Whether the user is currently muting the account |
| `requested` | Whether the user has requested to follow the account |
### Report
| Attribute | Description |
| ------------------------ | ----------- |
| `id` | The ID of the report |
| `action_taken` | The action taken in response to the report |
### Results
| Attribute | Description |
| ------------------------ | ----------- |
| `accounts` | An array of matched [Accounts](#account) |
| `statuses` | An array of matchhed [Statuses](#status) |
| `hashtags` | An array of matched hashtags, as strings |
### Status
| Attribute | Description |
| ------------------------ | ----------- |
| `id` | The ID of the status |
| `uri` | A Fediverse-unique resource ID |
| `url` | URL to the status page (can be remote) |
| `account` | The [Account](#account) which posted the status |
| `in_reply_to_id` | `null` or the ID of the status it replies to |
| `in_reply_to_account_id` | `null` or the ID of the account it replies to |
| `reblog` | `null` or the reblogged [Status](#status) |
| `content` | Body of the status; this will contain HTML (remote HTML already sanitized) |
| `created_at` | The time the status was created |
| `reblogs_count` | The number of reblogs for the status |
| `favourites_count` | The number of favourites for the status |
| `reblogged` | Whether the authenticated user has reblogged the status |
| `favourited` | Whether the authenticated user has favourited the status |
| `sensitive` | Whether media attachments should be hidden by default |
| `spoiler_text` | If not empty, warning text that should be displayed before the actual content |
| `visibility` | One of: `public`, `unlisted`, `private`, `direct` |
| `media_attachments` | An array of [Attachments](#attachment) |
| `mentions` | An array of [Mentions](#mention) |
| `tags` | An array of [Tags](#tag) |
| `application` | [Application](#application) from which the status was posted |
### Tag
| Attribute | Description |
| ------------------------ | ----------- |
| `name` | The hashtag, not including the preceding `#` |
| `url` | The URL of the hashtag |
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)

View File

@ -1,12 +1 @@
OAuth details
=============
We use the [Doorkeeper gem for OAuth](https://github.com/doorkeeper-gem/doorkeeper/wiki), so you can refer to their docs on specifics of the end-points.
The API is divided up into access scopes:
- `read`: Read data
- `write`: Post statuses and upload media for statuses
- `follow`: Follow, unfollow, block, unblock
Multiple scopes can be requested during the authorization phase with the `scope` query param (space-separate the scopes). If you do not specify a `scope` in your authorization request, the resulting access token will default to `read` access.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/OAuth-details.md)

View File

@ -1,4 +1 @@
Push notifications
==================
See <https://github.com/Gargron/tusky-api> for an example of how to create push notifications for a mobile app. It involves using the Mastodon streaming API on behalf of the app's users, as a sort of proxy.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Push-notifications.md)

View File

@ -1,40 +1 @@
Streaming API
=============
Your application can use a server-sent events endpoint to receive updates in real-time. Server-sent events is an incredibly simple transport method that relies entirely on chunked-encoding transfer, i.e. the HTTP connection is kept open and receives new data periodically.
### Endpoints:
**GET /api/v1/streaming/user**
Returns events that are relevant to the authorized user, i.e. home timeline and notifications
**GET /api/v1/streaming/public**
Returns all public statuses
**GET /api/v1/streaming/hashtag**
Returns all public statuses for a particular hashtag (query param `tag`)
### Stream contents
The stream will contain events as well as heartbeat comments. Lines that begin with a colon (`:`) can be ignored by parsers, they are simply there to keep the connection open. Events have this structure:
```
event: name
data: payload
```
[See MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format)
### Event types
|Event|Description|What's in the payload|
|-----|-----------|---------------------|
|`update`|A new status has appeared!|Status|
|`notification`|A new notification|Notification|
|`delete`|A status has been deleted|ID of the deleted status|
The payload is JSON-encoded.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Streaming-API.md)

View File

@ -1,18 +1 @@
Testing the API with cURL
=========================
Mastodon builds around the idea of being a server first, rather than a client itself. Similarly to how a XMPP chat server communicates with others and with its own clients, Mastodon takes care of federation to other networks, like other Mastodon or GNU Social instances. So Mastodon provides a REST API, and a 3rd-party app system for using it via OAuth2.
You can get a client ID and client secret required for OAuth [via an API end-point](API.md#apps).
From these two, you will need to acquire an access token. It is possible to do using your account's e-mail and password like this:
curl -X POST -d "client_id=CLIENT_ID_HERE&client_secret=CLIENT_SECRET_HERE&grant_type=password&username=YOUR_EMAIL&password=YOUR_PASSWORD" -Ss https://mastodon.social/oauth/token
The `/oauth/token` path will attempt to login with the given credentials, and then retrieve the access token for the current user. If the login failed the response will be a 302 redirect to `/auth/sign_in`. Otherwise the response will be a JSON object containing the key `access_token`.
Use that token in any API requests by setting a header like this:
curl --header "Authorization: Bearer ACCESS_TOKEN_HERE" -sS https://mastodon.social/api/v1/timelines/home
Please note that the password-based approach is not recommended especially if you're dealing with other user's accounts and not just your own. Usually you would use the authorization grant approach where you redirect the user to a web page on the original site where they can login and authorize the application and are then redirected back to your application with an access code.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Testing-with-cURL.md)

View File

@ -1,16 +1 @@
Tips for app developers
=======================
## Authentication
Make sure that you allow your users to specify the domain they want to connect to before login. Use that domain to acquire a client id/secret for OAuth2 and then proceed with normal OAuth2 also using that domain to build the URLs.
In my opinion it is easier for people to understand what is being asked of them if you ask for a `username@domain` type input, since it looks like an e-mail address. Though the username part is not required for anything in the OAuth2 process. Once the user is logged in, you get information about the logged in user from `/api/v1/accounts/verify_credentials`
## Usernames
Make sure that you make it possible to see the `acct` of any user in your app (since it includes the domain part for remote users), people must be able to tell apart users from different domains with the same username.
## Formatting
The API delivers already formatted HTML to your app. This isn't ideal since not all apps are based on HTML, but this is not fixable as it's part of the way OStatus federation works. Most importantly, you get some information on linked entities alongside the HTML of the status body. For example, you get a list of mentioned users, and a list of media attachments, and a list of hashtags. It is possible to convert the HTML to whatever you need in your app by parsing the HTML tags and matching their `href`s to the linked entities. If a match cannot be found, the link must stay a clickable link.
[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Tips-for-app-developers.md)

View File

@ -75,6 +75,13 @@ namespace :mastodon do
end
end
namespace :users do
desc 'clear unconfirmed users'
task clear: :environment do
User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).find_each(&:destroy)
end
end
namespace :maintenance do
desc 'Update counter caches'
task update_counter_caches: :environment do

View File

@ -30,7 +30,7 @@
"escape-html": "^1.0.3",
"eventsource": "^0.2.1",
"express": "^4.14.1",
"http-link-header": "^0.5.0",
"http-link-header": "^0.8.0",
"immutable": "^3.8.1",
"intl": "^1.2.5",
"jsdom": "^9.11.0",

View File

@ -0,0 +1,17 @@
require 'rails_helper'
describe Settings::Exports::BlockedAccountsController do
before do
sign_in Fabricate(:user), scope: :user
end
describe 'GET #index' do
it 'returns a csv of the blocking accounts' do
get :index, format: :csv
expect(response).to have_http_status(:success)
expect(response.content_type).to eq 'text/csv'
expect(response.headers['Content-Disposition']).to eq 'attachment; filename="blocking.csv"'
end
end
end

View File

@ -0,0 +1,17 @@
require 'rails_helper'
describe Settings::Exports::FollowingAccountsController do
before do
sign_in Fabricate(:user), scope: :user
end
describe 'GET #index' do
it 'returns a csv of the following accounts' do
get :index, format: :csv
expect(response).to have_http_status(:success)
expect(response.content_type).to eq 'text/csv'
expect(response.headers['Content-Disposition']).to eq 'attachment; filename="following.csv"'
end
end
end

Some files were not shown because too many files have changed in this diff Show More