diff options
105 files changed, 953 insertions, 601 deletions
diff --git a/.env.production.sample b/.env.production.sample index a3da10b97..52d519570 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -6,16 +6,13 @@ DB_USER=postgres DB_NAME=postgres DB_PASS= DB_PORT=5432 -NEO4J_HOST=neo4j -NEO4J_PORT=7474 # Federation LOCAL_DOMAIN=example.com LOCAL_HTTPS=true # Application secrets -# These are arbitrary strings. They should be long and cryptographically secure. -# For Docker, `docker-compose run --rm web rake secret` will generate them. +# Generate each with the `rake secret` task PAPERCLIP_SECRET= SECRET_KEY_BASE= @@ -25,3 +22,13 @@ SMTP_PORT=587 SMTP_LOGIN= SMTP_PASSWORD= SMTP_FROM_ADDRESS=notifications@example.com + +# Optional asset host for multi-server setups +# CDN_HOST=assets.example.com + +# S3 (optional) +S3_ENABLED=false +S3_BUCKET= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +S3_REGION= diff --git a/.travis.yml b/.travis.yml index f6841779d..fe4549edd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,8 +11,6 @@ env: - LOCAL_DOMAIN=cb6e6126.ngrok.io - LOCAL_HTTPS=true - RAILS_ENV=test - - NEO4J_HOST=localhost - - NEO4J_PORT=7575 addons: postgresql: 9.4 diff --git a/Dockerfile.app b/Dockerfile index dfc0ad5b7..dfc0ad5b7 100644 --- a/Dockerfile.app +++ b/Dockerfile diff --git a/Dockerfile.neo4j b/Dockerfile.neo4j deleted file mode 100644 index 373c2abb0..000000000 --- a/Dockerfile.neo4j +++ /dev/null @@ -1,17 +0,0 @@ -FROM neo4j:latest - -ENV NEO4J_AUTH=none - -RUN cd /var/lib/neo4j/plugins \ - && wget http://products.graphaware.com/download/framework-server-community/graphaware-server-community-all-3.0.6.43.jar \ - && wget http://products.graphaware.com/download/noderank/graphaware-noderank-3.0.6.43.3.jar -RUN echo "dbms.unmanaged_extension_classes=com.graphaware.server=/graphaware" >> /var/lib/neo4j/conf/neo4j.conf -RUN echo 'com.graphaware.runtime.enabled=true\n\ -com.graphaware.module.NR.1=com.graphaware.module.noderank.NodeRankModuleBootstrapper\n\ -com.graphaware.module.NR.maxTopRankNodes=10\n\ -com.graphaware.module.NR.dampingFactor=0.85\n\ -com.graphaware.module.NR.propertyKey=nodeRank\n'\ - >> /var/lib/neo4j/conf/neo4j.conf -RUN echo 'com.graphaware.runtime.stats.disabled=true\n\ -com.graphaware.server.stats.disabled=true\n'\ - >> /var/lib/neo4j/conf/neo4j.conf diff --git a/Gemfile b/Gemfile index 327a17ee9..95fd04629 100644 --- a/Gemfile +++ b/Gemfile @@ -17,9 +17,9 @@ gem 'pghero' gem 'dotenv-rails' gem 'font-awesome-rails' -gem 'paperclip', '~> 4.3' +gem 'paperclip', '~> 5.0' gem 'paperclip-av-transcoder' -gem 'aws-sdk', '< 2.0' +gem 'aws-sdk', '>= 2.0' gem 'http' gem 'httplog' @@ -41,20 +41,15 @@ gem 'simple_form' gem 'will_paginate' gem 'rack-attack' gem 'rack-cors', require: 'rack/cors' +gem 'rack-timeout-puma' gem 'sidekiq' gem 'ledermann-rails-settings' -gem 'neography' gem 'pg_search' gem 'react-rails' gem 'browserify-rails' gem 'autoprefixer-rails' -gem 'rack-mini-profiler', require: false -gem 'flamegraph' -gem 'stackprof' -gem 'memory_profiler' - group :development, :test do gem 'rspec-rails' gem 'pry-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 28ad1abb6..aa9f59da8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,11 +70,14 @@ GEM execjs av (0.9.0) cocaine (~> 0.5.3) - aws-sdk (1.66.0) - aws-sdk-v1 (= 1.66.0) - aws-sdk-v1 (1.66.0) - json (~> 1.4) - nokogiri (>= 1.4.4) + aws-sdk (2.6.28) + aws-sdk-resources (= 2.6.28) + aws-sdk-core (2.6.28) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-resources (2.6.28) + aws-sdk-core (= 2.6.28) + aws-sigv4 (1.0.0) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) @@ -132,11 +135,9 @@ GEM thread thread_safe erubis (2.7.0) - excon (0.53.0) execjs (2.7.0) fabrication (2.15.2) fast_blank (1.0.0) - flamegraph (0.9.5) font-awesome-rails (4.6.3.1) railties (>= 3.2, < 5.1) fuubar (2.1.1) @@ -186,6 +187,7 @@ GEM jbuilder (2.6.0) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) + jmespath (1.3.1) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -206,38 +208,29 @@ GEM nokogiri (>= 1.5.9) mail (2.6.4) mime-types (>= 1.16, < 4) - memory_profiler (0.9.7) method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) - mimemagic (0.3.0) + mimemagic (0.3.2) mini_portile2 (2.1.0) minitest (5.9.1) multi_json (1.12.1) - neography (1.8.0) - excon (>= 0.33.0) - json (>= 1.7.7) - multi_json (>= 1.3.2) - os (>= 0.9.6) - rake (>= 0.8.7) - rubyzip (>= 1.0.0) nio4r (1.2.1) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) oj (2.17.3) orm_adapter (0.5.0) - os (0.9.6) ostatus2 (1.0.2) addressable (~> 2.4) http (~> 2.0) nokogiri (~> 1.6) - paperclip (4.3.7) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) + paperclip (5.1.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) cocaine (~> 0.5.5) mime-types - mimemagic (= 0.3.0) + mimemagic (~> 0.3.0) paperclip-av-transcoder (0.6.4) av (~> 0.9.0) paperclip (>= 2.5.2) @@ -264,12 +257,13 @@ GEM rack-attack (5.0.1) rack rack-cors (0.4.0) - rack-mini-profiler (0.10.1) - rack (>= 1.2.0) rack-protection (1.5.3) rack rack-test (0.6.3) rack (>= 1.0) + rack-timeout (0.4.2) + rack-timeout-puma (0.0.1) + rack-timeout (~> 0.2, >= 0.2.0) rails-dom-testing (2.0.1) activesupport (>= 4.2.0, < 6.0) nokogiri (~> 1.6.0) @@ -343,7 +337,6 @@ GEM ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.8.1) - rubyzip (1.2.0) safe_yaml (1.0.4) sass (3.4.22) sass-rails (5.0.6) @@ -376,7 +369,6 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - stackprof (0.2.10) temple (0.7.7) term-ansicolor (1.4.0) tins (~> 1.0) @@ -414,7 +406,7 @@ DEPENDENCIES active_record_query_trace addressable autoprefixer-rails - aws-sdk (< 2.0) + aws-sdk (>= 2.0) better_errors binding_of_caller browserify-rails @@ -425,7 +417,6 @@ DEPENDENCIES dotenv-rails fabrication fast_blank - flamegraph font-awesome-rails fuubar goldfinger @@ -441,12 +432,10 @@ DEPENDENCIES letter_opener link_header lograge - memory_profiler - neography nokogiri oj ostatus2 - paperclip (~> 4.3) + paperclip (~> 5.0) paperclip-av-transcoder pg pg_search @@ -456,7 +445,7 @@ DEPENDENCIES rabl rack-attack rack-cors - rack-mini-profiler + rack-timeout-puma rails! rails_12factor rails_autolink @@ -471,7 +460,6 @@ DEPENDENCIES sidekiq simple_form simplecov - stackprof uglifier (>= 1.3.0) webmock will_paginate diff --git a/README.md b/README.md index 25d179d86..00472a616 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,6 @@ Consult the example configuration file, `.env.production.sample` for the full li - PostgreSQL - Redis -- Neo4J (optional) - - GraphAware NodeRank ## Running with Docker and Docker-Compose @@ -90,8 +88,8 @@ The container has two volumes, for the assets and for user uploads. The default - `rake mastodon:media:clear` removes uploads that have not been attached to any status after a while, you would want to run this from a periodic cronjob - `rake mastodon:push:clear` unsubscribes from PuSH notifications for remote users that have no local followers. You may not want to actually do that, to keep a fuller footprint of the fediverse or in case your users will soon re-follow - `rake mastodon:push:refresh` re-subscribes PuSH for expiring remote users, this should be run periodically from a cronjob and quite often as the expiration time depends on the particular hub of the remote user -- `rake mastodon:feeds:clear` removes all timelines, which forces them to be re-built on the fly next time a user tries to fetch their home/mentions timeline. Only for troubleshooting -- `rake mastodon:graphs:sync` re-imports all follow relationships into Neo4J. Only for troubleshooting +- `rake mastodon:feeds:clear_all` removes all timelines, which forces them to be re-built on the fly next time a user tries to fetch their home/mentions timeline. Only for troubleshooting +- `rake mastodon:feeds:clear` removes timelines of users who haven't signed in lately, which allows to save RAM and improve message distribution. This is required to be run periodically so that when they login again the regeneration process will trigger Running any of these tasks via docker-compose would look like this: @@ -117,6 +115,8 @@ Which will re-create the updated containers, leaving databases and data as is. D You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. This section may be updated with more details in the future. +**IRC channel**: #mastodon on irc.freenode.net + ### Extra credits - The [Emoji One](https://github.com/Ranks/emojione) pack has been used for the emojis diff --git a/app/assets/javascripts/components/actions/suggestions.jsx b/app/assets/javascripts/components/actions/suggestions.jsx deleted file mode 100644 index 6b3aa69dd..000000000 --- a/app/assets/javascripts/components/actions/suggestions.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import api from '../api'; - -export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; -export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; -export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; - -export function fetchSuggestions() { - return (dispatch, getState) => { - dispatch(fetchSuggestionsRequest()); - - api(getState).get('/api/v1/accounts/suggestions').then(response => { - dispatch(fetchSuggestionsSuccess(response.data)); - }).catch(error => { - dispatch(fetchSuggestionsFail(error)); - }); - }; -}; - -export function fetchSuggestionsRequest() { - return { - type: SUGGESTIONS_FETCH_REQUEST - }; -}; - -export function fetchSuggestionsSuccess(accounts) { - return { - type: SUGGESTIONS_FETCH_SUCCESS, - accounts: accounts - }; -}; - -export function fetchSuggestionsFail(error) { - return { - type: SUGGESTIONS_FETCH_FAIL, - error: error - }; -}; diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index 603561ab3..df5f0f2c2 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -82,7 +82,7 @@ const Status = React.createClass({ ); } - if (status.get('media_attachments').size > 0) { + if (status.get('media_attachments').size > 0 && !this.props.muted) { if (status.getIn(['media_attachments', 0, 'type']) === 'video') { media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />; } else { diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index c9f037ec2..c42582bfd 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -39,6 +39,8 @@ 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 getMessagesForLocale from '../locales'; const store = configureStore(); @@ -47,7 +49,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({ basename: '/web' }); -addLocaleData([...en, ...de, ...es, ...fr]); +addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu]); const Mastodon = React.createClass({ diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index d39a06062..b890e15c1 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -47,7 +47,7 @@ const Header = React.createClass({ const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; return ( - <div style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover', position: 'relative' }}> + <div style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover', backgroundPosition: 'center', position: 'relative' }}> <div style={{ background: 'rgba(47, 52, 65, 0.9)', padding: '20px 10px' }}> <a href={account.get('url')} target='_blank' rel='noopener' style={{ display: 'block', color: 'inherit', textDecoration: 'none' }}> <div style={{ width: '90px', margin: '0 auto', marginBottom: '10px' }}> diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index 5ad1ca172..b16731c05 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -55,7 +55,8 @@ const textareaStyle = { padding: '10px', fontFamily: 'Roboto', fontSize: '14px', - margin: '0' + margin: '0', + resize: 'vertical' }; const renderInputComponent = inputProps => ( diff --git a/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx b/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx deleted file mode 100644 index 6850629ba..000000000 --- a/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import AccountContainer from '../../../containers/account_container'; -import { FormattedMessage } from 'react-intl'; - -const outerStyle = { - position: 'relative' -}; - -const headerStyle = { - fontSize: '14px', - fontWeight: '500', - display: 'block', - padding: '10px', - color: '#9baec8', - background: '#454b5e', - overflow: 'hidden' -}; - -const nextStyle = { - display: 'inline-block', - float: 'right', - fontWeight: '400', - color: '#2b90d9' -}; - -const SuggestionsBox = React.createClass({ - - propTypes: { - accountIds: ImmutablePropTypes.list, - perWindow: React.PropTypes.number - }, - - getInitialState () { - return { - index: 0 - }; - }, - - getDefaultProps () { - return { - perWindow: 2 - }; - }, - - mixins: [PureRenderMixin], - - handleNextClick (e) { - e.preventDefault(); - - let newIndex = this.state.index + 1; - - if (this.props.accountIds.skip(this.props.perWindow * newIndex).size === 0) { - newIndex = 0; - } - - this.setState({ index: newIndex }); - }, - - render () { - const { accountIds, perWindow } = this.props; - - if (!accountIds || accountIds.size === 0) { - return <div />; - } - - let nextLink = ''; - - if (accountIds.size > perWindow) { - nextLink = <a href='#' style={nextStyle} onClick={this.handleNextClick}><FormattedMessage id='suggestions_box.refresh' defaultMessage='Refresh' /></a>; - } - - return ( - <div style={outerStyle}> - <strong style={headerStyle}> - <FormattedMessage id='suggestions_box.who_to_follow' defaultMessage='Who to follow' /> {nextLink} - </strong> - - {accountIds.skip(perWindow * this.state.index).take(perWindow).map(accountId => <AccountContainer key={accountId} id={accountId} withNote={false} />)} - </div> - ); - } - -}); - -export default SuggestionsBox; diff --git a/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx b/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx deleted file mode 100644 index 944ceed85..000000000 --- a/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import SuggestionsBox from '../components/suggestions_box'; - -const mapStateToProps = (state) => ({ - accountIds: state.getIn(['user_lists', 'suggestions']) -}); - -export default connect(mapStateToProps)(SuggestionsBox); diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx index 5c1b22e00..4017c8949 100644 --- a/app/assets/javascripts/components/features/compose/index.jsx +++ b/app/assets/javascripts/components/features/compose/index.jsx @@ -3,9 +3,7 @@ import ComposeFormContainer from './containers/compose_form_container'; import UploadFormContainer from './containers/upload_form_container'; import NavigationContainer from './containers/navigation_container'; import PureRenderMixin from 'react-addons-pure-render-mixin'; -import SuggestionsContainer from './containers/suggestions_container'; import SearchContainer from './containers/search_container'; -import { fetchSuggestions } from '../../actions/suggestions'; import { connect } from 'react-redux'; import { mountCompose, unmountCompose } from '../../actions/compose'; @@ -19,7 +17,6 @@ const Compose = React.createClass({ componentDidMount () { this.props.dispatch(mountCompose()); - this.props.dispatch(fetchSuggestions()); }, componentWillUnmount () { @@ -29,14 +26,10 @@ const Compose = React.createClass({ render () { return ( <Drawer> - <div style={{ flex: '1 1 auto' }}> - <SearchContainer /> - <NavigationContainer /> - <ComposeFormContainer /> - <UploadFormContainer /> - </div> - - <SuggestionsContainer /> + <SearchContainer /> + <NavigationContainer /> + <ComposeFormContainer /> + <UploadFormContainer /> </Drawer> ); } diff --git a/app/assets/javascripts/components/locales/de.jsx b/app/assets/javascripts/components/locales/de.jsx index 85412635e..4e2a70edb 100644 --- a/app/assets/javascripts/components/locales/de.jsx +++ b/app/assets/javascripts/components/locales/de.jsx @@ -41,8 +41,6 @@ const en = { "search.placeholder": "Suche", "search.account": "Konto", "search.hashtag": "Hashtag", - "suggestions_box.who_to_follow": "Wem folgen", - "suggestions_box.refresh": "Aktualisieren", "upload_button.label": "Media-Datei anfügen", "upload_form.undo": "Entfernen", "notification.follow": "{name} folgt dir", diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 0ea324f66..41a44e3dc 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -5,9 +5,9 @@ const en = { "status.mention": "Mention", "status.delete": "Delete", "status.reply": "Reply", - "status.reblog": "Reblog", + "status.reblog": "Boost", "status.favourite": "Favourite", - "status.reblogged_by": "{name} reblogged", + "status.reblogged_by": "{name} boosted", "status.sensitive_warning": "Sensitive content", "status.sensitive_toggle": "Click to view", "video_player.toggle_sound": "Toggle sound", @@ -45,13 +45,11 @@ const en = { "search.placeholder": "Search", "search.account": "Account", "search.hashtag": "Hashtag", - "suggestions_box.who_to_follow": "Who to follow", - "suggestions_box.refresh": "Refresh", "upload_button.label": "Add media", "upload_form.undo": "Undo", "notification.follow": "{name} followed you", "notification.favourite": "{name} favourited your status", - "notification.reblog": "{name} reblogged your status", + "notification.reblog": "{name} boosted your status", "notification.mention": "{name} mentioned you" }; diff --git a/app/assets/javascripts/components/locales/es.jsx b/app/assets/javascripts/components/locales/es.jsx index 47377e5ae..d4434bba7 100644 --- a/app/assets/javascripts/components/locales/es.jsx +++ b/app/assets/javascripts/components/locales/es.jsx @@ -42,8 +42,6 @@ const es = { "search.placeholder": "Buscar", "search.account": "Cuenta", "search.hashtag": "Etiqueta", - "suggestions_box.who_to_follow": "A quién seguir", - "suggestions_box.refresh": "Refrescar", "upload_button.label": "Añadir medio", "upload_form.undo": "Deshacer", "notification.follow": "{name} le esta ahora siguiendo", diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index 0cf4c5d52..c4458a145 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -7,22 +7,24 @@ const fr = { "status.reply": "Répondre", "status.reblog": "Partager", "status.favourite": "Ajouter aux favoris", - "status.reblogged_by": "{name} a partagé", + "status.reblogged_by": "{name} a partagé :", + "status.sensitive_warning": "Contenu délicat", + "status.sensitive_toggle": "Cliquer pour dévoiler", "video_player.toggle_sound": "Mettre/Couper le son", "account.mention": "Mentionner", "account.edit_profile": "Modifier le profil", "account.unblock": "Débloquer", - "account.unfollow": "Se désabonner", + "account.unfollow": "Ne plus suivre", "account.block": "Bloquer", - "account.follow": "S’abonner", + "account.follow": "Suivre", "account.posts": "Statuts", "account.follows": "Abonnements", "account.followers": "Abonnés", "account.follows_you": "Vous suit", "getting_started.heading": "Pour commencer", - "getting_started.about_addressing": "Vous pouvez vous abonner aux statuts de quelqu’un en entrant dans le champs de recherche leur nom d’utilisateur et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.", - "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, le nom d’utilisateur suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.", - "getting_started.about_developer": "Pour s’abonner au développeur de ce projet, c’est Gargron@mastodon.social", + "getting_started.about_addressing": "Vous pouvez vous suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.", + "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.", + "getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social", "column.home": "Accueil", "column.mentions": "Mentions", "column.public": "Fil public", @@ -32,23 +34,22 @@ const fr = { "tabs_bar.mentions": "Mentions", "tabs_bar.public": "Public", "tabs_bar.notifications": "Notifications", - "compose_form.placeholder": "Qu’avez vous en tête ?", + "compose_form.placeholder": "Qu’avez-vous en tête ?", "compose_form.publish": "Pouet", + "compose_form.sensitive": "Marquer le contenu comme délicat", "navigation_bar.settings": "Paramètres", "navigation_bar.public_timeline": "Public", - "navigation_bar.logout": "Se déconnecter", + "navigation_bar.logout": "Déconnexion", "reply_indicator.cancel": "Annuler", "search.placeholder": "Chercher", "search.account": "Compte", "search.hashtag": "Mot-clé", - "suggestions_box.who_to_follow": "Suggestions", - "suggestions_box.refresh": "Rafraîchir", "upload_button.label": "Joindre un média", "upload_form.undo": "Annuler", - "notification.follow": "{name} s’est abonné⋅e à vos statuts", - "notification.favourite": "{name} a ajouté votre statut à ses favoris", - "notification.reblog": "{name} a partagé votre statut", - "notification.mention": "{name} vous a mentionné⋅e" + "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 :" }; export default fr; diff --git a/app/assets/javascripts/components/locales/hu.jsx b/app/assets/javascripts/components/locales/hu.jsx new file mode 100644 index 000000000..4a446965c --- /dev/null +++ b/app/assets/javascripts/components/locales/hu.jsx @@ -0,0 +1,55 @@ +const hu = { + "column_back_button.label": "Vissza", + "lightbox.close": "Bezárás", + "loading_indicator.label": "Betöltés...", + "status.mention": "Említés", + "status.delete": "Törlés", + "status.reply": "Válasz", + "status.reblog": "Reblog", + "status.favourite": "Kedvenc", + "status.reblogged_by": "{name} reblogolta", + "status.sensitive_warning": "Érzékeny tartalom", + "status.sensitive_toggle": "Katt a megtekintéshez", + "video_player.toggle_sound": "Hang kapcsolása", + "account.mention": "Említés", + "account.edit_profile": "Profil szerkesztése", + "account.unblock": "Blokkolás levétele", + "account.unfollow": "Követés abbahagyása", + "account.block": "Blokkolás", + "account.follow": "Követés", + "account.posts": "Posts", + "account.follows": "Követők", + "account.followers": "Követők", + "account.follows_you": "Követnek téged", + "getting_started.heading": "Első lépések", + "getting_started.about_addressing": "Követhetsz embereket felhasználónevük és a doménjük ismeretében, amennyiben megadod ezt az e-mail-szerű címet az oldalsáv tetején lévő rubrikában.", + "getting_started.about_shortcuts": "Ha a célzott személy azonos doménen tartózkodik, a felhasználónév elegendő. Ugyanez érvényes mikor személyeket említesz az állapotokban.", + "getting_started.about_developer": "A projekt fejlesztője követhető, mint Gargron@mastodon.social", + "column.home": "Kezdőlap", + "column.mentions": "Említések", + "column.public": "Nyilvános", + "column.notifications": "Értesítések", + "tabs_bar.compose": "Összeállítás", + "tabs_bar.home": "Kezdőlap", + "tabs_bar.mentions": "Említések", + "tabs_bar.public": "Nyilvános", + "tabs_bar.notifications": "Notifications", + "compose_form.placeholder": "Mire gondolsz?", + "compose_form.publish": "Tülk!", + "compose_form.sensitive": "Tartalom érzékenynek jelölése", + "navigation_bar.settings": "Beállítások", + "navigation_bar.public_timeline": "Nyilvános időfolyam", + "navigation_bar.logout": "Kijelentkezés", + "reply_indicator.cancel": "Mégsem", + "search.placeholder": "Keresés", + "search.account": "Fiók", + "search.hashtag": "Hashtag", + "upload_button.label": "Média hozzáadása", + "upload_form.undo": "Mégsem", + "notification.follow": "{name} követ téged", + "notification.favourite": "{name} kedvencnek jelölte az állapotod", + "notification.reblog": "{name} reblogolta az állapotod", + "notification.mention": "{name} megemlített" +}; + +export default hu; diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index 7fb43dd33..f172b1c51 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -1,11 +1,17 @@ import en from './en'; import de from './de'; import es from './es'; +import hu from './hu'; +import fr from './fr'; +import pt from './pt'; const locales = { en, de, - es + es, + hu, + fr, + pt }; export default function getMessagesForLocale (locale) { diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx index 02b21f3cb..e67bd80ac 100644 --- a/app/assets/javascripts/components/locales/pt.jsx +++ b/app/assets/javascripts/components/locales/pt.jsx @@ -40,8 +40,6 @@ const pt = { "search.placeholder": "Busca", "search.account": "Conta", "search.hashtag": "Hashtag", - "suggestions_box.who_to_follow": "Quem seguir", - "suggestions_box.refresh": "Recarregar", "upload_button.label": "Adicionar media", "upload_form.undo": "Desfazer" }; diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx index 68247a98c..52be648b3 100644 --- a/app/assets/javascripts/components/reducers/accounts.jsx +++ b/app/assets/javascripts/components/reducers/accounts.jsx @@ -8,7 +8,6 @@ import { ACCOUNT_TIMELINE_FETCH_SUCCESS, ACCOUNT_TIMELINE_EXPAND_SUCCESS } from '../actions/accounts'; -import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions'; import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose'; import { REBLOG_SUCCESS, @@ -71,7 +70,6 @@ export default function accounts(state = initialState, action) { case ACCOUNT_FETCH_SUCCESS: case NOTIFICATIONS_UPDATE: return normalizeAccount(state, action.account); - case SUGGESTIONS_FETCH_SUCCESS: case FOLLOWERS_FETCH_SUCCESS: case FOLLOWERS_EXPAND_SUCCESS: case FOLLOWING_FETCH_SUCCESS: diff --git a/app/assets/javascripts/components/reducers/user_lists.jsx b/app/assets/javascripts/components/reducers/user_lists.jsx index 65598f8a0..3608e4209 100644 --- a/app/assets/javascripts/components/reducers/user_lists.jsx +++ b/app/assets/javascripts/components/reducers/user_lists.jsx @@ -4,7 +4,6 @@ import { FOLLOWING_FETCH_SUCCESS, FOLLOWING_EXPAND_SUCCESS } from '../actions/accounts'; -import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions'; import { REBLOGS_FETCH_SUCCESS, FAVOURITES_FETCH_SUCCESS @@ -14,7 +13,6 @@ import Immutable from 'immutable'; const initialState = Immutable.Map({ followers: Immutable.Map(), following: Immutable.Map(), - suggestions: Immutable.List(), reblogged_by: Immutable.Map(), favourited_by: Immutable.Map() }); @@ -42,8 +40,6 @@ export default function userLists(state = initialState, action) { return normalizeList(state, 'following', action.id, action.accounts, action.next); case FOLLOWING_EXPAND_SUCCESS: return appendToList(state, 'following', action.id, action.accounts, action.next); - case SUGGESTIONS_FETCH_SUCCESS: - return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))); case REBLOGS_FETCH_SUCCESS: return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id))); case FAVOURITES_FETCH_SUCCESS: diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 05a309365..bbbeafefe 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -234,3 +234,4 @@ body { @import 'stream_entries'; @import 'components'; @import 'about'; +@import 'tables'; diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 306f474d6..81270edf6 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -44,15 +44,20 @@ code { label { font-family: 'Roboto'; font-size: 14px; - color: #9baec8; + color: white; display: block; } - input[type=checkbox] { - display: inline-block; + label.checkbox { position: relative; - top: 3px; - margin-right: 8px; + padding-left: 25px; + } + + input[type=checkbox] { + position: absolute; + left: 0; + top: 1px; + margin: 0; } } @@ -161,11 +166,10 @@ code { text-align: center; a { - color: #9baec8; + color: white; text-decoration: none; &:hover { - color: #d9e1e8; text-decoration: underline; } } diff --git a/app/assets/stylesheets/tables.scss b/app/assets/stylesheets/tables.scss new file mode 100644 index 000000000..89b35891d --- /dev/null +++ b/app/assets/stylesheets/tables.scss @@ -0,0 +1,25 @@ +.table { + width: 100%; + max-width: 100%; + border-spacing: 0; + border-collapse: collapse; + + th, td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; + text-align: left; + } + + & > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; + border-top: 0; + font-weight: 500; + } +} + +samp { + font-family: 'Roboto Mono', monospace; +} diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb new file mode 100644 index 000000000..7e6bc75ea --- /dev/null +++ b/app/controllers/admin/pubsubhubbub_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Admin::PubsubhubbubController < ApplicationController + before_action :require_admin! + + layout 'public' + + def index + @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40) + end +end diff --git a/app/controllers/api/push_controller.rb b/app/controllers/api/push_controller.rb new file mode 100644 index 000000000..78d4e36e6 --- /dev/null +++ b/app/controllers/api/push_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class Api::PushController < ApiController + def update + mode = params['hub.mode'] + topic = params['hub.topic'] + callback = params['hub.callback'] + lease_seconds = params['hub.lease_seconds'] + secret = params['hub.secret'] + + case mode + when 'subscribe' + response, status = Pubsubhubbub::SubscribeService.new.call(topic_to_account(topic), callback, secret, lease_seconds) + when 'unsubscribe' + response, status = Pubsubhubbub::UnsubscribeService.new.call(topic_to_account(topic), callback) + else + response = "Unknown mode: #{mode}" + status = 422 + end + + render plain: response, status: status + end + + private + + def topic_to_account(topic_url) + return if topic_url.blank? + + uri = Addressable::URI.parse(topic_url) + params = Rails.application.routes.recognize_path(uri.path) + domain = uri.host + (uri.port ? ":#{uri.port}" : '') + + return unless TagManager.instance.local_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom' + + Account.find_local(params[:username]) + end +end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 4ae900583..9a356196c 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -46,19 +46,9 @@ class Api::V1::AccountsController < ApiController render action: :index end - def common_followers - @accounts = @account.common_followers_with(current_user.account) - render action: :index - end - - def suggestions - @accounts = FollowSuggestion.get(current_user.account_id) - render action: :index - end - def statuses @statuses = @account.statuses.paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a - @statuses = cache(@statuses) + @statuses = cache_collection(@statuses, Status) set_maps(@statuses) set_counters_maps(@statuses) @@ -121,23 +111,4 @@ class Api::V1::AccountsController < ApiController @followed_by = Account.followed_by_map([@account.id], current_user.account_id) @blocking = Account.blocking_map([@account.id], current_user.account_id) end - - def cache(raw) - uncached_ids = [] - cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key)) - - raw.each do |status| - uncached_ids << status.id unless cached_keys_with_value.key?(status.cache_key) - end - - unless uncached_ids.empty? - uncached = Status.where(id: uncached_ids).with_includes.map { |s| [s.id, s] }.to_h - - uncached.values.each do |status| - Rails.cache.write(status.cache_key, status) - end - end - - raw.map { |status| cached_keys_with_value[status.cache_key] || uncached[status.id] } - end end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index d74b99a86..a24e0beb7 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -8,7 +8,7 @@ class Api::V1::NotificationsController < ApiController def index @notifications = Notification.where(account: current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) - @notifications = cache(@notifications) + @notifications = cache_collection(@notifications, Notification) statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status) set_maps(statuses) @@ -20,25 +20,4 @@ class Api::V1::NotificationsController < ApiController set_pagination_headers(next_path, prev_path) end - - private - - def cache(raw) - uncached_ids = [] - cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key)) - - raw.each do |notification| - uncached_ids << notification.id unless cached_keys_with_value.key?(notification.cache_key) - end - - unless uncached_ids.empty? - uncached = Notification.where(id: uncached_ids).with_includes.map { |n| [n.id, n] }.to_h - - uncached.values.each do |notification| - Rails.cache.write(notification.cache_key, notification) - end - end - - raw.map { |notification| cached_keys_with_value[notification.cache_key] || uncached[notification.id] } - end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index a693ce00d..a0b15cfbc 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -58,7 +58,7 @@ class Api::V1::StatusesController < ApiController def destroy @status = Status.where(account_id: current_user.account).find(params[:id]) - RemoveStatusService.new.call(@status) + RemovalWorker.perform_async(@status.id) render_empty end @@ -68,8 +68,12 @@ class Api::V1::StatusesController < ApiController end def unreblog - RemoveStatusService.new.call(Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first!) - @status = Status.find(params[:id]) + reblog = Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first! + @status = reblog.reblog + @reblogged_map = { @status.id => false } + + RemovalWorker.perform_async(reblog.id) + render action: :show end diff --git a/app/controllers/api/v1/timelines_controller.rb b/app/controllers/api/v1/timelines_controller.rb index b1d7c3052..89e54e2cf 100644 --- a/app/controllers/api/v1/timelines_controller.rb +++ b/app/controllers/api/v1/timelines_controller.rb @@ -8,6 +8,7 @@ class Api::V1::TimelinesController < ApiController def home @statuses = Feed.new(:home, current_account).get(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a + @statuses = cache_collection(@statuses) set_maps(@statuses) set_counters_maps(@statuses) @@ -23,6 +24,7 @@ class Api::V1::TimelinesController < ApiController def mentions @statuses = Feed.new(:mentions, current_account).get(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a + @statuses = cache_collection(@statuses) set_maps(@statuses) set_counters_maps(@statuses) @@ -38,7 +40,7 @@ class Api::V1::TimelinesController < ApiController def public @statuses = Status.as_public_timeline(current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a - @statuses = cache(@statuses) + @statuses = cache_collection(@statuses) set_maps(@statuses) set_counters_maps(@statuses) @@ -55,7 +57,7 @@ class Api::V1::TimelinesController < ApiController def tag @tag = Tag.find_by(name: params[:id].downcase) @statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a - @statuses = cache(@statuses) + @statuses = cache_collection(@statuses) set_maps(@statuses) set_counters_maps(@statuses) @@ -71,22 +73,7 @@ class Api::V1::TimelinesController < ApiController private - def cache(raw) - uncached_ids = [] - cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key)) - - raw.each do |status| - uncached_ids << status.id unless cached_keys_with_value.key?(status.cache_key) - end - - unless uncached_ids.empty? - uncached = Status.where(id: uncached_ids).with_includes.map { |s| [s.id, s] }.to_h - - uncached.values.each do |status| - Rails.cache.write(status.cache_key, status) - end - end - - raw.map { |status| cached_keys_with_value[status.cache_key] || uncached[status.id] } + def cache_collection(raw) + super(raw, Status) end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index a3a2a3275..d2d3bc4a4 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -48,7 +48,7 @@ class ApiController < ApplicationController response.headers['X-RateLimit-Limit'] = match_data[:limit].to_s response.headers['X-RateLimit-Remaining'] = (match_data[:limit] - match_data[:count]).to_s - response.headers['X-RateLimit-Reset'] = (now + (match_data[:period] - now.to_i % match_data[:period])).to_s + response.headers['X-RateLimit-Reset'] = (now + (match_data[:period] - now.to_i % match_data[:period])).iso8601(6) end def set_pagination_headers(next_path = nil, prev_path = nil) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index effb4ed78..ba0098c71 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,7 +14,6 @@ class ApplicationController < ActionController::Base before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :set_locale - before_action :check_rack_mini_profiler def raise_not_found raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}" @@ -32,8 +31,8 @@ class ApplicationController < ActionController::Base I18n.locale = I18n.default_locale end - def check_rack_mini_profiler - Rack::MiniProfiler.authorize_request if current_user && current_user.admin? + def require_admin! + redirect_to root_path unless current_user&.admin? end protected @@ -53,4 +52,23 @@ class ApplicationController < ActionController::Base def current_account @current_account ||= current_user.try(:account) end + + def cache_collection(raw, klass) + uncached_ids = [] + cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key)) + + raw.each do |item| + uncached_ids << item.id unless cached_keys_with_value.key?(item.cache_key) + end + + unless uncached_ids.empty? + uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h + + uncached.values.each do |item| + Rails.cache.write(item.cache_key, item) + end + end + + raw.map { |item| cached_keys_with_value[item.cache_key] || uncached[item.id] }.compact + end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 5be8719ae..cacc03b65 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -14,7 +14,10 @@ class Settings::PreferencesController < ApplicationController current_user.settings(:notification_emails).favourite = user_params[:notification_emails][:favourite] == '1' current_user.settings(:notification_emails).mention = user_params[:notification_emails][:mention] == '1' - if current_user.update(user_params.except(:notification_emails)) + current_user.settings(:interactions).must_be_follower = user_params[:interactions][:must_be_follower] == '1' + current_user.settings(:interactions).must_be_following = user_params[:interactions][:must_be_following] == '1' + + if current_user.update(user_params.except(:notification_emails, :interactions)) redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg') else render action: :show @@ -24,6 +27,6 @@ class Settings::PreferencesController < ApplicationController private def user_params - params.require(:user).permit(:locale, notification_emails: [:follow, :reblog, :favourite, :mention]) + params.require(:user).permit(:locale, notification_emails: [:follow, :reblog, :favourite, :mention], interactions: [:must_be_follower, :must_be_following]) end end diff --git a/app/helpers/admin/pubsubhubbub_helper.rb b/app/helpers/admin/pubsubhubbub_helper.rb new file mode 100644 index 000000000..41c874a62 --- /dev/null +++ b/app/helpers/admin/pubsubhubbub_helper.rb @@ -0,0 +1,2 @@ +module Admin::PubsubhubbubHelper +end diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb index 52190adae..13faaa261 100644 --- a/app/helpers/atom_builder_helper.rb +++ b/app/helpers/atom_builder_helper.rb @@ -116,9 +116,9 @@ module AtomBuilderHelper end def link_avatar(xml, account) - single_link_avatar(xml, account, :large, 300) - single_link_avatar(xml, account, :medium, 96) - single_link_avatar(xml, account, :small, 48) + single_link_avatar(xml, account, :large, 300) + # single_link_avatar(xml, account, :medium, 96) + # single_link_avatar(xml, account, :small, 48) end def logo(xml, url) diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 75ee2f8d9..26c4cd58f 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,7 +5,9 @@ module SettingsHelper en: 'English', de: 'Deutsch', es: 'Español', + pt: 'Português', fr: 'Français', + hu: 'Magyar', }.freeze def human_locale(locale) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index c8512476d..b812ad1f4 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -68,30 +68,34 @@ class FeedManager def filter_from_home?(status, receiver) should_filter = false - if status.reply? && !status.thread.account.nil? # Filter out if it's a reply - should_filter = !receiver.following?(status.thread.account) # and I'm not following the person it's a reply to - should_filter &&= !(receiver.id == status.thread.account_id) # and it's not a reply to me - should_filter &&= !(status.account_id == status.thread.account_id) # and it's not a self-reply - elsif status.reblog? # Filter out a reblog - should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person + if status.reply? && !status.thread.account.nil? # Filter out if it's a reply + should_filter = !receiver.following?(status.thread.account) # and I'm not following the person it's a reply to + should_filter &&= !(receiver.id == status.thread.account_id) # and it's not a reply to me + should_filter &&= !(status.account_id == status.thread.account_id) # and it's not a self-reply + elsif status.reblog? # Filter out a reblog + should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person end + should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked + should_filter end def filter_from_mentions?(status, receiver) - should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself - should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked + should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself + should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked + should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked - if status.reply? && !status.thread.account.nil? # or it's a reply - should_filter ||= receiver.blocking?(status.thread.account) # to a user I blocked + if status.reply? && !status.thread.account.nil? # or it's a reply + should_filter ||= receiver.blocking?(status.thread.account) # to a user I blocked end should_filter end def filter_from_public?(status, receiver) - should_filter = receiver.blocking?(status.account) + should_filter = receiver.blocking?(status.account) + should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) if status.reply? && !status.thread.account.nil? should_filter ||= receiver.blocking?(status.thread.account) diff --git a/app/models/account.rb b/app/models/account.rb index 16d654195..0f3d0dda2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -13,12 +13,12 @@ class Account < ApplicationRecord validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?' # Avatar upload - has_attached_file :avatar, styles: { large: '300x300#', medium: '96x96#', small: '48x48#' } + has_attached_file :avatar, styles: { large: '300x300#' }, convert_options: { all: '-strip' } validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES validates_attachment_size :avatar, less_than: 2.megabytes # Header upload - has_attached_file :header, styles: { medium: '700x335#' } + has_attached_file :header, styles: { medium: '700x335#' }, convert_options: { all: '-strip' } validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES validates_attachment_size :header, less_than: 2.megabytes @@ -44,8 +44,12 @@ class Account < ApplicationRecord has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account + # Media has_many :media_attachments, dependent: :destroy + # PuSH subscriptions + has_many :subscriptions, dependent: :destroy + pg_search_scope :search_for, against: { username: 'A', domain: 'B' }, using: { tsearch: { prefix: true } } scope :remote, -> { where.not(domain: nil) } @@ -66,12 +70,12 @@ class Account < ApplicationRecord def unfollow!(other_account) follow = active_relationships.find_by(target_account: other_account) - follow.destroy unless follow.nil? + follow&.destroy end def unblock!(other_account) block = block_relationships.find_by(target_account: other_account) - block.destroy unless block.nil? + block&.destroy end def following?(other_account) @@ -116,7 +120,11 @@ class Account < ApplicationRecord end def avatar_remote_url=(url) - self.avatar = URI.parse(url) unless self[:avatar_remote_url] == url + parsed_url = URI.parse(url) + + return if !%w(http https).include?(parsed_url.scheme) || self[:avatar_remote_url] == url + + self.avatar = parsed_url self[:avatar_remote_url] = url rescue OpenURI::HTTPError => e Rails.logger.debug "Error fetching remote avatar: #{e}" @@ -130,15 +138,6 @@ class Account < ApplicationRecord username end - def common_followers_with(other_account) - results = Neography::Rest.new.execute_query('MATCH (a {account_id: {a_id}})-[:follows]->(b)-[:follows]->(c {account_id: {c_id}}) RETURN b.account_id', a_id: id, c_id: other_account.id) - ids = results['data'].map(&:first) - accounts = Account.where(id: ids).with_counters.limit(20).map { |a| [a.id, a] }.to_h - ids.map { |id| accounts[id] }.compact - rescue Neography::NeographyError, Excon::Error::Socket - [] - end - class << self def find_local!(username) find_remote!(username, nil) diff --git a/app/models/feed.rb b/app/models/feed.rb index 45cb923d1..7b181d529 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -16,8 +16,8 @@ class Feed RegenerationWorker.perform_async(@account.id, @type) @statuses = Status.send("as_#{@type}_timeline", @account).paginate_by_max_id(limit, nil, nil) else - status_map = cache(unhydrated) - @statuses = unhydrated.map { |id| status_map[id] }.compact + status_map = Status.where(id: unhydrated).map { |s| [s.id, s] }.to_h + @statuses = unhydrated.map { |id| status_map[id] }.compact end @statuses @@ -25,29 +25,6 @@ class Feed private - def cache(ids) - raw = Status.where(id: ids).to_a - uncached_ids = [] - cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key)) - - raw.each do |status| - uncached_ids << status.id unless cached_keys_with_value.key?(status.cache_key) - end - - unless uncached_ids.empty? - uncached = Status.where(id: uncached_ids).with_includes.map { |s| [s.id, s] }.to_h - - uncached.values.each do |status| - Rails.cache.write(status.cache_key, status) - end - end - - cached = cached_keys_with_value.values.map { |s| [s.id, s] }.to_h - cached.merge!(uncached) unless uncached_ids.empty? - - cached - end - def key FeedManager.instance.key(@type, @account.id) end diff --git a/app/models/follow.rb b/app/models/follow.rb index cc5bceb75..f83490caa 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -27,32 +27,4 @@ class Follow < ApplicationRecord def title destroyed? ? "#{account.acct} is no longer following #{target_account.acct}" : "#{account.acct} started following #{target_account.acct}" end - - after_create :add_to_graph - after_destroy :remove_from_graph - - def sync! - add_to_graph - end - - private - - def add_to_graph - neo = Neography::Rest.new - - a = neo.create_unique_node('account_index', 'Account', account_id.to_s, account_id: account_id) - b = neo.create_unique_node('account_index', 'Account', target_account_id.to_s, account_id: target_account_id) - - neo.create_unique_relationship('follow_index', 'Follow', id.to_s, 'follows', a, b) - rescue Neography::NeographyError, Excon::Error::Socket => e - Rails.logger.error e - end - - def remove_from_graph - neo = Neography::Rest.new - rel = neo.get_relationship_index('follow_index', 'Follow', id.to_s) - neo.delete_relationship(rel) - rescue Neography::NeographyError, Excon::Error::Socket => e - Rails.logger.error e - end end diff --git a/app/models/follow_suggestion.rb b/app/models/follow_suggestion.rb deleted file mode 100644 index 2daa40dcb..000000000 --- a/app/models/follow_suggestion.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -class FollowSuggestion - class << self - def get(for_account_id, limit = 10) - neo = Neography::Rest.new - - query = <<END -MATCH (a {account_id: {id}})-[:follows]->(b)-[:follows]->(c) -WHERE a <> c -AND NOT (a)-[:follows]->(c) -RETURN DISTINCT c.account_id, count(b), c.nodeRank -ORDER BY count(b) DESC, c.nodeRank DESC -LIMIT {limit} -END - - results = neo.execute_query(query, id: for_account_id, limit: limit) - - if results.empty? || results['data'].empty? - results = fallback(for_account_id, limit) - elsif results['data'].size < limit - results['data'] = (results['data'] + fallback(for_account_id, limit - results['data'].size)['data']).uniq - end - - account_ids = results['data'].map(&:first) - blocked_ids = Block.where(account_id: for_account_id).pluck(:target_account_id) - accounts_map = Account.where(id: account_ids - blocked_ids).with_counters.map { |a| [a.id, a] }.to_h - - account_ids.map { |id| accounts_map[id] }.compact - rescue Neography::NeographyError, Excon::Error::Socket => e - Rails.logger.error e - return [] - end - - private - - def fallback(for_account_id, limit) - neo = Neography::Rest.new - - query = <<END -MATCH (b) -RETURN b.account_id -ORDER BY b.nodeRank DESC -LIMIT {limit} -END - - neo.execute_query(query, id: for_account_id, limit: limit) - end - end -end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index bfbf00d76..f1b9b8112 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -16,6 +16,8 @@ class MediaAttachment < ApplicationRecord validates :account, presence: true + default_scope { order('id asc') } + def local? remote_url.blank? end diff --git a/app/models/status.rb b/app/models/status.rb index 3402929bf..f9dcd97e4 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -97,7 +97,10 @@ class Status < ApplicationRecord end def as_public_timeline(account = nil) - query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id').where('accounts.silenced = FALSE') + query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') + .where('accounts.silenced = FALSE') + .where('statuses.in_reply_to_id IS NULL') + .where('statuses.reblog_of_id IS NULL') query = filter_timeline(query, account) unless account.nil? query end @@ -106,6 +109,8 @@ class Status < ApplicationRecord query = tag.statuses .joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .where('accounts.silenced = FALSE') + .where('statuses.in_reply_to_id IS NULL') + .where('statuses.reblog_of_id IS NULL') query = filter_timeline(query, account) unless account.nil? query end @@ -123,13 +128,7 @@ class Status < ApplicationRecord def filter_timeline(query, account) blocked = Block.where(account: account).pluck(:target_account_id) return query if blocked.empty? - - query - .joins('LEFT OUTER JOIN statuses AS parents ON statuses.in_reply_to_id = parents.id') - .joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.reblog_of_id = reblogs.id') - .where('statuses.account_id NOT IN (?)', blocked) - .where('(parents.id IS NULL OR parents.account_id NOT IN (?))', blocked) - .where('(reblogs.id IS NULL OR reblogs.account_id NOT IN (?))', blocked) + query.where('statuses.account_id NOT IN (?)', blocked) end end diff --git a/app/models/subscription.rb b/app/models/subscription.rb new file mode 100644 index 000000000..497cabb09 --- /dev/null +++ b/app/models/subscription.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Subscription < ApplicationRecord + MIN_EXPIRATION = 3600 * 24 * 7 + MAX_EXPIRATION = 3600 * 24 * 30 + + belongs_to :account + + validates :callback_url, presence: true + validates :callback_url, uniqueness: { scope: :account_id } + + scope :active, -> { where(confirmed: true).where('expires_at > ?', Time.now.utc) } + + def lease_seconds=(str) + self.expires_at = Time.now.utc + [[MIN_EXPIRATION, str.to_i].max, MAX_EXPIRATION].min.seconds + end + + def lease_seconds + (expires_at - Time.now.utc).to_i + end + + before_validation :set_min_expiration + + private + + def set_min_expiration + self.lease_seconds = 0 unless expires_at + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 366172e9a..423833d47 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,6 +15,7 @@ class User < ApplicationRecord has_settings do |s| s.key :notification_emails, defaults: { follow: false, reblog: false, favourite: false, mention: false } + s.key :interactions, defaults: { must_be_follower: false, must_be_following: false } end def send_devise_notification(notification, *args) diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 78301c6ca..40d8a0fee 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -41,14 +41,17 @@ class FanOutOnWriteService < BaseService end def deliver_to_hashtags(status) - Rails.logger.debug "Delivering status #{status.id} to hashtags" + return if status.reblog? || status.reply? + Rails.logger.debug "Delivering status #{status.id} to hashtags" status.tags.find_each do |tag| FeedManager.instance.broadcast("hashtag:#{tag.name}", type: 'update', id: status.id) end end def deliver_to_public(status) + return if status.reblog? || status.reply? + Rails.logger.debug "Delivering status #{status.id} to public timeline" FeedManager.instance.broadcast(:public, type: 'update', id: status.id) end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 781b03b40..2f280e03f 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -7,7 +7,9 @@ class FavouriteService < BaseService # @return [Favourite] def call(account, status) favourite = Favourite.create!(account: account, status: status) + HubPingWorker.perform_async(account.id) + Pubsubhubbub::DistributionWorker.perform_async(favourite.stream_entry.id) if status.local? NotifyService.new.call(status.account, favourite) diff --git a/app/services/follow_remote_account_service.rb b/app/services/follow_remote_account_service.rb index 37339d8ed..f640222b0 100644 --- a/app/services/follow_remote_account_service.rb +++ b/app/services/follow_remote_account_service.rb @@ -80,8 +80,7 @@ class FollowRemoteAccountService < BaseService end def get_profile(xml, account) - author = xml.at_xpath('/xmlns:feed/xmlns:author') || xml.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS) - update_remote_profile_service.call(author, account) + update_remote_profile_service.call(xml.at_xpath('/xmlns:feed'), account) end def update_remote_profile_service diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index a57e1b28a..09fa295e3 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -19,7 +19,10 @@ class FollowService < BaseService end merge_into_timeline(target_account, source_account) + HubPingWorker.perform_async(source_account.id) + Pubsubhubbub::DistributionWorker.perform_async(follow.stream_entry.id) + follow end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 772adfb90..1efd326b0 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -36,6 +36,8 @@ class NotifyService < BaseService blocked = false blocked ||= @recipient.id == @notification.from_account.id blocked ||= @recipient.blocking?(@notification.from_account) + blocked ||= (@recipient.user.settings(:interactions).must_be_follower && !@notification.from_account.following?(@recipient)) + blocked ||= (@recipient.user.settings(:interactions).must_be_following && !@recipient.following?(@notification.from_account)) blocked ||= send("blocked_#{@notification.type}?") blocked end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 76366e984..979a157e9 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -14,8 +14,11 @@ class PostStatusService < BaseService attach_media(status, options[:media_ids]) process_mentions_service.call(status) process_hashtags_service.call(status) + DistributionWorker.perform_async(status.id) HubPingWorker.perform_async(account.id) + Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id) + status end diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index 1cd801b80..a7a4cb2b0 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -16,7 +16,7 @@ class ProcessFeedService < BaseService def update_author(xml, account) return if xml.at_xpath('/xmlns:feed').nil? - UpdateRemoteProfileService.new.call(xml.at_xpath('/xmlns:feed/xmlns:author'), account) + UpdateRemoteProfileService.new.call(xml.at_xpath('/xmlns:feed'), account, true) end def process_entries(xml, account) diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index 3bf3471ec..fa14c44da 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -4,7 +4,7 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) tags = status.text.scan(Tag::HASHTAG_RE).map(&:first) if status.local? - tags.map(&:downcase).uniq.each do |tag| + tags.map { |str| str.mb_chars.downcase }.uniq.each do |tag| status.tags << Tag.where(name: tag).first_or_initialize(name: tag) end end diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb index e7bb3c73b..6b2f6e2d2 100644 --- a/app/services/process_interaction_service.rb +++ b/app/services/process_interaction_service.rb @@ -26,7 +26,7 @@ class ProcessInteractionService < BaseService end if salmon.verify(envelope, account.keypair) - update_remote_profile_service.call(xml.at_xpath('/xmlns:entry/xmlns:author'), account) + update_remote_profile_service.call(xml.at_xpath('/xmlns:entry'), account, true) case verb(xml) when :follow @@ -74,7 +74,7 @@ class ProcessInteractionService < BaseService end def delete_post!(xml, account) - status = Status.find(activity_id(xml)) + status = Status.find(xml.at_xpath('//xmlns:id').content) return if status.nil? diff --git a/app/services/pubsubhubbub/subscribe_service.rb b/app/services/pubsubhubbub/subscribe_service.rb new file mode 100644 index 000000000..343376d77 --- /dev/null +++ b/app/services/pubsubhubbub/subscribe_service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Pubsubhubbub::SubscribeService < BaseService + def call(account, callback, secret, lease_seconds) + return ['Invalid topic URL', 422] if account.nil? + return ['Invalid callback URL', 422] unless !callback.blank? && callback =~ /\A#{URI.regexp(%w(http https))}\z/ + + subscription = Subscription.where(account: account, callback_url: callback).first_or_create!(account: account, callback_url: callback) + Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'subscribe', secret, lease_seconds) + + ['', 202] + end +end diff --git a/app/services/pubsubhubbub/unsubscribe_service.rb b/app/services/pubsubhubbub/unsubscribe_service.rb new file mode 100644 index 000000000..62459a0aa --- /dev/null +++ b/app/services/pubsubhubbub/unsubscribe_service.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Pubsubhubbub::UnsubscribeService < BaseService + def call(account, callback) + return ['Invalid topic URL', 422] if account.nil? + + subscription = Subscription.where(account: account, callback_url: callback) + + unless subscription.nil? + Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'unsubscribe') + end + + ['', 202] + end +end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 6543d4ae7..39fdb4ea7 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -7,8 +7,10 @@ class ReblogService < BaseService # @return [Status] def call(account, reblogged_status) reblog = account.statuses.create!(reblog: reblogged_status, text: '') + DistributionWorker.perform_async(reblog.id) HubPingWorker.perform_async(account.id) + Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id) if reblogged_status.local? NotifyService.new.call(reblogged_status.account, reblog) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 689abc97b..4e03661da 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -10,6 +10,11 @@ class RemoveStatusService < BaseService remove_from_public(status) status.destroy! + + if status.account.local? + HubPingWorker.perform_async(status.account.id) + Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id) + end end private diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 598c7d02c..1ae1d5a80 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -2,9 +2,9 @@ class SearchService < BaseService def call(query, limit, resolve = false) - return if query.blank? + return if query.blank? || query.start_with?('#') - username, domain = query.split('@') + username, domain = query.gsub(/\A@/, '').split('@') results = if domain.nil? Account.search_for(username) diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb index 2909ae12a..56b25816f 100644 --- a/app/services/update_remote_profile_service.rb +++ b/app/services/update_remote_profile_service.rb @@ -2,24 +2,24 @@ class UpdateRemoteProfileService < BaseService POCO_NS = 'http://portablecontacts.net/spec/1.0' + DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' - def call(author_xml, account) - return if author_xml.nil? + def call(xml, account, resubscribe = false) + return if xml.nil? - account.display_name = if author_xml.at_xpath('./poco:displayName', poco: POCO_NS).nil? - account.username - else - author_xml.at_xpath('./poco:displayName', poco: POCO_NS).content - end + author_xml = xml.at_xpath('./xmlns:author') || xml.at_xpath('./dfrn:owner', dfrn: DFRN_NS) + hub_link = xml.at_xpath('./xmlns:link[@rel="hub"]') - unless author_xml.at_xpath('./poco:note').nil? - account.note = author_xml.at_xpath('./poco:note', poco: POCO_NS).content - end - - unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]').nil? - account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]').attribute('href').value + unless author_xml.nil? + account.display_name = author_xml.at_xpath('./poco:displayName', poco: POCO_NS).content unless author_xml.at_xpath('./poco:displayName', poco: POCO_NS).nil? + account.note = author_xml.at_xpath('./poco:note', poco: POCO_NS).content unless author_xml.at_xpath('./poco:note').nil? + account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]')['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]').nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]')['href'].blank? end + old_hub_url = account.hub_url + account.hub_url = hub_link['href'] if !hub_link.nil? && !hub_link['href'].blank? && (hub_link['href'] != old_hub_url) account.save! + + SubscribeService.new.call(account) if resubscribe && (account.hub_url != old_hub_url) end end diff --git a/app/views/accounts/show.atom.ruby b/app/views/accounts/show.atom.ruby index d7b2201d4..558c777f0 100644 --- a/app/views/accounts/show.atom.ruby +++ b/app/views/accounts/show.atom.ruby @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Nokogiri::XML::Builder.new do |xml| feed(xml) do simple_id xml, account_url(@account, format: 'atom') @@ -12,6 +14,7 @@ Nokogiri::XML::Builder.new do |xml| link_alternate xml, TagManager.instance.url_for(@account) link_self xml, account_url(@account, format: 'atom') + link_hub xml, api_push_url link_hub xml, Rails.configuration.x.hub_url link_salmon xml, api_salmon_url(@account.id) diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml new file mode 100644 index 000000000..bb897eb89 --- /dev/null +++ b/app/views/admin/pubsubhubbub/index.html.haml @@ -0,0 +1,20 @@ +%table.table + %thead + %tr + %th Topic + %th Callback URL + %th Confirmed + %th Expires in + %tbody + - @subscriptions.each do |subscription| + %tr + %td + %samp= subscription.account.acct + %td + %samp= subscription.callback_url + %td + - if subscription.confirmed? + %i.fa.fa-check + %td= distance_of_time_in_words(Time.now, subscription.expires_at) + += will_paginate @subscriptions, pagination_options diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 693702ff7..db5b9fb48 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -12,6 +12,10 @@ = ff.input :favourite, as: :boolean, wrapper: :with_label = ff.input :mention, as: :boolean, wrapper: :with_label + = f.simple_fields_for :interactions, current_user.settings(:interactions) do |ff| + = ff.input :must_be_follower, as: :boolean, wrapper: :with_label + = ff.input :must_be_following, as: :boolean, wrapper: :with_label + .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb index 3b11a4c5e..b31cd0aaf 100644 --- a/app/workers/processing_worker.rb +++ b/app/workers/processing_worker.rb @@ -2,6 +2,8 @@ class ProcessingWorker include Sidekiq::Worker + + sidekiq_options backtrace: true def perform(account_id, body) ProcessFeedService.new.call(body, Account.find(account_id)) diff --git a/app/workers/pubsubhubbub/confirmation_worker.rb b/app/workers/pubsubhubbub/confirmation_worker.rb new file mode 100644 index 000000000..489bd8359 --- /dev/null +++ b/app/workers/pubsubhubbub/confirmation_worker.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class Pubsubhubbub::ConfirmationWorker + include Sidekiq::Worker + include RoutingHelper + + sidekiq_options queue: 'push' + + def perform(subscription_id, mode, secret = nil, lease_seconds = nil) + subscription = Subscription.find(subscription_id) + challenge = SecureRandom.hex + + subscription.secret = secret + subscription.lease_seconds = lease_seconds + subscription.confirmed = true + + response = HTTP.headers(user_agent: 'Mastodon/PubSubHubbub') + .timeout(:per_operation, write: 20, connect: 20, read: 50) + .get(subscription.callback_url, params: { + 'hub.topic' => account_url(subscription.account, format: :atom), + 'hub.mode' => mode, + 'hub.challenge' => challenge, + 'hub.lease_seconds' => subscription.lease_seconds, + }) + + body = response.body.to_s + + Rails.logger.debug "Confirming PuSH subscription for #{subscription.callback_url} with challenge #{challenge}: #{body}" + + if mode == 'subscribe' && body == challenge + subscription.save! + elsif (mode == 'unsubscribe' && body == challenge) || !subscription.confirmed? + subscription.destroy! + end + end +end diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb new file mode 100644 index 000000000..6d526c2b1 --- /dev/null +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Pubsubhubbub::DeliveryWorker + include Sidekiq::Worker + include RoutingHelper + + sidekiq_options queue: 'push' + + def perform(subscription_id, payload) + subscription = Subscription.find(subscription_id) + headers = {} + + headers['User-Agent'] = 'Mastodon/PubSubHubbub' + headers['Link'] = LinkHeader.new([[api_push_url, [%w(rel hub)]], [account_url(subscription.account, format: :atom), [%w(rel self)]]]).to_s + headers['X-Hub-Signature'] = signature(subscription.secret, payload) unless subscription.secret.blank? + + response = HTTP.timeout(:per_operation, write: 50, connect: 20, read: 50) + .headers(headers) + .post(subscription.callback_url, body: payload) + + raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300 + end + + private + + def signature(secret, payload) + hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), secret, payload) + "sha1=#{hmac}" + end +end diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb new file mode 100644 index 000000000..b0ddc71c1 --- /dev/null +++ b/app/workers/pubsubhubbub/distribution_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Pubsubhubbub::DistributionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'push' + + def perform(stream_entry_id) + stream_entry = StreamEntry.find(stream_entry_id) + account = stream_entry.account + renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https) + payload = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom]) + + Subscription.where(account: account).active.select('id').find_each do |subscription| + Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload) + end + end +end diff --git a/app/workers/removal_worker.rb b/app/workers/removal_worker.rb new file mode 100644 index 000000000..7470c54f5 --- /dev/null +++ b/app/workers/removal_worker.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class RemovalWorker + include Sidekiq::Worker + + def perform(status_id) + RemoveStatusService.new.call(Status.find(status_id)) + end +end \ No newline at end of file diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb index 24fb94012..0903ca487 100644 --- a/app/workers/salmon_worker.rb +++ b/app/workers/salmon_worker.rb @@ -2,6 +2,8 @@ class SalmonWorker include Sidekiq::Worker + + sidekiq_options backtrace: true def perform(account_id, body) ProcessInteractionService.new.call(body, Account.find(account_id)) diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb index 700161989..84eae73be 100644 --- a/app/workers/thread_resolve_worker.rb +++ b/app/workers/thread_resolve_worker.rb @@ -7,9 +7,9 @@ class ThreadResolveWorker child_status = Status.find(child_status_id) parent_status = FetchRemoteStatusService.new.call(parent_url) - unless parent_status.nil? - child_status.thread = parent_status - child_status.save! - end + return if parent_status.nil? + + child_status.thread = parent_status + child_status.save! end end diff --git a/config/application.rb b/config/application.rb index 7ba13bfbe..1e5fd9c7c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -20,7 +20,7 @@ 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, :fr] + config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu] config.i18n.default_locale = :en # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') diff --git a/config/environments/production.rb b/config/environments/production.rb index dcb659d6c..9254d494c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -13,7 +13,7 @@ Rails.application.configure do # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true - config.action_controller.asset_host = ENV['CDN_HOST'] + config.action_controller.asset_host = ENV['CDN_HOST'] if ENV.key?('CDN_HOST') # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. @@ -30,7 +30,7 @@ Rails.application.configure do # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = false diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index d345ce6c0..4dc6985b7 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -30,9 +30,6 @@ search: - app/assets/fonts - app/assets/videos -ignore_missing: - - '{devise,simple_form}.*' - ignore_unused: - 'activerecord.attributes.*' - '{devise,will_paginate,doorkeeper}.*' diff --git a/config/initializers/mini_profiler.rb b/config/initializers/mini_profiler.rb deleted file mode 100644 index 265783618..000000000 --- a/config/initializers/mini_profiler.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rack-mini-profiler' - -Rack::MiniProfilerRails.initialize!(Rails.application) - -Rails.application.middleware.swap(Rack::Deflater, Rack::MiniProfiler) -Rails.application.middleware.swap(Rack::MiniProfiler, Rack::Deflater) - -Rack::MiniProfiler.config.storage = Rack::MiniProfiler::MemoryStore - -if Rails.env.production? - Rack::MiniProfiler.config.storage_options = { - host: ENV.fetch('REDIS_HOST') { 'localhost' }, - port: ENV.fetch('REDIS_PORT') { 6379 }, - } - - Rack::MiniProfiler.config.storage = Rack::MiniProfiler::RedisStore -end diff --git a/config/initializers/neography.rb b/config/initializers/neography.rb deleted file mode 100644 index bd6ead3b0..000000000 --- a/config/initializers/neography.rb +++ /dev/null @@ -1,5 +0,0 @@ -Neography.configure do |config| - config.protocol = "http" - config.server = ENV.fetch('NEO4J_HOST') { 'localhost' } - config.port = ENV.fetch('NEO4J_PORT') { 7474 } -end diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb index 3dd501b65..4ba432b6a 100644 --- a/config/initializers/ostatus.rb +++ b/config/initializers/ostatus.rb @@ -1,14 +1,16 @@ -port = ENV.fetch('PORT') { 3000 } - +port = ENV.fetch('PORT') { 3000 } +host = ENV.fetch('LOCAL_DOMAIN') { "localhost:#{port}" } +https = ENV['LOCAL_HTTPS'] == 'true' + Rails.application.configure do - config.x.local_domain = ENV.fetch('LOCAL_DOMAIN') { "localhost:#{port}" } - config.x.hub_url = ENV.fetch('HUB_URL') { 'https://pubsubhubbub.superfeedr.com' } - config.x.use_https = ENV['LOCAL_HTTPS'] == 'true' + config.x.local_domain = host + config.x.hub_url = ENV.fetch('HUB_URL') { 'https://pubsubhubbub.superfeedr.com' } + config.x.use_https = https config.x.use_s3 = ENV['S3_ENABLED'] == 'true' - config.action_mailer.default_url_options = { host: config.x.local_domain, protocol: config.x.use_https ? 'https://' : 'http://', trailing_slash: false } + config.action_mailer.default_url_options = { host: host, protocol: https ? 'https://' : 'http://', trailing_slash: false } if Rails.env.production? - config.action_cable.allowed_request_origins = ["http#{config.x.use_https ? 's' : ''}://#{config.x.local_domain}"] + config.action_cable.allowed_request_origins = ["http#{https ? 's' : ''}://#{host}"] end end diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 643c5d384..80effc05e 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -1,4 +1,6 @@ if ENV['S3_ENABLED'] == 'true' + Aws.eager_autoload!(services: %w(S3)) + Paperclip::Attachment.default_options[:storage] = :s3 Paperclip::Attachment.default_options[:s3_protocol] = 'https' Paperclip::Attachment.default_options[:url] = ':s3_domain_url' @@ -9,6 +11,6 @@ if ENV['S3_ENABLED'] == 'true' bucket: ENV.fetch('S3_BUCKET'), access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'), secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY'), - s3_region: ENV.fetch('S3_REGION') + s3_region: ENV.fetch('S3_REGION'), } end diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb index 6d9286e66..3f0ee1d7a 100644 --- a/config/initializers/rack-attack.rb +++ b/config/initializers/rack-attack.rb @@ -1,7 +1,7 @@ class Rack::Attack # Rate limits for the API throttle('api', limit: 150, period: 5.minutes) do |req| - req.ip if req.path.match(/\A\/api\//) + req.ip if req.path.match(/\A\/api\/v/) end self.throttled_response = lambda do |env| @@ -11,7 +11,7 @@ class Rack::Attack headers = { 'X-RateLimit-Limit' => match_data[:limit].to_s, 'X-RateLimit-Remaining' => '0', - 'X-RateLimit-Reset' => (now + (match_data[:period] - now.to_i % match_data[:period])).to_s + 'X-RateLimit-Reset' => (now + (match_data[:period] - now.to_i % match_data[:period])).iso8601(6) } [429, headers, [{ error: 'Throttled' }.to_json]] diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb new file mode 100644 index 000000000..8b7311e39 --- /dev/null +++ b/config/initializers/timeout.rb @@ -0,0 +1 @@ +Rack::Timeout.timeout = 30 \ No newline at end of file diff --git a/config/locales/devise.hu.yml b/config/locales/devise.hu.yml new file mode 100644 index 000000000..2eb7da45c --- /dev/null +++ b/config/locales/devise.hu.yml @@ -0,0 +1,61 @@ +--- +hu: + devise: + confirmations: + confirmed: Az e-mail címed sikeresen meg lett erősítve. + send_instructions: Pár percen belül kapni fogsz egy e-mailt az e-mail címed megerősítéséhez szükséges lépésekről. + send_paranoid_instructions: Ha az e-mail címed létezik az adatbázisunkban, pár percen belül kapni fogsz egy e-mailt az e-mail címed megerősítéséhez szükséges lépésekről. + failure: + already_authenticated: Már bejelentkeztél + inactive: Fiókod még nem lett aktiválva. + invalid: Helytelen %{authentication_keys} vagy jelszó. + last_attempt: Már csak egy próbálkozásod maradt mielőtt a fiókod lezárásra kerül. + locked: Fiókod le van zárva. + not_found_in_database: Helytelen %{authentication_keys} vagy jelszó. + timeout: A munkamenet lejárt. Jelentkezz be újra a folytatáshoz. + unauthenticated: A folytatás előtt be kell jelentkezned. + unconfirmed: A folytatás előtt meg kell erősítened az e-mail címed. + mailer: + confirmation_instructions: + subject: 'Mastodon: Megerősítési lépések' + password_change: + subject: 'Mastodon: Jelszó megváltoztatva' + reset_password_instructions: + subject: 'Mastodon: Jelszó visszaállítási lépések' + unlock_instructions: + subject: 'Mastodon: Feloldási lépések' + omniauth_callbacks: + failure: "%{kind} nem hitelesíthető, mert %{reason}." + success: Sikeres hitelesítés %{kind} fiókról. + passwords: + no_token: Nem férhetsz hozzá az oldalhoz jelszó visszaállító e-mail nélkül. Ha egy jelszó visszaállító e-mail hozott ide, ellenőrizd, hogy a megadott teljes URL-t használd. + send_instructions: Pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat. + send_paranoid_instructions: Ha létezik az e-mail cím, pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat. + updated: Jelszavad sikeresen frissült. Bejelentkeztél. + updated_not_active: Jelszavad sikeresen meg lett változtatva. + registrations: + destroyed: Viszlát! A fiókod sikeresen törölve. Reméljük hamarosan viszontláthatunk. + signed_up: Üdvözlünk! Sikeresen regisztráltál. + signed_up_but_inactive: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod még nem lett aktiválva. + signed_up_but_locked: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod le lett zárva. + signed_up_but_unconfirmed: Egy üzenet a megerősítési linkkel kiküldésre került az e-mail címedre. Kérjük használd a linket a fiókod aktiválásához. + update_needs_confirmation: Sikeresen frissítetted a fiókodat, de szükségünk van az e-mail címed megerősítésére. Kérlek ellenőrizd az e-mailedet és kövesd a levélben szereplő megerősítési linket az e-mail címed megerősítéséhez. + updated: Fiókod frissítése sikeres. + sessions: + already_signed_out: Sikeres kijelenkezés. + signed_in: Sikeres bejelentkezés. + signed_out: Sikeres kijelentkezés. + unlocks: + send_instructions: Pár percen belül egy e-mailt fogsz kapni a feloldáshoz szükséges lépésekkel. + send_paranoid_instructions: Ha a fiókod létezik, pár percen belül egy e-mailt fogsz kapni a feloldáshoz szükséges lépésekkel. + unlocked: A fiókod sikeresen fel lett oldva. Jelentkezz be a folytatáshoz. + errors: + messages: + already_confirmed: már meg lett erősítve, kérjük jelentkezz be + confirmation_period_expired: "%{period} belül kellett megerősíteni, kérjük igényelj újat" + expired: lejárt, kérjük igényelj újat + not_found: nem található + not_locked: nincs lezárva + not_saved: + one: '1 hiba megakadályozta %{resource} mentését:' + other: "%{count} számú hiba megakadályozta %{resource} mentését:" diff --git a/config/locales/doorkeeper.hu.yml b/config/locales/doorkeeper.hu.yml new file mode 100644 index 000000000..b1c6dd6c9 --- /dev/null +++ b/config/locales/doorkeeper.hu.yml @@ -0,0 +1,112 @@ +--- +hu: + activerecord: + attributes: + doorkeeper/application: + name: Név + redirect_uri: Visszairányító URI + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: nem tartalmazhat töredéket. + invalid_uri: érvényes URI-nak kell lennie. + relative_uri: abszolút URI-nak kell lennie. + secured_uri: HTTPS/SSL URI-nak kell lennie. + doorkeeper: + applications: + buttons: + authorize: Engedélyezés + cancel: Mégsem + destroy: Törlés + edit: Szerkesztés + submit: Elküldés + confirmations: + destroy: Biztos vagy benne? + edit: + title: Alkalmazás szerkesztése + form: + error: Hoppá! Ellenőrizd az űrlapot az esetleges hibák miatt + help: + native_redirect_uri: Használj %{native_redirect_uri} a helyi tesztekhez + redirect_uri: Egy sor URI-nként + scopes: A nézeteket szóközzel válaszd el. Hagyd üresen az alapértelmezett nézetekhez. + index: + callback_url: Callback URL + name: Név + new: Új alkalmazás + title: Alkalmazásod + new: + title: Új alkalmazás + show: + actions: Műveletek + application_id: Alkalmazás azonosító + callback_urls: Callback urlek + scopes: Nézetek + secret: Titok + title: 'Alkalmazás: %{name}' + authorizations: + buttons: + authorize: Engedélyezés + deny: Tiltás + error: + title: Hiba történt + new: + able_to: Képes lesz + prompt: "%{client_name} nevű alkalmazás engedélyt kér a fiókodhoz való hozzáféréshez." + title: Engedély szükséges + show: + title: Engedély kódja + authorized_applications: + buttons: + revoke: Visszavonás + confirmations: + revoke: Biztos vagy benne? + index: + application: Alkalmazás + created_at: Készítve + date_format: "%Y-%m-%d %H:%M:%S" + title: Engedélyezett alkalmazásaid + errors: + messages: + access_denied: Az erőforrás tulajdonosa vagy hitelesítő kiszolgálója megtakadta a kérést. + credential_flow_not_configured: Az erőforrás tulajdonos jelszóadatainak átadása megszakadt, mert a Doorkeeper.configure.resource_owner_from_credentials beállítatlan. + invalid_client: A kliens hitelesítése megszakadt, mert a ismeretlen a kliens, kliens nem küldött hitelesítést, vagy ismeretlen a kliens + invalid_grant: A biztosított hitelesítés érvénytelen, lejárt, visszavont, vagy nem egyezik a hitelesítéi kérésben használt URIval, vagy más kliensnek lett címezve. + invalid_redirect_uri: A redirect uri nem valós. + invalid_request: A kérésből hiányzik egy szükséges paraméter, nem támogatott paramétert tartalmaz, vagy egyéb módon hibás. + invalid_resource_owner: A biztosított erőforrás tulajdonosának hitelesítő adatai nem valósak, vagy az erőforrás tulajdonosa nem található. + invalid_scope: A kért nézet érvénytelen, ismeretlen, vagy hibás. + invalid_token: + expired: Hozzáférési kulcs lejárt + revoked: Hozzáférési kulcs vissza lett vonva + unknown: Hozzáférési kulcs érvénytelen + resource_owner_authenticator_not_configured: Erőforrás tulajdonos keresés megszakadt, ugyanis a Doorkeeper.configure.resource_owner_authenticator beállítatlan. + server_error: Hitelesítő szervert váratlan esemény érte, mely meggátolta a kérés teljesítését. + temporarily_unavailable: A hitelesítő szerver jelenleg nem tudja teljesíteni a kérést egy átmeneti túlterheltség vagy a kiszolgáló karbantartása miatt. + unauthorized_client: A kliens nincs feljogosítva a kérés teljesítésére. + unsupported_grant_type: A hitelesítés módja nem támogatott a hitelesítő kiszolgálón. + unsupported_response_type: A hitelesítő kiszolgáló nem támogatja ezt a választ. + flash: + applications: + create: + notice: Alkalmazás létrehozva. + destroy: + notice: Alkalmazás törölve. + update: + notice: Alkalmazás frissítve. + authorized_applications: + destroy: + notice: Alkalmazás visszavonva. + layouts: + admin: + nav: + applications: Alkalmazások + oauth2_provider: OAuth2 szolgáltató + application: + title: OAuth engedély szükséges + scopes: + follow: fiókok követése, blokkoláse, blokkolás feloldása és követés abbahagyása + read: fiókod adatainak olvasása + write: bejegyzés írása a nevedben diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c9258381c..f78cd0de5 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -7,44 +7,44 @@ fr: source_code: "Code source" terms: "Conditions d’utilisation" accounts: - follow: "S’abonner" + follow: "Suivre" followers: "Abonnés" following: "Abonnements" - nothing_here: "Rien à voir ici !" - people_followed_by: "Personnes auxquelles %{name} est abonné⋅e" - people_who_follow: "Personnes abonnées à %{name}" + nothing_here: "Rien à voir ici !" + people_followed_by: "Personnes suivies par %{name}" + people_who_follow: "Personnes qui suivent %{name}" posts: "Statuts" - unfollow: "Se désabonner" + unfollow: "Ne plus suivre" application_mailer: signature: "Notifications de Mastodon depuis %{instance}" auth: change_password: "Changer de mot de passe" - didnt_get_confirmation: "Vous n’avez pas reçu les consignes de confirmation ?" - forgot_password: "Mode passe oublié ?" + didnt_get_confirmation: "Vous n’avez pas reçu les consignes de confirmation ?" + forgot_password: "Mode passe oublié ?" login: "Se connecter" register: "S’inscrire" resend_confirmation: "Envoyer à nouveau les consignes de confirmation" reset_password: "Réinitialiser le mot de passe" set_new_password: "Établir le nouveau mot de passe" generic: - changes_saved_msg: "Les modifications ont été enregistrées avec succès !" + changes_saved_msg: "Les modifications ont été enregistrées avec succès !" powered_by: "propulsé par %{link}" save_changes: "Enregistrer les modifications" validation_errors: - one: "Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous." - other: "Quelques choses ne vont pas ! Vérifiez les erreurs ci-dessous." + one: "Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous." + other: "Quelques choses ne vont pas ! Vérifiez les erreurs ci-dessous." notification_mailer: favourite: - body: "%{name} a ajouté votre statut à ses favoris :" + body: "%{name} a ajouté votre statut à ses favoris :" subject: "%{name} a ajouté votre statut à ses favoris" follow: - body: "%{name} s’est abonné⋅e à vos statuts !" - subject: "%{name} s’est abonné⋅e à vos statuts" + body: "%{name} vous suit !" + subject: "%{name} vous suit" mention: - body: "%{name} vous a mentionné⋅e dans :" + body: "%{name} vous a mentionné⋅e dans :" subject: "%{name} vous a mentionné⋅e" reblog: - body: "%{name} a partagé votre statut :" + body: "%{name} a partagé votre statut :" subject: "%{name} a partagé votre statut" pagination: next: "Suivant" @@ -54,6 +54,6 @@ fr: preferences: "Préférences" stream_entries: favourited: "a ajouté à ses favoris un statut de" - is_now_following: "s’est abonné⋅e à" + is_now_following: "suit désormais" will_paginate: page_gap: "…" diff --git a/config/locales/hu.yml b/config/locales/hu.yml new file mode 100644 index 000000000..d891b2b28 --- /dev/null +++ b/config/locales/hu.yml @@ -0,0 +1,59 @@ +--- +hu: + about: + about_instance: "<em>%{instance}</em> egy Mastodon másolat." + about_mastodon: Mastodon egy <em>szabad, nyílt forráskódú</em> szociális hálózati kiszolgálo. Egy <em>központosítatlan</em> alternatíva a kereskedelmi platformokra, elkerüli a kommunikációd monopolizációját veszélyét. Bárki futtathatja a Mastodon-t és részt vehet a <em>szociális hálózatban</em>. + get_started: Első lépések + source_code: Forráskód + terms: Feltételek + accounts: + follow: Követés + followers: Követők + following: Követed őket + nothing_here: Nincs itt semmi! + people_followed_by: "%{name} követett személyei" + people_who_follow: "%{name} követői" + posts: Bejegyzések + unfollow: Követés abbahagyása + application_mailer: + signature: "%{instance} Mastodon értesítései" + auth: + change_password: Jelszó változtatása + didnt_get_confirmation: Nem kaptad meg a megerősítési lépéseket? + forgot_password: Elfelejtetted a jelszavad? + login: Belépés + register: Regisztráció + resend_confirmation: Megerősítési lépések újraküldése + reset_password: Jelszó visszaállítása + set_new_password: Új jelszó beállítása + generic: + changes_saved_msg: Változások sikeresen elmentve! + powered_by: powered by %{link} + save_changes: Változások mentése + validation_errors: + one: Valami nincs rendjén! Kérlek tekintsd meg a hibát alant + other: Valami nincs rendjén! Kérlek tekintsd meg a %{count} darab hibát alant. + notification_mailer: + favourite: + body: 'Az állapotodat kedvencnek jelölte %{name}:' + subject: "%{name} kedvencnek jelölte az állapotod" + follow: + body: "%{name} mostantól követ téged!" + subject: "%{name} mostantól követ téged" + mention: + body: '%{name} megemlített téged:' + subject: "%{name} megemlített téged" + reblog: + body: 'Az állapotod reblogolta %{name}:' + subject: "%{name} reblogolta az állapotod" + pagination: + next: Következő + prev: Előző + settings: + edit_profile: Profil szerkesztése + preferences: Beállítások + stream_entries: + favourited: kedvencnek jelölték a bejegyzésedet + is_now_following: mostantól követ + will_paginate: + page_gap: "…" diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 36c5141a2..47e30ccb4 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -16,6 +16,9 @@ de: password: Passwort silenced: Öffentliche Beiträge nicht auflisten username: Nutzername + interactions: + must_be_follower: Benachrichtigungen von nicht-Folgern blockieren + must_be_following: Benachrichtigungen von Nutzern blockieren, denen ich nicht folge notification_emails: favourite: E-mail senden, wenn jemand meinen Beitrag favorisiert follow: E-mail senden, wenn mir jemand folgt diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index a7d958c06..1e975af14 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -16,6 +16,9 @@ en: password: Password silenced: Unlisted mode username: Username + interactions: + must_be_follower: Block notifications from non-followers + must_be_following: Block notifications from people you don't follow notification_emails: favourite: Send e-mail when someone favourites your status follow: Send e-mail when someone follows you diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 7333c9e11..73905a7b3 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -15,12 +15,15 @@ fr: note: Présentation password: Mot de passe silenced: Ne pas apparaître dans le fil public - username: Nom d’utilisateur + username: Identifiant notification_emails: - favourite: Envoyer un courriel lorsque quelqu’un ajoute un de mes statuts à ses favoris - follow: Envoyer un courriel lorsque quelqu’un s’abonne à mes statuts + favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statut à ses favoris + follow: Envoyer un courriel lorsque quelqu’un me suit mention: Envoyer un courriel lorsque quelqu’un me mentionne - reblog: Envoyer un courriel lorsque quelqu’un partage un de mes statuts + reblog: Envoyer un courriel lorsque quelqu’un partage mes statuts + interactions: + must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas + must_be_following: Masquer les notifications des personnes que vous ne suivez pas 'no': Non required: mark: "*" diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml new file mode 100644 index 000000000..39c450087 --- /dev/null +++ b/config/locales/simple_form.hu.yml @@ -0,0 +1,28 @@ +--- +hu: + simple_form: + labels: + defaults: + avatar: Profilkép + confirm_new_password: Új jelszó megerősítése + confirm_password: Jelszó megerősítése + current_password: Jelenlegi jelszó + display_name: Megjelenített név + email: E-mail cím + header: Fejléc + locale: Nyelv + new_password: Új jelszó + note: Önéletrajz + password: Jelszó + silenced: Listázatlan mód + username: Felhasználónév + notification_emails: + favourite: E-mail küldése amikor valaki kedvencnek jelöli az állapotod + follow: E-mail küldése amikor valaki követni kezd téged + mention: E-mail küldése amikor valaki megemlít téged + reblog: E-mail küldése amikor valaki reblogolja az állapotod + 'no': 'Nem' + required: + mark: "*" + text: kötelező + 'yes': 'Igen' diff --git a/config/routes.rb b/config/routes.rb index 00185c5e8..cd544a62b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + require 'sidekiq/web' Rails.application.routes.draw do - mount ActionCable.server => '/cable' + mount ActionCable.server, at: 'cable' authenticate :user, lambda { |u| u.admin? } do mount Sidekiq::Web, at: 'sidekiq' @@ -19,7 +21,7 @@ Rails.application.routes.draw do sessions: 'auth/sessions', registrations: 'auth/registrations', passwords: 'auth/passwords', - confirmations: 'auth/confirmations' + confirmations: 'auth/confirmations', } resources :accounts, path: 'users', only: [:show], param: :username do @@ -42,11 +44,18 @@ Rails.application.routes.draw do resources :media, only: [:show] resources :tags, only: [:show] + namespace :admin do + resources :pubsubhubbub, only: [:index] + end + namespace :api do - # PubSubHubbub + # PubSubHubbub outgoing subscriptions resources :subscriptions, only: [:show] post '/subscriptions/:id', to: 'subscriptions#update' + # PubSubHubbub incoming subscriptions + post '/push', to: 'push#update', as: :push + # Salmon post '/salmon/:id', to: 'salmon#update', as: :salmon @@ -80,7 +89,6 @@ Rails.application.routes.draw do collection do get :relationships get :verify_credentials - get :suggestions get :search end @@ -88,7 +96,6 @@ Rails.application.routes.draw do get :statuses get :followers get :following - get :common_followers post :follow post :unfollow diff --git a/db/migrate/20161128103007_create_subscriptions.rb b/db/migrate/20161128103007_create_subscriptions.rb new file mode 100644 index 000000000..46443680a --- /dev/null +++ b/db/migrate/20161128103007_create_subscriptions.rb @@ -0,0 +1,15 @@ +class CreateSubscriptions < ActiveRecord::Migration[5.0] + def change + create_table :subscriptions do |t| + t.string :callback_url, null: false, default: '' + t.string :secret + t.datetime :expires_at, null: true, default: nil + t.boolean :confirmed, null: false, default: false + t.integer :account_id, null: false + + t.timestamps + end + + add_index :subscriptions, [:callback_url, :account_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 356badf8e..2c0e6de5b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161123093447) do +ActiveRecord::Schema.define(version: 20161128103007) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -143,6 +143,19 @@ ActiveRecord::Schema.define(version: 20161123093447) do t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree end + create_table "pubsubhubbub_subscriptions", force: :cascade do |t| + t.string "topic", default: "", null: false + t.string "callback", default: "", null: false + t.string "mode", default: "", null: false + t.string "challenge", default: "", null: false + t.string "secret" + t.boolean "confirmed", default: false, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["topic", "callback"], name: "index_pubsubhubbub_subscriptions_on_topic_and_callback", unique: true, using: :btree + end + create_table "settings", force: :cascade do |t| t.string "var", null: false t.text "value" @@ -185,6 +198,17 @@ ActiveRecord::Schema.define(version: 20161123093447) do t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type", using: :btree end + create_table "subscriptions", force: :cascade do |t| + t.string "callback_url", default: "", null: false + t.string "secret" + t.datetime "expires_at" + t.boolean "confirmed", default: false, null: false + t.integer "account_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["callback_url", "account_id"], name: "index_subscriptions_on_callback_url_and_account_id", unique: true, using: :btree + end + create_table "tags", force: :cascade do |t| t.string "name", default: "", null: false t.datetime "created_at", null: false diff --git a/docker-compose.yml b/docker-compose.yml index 9f9126805..e1f1f1c4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,16 +6,9 @@ services: redis: restart: always image: redis - neo4j: - restart: always - build: - context: . - dockerfile: Dockerfile.neo4j web: restart: always - build: - context: . - dockerfile: Dockerfile.app + build: . env_file: .env.production command: bundle exec rails s -p 3000 -b '0.0.0.0' ports: @@ -23,20 +16,16 @@ services: depends_on: - db - redis - - neo4j volumes: - ./public/assets:/mastodon/public/assets - ./public/system:/mastodon/public/system sidekiq: restart: always - build: - context: . - dockerfile: Dockerfile.app + build: . env_file: .env.production - command: bundle exec sidekiq -q default -q mailers + command: bundle exec sidekiq -q default -q mailers -q push depends_on: - db - redis - - neo4j volumes: - ./public/system:/mastodon/public/system diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 93461bd0a..8730a64cd 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -49,11 +49,4 @@ namespace :mastodon do Redis.current.keys('feed:*').each { |key| Redis.current.del(key) } end end - - namespace :graphs do - desc 'Syncs all follow relationships to Neo4J' - task sync: :environment do - Follow.find_each(&:sync!) - end - end end diff --git a/spec/controllers/admin/pubsubhubbub_controller_spec.rb b/spec/controllers/admin/pubsubhubbub_controller_spec.rb new file mode 100644 index 000000000..068bd09a6 --- /dev/null +++ b/spec/controllers/admin/pubsubhubbub_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe Admin::PubsubhubbubController, type: :controller do + describe 'GET #index' do + before do + sign_in Fabricate(:user, admin: true), scope: :user + end + + it 'returns http success' do + get :index + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/api/push_controller_spec.rb b/spec/controllers/api/push_controller_spec.rb new file mode 100644 index 000000000..e699006f7 --- /dev/null +++ b/spec/controllers/api/push_controller_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe Api::PushController, type: :controller do + describe 'POST #update' do + context 'with hub.mode=subscribe' do + pending + end + + context 'with hub.mode=unsubscribe' do + pending + end + end +end diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb index 6897caeeb..3d3a973d2 100644 --- a/spec/controllers/api/salmon_controller_spec.rb +++ b/spec/controllers/api/salmon_controller_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Api::SalmonController, type: :controller do let(:account) { Fabricate(:user, account: Fabricate(:account, username: 'catsrgr8')).account } before do + stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {}) stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb index 2af6cb725..44841176a 100644 --- a/spec/controllers/api/subscriptions_controller_spec.rb +++ b/spec/controllers/api/subscriptions_controller_spec.rb @@ -23,6 +23,7 @@ RSpec.describe Api::SubscriptionsController, type: :controller do let(:feed) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom')) } before do + stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {}) stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) stub_request(:head, "https://quitter.no/notice/1269244").to_return(status: 404) stub_request(:head, "https://quitter.no/notice/1265331").to_return(status: 404) @@ -37,7 +38,7 @@ RSpec.describe Api::SubscriptionsController, type: :controller do stub_request(:head, "https://social.umeahackerspace.se/user/2").to_return(status: 404) stub_request(:head, "https://gs.kawa-kun.com/user/2").to_return(status: 404) stub_request(:head, "https://mastodon.social/users/Gargron").to_return(status: 404) - + request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}" request.env['RAW_POST_DATA'] = feed diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 98eea28ce..e4532305b 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -46,20 +46,6 @@ RSpec.describe Api::V1::AccountsController, type: :controller do end end - describe 'GET #suggestions' do - it 'returns http success' do - get :suggestions - expect(response).to have_http_status(:success) - end - end - - describe 'GET #common_followers' do - it 'returns http success' do - get :common_followers, params: { id: user.account.id } - expect(response).to have_http_status(:success) - end - end - describe 'POST #follow' do let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } diff --git a/spec/fabricators/subscription_fabricator.rb b/spec/fabricators/subscription_fabricator.rb new file mode 100644 index 000000000..0c8290494 --- /dev/null +++ b/spec/fabricators/subscription_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:subscription) do + callback_url "http://example.com/callback" + secret "foobar" + expires_at "2016-11-28 11:30:07" + confirmed false +end diff --git a/spec/helpers/admin/pubsubhubbub_helper_spec.rb b/spec/helpers/admin/pubsubhubbub_helper_spec.rb new file mode 100644 index 000000000..6603e6dc0 --- /dev/null +++ b/spec/helpers/admin/pubsubhubbub_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Admin::PubsubhubbubHelper. For example: +# +# describe Admin::PubsubhubbubHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe Admin::PubsubhubbubHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb new file mode 100644 index 000000000..d40bf0b44 --- /dev/null +++ b/spec/models/subscription_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Subscription, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb index e4e5858ea..5e57d823b 100644 --- a/spec/services/process_feed_service_spec.rb +++ b/spec/services/process_feed_service_spec.rb @@ -7,6 +7,7 @@ RSpec.describe ProcessFeedService do subject { ProcessFeedService.new } before do + stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {}) stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt')) stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/002/original/morpheus_linux.jpg?1476059910").to_return(request_fixture('attachment1.txt')) stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/003/original/gizmo.jpg?1476060065").to_return(request_fixture('attachment2.txt')) diff --git a/spec/services/update_remote_profile_service_spec.rb b/spec/services/update_remote_profile_service_spec.rb index 1ffcfbfac..c3d76c653 100644 --- a/spec/services/update_remote_profile_service_spec.rb +++ b/spec/services/update_remote_profile_service_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe UpdateRemoteProfileService do - let(:xml) { Nokogiri::XML(File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom'))).at_xpath('//xmlns:author') } + let(:xml) { Nokogiri::XML(File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom'))).at_xpath('//xmlns:feed') } subject { UpdateRemoteProfileService.new } @@ -13,7 +13,7 @@ RSpec.describe UpdateRemoteProfileService do let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } before do - subject.(xml, remote_account) + subject.call(xml, remote_account) end it 'downloads new avatar' do @@ -34,10 +34,10 @@ RSpec.describe UpdateRemoteProfileService do end context 'with unchanged details' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com',display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') } + let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com', display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') } before do - subject.(xml, remote_account) + subject.call(xml, remote_account) end it 'does not re-download avatar' do |