mirror of https://github.com/mastodon/mastodon
Merge branch 'master' into patch-1
This commit is contained in:
commit
03fe20acf0
|
@ -17,7 +17,7 @@ Click on the screenshot to watch a demo of the UI:
|
|||
|
||||
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
|
||||
|
||||
Focus of the project on a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
|
||||
The project focus is a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
|
||||
|
||||
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
|
||||
|
||||
|
|
4
app.json
4
app.json
|
@ -26,6 +26,10 @@
|
|||
"description": "The secret key base",
|
||||
"generator": "secret"
|
||||
},
|
||||
"OTP_SECRET": {
|
||||
"description": "One-time password secret",
|
||||
"generator": "secret"
|
||||
},
|
||||
"SINGLE_USER_MODE": {
|
||||
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
|
||||
"value": "false",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">
|
||||
<path d="M527.194 543.7a28.362 28.362 0 0 0-56.723 0 25.73 25.73 0 0 0 2.67 11.674 26.42 26.42 0 0 0 5.672 8.34 28.2 28.2 0 0 0 40.04 0 31.87 31.87 0 0 0 6.006-8.34 28.8 28.8 0 0 0 2.336-11.674m-48.382-113.413a28.308 28.308 0 1 0 40.04 40.027 37.2 37.2 0 0 0 4.67-5.67 28.092 28.092 0 0 0 3.67-14.343 27.29 27.29 0 0 0-8.34-20.012 28.24 28.24 0 0 0-5.006-4 26.958 26.958 0 0 0-15.015-4.336 27.31 27.31 0 0 0-20.02 8.34m20.02-101.735a28.476 28.476 0 1 0 20.02 8.34 27.31 27.31 0 0 0-20.02-8.34M231.9 573.717a28.18 28.18 0 1 0 8.342 20.012 27.308 27.308 0 0 0-8.342-20.014m-40.04-93.4a28.352 28.352 0 0 0 20.02 48.366 26.958 26.958 0 0 0 15.015-4.336 28.255 28.255 0 0 0 5.005-4 27.29 27.29 0 0 0 8.342-20.013 28.09 28.09 0 0 0-3.67-14.343 37.21 37.21 0 0 0-4.67-5.67 28.2 28.2 0 0 0-40.04 0m40.04-93.4a28.2 28.2 0 0 0-40.04 0 26.425 26.425 0 0 0-5.673 8.34 25.73 25.73 0 0 0-2.67 11.673 28.315 28.315 0 0 0 48.38 20.018 27.29 27.29 0 0 0 8.342-20.012 28.8 28.8 0 0 0-2.336-11.674 31.87 31.87 0 0 0-6.006-8.34m550.55 178.453a28.476 28.476 0 1 0 20.02 8.34 27.31 27.31 0 0 0-20.02-8.34m20.02-85.057a28.2 28.2 0 0 0-40.04 0 37.2 37.2 0 0 0-4.672 5.67 28.092 28.092 0 0 0-3.67 14.343 27.29 27.29 0 0 0 8.342 20.013 28.248 28.248 0 0 0 5.005 4 26.96 26.96 0 0 0 15.015 4.336 28.3 28.3 0 0 0 20.02-48.366m-46.046-85.057a28.8 28.8 0 0 0-2.336 11.673 28.362 28.362 0 0 0 56.723 0 25.73 25.73 0 0 0-2.668-11.674 26.427 26.427 0 0 0-5.672-8.34 28.2 28.2 0 0 0-40.04 0 31.86 31.86 0 0 0-6.007 8.343z" fill="#2b90d9"/>
|
||||
<path d="M853.52 146.764Q707.04 0 499.833 0 292.96 0 146.48 146.764 0 293.2 0 500q0 207.138 146.48 353.57T499.833 1000q207.207 0 353.687-146.43T1000 500q0-206.8-146.48-353.236zM213.547 708.806h-3.337q-43.043 0-73.407-30.02-30.03-30.354-30.03-73.382V395.93v-.666q1.335-41.027 30.03-69.713 30.364-30.35 73.407-30.35t73.073 30.354q29.363 29.02 30.364 70.38V615.41q2.336 55.037 46.713 93.4zM600.6 554.7q-1 41.36-30.364 70.38-30.03 30.353-73.073 30.354t-73.407-30.354q-28.7-28.686-30.03-69.713V345.23q0-43.03 30.03-73.382 30.364-30.02 73.407-30.02h150.15q-44.378 38.36-46.713 93.4zm286.954 50.7q0 43.03-30.03 73.382-30.364 30.02-73.407 30.02h-150.15q44.378-38.36 46.713-93.4v-219.47q1-41.362 30.364-70.38 30.03-30.355 73.073-30.355t73.407 30.354q28.7 28.687 30.03 69.714V605.4z" fill="#2b90d9"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -15,6 +15,7 @@ const ColumnCollapsable = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string,
|
||||
fullHeight: React.PropTypes.number.isRequired,
|
||||
children: React.PropTypes.node,
|
||||
onCollapse: React.PropTypes.func
|
||||
|
@ -39,13 +40,13 @@ const ColumnCollapsable = React.createClass({
|
|||
},
|
||||
|
||||
render () {
|
||||
const { icon, fullHeight, children } = this.props;
|
||||
const { icon, title, fullHeight, children } = this.props;
|
||||
const { collapsed } = this.state;
|
||||
const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
|
||||
<div title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
|
||||
|
||||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
||||
{({ opacity, height }) =>
|
||||
|
|
|
@ -6,7 +6,8 @@ import SettingToggle from '../../notifications/components/setting_toggle';
|
|||
import SettingText from './setting_text';
|
||||
|
||||
const messages = defineMessages({
|
||||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }
|
||||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
|
||||
settings: { id: 'home.settings', defaultMessage: 'Column settings' }
|
||||
});
|
||||
|
||||
const outerStyle = {
|
||||
|
@ -39,7 +40,7 @@ const ColumnSettings = React.createClass({
|
|||
const { settings, onChange, onSave, intl } = this.props;
|
||||
|
||||
return (
|
||||
<ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}>
|
||||
<ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={209} onCollapse={onSave}>
|
||||
<div className='column-settings--outer' style={outerStyle}>
|
||||
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
|
||||
});
|
||||
|
||||
const iconStyle = {
|
||||
fontSize: '16px',
|
||||
padding: '15px',
|
||||
|
@ -8,14 +14,22 @@ const iconStyle = {
|
|||
zIndex: '2'
|
||||
};
|
||||
|
||||
const ClearColumnButton = ({ onClick }) => (
|
||||
<div className='column-icon' tabindex='0' style={iconStyle} onClick={onClick}>
|
||||
<i className='fa fa-trash' />
|
||||
</div>
|
||||
);
|
||||
const ClearColumnButton = React.createClass({
|
||||
|
||||
ClearColumnButton.propTypes = {
|
||||
onClick: React.PropTypes.func.isRequired
|
||||
};
|
||||
propTypes: {
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
export default ClearColumnButton;
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<div title={intl.formatMessage(messages.clear)} className='column-icon' tabIndex='0' style={iconStyle} onClick={this.onClick}>
|
||||
<i className='fa fa-eraser' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
export default injectIntl(ClearColumnButton);
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ColumnCollapsable from '../../../components/column_collapsable';
|
||||
import SettingToggle from './setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
settings: { id: 'notifications.settings', defaultMessage: 'Column settings' }
|
||||
});
|
||||
|
||||
const outerStyle = {
|
||||
padding: '15px'
|
||||
};
|
||||
|
@ -30,14 +34,14 @@ const ColumnSettings = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
render () {
|
||||
const { settings, onChange, onSave } = this.props;
|
||||
const { settings, intl, onChange, onSave } = this.props;
|
||||
|
||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||
|
||||
return (
|
||||
<ColumnCollapsable icon='sliders' fullHeight={616} onCollapse={onSave}>
|
||||
<ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={616} onCollapse={onSave}>
|
||||
<div className='column-settings--outer' style={outerStyle}>
|
||||
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||
|
||||
|
@ -77,4 +81,4 @@ const ColumnSettings = React.createClass({
|
|||
|
||||
});
|
||||
|
||||
export default ColumnSettings;
|
||||
export default injectIntl(ColumnSettings);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
const en = {
|
||||
"column_back_button.label": "Zurück",
|
||||
"lightbox.close": "Schließen",
|
||||
"loading_indicator.label": "Lade...",
|
||||
"loading_indicator.label": "Lade…",
|
||||
"status.mention": "Erwähnen",
|
||||
"status.delete": "Löschen",
|
||||
"status.reply": "Antworten",
|
||||
"status.reblog": "Teilen",
|
||||
"status.favourite": "Favorisieren",
|
||||
"status.reblogged_by": "{name} teilte",
|
||||
"status.sensitive_warning": "Sensible Inhalte",
|
||||
"status.sensitive_toggle": "Klicken um zu zeigen",
|
||||
"status.sensitive_warning": "Heikle Inhalte",
|
||||
"status.sensitive_toggle": "Klicke, um sie zu sehen",
|
||||
"status.open": "Öffnen",
|
||||
"video_player.toggle_sound": "Ton umschalten",
|
||||
"account.mention": "Erwähnen",
|
||||
|
@ -20,17 +20,17 @@ const en = {
|
|||
"account.follow": "Folgen",
|
||||
"account.posts": "Beiträge",
|
||||
"account.follows": "Folgt",
|
||||
"account.followers": "Folger",
|
||||
"account.followers": "Folgende",
|
||||
"account.follows_you": "Folgt dir",
|
||||
"account.requested": "Warte auf Erlaubnis",
|
||||
"getting_started.heading": "Erste Schritte",
|
||||
"getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.",
|
||||
"getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.",
|
||||
"getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben auf der Seite eingibst.",
|
||||
"getting_started.about_shortcuts": "Falls die Person auf derselben Domain ist wie du, reicht auch ihr Nutzername alleine. Das gilt auch für Erwähnungen in Beiträgen.",
|
||||
"getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden",
|
||||
"getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
|
||||
"column.home": "Home",
|
||||
"column.mentions": "Erwähnungen",
|
||||
"column.public": "Gesamtes Bekanntes Netz",
|
||||
"column.public": "Gesamtes bekanntes Netz",
|
||||
"column.notifications": "Mitteilungen",
|
||||
"column.follow_requests": "Folgeanfragen",
|
||||
"tabs_bar.compose": "Schreiben",
|
||||
|
@ -38,11 +38,11 @@ const en = {
|
|||
"tabs_bar.mentions": "Erwähnungen",
|
||||
"tabs_bar.public": "Gesamtes Netz",
|
||||
"tabs_bar.notifications": "Mitteilungen",
|
||||
"compose_form.placeholder": "Worüber möchstest du schreiben?",
|
||||
"compose_form.placeholder": "Worüber möchtest du schreiben?",
|
||||
"compose_form.publish": "Tröt",
|
||||
"compose_form.sensitive": "Medien als sensitiv markieren",
|
||||
"compose_form.unlisted": "Öffentlich nicht auflisten",
|
||||
"compose_form.sensitive": "Medien als heikel markieren",
|
||||
"compose_form.private": "Als privat markieren",
|
||||
"compose_form.unlisted": "Nicht öffentlich auflisten",
|
||||
"navigation_bar.edit_profile": "Profil bearbeiten",
|
||||
"navigation_bar.preferences": "Einstellungen",
|
||||
"navigation_bar.public_timeline": "Öffentlich",
|
||||
|
@ -52,15 +52,15 @@ const en = {
|
|||
"search.placeholder": "Suche",
|
||||
"search.account": "Konto",
|
||||
"search.hashtag": "Hashtag",
|
||||
"upload_button.label": "Media-Datei anfügen",
|
||||
"upload_button.label": "Mediendatei hinzufügen",
|
||||
"upload_form.undo": "Entfernen",
|
||||
"notification.follow": "{name} folgt dir",
|
||||
"notification.favourite": "{name} favorisierte deinen Status",
|
||||
"notification.reblog": "{name} teilte deinen Status",
|
||||
"notification.mention": "{name} erwähnte dich",
|
||||
"notifications.column_settings.alert": "Desktop-Benachrichtigunen",
|
||||
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
|
||||
"notifications.column_settings.show": "In der Spalte anzeigen",
|
||||
"notifications.column_settings.follow": "Neue Folger:",
|
||||
"notifications.column_settings.follow": "Neue Folgende:",
|
||||
"notifications.column_settings.favourite": "Favorisierungen:",
|
||||
"notifications.column_settings.mention": "Erwähnungen:",
|
||||
"notifications.column_settings.reblog": "Geteilte Beiträge:",
|
||||
|
|
|
@ -10,6 +10,10 @@ const en = {
|
|||
"status.reblogged_by": "{name} boosted",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_toggle": "Click to view",
|
||||
"status.show_more": "Show more",
|
||||
"status.show_less": "Show less",
|
||||
"status.open": "Expand this status",
|
||||
"status.report": "Report @{name}",
|
||||
"video_player.toggle_sound": "Toggle sound",
|
||||
"account.mention": "Mention @{name}",
|
||||
"account.edit_profile": "Edit profile",
|
||||
|
|
|
@ -10,6 +10,10 @@ const fr = {
|
|||
"status.reblogged_by": "{name} a partagé :",
|
||||
"status.sensitive_warning": "Contenu délicat",
|
||||
"status.sensitive_toggle": "Cliquer pour dévoiler",
|
||||
"status.show_more": "Déplier",
|
||||
"status.show_less": "Replier",
|
||||
"status.open": "Déplier ce status",
|
||||
"status.report": "Signaler @{name}",
|
||||
"video_player.toggle_sound": "Mettre/Couper le son",
|
||||
"account.mention": "Mentionner",
|
||||
"account.edit_profile": "Modifier le profil",
|
||||
|
@ -35,7 +39,6 @@ const fr = {
|
|||
"column.community": "Fil public local",
|
||||
"column.public": "Fil public global",
|
||||
"column.notifications": "Notifications",
|
||||
"column.public": "Fil public",
|
||||
"column.blocks": "Utilisateurs bloqués",
|
||||
"column.favourites": "Favoris",
|
||||
"tabs_bar.compose": "Composer",
|
||||
|
@ -44,9 +47,9 @@ const fr = {
|
|||
"tabs_bar.public": "Fil public global",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"compose_form.placeholder": "Qu’avez-vous en tête ?",
|
||||
"compose_form.publish": "Pouet ",
|
||||
"compose_form.publish": "Pouet",
|
||||
"compose_form.sensitive": "Marquer le média comme délicat",
|
||||
"compose_form.spoiler": "Masquer le texte par un avertissement",
|
||||
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
|
||||
"compose_form.private": "Rendre privé",
|
||||
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
|
||||
"compose_form.unlisted": "Ne pas afficher dans les fils publics",
|
||||
|
@ -58,7 +61,6 @@ const fr = {
|
|||
"navigation_bar.blocks": "Utilisateurs bloqués",
|
||||
"navigation_bar.favourites": "Favoris",
|
||||
"navigation_bar.info": "Plus d'informations",
|
||||
"notification.favourite": "{name} a ajouté à ses favoris :",
|
||||
"navigation_bar.logout": "Déconnexion",
|
||||
"reply_indicator.cancel": "Annuler",
|
||||
"search.placeholder": "Chercher",
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include Localized
|
||||
|
||||
# Prevent CSRF attacks by raising an exception.
|
||||
# For APIs, you may want to use :null_session instead.
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
|
||||
|
||||
include Localized
|
||||
helper_method :current_account
|
||||
|
||||
rescue_from ActionController::RoutingError, with: :not_found
|
||||
|
@ -41,7 +40,6 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
# If the sign in is after a two week break, we need to regenerate their feed
|
||||
RegenerationWorker.perform_async(current_user.account_id) if current_user.last_sign_in_at < 14.days.ago
|
||||
return
|
||||
end
|
||||
|
||||
def check_suspension
|
||||
|
|
|
@ -4,13 +4,25 @@ module Localized
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_locale
|
||||
around_action :set_locale
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_locale
|
||||
I18n.locale = current_user.try(:locale) || default_locale
|
||||
rescue I18n::InvalidLocale
|
||||
I18n.locale = default_locale
|
||||
locale = default_locale
|
||||
|
||||
if user_signed_in?
|
||||
begin
|
||||
locale = current_user.try(:locale) || default_locale
|
||||
rescue I18n::InvalidLocale
|
||||
locale = default_locale
|
||||
end
|
||||
end
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def default_locale
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
include Localized
|
||||
|
||||
skip_before_action :authenticate_resource_owner!
|
||||
|
||||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
|
||||
include Localized
|
||||
|
||||
private
|
||||
|
||||
def store_current_location
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
||||
include Localized
|
||||
|
||||
skip_before_action :authenticate_resource_owner!
|
||||
|
||||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
|
||||
include Localized
|
||||
|
||||
private
|
||||
|
||||
def store_current_location
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SiteTitleHelper
|
||||
def site_title
|
||||
Setting.site_title.to_s
|
||||
end
|
||||
end
|
|
@ -20,8 +20,6 @@ class FollowRemoteAccountService < BaseService
|
|||
|
||||
Rails.logger.debug "Looking up webfinger for #{uri}"
|
||||
|
||||
account = Account.new(username: username, domain: domain)
|
||||
|
||||
data = Goldfinger.finger("acct:#{uri}")
|
||||
|
||||
raise Goldfinger::Error, 'Missing resource links' if data.link('http://schemas.google.com/g/2010#updates-from').nil? || data.link('salmon').nil? || data.link('http://webfinger.net/rel/profile-page').nil? || data.link('magic-public-key').nil?
|
||||
|
@ -37,6 +35,7 @@ class FollowRemoteAccountService < BaseService
|
|||
|
||||
domain_block = DomainBlock.find_by(domain: domain)
|
||||
|
||||
account = Account.new(username: confirmed_username, domain: confirmed_domain)
|
||||
account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href
|
||||
account.salmon_url = data.link('salmon').href
|
||||
account.url = data.link('http://webfinger.net/rel/profile-page').href
|
||||
|
@ -51,8 +50,8 @@ class FollowRemoteAccountService < BaseService
|
|||
account.uri = get_account_uri(xml)
|
||||
account.hub_url = hubs.first.attribute('href').value
|
||||
|
||||
get_profile(body, account)
|
||||
account.save!
|
||||
get_profile(body, account)
|
||||
|
||||
account
|
||||
end
|
||||
|
|
|
@ -5,14 +5,13 @@ class ProcessFeedService < BaseService
|
|||
xml = Nokogiri::XML(body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
update_author(body, xml, account)
|
||||
update_author(body, account)
|
||||
process_entries(xml, account)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_author(body, xml, account)
|
||||
return if xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS).nil?
|
||||
def update_author(body, account)
|
||||
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateRemoteProfileService < BaseService
|
||||
def call(xml, account, resubscribe = false)
|
||||
def call(body, account, resubscribe = false)
|
||||
xml = Nokogiri::XML(body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
xml = xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS) || xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)
|
||||
|
||||
return if xml.nil?
|
||||
|
||||
author_xml = xml.at_xpath('./xmlns:author', xmlns: TagManager::XMLNS) || xml.at_xpath('./dfrn:owner', dfrn: TagManager::DFRN_XMLNS)
|
||||
|
@ -12,9 +17,9 @@ class UpdateRemoteProfileService < BaseService
|
|||
account.note = author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).nil?
|
||||
account.locked = author_xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content == 'private'
|
||||
|
||||
unless account.suspended? || DomainBlock.find_by(domain: account.domain)&.reject_media?
|
||||
account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'].blank?
|
||||
account.header_remote_url = author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS)['href'].blank?
|
||||
if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media?
|
||||
account.avatar_remote_url = link_href_from_xml(author_xml, 'avatar') if link_has_href?(author_xml, 'avatar')
|
||||
account.header_remote_url = link_href_from_xml(author_xml, 'header') if link_has_href?(author_xml, 'header')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -25,4 +30,14 @@ class UpdateRemoteProfileService < BaseService
|
|||
|
||||
SubscribeService.new.call(account) if resubscribe && (account.hub_url != old_hub_url)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_href_from_xml(xml, type)
|
||||
xml.at_xpath('./xmlns:link[@rel="' + type + '"]', xmlns: TagManager::XMLNS)['href']
|
||||
end
|
||||
|
||||
def link_has_href?(xml, type)
|
||||
!(xml.at_xpath('./xmlns:link[@rel="' + type + '"]', xmlns: TagManager::XMLNS).nil? || xml.at_xpath('./xmlns:link[@rel="' + type + '"]', xmlns: TagManager::XMLNS)['href'].blank?)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
= Rails.configuration.x.local_domain
|
||||
|
||||
- content_for :header_tags do
|
||||
%meta{ property: 'og:site_name', content: 'Mastodon' }/
|
||||
%meta{ property: 'og:site_name', content: site_title }/
|
||||
%meta{ property: 'og:type', content: 'website' }/
|
||||
%meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/
|
||||
%meta{ property: 'og:description', content: @description.blank? ? "Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly" : strip_tags(@description) }/
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
%link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
|
||||
%link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
|
||||
|
||||
%meta{ property: 'og:site_name', content: 'Mastodon' }/
|
||||
%meta{ property: 'og:site_name', content: site_title }/
|
||||
%meta{ property: 'og:type', content: 'profile' }/
|
||||
%meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
|
||||
%meta{ property: 'og:description', content: @account.note }/
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
- if content_for?(:page_title)
|
||||
= yield(:page_title)
|
||||
= ' - '
|
||||
= Setting.site_title
|
||||
= site_title
|
||||
|
||||
= stylesheet_link_tag 'application', media: 'all'
|
||||
= csrf_meta_tags
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
%link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/
|
||||
%link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/
|
||||
|
||||
%meta{ property: 'og:site_name', content: 'Mastodon' }/
|
||||
%meta{ property: 'og:site_name', content: site_title }/
|
||||
%meta{ property: 'og:type', content: 'article' }/
|
||||
%meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<p>Bienvenue <%= @resource.email %> !</p>
|
||||
|
||||
<p>Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous :</p>
|
||||
|
||||
<p><%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %></p>
|
|
@ -0,0 +1,5 @@
|
|||
Bienvenue <%= @resource.email %> !
|
||||
|
||||
Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous :
|
||||
|
||||
<%= confirmation_url(@resource, confirmation_token: @token) %>
|
|
@ -0,0 +1,3 @@
|
|||
<p>Bonjour <%= @resource.email %> !</p>
|
||||
|
||||
<p>Nous vous contactons pour vous informer que votre mot de passe sur Mastodon a bien été modifié.</p>
|
|
@ -0,0 +1,3 @@
|
|||
Bonjour <%= @resource.email %> !
|
||||
|
||||
Nous vous contactons pour vous informer que votre mot de passe sur Mastodon a bien été modifié.
|
|
@ -0,0 +1,8 @@
|
|||
<p>Bonjour <%= @resource.email %> !</p>
|
||||
|
||||
<p>Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.</p>
|
||||
|
||||
<p><%= link_to 'Modifier mon mot de passe', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||
|
||||
<p>Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.</p>
|
||||
<p>Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.</p>
|
|
@ -0,0 +1,8 @@
|
|||
Bonjour <%= @resource.email %> !
|
||||
|
||||
Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.
|
||||
|
||||
<%= edit_password_url(@resource, reset_password_token: @token) %>
|
||||
|
||||
Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.
|
||||
Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.
|
|
@ -6,14 +6,7 @@ class RemoteProfileUpdateWorker
|
|||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id, body, resubscribe)
|
||||
account = Account.find(account_id)
|
||||
|
||||
xml = Nokogiri::XML(body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
author_container = xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS) || xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)
|
||||
|
||||
UpdateRemoteProfileService.new.call(author_container, account, resubscribe)
|
||||
UpdateRemoteProfileService.new.call(body, Account.find(account_id), resubscribe)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
|
|
@ -7,8 +7,8 @@ de:
|
|||
terms: AGB
|
||||
accounts:
|
||||
follow: Folgen
|
||||
followers: Follower
|
||||
following: Gefolgt
|
||||
followers: Folger
|
||||
following: Folgt
|
||||
nothing_here: Hier gibt es nichts!
|
||||
people_followed_by: Nutzer, denen %{name} folgt
|
||||
people_who_follow: Nutzer, die %{name} folgen
|
||||
|
|
|
@ -7,7 +7,7 @@ fi:
|
|||
business_email: 'Business e-mail:'
|
||||
contact: Ota yhteyttä
|
||||
description_headline: Mikä on %{domain}?
|
||||
domain_count_after: muut palvelimet
|
||||
domain_count_after: muuhun palvelimeen
|
||||
domain_count_before: Yhdistyneenä
|
||||
features:
|
||||
api: Avoin API ohjelmille ja palveluille
|
||||
|
|
|
@ -5,13 +5,14 @@ fr:
|
|||
about_this: À propos de cette instance
|
||||
apps: Applications
|
||||
business_email: E-mail professionnel
|
||||
closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. .
|
||||
closed_registrations: Les inscriptions sont actuellement fermées sur cette instance.
|
||||
contact: Contact
|
||||
description_headline: Qu'est-ce que %{domain} ?
|
||||
domain_count_after: autres instances
|
||||
domain_count_before: Connectés à
|
||||
features:
|
||||
api: API ouverte aux apps et services
|
||||
blocking: Outils complets de bloquage et masquage
|
||||
blocks: Outils complets de bloquage et masquage
|
||||
characters: 500 caractères par post
|
||||
chronology: Fil chronologique
|
||||
ethics: 'Pas de pubs, pas de pistage'
|
||||
|
@ -21,6 +22,7 @@ fr:
|
|||
features_headline: Ce qui rend Mastodon différent
|
||||
get_started: Rejoindre le réseau
|
||||
links: Liens
|
||||
other_instances: Autres instances
|
||||
source_code: Code source
|
||||
status_count_after: posts
|
||||
status_count_before: Ayant publié
|
||||
|
@ -54,9 +56,24 @@ fr:
|
|||
reset_password: Réinitialiser le mot de passe
|
||||
set_new_password: Définir le nouveau mot de passe
|
||||
authorize_follow:
|
||||
error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant
|
||||
follow: Suivre
|
||||
prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre:'
|
||||
title: Suivre %{acct}
|
||||
datetime:
|
||||
distance_in_words:
|
||||
about_x_hours: "%{count}h"
|
||||
about_x_months: "%{count}mo"
|
||||
about_x_years: "%{count}y"
|
||||
almost_x_years: "%{count}y"
|
||||
half_a_minute: A l'instant
|
||||
less_than_x_minutes: "%{count}m"
|
||||
less_than_x_seconds: A l'instant
|
||||
over_x_years: "%{count}y"
|
||||
x_days: "%{count}d"
|
||||
x_minutes: "%{count}m"
|
||||
x_months: "%{count}mo"
|
||||
x_seconds: "%{count}s"
|
||||
exports:
|
||||
blocks: Vous bloquez
|
||||
csv: CSV
|
||||
|
@ -93,6 +110,9 @@ fr:
|
|||
follow:
|
||||
body: "%{name} vous suit !"
|
||||
subject: "%{name} vous suit"
|
||||
follow_request:
|
||||
body: "%{name} a demandé à vous suivre"
|
||||
subject: 'Abonné⋅es en attente : %{name}'
|
||||
mention:
|
||||
body: "%{name} vous a mentionné⋅e dans :"
|
||||
subject: "%{name} vous a mentionné⋅e"
|
||||
|
@ -132,7 +152,7 @@ fr:
|
|||
formats:
|
||||
default: '%d %b %Y, %H:%M'
|
||||
two_factor_auth:
|
||||
description_html: Si vous activez <strong>l'identification à deux facteurs</strong> vous devrez être en posession de votre téléphone afin de générer un code de connexion.
|
||||
description_html: Si vous activez <strong>l'identification à deux facteurs</strong>, vous devrez être en possession de votre téléphone afin de générer un code de connexion.
|
||||
disable: Désactiver
|
||||
enable: Activer
|
||||
instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion."
|
||||
|
|
|
@ -3,7 +3,7 @@ de:
|
|||
simple_form:
|
||||
hints:
|
||||
defaults:
|
||||
locked: Erlaubt dir, Folger zu überprüfen, bevor sie dir folgen können
|
||||
locked: Erlaubt dir, Nutzer zu überprüfen, bevor sie dir folgen können
|
||||
labels:
|
||||
defaults:
|
||||
avatar: Avatar
|
||||
|
@ -11,16 +11,16 @@ de:
|
|||
confirm_password: Passwort bestätigen
|
||||
current_password: Derzeitiges Passwort
|
||||
display_name: Anzeigename
|
||||
email: E-mail-Addresse
|
||||
email: E-Mail-Addresse
|
||||
header: Kopfbild
|
||||
locale: Sprache
|
||||
locked: Gesperrter Profil
|
||||
locked: Gesperrtes Profil
|
||||
new_password: Neues Passwort
|
||||
note: Über mich
|
||||
password: Passwort
|
||||
username: Nutzername
|
||||
interactions:
|
||||
must_be_follower: Benachrichtigungen von nicht-Folgern blockieren
|
||||
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
|
||||
|
|
|
@ -33,7 +33,7 @@ fr:
|
|||
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
|
||||
notification_emails:
|
||||
digest: Envoyer des emails récapitulatifs
|
||||
digest: Envoyer des courriels récapitulatifs
|
||||
favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statuts à ses favoris
|
||||
follow: Envoyer un courriel lorsque quelqu’un me suit
|
||||
follow_request: Envoyer un courriel lorsque quelqu'un demande à me suivre
|
||||
|
|
|
@ -10,7 +10,7 @@ These people make the development of Mastodon possible through [Patreon](https:/
|
|||
- [Kurtis Rainbolt-Greene](https://mastodon.social/users/krainboltgreene)
|
||||
- [Kit Redgrave](https://socially.constructed.space/users/KitRedgrave)
|
||||
- [Zeipher](https://mastodon.social/users/Zeipher)
|
||||
- [Effy Elden](https://toot.zone/users/effy)
|
||||
- [Effy Elden](https://mastodon.social/users/effy)
|
||||
- [Zoë Quinn](https://mastodon.social/users/zoequinn)
|
||||
|
||||
**Thank you to the following people**
|
||||
|
|
|
@ -39,6 +39,40 @@ You will want Amazon S3 for file storage. The only exception is for development
|
|||
purposes, where you may not care if files are not saved. Follow a guide online
|
||||
for creating a free Amazon S3 bucket and Access Key, then enter the details.
|
||||
|
||||
If you deploy from the web, the format for all the S3 bits use Paperclip conventions:
|
||||
|
||||
S3 Bucket is just the name of the bucket, e.g. `bucketname` not the full ARN.
|
||||
|
||||
S3 Region is the AWS code for the region e.g. `ap-northeast-1` not the name of the city displayed on the AWS Dashboard.
|
||||
|
||||
To protect the privacy of the users of the your instance, you should have permissons on the your S3 bucket set to no-read and no-write for the public and non-application-specific AWS users, with only one authorized IAM user or group set up to be able to upload or display content. This is an example of an IAM policy used for the S3 bucket used Mastadon instance hentai.loan:
|
||||
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:ListAllMyBuckets"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::hentailoan”,
|
||||
"arn:aws:s3:::hentailoan/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy from the Heroku web interface or from the command line. Run:
|
||||
|
|
|
@ -90,7 +90,9 @@ It is recommended to create a special user for mastodon on the server (you could
|
|||
|
||||
sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file git curl
|
||||
curl -sL https://deb.nodesource.com/setup_4.x | sudo bash -
|
||||
apt-get install nodejs
|
||||
|
||||
sudo apt-get install nodejs
|
||||
|
||||
sudo npm install -g yarn
|
||||
|
||||
## Redis
|
||||
|
|
|
@ -7,7 +7,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
|
|||
| -------------|-------------|---|---|
|
||||
| [mastodon.social](https://mastodon.social) |Flagship, quick updates|No|No|
|
||||
| [securitymastod.one](https://securitymastod.one/) |Information security enthusiasts and pros|Yes|Yes|
|
||||
| [mastodon.nuzgo.net](https://mastodon.nuzgo.net/) |Mastodon instance hosted in Paris |Yes|No|
|
||||
| [mastodon.nuzgo.net](https://mastodon.nuzgo.net/) |Mastodon instance hosted in Paris |Yes|Yes|
|
||||
| [mastodon.cx](https://mastodon.cx/) |Alternative Mastodon instance hosted in France|Yes|Yes|
|
||||
| [mastodon.network](https://mastodon.network) |N/A|Yes|Yes|
|
||||
| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No|
|
||||
|
@ -69,7 +69,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
|
|||
| [meow.social](https://meow.social)|A furry fandom focused instance|Yes|No|
|
||||
| [neumastodon.com](https://neumastodon.com/)|Northeastern University Mastodon |Yes|No|
|
||||
| [dancingbanana.party](https://dancingbanana.party)|La banane qui danse.|Yes|No|
|
||||
| [mastodon.brussels.fr](https://mastodon.brussels/)|Le mastodon pour les belges, si vous aimez la bonne ambiance venez nous rejoindre !|Yes|Yes|
|
||||
| [mastodon.brussels](https://mastodon.brussels/)|Le mastodon pour les belges, si vous aimez la bonne ambiance venez nous rejoindre !|Yes|Yes|
|
||||
| [mastodon.llamasweet.tech](https://mastodon.llamasweet.tech/)|Mastodon about Android developement|Yes|No|
|
||||
| [manx.social](https://manx.social/)|Instance for the Isle of Man|Yes|Yes|
|
||||
| [mastodon.host](https://mastodon.host/)|Lightly moderated, federates everywhere and has a follow bot ( Huge federated timeline )|Yes|No|
|
||||
|
@ -77,6 +77,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
|
|||
| [oulipo.social](https://oulipo.social/)|An Oulipo Mastodon in which that fifth symbol in Latin script is taboo|Yes|No|
|
||||
| [indigo.zone](https://indigo.zone)|Open Registrations, General Purpose|Yes|No|
|
||||
| [mastodones.club](https://mastodones.club)|Mastodon en español|Yes|Yes|
|
||||
| [mst3k.interlinked.me](https://mst3k.interlinked.me)|Open registrations, general purpose|Yes|Yes|
|
||||
|
||||
|
||||
Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request).
|
||||
We are no longer maintaining this list as instances are popping up too quickly for using GitHub to be a tenable system for tracking them. Please standby while we work on another solution
|
||||
|
|
|
@ -43,6 +43,7 @@ ___
|
|||
- [For Python](https://github.com/halcy/Mastodon.py)
|
||||
- [For JavaScript](https://github.com/Zatnosk/libodonjs)
|
||||
- [For JavaScript (Node.js)](https://github.com/jessicahayley/node-mastodon)
|
||||
- [For Elixir](https://github.com/milmazz/hunter)
|
||||
|
||||
___
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
require "rails_helper"
|
||||
|
||||
describe "site_title" do
|
||||
it "Uses the Setting.site_title value when it exists" do
|
||||
Setting.site_title = "New site title"
|
||||
|
||||
expect(helper.site_title).to eq "New site title"
|
||||
end
|
||||
|
||||
it "returns empty string when Setting.site_title is nil" do
|
||||
Setting.site_title = nil
|
||||
|
||||
expect(helper.site_title).to eq ""
|
||||
end
|
||||
end
|
|
@ -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:feed') }
|
||||
let(:xml) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom')) }
|
||||
|
||||
subject { UpdateRemoteProfileService.new }
|
||||
|
||||
|
|
Loading…
Reference in New Issue