diff --git a/app/controllers/admin/terms_of_service/distributions_controller.rb b/app/controllers/admin/terms_of_service/distributions_controller.rb new file mode 100644 index 0000000000..c639b083dd --- /dev/null +++ b/app/controllers/admin/terms_of_service/distributions_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::DistributionsController < Admin::BaseController + before_action :set_terms_of_service + + def create + authorize @terms_of_service, :distribute? + @terms_of_service.touch(:notification_sent_at) + Admin::DistributeTermsOfServiceNotificationWorker.perform_async(@terms_of_service.id) + redirect_to admin_terms_of_service_index_path + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) + end +end diff --git a/app/controllers/admin/terms_of_service/drafts_controller.rb b/app/controllers/admin/terms_of_service/drafts_controller.rb new file mode 100644 index 0000000000..5d32c0bd83 --- /dev/null +++ b/app/controllers/admin/terms_of_service/drafts_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::DraftsController < Admin::BaseController + before_action :set_terms_of_service + + def show + authorize :terms_of_service, :create? + end + + def update + authorize @terms_of_service, :update? + + @terms_of_service.published_at = Time.now.utc if params[:action_type] == 'publish' + + if @terms_of_service.update(resource_params) + log_action(:publish, @terms_of_service) if @terms_of_service.published? + redirect_to @terms_of_service.published? ? admin_terms_of_service_index_path : admin_terms_of_service_draft_path + else + render :show + end + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text) + end + + def current_terms_of_service + TermsOfService.live.first + end + + def resource_params + params.require(:terms_of_service).permit(:text, :changelog) + end +end diff --git a/app/controllers/admin/terms_of_service/generates_controller.rb b/app/controllers/admin/terms_of_service/generates_controller.rb new file mode 100644 index 0000000000..28037674a3 --- /dev/null +++ b/app/controllers/admin/terms_of_service/generates_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::GeneratesController < Admin::BaseController + before_action :set_instance_presenter + + def show + authorize :terms_of_service, :create? + + @generator = TermsOfService::Generator.new( + domain: @instance_presenter.domain, + admin_email: @instance_presenter.contact.email + ) + end + + def create + authorize :terms_of_service, :create? + + @generator = TermsOfService::Generator.new(resource_params) + + if @generator.valid? + TermsOfService.create!(text: @generator.render) + redirect_to admin_terms_of_service_draft_path + else + render :show + end + end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + + def resource_params + params.require(:terms_of_service_generator).permit(*TermsOfService::Generator::VARIABLES) + end +end diff --git a/app/controllers/admin/terms_of_service/histories_controller.rb b/app/controllers/admin/terms_of_service/histories_controller.rb new file mode 100644 index 0000000000..8f12341aea --- /dev/null +++ b/app/controllers/admin/terms_of_service/histories_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::HistoriesController < Admin::BaseController + def show + authorize :terms_of_service, :index? + @terms_of_service = TermsOfService.published.all + end +end diff --git a/app/controllers/admin/terms_of_service/previews_controller.rb b/app/controllers/admin/terms_of_service/previews_controller.rb new file mode 100644 index 0000000000..0a1a966751 --- /dev/null +++ b/app/controllers/admin/terms_of_service/previews_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::PreviewsController < Admin::BaseController + before_action :set_terms_of_service + + def show + authorize @terms_of_service, :distribute? + @user_count = @terms_of_service.scope_for_notification.count + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) + end +end diff --git a/app/controllers/admin/terms_of_service/tests_controller.rb b/app/controllers/admin/terms_of_service/tests_controller.rb new file mode 100644 index 0000000000..e2483c1005 --- /dev/null +++ b/app/controllers/admin/terms_of_service/tests_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::TestsController < Admin::BaseController + before_action :set_terms_of_service + + def create + authorize @terms_of_service, :distribute? + UserMailer.terms_of_service_changed(current_user, @terms_of_service).deliver_later! + redirect_to admin_terms_of_service_preview_path(@terms_of_service) + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) + end +end diff --git a/app/controllers/admin/terms_of_service_controller.rb b/app/controllers/admin/terms_of_service_controller.rb new file mode 100644 index 0000000000..f70bfd2071 --- /dev/null +++ b/app/controllers/admin/terms_of_service_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Admin::TermsOfServiceController < Admin::BaseController + def index + authorize :terms_of_service, :index? + @terms_of_service = TermsOfService.live.first + end +end diff --git a/app/controllers/api/v1/instances/terms_of_services_controller.rb b/app/controllers/api/v1/instances/terms_of_services_controller.rb new file mode 100644 index 0000000000..e9e8e8ef55 --- /dev/null +++ b/app/controllers/api/v1/instances/terms_of_services_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseController + before_action :set_terms_of_service + + def show + cache_even_if_authenticated! + render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.live.first! + end +end diff --git a/app/controllers/terms_of_service_controller.rb b/app/controllers/terms_of_service_controller.rb new file mode 100644 index 0000000000..672fb07915 --- /dev/null +++ b/app/controllers/terms_of_service_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class TermsOfServiceController < ApplicationController + include WebAppControllerConcern + + skip_before_action :require_functional! + + def show + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + end +end diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 9d5a2e2478..e827834975 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -64,6 +64,10 @@ module FormattingHelper end end + def markdown(text) + Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true).render(text).html_safe # rubocop:disable Rails/OutputSafety + end + private def wrapped_status_content_format(status) diff --git a/app/javascript/mastodon/api/instance.ts b/app/javascript/mastodon/api/instance.ts new file mode 100644 index 0000000000..ec9146fb34 --- /dev/null +++ b/app/javascript/mastodon/api/instance.ts @@ -0,0 +1,11 @@ +import { apiRequestGet } from 'mastodon/api'; +import type { + ApiTermsOfServiceJSON, + ApiPrivacyPolicyJSON, +} from 'mastodon/api_types/instance'; + +export const apiGetTermsOfService = () => + apiRequestGet('v1/instance/terms_of_service'); + +export const apiGetPrivacyPolicy = () => + apiRequestGet('v1/instance/privacy_policy'); diff --git a/app/javascript/mastodon/api_types/instance.ts b/app/javascript/mastodon/api_types/instance.ts new file mode 100644 index 0000000000..ead9774515 --- /dev/null +++ b/app/javascript/mastodon/api_types/instance.ts @@ -0,0 +1,9 @@ +export interface ApiTermsOfServiceJSON { + updated_at: string; + content: string; +} + +export interface ApiPrivacyPolicyJSON { + updated_at: string; + content: string; +} diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index 3b24a76368..24141b13cb 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -18,7 +18,7 @@ import Column from 'mastodon/components/column'; import { Icon } from 'mastodon/components/icon'; import { ServerHeroImage } from 'mastodon/components/server_hero_image'; import { Skeleton } from 'mastodon/components/skeleton'; -import LinkFooter from 'mastodon/features/ui/components/link_footer'; +import { LinkFooter} from 'mastodon/features/ui/components/link_footer'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, diff --git a/app/javascript/mastodon/features/getting_started/index.jsx b/app/javascript/mastodon/features/getting_started/index.jsx index 8d26115dfa..ece06953ea 100644 --- a/app/javascript/mastodon/features/getting_started/index.jsx +++ b/app/javascript/mastodon/features/getting_started/index.jsx @@ -25,7 +25,7 @@ import StarIcon from '@/material-icons/400-24px/star.svg?react'; import { fetchFollowRequests } from 'mastodon/actions/accounts'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import LinkFooter from 'mastodon/features/ui/components/link_footer'; +import { LinkFooter } from 'mastodon/features/ui/components/link_footer'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions'; diff --git a/app/javascript/mastodon/features/privacy_policy/index.jsx b/app/javascript/mastodon/features/privacy_policy/index.jsx deleted file mode 100644 index d420546e4f..0000000000 --- a/app/javascript/mastodon/features/privacy_policy/index.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl'; - -import { Helmet } from 'react-helmet'; - -import api from 'mastodon/api'; -import Column from 'mastodon/components/column'; -import { Skeleton } from 'mastodon/components/skeleton'; - -const messages = defineMessages({ - title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, -}); - -class PrivacyPolicy extends PureComponent { - - static propTypes = { - intl: PropTypes.object, - multiColumn: PropTypes.bool, - }; - - state = { - content: null, - lastUpdated: null, - isLoading: true, - }; - - componentDidMount () { - api().get('/api/v1/instance/privacy_policy').then(({ data }) => { - this.setState({ content: data.content, lastUpdated: data.updated_at, isLoading: false }); - }).catch(() => { - this.setState({ isLoading: false }); - }); - } - - render () { - const { intl, multiColumn } = this.props; - const { isLoading, content, lastUpdated } = this.state; - - return ( - -
-
-

-

: }} />

-
- -
-
- - - {intl.formatMessage(messages.title)} - - - - ); - } - -} - -export default injectIntl(PrivacyPolicy); diff --git a/app/javascript/mastodon/features/privacy_policy/index.tsx b/app/javascript/mastodon/features/privacy_policy/index.tsx new file mode 100644 index 0000000000..f0309c2712 --- /dev/null +++ b/app/javascript/mastodon/features/privacy_policy/index.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect } from 'react'; + +import { + FormattedMessage, + FormattedDate, + useIntl, + defineMessages, +} from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { apiGetPrivacyPolicy } from 'mastodon/api/instance'; +import type { ApiPrivacyPolicyJSON } from 'mastodon/api_types/instance'; +import { Column } from 'mastodon/components/column'; +import { Skeleton } from 'mastodon/components/skeleton'; + +const messages = defineMessages({ + title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, +}); + +const PrivacyPolicy: React.FC<{ + multiColumn: boolean; +}> = ({ multiColumn }) => { + const intl = useIntl(); + const [response, setResponse] = useState(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + apiGetPrivacyPolicy() + .then((data) => { + setResponse(data); + setLoading(false); + return ''; + }) + .catch(() => { + setLoading(false); + }); + }, []); + + return ( + +
+
+

+ +

+

+ + ) : ( + + ), + }} + /> +

+
+ + {response && ( +
+ )} +
+ + + {intl.formatMessage(messages.title)} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default PrivacyPolicy; diff --git a/app/javascript/mastodon/features/terms_of_service/index.tsx b/app/javascript/mastodon/features/terms_of_service/index.tsx new file mode 100644 index 0000000000..05033bffec --- /dev/null +++ b/app/javascript/mastodon/features/terms_of_service/index.tsx @@ -0,0 +1,95 @@ +import { useState, useEffect } from 'react'; + +import { + FormattedMessage, + FormattedDate, + useIntl, + defineMessages, +} from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { apiGetTermsOfService } from 'mastodon/api/instance'; +import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance'; +import { Column } from 'mastodon/components/column'; +import { Skeleton } from 'mastodon/components/skeleton'; +import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; + +const messages = defineMessages({ + title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' }, +}); + +const TermsOfService: React.FC<{ + multiColumn: boolean; +}> = ({ multiColumn }) => { + const intl = useIntl(); + const [response, setResponse] = useState(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + apiGetTermsOfService() + .then((data) => { + setResponse(data); + setLoading(false); + return ''; + }) + .catch(() => { + setLoading(false); + }); + }, []); + + if (!loading && !response) { + return ; + } + + return ( + +
+
+

+ +

+

+ + ) : ( + + ), + }} + /> +

+
+ + {response && ( +
+ )} +
+ + + {intl.formatMessage(messages.title)} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default TermsOfService; diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.jsx b/app/javascript/mastodon/features/ui/components/compose_panel.jsx index 18321cbe63..b085b2dc2a 100644 --- a/app/javascript/mastodon/features/ui/components/compose_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/compose_panel.jsx @@ -7,10 +7,9 @@ import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/ import ServerBanner from 'mastodon/components/server_banner'; import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; import SearchContainer from 'mastodon/features/compose/containers/search_container'; +import { LinkFooter } from 'mastodon/features/ui/components/link_footer'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import LinkFooter from './link_footer'; - class ComposePanel extends PureComponent { static propTypes = { identity: identityContextPropShape, diff --git a/app/javascript/mastodon/features/ui/components/link_footer.jsx b/app/javascript/mastodon/features/ui/components/link_footer.jsx deleted file mode 100644 index 49b21c2e48..0000000000 --- a/app/javascript/mastodon/features/ui/components/link_footer.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage, injectIntl } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { connect } from 'react-redux'; - -import { openModal } from 'mastodon/actions/modal'; -import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state'; -import { PERMISSION_INVITE_USERS } from 'mastodon/permissions'; - -const mapDispatchToProps = (dispatch) => ({ - onLogout () { - dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' })); - - }, -}); - -class LinkFooter extends PureComponent { - static propTypes = { - identity: identityContextPropShape, - multiColumn: PropTypes.bool, - onLogout: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleLogoutClick = e => { - e.preventDefault(); - e.stopPropagation(); - - this.props.onLogout(); - - return false; - }; - - render () { - const { signedIn, permissions } = this.props.identity; - const { multiColumn } = this.props; - - const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS); - const canProfileDirectory = profileDirectory; - - const DividingCircle = {' · '}; - - return ( -
-

- {domain}: - {' '} - - {statusPageUrl && ( - <> - {DividingCircle} - - - )} - {canInvite && ( - <> - {DividingCircle} - - - )} - {canProfileDirectory && ( - <> - {DividingCircle} - - - )} - {DividingCircle} - -

- -

- Mastodon: - {' '} - - {DividingCircle} - - {DividingCircle} - - {DividingCircle} - - {DividingCircle} - v{version} -

-
- ); - } - -} - -export default injectIntl(withIdentity(connect(null, mapDispatchToProps)(LinkFooter))); diff --git a/app/javascript/mastodon/features/ui/components/link_footer.tsx b/app/javascript/mastodon/features/ui/components/link_footer.tsx new file mode 100644 index 0000000000..06cb92fe4f --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/link_footer.tsx @@ -0,0 +1,105 @@ +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { + domain, + version, + source_url, + statusPageUrl, + profile_directory as canProfileDirectory, + termsOfServiceEnabled, +} from 'mastodon/initial_state'; + +const DividingCircle: React.FC = () => {' · '}; + +export const LinkFooter: React.FC<{ + multiColumn: boolean; +}> = ({ multiColumn }) => { + return ( +
+

+ {domain}:{' '} + + + + {statusPageUrl && ( + <> + + + + + + )} + {canProfileDirectory && ( + <> + + + + + + )} + + + + + {termsOfServiceEnabled && ( + <> + + + + + + )} +

+ +

+ Mastodon:{' '} + + + + + + + + + + + + + + + + + v{version} +

+
+ ); +}; diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index a2521a93d2..2766bf3c61 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -71,6 +71,7 @@ import { Explore, About, PrivacyPolicy, + TermsOfService, } from './util/async-components'; import { ColumnsContextProvider } from './util/columns_context'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; @@ -198,6 +199,7 @@ class SwitchingColumnsArea extends PureComponent { + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 8c491e5a2f..1690794661 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -198,6 +198,10 @@ export function PrivacyPolicy () { return import(/*webpackChunkName: "features/privacy_policy" */'../../privacy_policy'); } +export function TermsOfService () { + return import(/*webpackChunkName: "features/terms_of_service" */'../../terms_of_service'); +} + export function NotificationRequests () { return import(/*webpackChunkName: "features/notifications/requests" */'../../notifications/requests'); } diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 60b35cb31a..0c59fc9ee0 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -43,6 +43,8 @@ * @property {boolean=} use_pending_items * @property {string} version * @property {string} sso_redirect + * @property {string} status_page_url + * @property {boolean} terms_of_service_enabled */ /** @@ -115,10 +117,9 @@ export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); export const languages = initialState?.languages; export const criticalUpdatesPending = initialState?.critical_updates_pending; -// @ts-expect-error export const statusPageUrl = getMeta('status_page_url'); export const sso_redirect = getMeta('sso_redirect'); - +export const termsOfServiceEnabled = getMeta('terms_of_service_enabled'); /** * @returns {string | undefined} */ diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 36e1807228..ace2b05ba3 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -359,11 +359,11 @@ "footer.about": "About", "footer.directory": "Profiles directory", "footer.get_app": "Get the app", - "footer.invite": "Invite people", "footer.keyboard_shortcuts": "Keyboard shortcuts", "footer.privacy_policy": "Privacy policy", "footer.source_code": "View source code", "footer.status": "Status", + "footer.terms_of_service": "Terms of service", "generic.saved": "Saved", "getting_started.heading": "Getting started", "hashtag.admin_moderation": "Open moderation interface for #{name}", @@ -857,6 +857,7 @@ "subscribed_languages.target": "Change subscribed languages for {target}", "tabs_bar.home": "Home", "tabs_bar.notifications": "Notifications", + "terms_of_service.title": "Terms of Service", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss index f46160889a..1f3310877a 100644 --- a/app/javascript/styles/mailer.scss +++ b/app/javascript/styles/mailer.scss @@ -173,7 +173,9 @@ table + p { } .email-prose { - p { + p, + ul, + ol { color: #17063b; font-size: 14px; line-height: 20px; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index b5f8570ae2..611cb2884a 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -253,6 +253,10 @@ $content-width: 840px; .time-period { padding: 0 10px; } + + .back-link { + margin-bottom: 0; + } } h2 small { @@ -1940,3 +1944,76 @@ a.sparkline { } } } + +.admin { + &__terms-of-service { + &__container { + background: var(--surface-background-color); + border-radius: 8px; + border: 1px solid var(--background-border-color); + overflow: hidden; + + &__header { + padding: 16px; + font-size: 14px; + line-height: 20px; + color: $secondary-text-color; + display: flex; + align-items: center; + gap: 12px; + } + + &__body { + background: var(--background-color); + padding: 16px; + overflow-y: scroll; + height: 30vh; + } + } + + &__history { + & > li { + border-bottom: 1px solid var(--background-border-color); + + &:last-child { + border-bottom: 0; + } + } + + &__item { + padding: 16px 0; + padding-bottom: 8px; + + h5 { + font-size: 14px; + line-height: 20px; + font-weight: 600; + margin-bottom: 16px; + } + } + } + } +} + +.dot-indicator { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 500; + + &__indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: $dark-text-color; + } + + &.success { + color: $valid-value-color; + + .dot-indicator__indicator { + background-color: $valid-value-color; + } + } +} diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 5c9e5c96d9..b02c462217 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -209,6 +209,16 @@ class UserMailer < Devise::Mailer end end + def terms_of_service_changed(user, terms_of_service) + @resource = user + @terms_of_service = terms_of_service + @markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true) + + I18n.with_locale(locale) do + mail subject: default_i18n_subject + end + end + private def default_devise_subject diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index dfb7fd00ed..fd6b4289ce 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -57,6 +57,7 @@ class Admin::ActionLogFilter enable_relay: { target_type: 'Relay', action: 'enable' }.freeze, memorialize_account: { target_type: 'Account', action: 'memorialize' }.freeze, promote_user: { target_type: 'User', action: 'promote' }.freeze, + publish_terms_of_service: { target_type: 'TermsOfService', action: 'publish' }.freeze, remove_avatar_user: { target_type: 'User', action: 'remove_avatar' }.freeze, reopen_report: { target_type: 'Report', action: 'reopen' }.freeze, resend_user: { target_type: 'User', action: 'resend' }.freeze, diff --git a/app/models/terms_of_service.rb b/app/models/terms_of_service.rb new file mode 100644 index 0000000000..1f0832dc9a --- /dev/null +++ b/app/models/terms_of_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: terms_of_services +# +# id :bigint(8) not null, primary key +# changelog :text default(""), not null +# notification_sent_at :datetime +# published_at :datetime +# text :text default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# +class TermsOfService < ApplicationRecord + scope :published, -> { where.not(published_at: nil).order(published_at: :desc) } + scope :live, -> { published.limit(1) } + scope :draft, -> { where(published_at: nil).order(id: :desc).limit(1) } + + validates :text, presence: true + validates :changelog, presence: true, if: -> { published? } + + def published? + published_at.present? + end + + def notification_sent? + notification_sent_at.present? + end + + def scope_for_notification + User.confirmed.joins(:account).merge(Account.without_suspended).where(created_at: (..published_at)) + end +end diff --git a/app/models/terms_of_service/generator.rb b/app/models/terms_of_service/generator.rb new file mode 100644 index 0000000000..83c229720e --- /dev/null +++ b/app/models/terms_of_service/generator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class TermsOfService::Generator + include ActiveModel::Model + + TEMPLATE = Rails.root.join('config', 'templates', 'terms-of-service.md').read + + VARIABLES = %i( + admin_email + arbitration_address + arbitration_website + dmca_address + dmca_email + domain + jurisdiction + ).freeze + + attr_accessor(*VARIABLES) + + validates(*VARIABLES, presence: true) + + def render + format(TEMPLATE, VARIABLES.index_with { |key| public_send(key) }) + end +end diff --git a/app/policies/terms_of_service_policy.rb b/app/policies/terms_of_service_policy.rb new file mode 100644 index 0000000000..b4f0c71bc8 --- /dev/null +++ b/app/policies/terms_of_service_policy.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class TermsOfServicePolicy < ApplicationPolicy + def index? + role.can?(:manage_settings) + end + + def create? + role.can?(:manage_settings) + end + + def distribute? + record.published? && !record.notification_sent? && role.can?(:manage_settings) + end + + def update? + !record.published? && role.can?(:manage_settings) + end + + def destroy? + !record.published? && role.can?(:manage_settings) + end +end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 13f332c95c..5778bcc60f 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -109,6 +109,7 @@ class InitialStateSerializer < ActiveModel::Serializer trends_as_landing_page: Setting.trends_as_landing_page, trends_enabled: Setting.trends, version: instance_presenter.version, + terms_of_service_enabled: TermsOfService.live.exists?, } end diff --git a/app/views/admin/terms_of_service/_links.html.haml b/app/views/admin/terms_of_service/_links.html.haml new file mode 100644 index 0000000000..aaf0f2c7b7 --- /dev/null +++ b/app/views/admin/terms_of_service/_links.html.haml @@ -0,0 +1,6 @@ +.content__heading__tabs + = render_navigation renderer: :links do |primary| + :ruby + primary.item :current, safe_join([material_symbol('description'), t('admin.terms_of_service.current')]), admin_terms_of_service_index_path + primary.item :draft, safe_join([material_symbol('description'), t('admin.terms_of_service.draft')]), admin_terms_of_service_draft_path + primary.item :previous, safe_join([material_symbol('history'), t('admin.terms_of_service.history')]), admin_terms_of_service_history_path diff --git a/app/views/admin/terms_of_service/drafts/show.html.haml b/app/views/admin/terms_of_service/drafts/show.html.haml new file mode 100644 index 0000000000..7a9a6fd3c4 --- /dev/null +++ b/app/views/admin/terms_of_service/drafts/show.html.haml @@ -0,0 +1,19 @@ +- content_for :page_title do + = t('admin.terms_of_service.title') + +- content_for :heading do + %h2= t('admin.terms_of_service.title') + = render partial: 'admin/terms_of_service/links' + += simple_form_for @terms_of_service, url: admin_terms_of_service_draft_path, method: :put do |form| + = render 'shared/error_messages', object: @terms_of_service + + .fields-group + = form.input :text, wrapper: :with_block_label, input_html: { rows: 8 } + + .fields-group + = form.input :changelog, wrapper: :with_block_label, input_html: { rows: 8 } + + .actions + = form.button :button, t('admin.terms_of_service.save_draft'), type: :submit, name: :action_type, value: :save_draft, class: 'button button-secondary' + = form.button :button, t('admin.terms_of_service.publish'), type: :submit, name: :action_type, value: :publish diff --git a/app/views/admin/terms_of_service/generates/show.html.haml b/app/views/admin/terms_of_service/generates/show.html.haml new file mode 100644 index 0000000000..46737e8355 --- /dev/null +++ b/app/views/admin/terms_of_service/generates/show.html.haml @@ -0,0 +1,41 @@ +- content_for :page_title do + = t('admin.terms_of_service.generates.title') + +- content_for :heading_actions do + .back-link + = link_to admin_terms_of_service_index_path do + = material_symbol 'chevron_left' + = t('admin.terms_of_service.back') + +%p.lead= t('admin.terms_of_service.generates.explanation_html') + +%p.lead= t('admin.terms_of_service.generates.chance_to_review_html') + +%hr.spacer/ + += simple_form_for @generator, url: admin_terms_of_service_generate_path, method: :post do |form| + = render 'shared/error_messages', object: @generator + + .fields-group + = form.input :domain, wrapper: :with_label + + .fields-group + = form.input :jurisdiction, wrapper: :with_label + + .fields-group + = form.input :admin_email, wrapper: :with_label + + .fields-group + = form.input :dmca_email, wrapper: :with_label + + .fields-group + = form.input :dmca_address, wrapper: :with_label + + .fields-group + = form.input :arbitration_address, wrapper: :with_label + + .fields-group + = form.input :arbitration_website, wrapper: :with_label + + .actions + = form.button :button, t('admin.terms_of_service.generates.action'), type: :submit diff --git a/app/views/admin/terms_of_service/histories/show.html.haml b/app/views/admin/terms_of_service/histories/show.html.haml new file mode 100644 index 0000000000..8b7b8eb82e --- /dev/null +++ b/app/views/admin/terms_of_service/histories/show.html.haml @@ -0,0 +1,16 @@ +- content_for :page_title do + = t('admin.terms_of_service.history') + +- content_for :heading do + %h2= t('admin.terms_of_service.title') + = render partial: 'admin/terms_of_service/links' + +- if @terms_of_service.empty? + %p= t('admin.terms_of_service.no_history') +- else + %ol.admin__terms-of-service__history + - @terms_of_service.each do |terms_of_service| + %li + .admin__terms-of-service__history__item + %h5= l(terms_of_service.published_at) + .prose= markdown(terms_of_service.changelog) diff --git a/app/views/admin/terms_of_service/index.html.haml b/app/views/admin/terms_of_service/index.html.haml new file mode 100644 index 0000000000..809d567674 --- /dev/null +++ b/app/views/admin/terms_of_service/index.html.haml @@ -0,0 +1,39 @@ +- content_for :page_title do + = t('admin.terms_of_service.title') + +- content_for :heading do + %h2= t('admin.terms_of_service.title') + = render partial: 'links' + +- if @terms_of_service.present? + .admin__terms-of-service__container + .admin__terms-of-service__container__header + .dot-indicator.success + .dot-indicator__indicator + %span= t('admin.terms_of_service.live') + · + %span + = t('admin.terms_of_service.published_on_html', date: tag.time(l(@terms_of_service.published_at.to_date), class: 'formatted', date: @terms_of_service.published_at.to_date.iso8601)) + · + - if @terms_of_service.notification_sent? + %span + = t('admin.terms_of_service.notified_on_html', date: tag.time(l(@terms_of_service.notification_sent_at.to_date), class: 'formatted', date: @terms_of_service.notification_sent_at.to_date.iso8601)) + - else + = link_to t('admin.terms_of_service.notify_users'), admin_terms_of_service_preview_path(@terms_of_service), class: 'link-button' + + .admin__terms-of-service__container__body + .prose + = markdown(@terms_of_service.text) + + %hr.spacer/ + + %h3= t('admin.terms_of_service.changelog') + + .prose + = markdown(@terms_of_service.changelog) +- else + %p.lead= t('admin.terms_of_service.no_terms_of_service_html') + + .content__heading__actions + = link_to t('admin.terms_of_service.create'), admin_terms_of_service_draft_path, class: 'button' + = link_to t('admin.terms_of_service.generate'), admin_terms_of_service_generate_path, class: 'button button-secondary' diff --git a/app/views/admin/terms_of_service/previews/show.html.haml b/app/views/admin/terms_of_service/previews/show.html.haml new file mode 100644 index 0000000000..48c94cb052 --- /dev/null +++ b/app/views/admin/terms_of_service/previews/show.html.haml @@ -0,0 +1,20 @@ +- content_for :page_title do + = t('admin.terms_of_service.preview.title') + +- content_for :heading_actions do + .back-link + = link_to admin_terms_of_service_index_path do + = material_symbol 'chevron_left' + = t('admin.terms_of_service.back') + +%p.lead + = t('admin.terms_of_service.preview.explanation_html', count: @user_count, display_count: number_with_delimiter(@user_count), date: l(@terms_of_service.published_at.to_date)) + +.prose + = markdown(@terms_of_service.changelog) + +%hr.spacer/ + +.content__heading__actions + = link_to t('admin.terms_of_service.preview.send_preview', email: current_user.email), admin_terms_of_service_test_path(@terms_of_service), method: :post, class: 'button button-secondary' + = link_to t('admin.terms_of_service.preview.send_to_all', count: @user_count, display_count: number_with_delimiter(@user_count)), admin_terms_of_service_distribution_path(@terms_of_service), method: :post, class: 'button', data: { confirm: t('admin.reports.are_you_sure') } diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index 6b2e6edb2b..afeeef0c6d 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -72,7 +72,7 @@ .fields-group = f.input :agreement, as: :boolean, - label: t('auth.privacy_policy_agreement_html', rules_path: about_more_path, privacy_policy_path: privacy_policy_path), + label: t('auth.user_agreement_html', privacy_policy_path: privacy_policy_path, terms_of_service_path: terms_of_service_path), required: false, wrapper: :with_label diff --git a/app/views/terms_of_service/show.html.haml b/app/views/terms_of_service/show.html.haml new file mode 100644 index 0000000000..f6ca023da7 --- /dev/null +++ b/app/views/terms_of_service/show.html.haml @@ -0,0 +1,6 @@ +- content_for :page_title, t('terms_of_service.title') + +- content_for :header_tags do + = render partial: 'shared/og' + += render 'shared/web_app' diff --git a/app/views/user_mailer/terms_of_service_changed.html.haml b/app/views/user_mailer/terms_of_service_changed.html.haml new file mode 100644 index 0000000000..95cc976418 --- /dev/null +++ b/app/views/user_mailer/terms_of_service_changed.html.haml @@ -0,0 +1,17 @@ += content_for :heading do + = render 'application/mailer/heading', + image_url: frontend_asset_url('images/mailer-new/heading/user.png'), + subtitle: t('user_mailer.terms_of_service_changed.subtitle', domain: site_hostname), + title: t('user_mailer.terms_of_service_changed.title') +%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' } + %tr + %td.email-body-padding-td + %table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' } + %tr + %td.email-inner-card-td.email-prose + %p= t('user_mailer.terms_of_service_changed.description_html', path: terms_of_service_url, domain: site_hostname) + %p + %strong= t('user_mailer.terms_of_service_changed.changelog') + = markdown(@terms_of_service.changelog) + %p= t('user_mailer.terms_of_service_changed.agreement', domain: site_hostname) + %p= t('user_mailer.terms_of_service_changed.sign_off', domain: site_hostname) diff --git a/app/views/user_mailer/terms_of_service_changed.text.erb b/app/views/user_mailer/terms_of_service_changed.text.erb new file mode 100644 index 0000000000..8416572f0a --- /dev/null +++ b/app/views/user_mailer/terms_of_service_changed.text.erb @@ -0,0 +1,14 @@ +<%= t('user_mailer.terms_of_service_changed.title') %> + +=== + +<%= t('user_mailer.terms_of_service_changed.description', domain: site_hostname) %> + +=> <%= terms_of_service_url %> + +<%= t('user_mailer.terms_of_service_changed.changelog') %> + +<%= @terms_of_service.changelog %> +<%= t('user_mailer.terms_of_service_changed.agreement', domain: site_hostname) %> + +<%= t('user_mailer.terms_of_service_changed.sign_off', domain: site_hostname) %> diff --git a/app/workers/admin/distribute_terms_of_service_notification_worker.rb b/app/workers/admin/distribute_terms_of_service_notification_worker.rb new file mode 100644 index 0000000000..7370ee87e8 --- /dev/null +++ b/app/workers/admin/distribute_terms_of_service_notification_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Admin::DistributeTermsOfServiceNotificationWorker + include Sidekiq::Worker + + def perform(terms_of_service_id) + terms_of_service = TermsOfService.find(terms_of_service_id) + + terms_of_service.scope_for_notification.find_each do |user| + UserMailer.terms_of_service_changed(user, terms_of_service).deliver_later! + end + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/config/locales/an.yml b/config/locales/an.yml index fccfcae53e..6598ea3b90 100644 --- a/config/locales/an.yml +++ b/config/locales/an.yml @@ -919,7 +919,6 @@ an: migrate_account: Mudar-se a unatra cuenta migrate_account_html: Si deseyas reendrezar esta cuenta a unatra distinta, puetz configurar-lo aquí. or_log_in_with: U inicia sesión con - privacy_policy_agreement_html: He leyiu y accepto la politica de privacidat providers: cas: CAS saml: SAML diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 3bbabdcaad..5eea1390ce 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -1118,7 +1118,6 @@ ar: migrate_account: الانتقال إلى حساب مختلف migrate_account_html: إن كنت ترغب في تحويل هذا الحساب نحو حساب آخَر، يُمكِنُك إعداده هنا. or_log_in_with: أو قم بتسجيل الدخول بواسطة - privacy_policy_agreement_html: لقد قرأتُ وأوافق على سياسة الخصوصية progress: confirm: تأكيد عنوان البريد الإلكتروني details: تفاصيلك diff --git a/config/locales/ast.yml b/config/locales/ast.yml index 69f887de55..6c2ebbfd72 100644 --- a/config/locales/ast.yml +++ b/config/locales/ast.yml @@ -459,7 +459,6 @@ ast: logout: Zarrar la sesión migrate_account: Cambéu de cuenta migrate_account_html: Si quies redirixir esta cuenta a otra diferente, pues configurar esta opción equí. - privacy_policy_agreement_html: Lleí y acepto la política de privacidá providers: cas: CAS saml: SAML diff --git a/config/locales/be.yml b/config/locales/be.yml index 91f949b8e1..eaeb5aba43 100644 --- a/config/locales/be.yml +++ b/config/locales/be.yml @@ -1134,7 +1134,6 @@ be: migrate_account: Пераехаць на іншы ўліковы запіс migrate_account_html: Калі вы хочаце перанакіраваць гэты ўліковы запіс на іншы, то можаце наладзіць яго тут. or_log_in_with: Або ўвайсці з дапамогай - privacy_policy_agreement_html: Я азнаёміўся і пагаджаюся з палітыкай канфідэнцыйнасці progress: confirm: Пацвердзіць email details: Вашы дадзеныя diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 41a62ecc2e..081418ef0a 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -1121,7 +1121,6 @@ bg: migrate_account: Преместване в различен акаунт migrate_account_html: Ако желаете да пренасочите този акаунт към друг, можете да настроите това тук. or_log_in_with: Или влизане с помощта на - privacy_policy_agreement_html: Прочетох и има съгласието ми за политиката за поверителност progress: details: Вашите подробности review: Нашият преглед diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 60d58a68e1..9e4ee5a473 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1132,7 +1132,6 @@ ca: migrate_account: Mou a un compte diferent migrate_account_html: Si vols redirigir aquest compte a un altre diferent, el pots configurar aquí. or_log_in_with: O inicia sessió amb - privacy_policy_agreement_html: He llegit i estic d'acord amb la política de privacitat progress: confirm: Confirmar email details: Els teus detalls diff --git a/config/locales/cs.yml b/config/locales/cs.yml index d8c721591b..3c5d6c9ded 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -1099,7 +1099,6 @@ cs: migrate_account: Přesunout se na jiný účet migrate_account_html: Zde můžete nastavit přesměrování tohoto účtu na jiný. or_log_in_with: Nebo se přihlaste pomocí - privacy_policy_agreement_html: Četl jsem a souhlasím se zásadami ochrany osobních údajů progress: details: Vaše údaje review: Naše hodnocení diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 168c37f106..8d7e841501 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -1204,7 +1204,6 @@ cy: migrate_account: Symud i gyfrif gwahanol migrate_account_html: Os hoffech chi ailgyfeirio'r cyfrif hwn at un gwahanol, mae modd ei ffurfweddu yma. or_log_in_with: Neu mewngofnodwch gyda - privacy_policy_agreement_html: Rwyf wedi darllen ac yn cytuno i'r polisi preifatrwydd progress: confirm: Cadarnhau'r e-bost details: Eich manylion diff --git a/config/locales/da.yml b/config/locales/da.yml index 04f5e46fd0..2c4fe9437d 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1132,7 +1132,6 @@ da: migrate_account: Flyt til en anden konto migrate_account_html: Ønsker du at omdirigere denne konto til en anden, kan du opsætte dette hér. or_log_in_with: Eller log ind med - privacy_policy_agreement_html: Jeg accepterer privatlivspolitikken progress: confirm: Bekræft e-mail details: Dine detaljer diff --git a/config/locales/de.yml b/config/locales/de.yml index e1c20e2931..ca7dbf2421 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1132,7 +1132,6 @@ de: migrate_account: Zu einem anderen Konto umziehen migrate_account_html: Wenn du dieses Konto auf ein anderes weiterleiten möchtest, kannst du es hier konfigurieren. or_log_in_with: Oder anmelden mit - privacy_policy_agreement_html: Ich habe die Datenschutzerklärung gelesen und stimme ihr zu progress: confirm: E-Mail bestätigen details: Deine Daten diff --git a/config/locales/el.yml b/config/locales/el.yml index 5f1770ace9..44961b4b3e 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -1132,7 +1132,6 @@ el: migrate_account: Μεταφορά σε διαφορετικό λογαριασμό migrate_account_html: Αν θέλεις να ανακατευθύνεις αυτό τον λογαριασμό σε έναν διαφορετικό, μπορείς να το διαμορφώσεις εδώ. or_log_in_with: Ή συνδέσου με - privacy_policy_agreement_html: Έχω διαβάσει και συμφωνώ με την πολιτική απορρήτου progress: confirm: Επιβεβαίωση email details: Τα στοιχεία σας diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 583b3dc14d..20f134c959 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -1132,7 +1132,6 @@ en-GB: migrate_account: Move to a different account migrate_account_html: If you wish to redirect this account to a different one, you can configure it here. or_log_in_with: Or log in with - privacy_policy_agreement_html: I have read and agree to the privacy policy progress: confirm: Confirm email details: Your details diff --git a/config/locales/en.yml b/config/locales/en.yml index d8c8d95303..ab76b462ee 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -214,6 +214,7 @@ en: enable_user: Enable User memorialize_account: Memorialize Account promote_user: Promote User + publish_terms_of_service: Publish Terms of Service reject_appeal: Reject Appeal reject_user: Reject User remove_avatar_user: Remove Avatar @@ -278,6 +279,7 @@ en: enable_user_html: "%{name} enabled login for user %{target}" memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page" promote_user_html: "%{name} promoted user %{target}" + publish_terms_of_service_html: "%{name} published updates to the terms of service" reject_appeal_html: "%{name} rejected moderation decision appeal from %{target}" reject_user_html: "%{name} rejected sign-up from %{target}" remove_avatar_user_html: "%{name} removed %{target}'s avatar" @@ -925,6 +927,35 @@ en: search: Search title: Hashtags updated_msg: Hashtag settings updated successfully + terms_of_service: + back: Back to terms of service + changelog: What's changed + create: Use your own + current: Current + draft: Draft + generate: Use template + generates: + action: Generate + chance_to_review_html: "The generated terms of service will not be published automatically. You will have a chance to review the results. Please fill in the necessary details to proceed." + explanation_html: The terms of service template provided is for informational purposes only, and should not be construed as legal advice on any subject matter. Please consult with your own legal counsel on your situation and specific legal questions you have. + title: Terms of Service Setup + history: History + live: Live + no_history: There are no recorded changes of the terms of service yet. + no_terms_of_service_html: You don't currently have any terms of service configured. Terms of service are meant to provide clarity and protect you from potential liabilities in disputes with your users. + notified_on_html: Users notified on %{date} + notify_users: Notify users + preview: + explanation_html: 'The email will be sent to %{display_count} users who have signed up before %{date}. The following text will be included in the e-mail:' + send_preview: Send preview to %{email} + send_to_all: + one: Send %{display_count} email + other: Send %{display_count} emails + title: Preview terms of service notification + publish: Publish + published_on_html: Published on %{date} + save_draft: Save draft + title: Terms of Service title: Administration trends: allow: Allow @@ -1132,7 +1163,6 @@ en: migrate_account: Move to a different account migrate_account_html: If you wish to redirect this account to a different one, you can configure it here. or_log_in_with: Or log in with - privacy_policy_agreement_html: I have read and agree to the privacy policy progress: confirm: Confirm email details: Your details @@ -1178,6 +1208,7 @@ en: view_strikes: View past strikes against your account too_fast: Form submitted too fast, try again. use_security_key: Use security key + user_agreement_html: I have read and agree to the terms of service and privacy policy author_attribution: example_title: Sample text hint_html: Are you writing news or blog articles outside of Mastodon? Control how you get credited when they are shared on Mastodon. @@ -1840,6 +1871,8 @@ en: too_late: It is too late to appeal this strike tags: does_not_match_previous_name: does not match the previous name + terms_of_service: + title: Terms of Service themes: contrast: Mastodon (High contrast) default: Mastodon (Dark) @@ -1900,6 +1933,15 @@ en: further_actions_html: If this wasn't you, we recommend that you %{action} immediately and enable two-factor authentication to keep your account secure. subject: Your account has been accessed from a new IP address title: A new sign-in + terms_of_service_changed: + agreement: By continuing to use %{domain}, you are agreeing to these terms. If you disagree with the updated terms, you may terminate your agreement with %{domain} at any time by deleting your account. + changelog: 'At a glance, here is what this update means for you:' + description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. We encourage you to review the updated terms in full here:' + description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. We encourage you to review the updated terms in full here. + sign_off: The %{domain} team + subject: Updates to our terms of service + subtitle: The terms of service of %{domain} are changing + title: Important update warning: appeal: Submit an appeal appeal_description: If you believe this is an error, you can submit an appeal to the staff of %{instance}. diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 74fc0c64a8..a09181ef31 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -1132,7 +1132,6 @@ eo: migrate_account: Movi al alia konto migrate_account_html: Se vi deziras alidirekti ĉi tiun konton al alia, vi povas agordi ĝin ĉi tie. or_log_in_with: Aŭ saluti per - privacy_policy_agreement_html: Mi legis kaj konsentis pri privatpolitiko progress: confirm: Konfirmi retadreson details: Viaj detaloj diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index a33865a0b6..78b30921c8 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -1132,7 +1132,6 @@ es-AR: migrate_account: Mudarse a otra cuenta migrate_account_html: Si querés redireccionar esta cuenta a otra distinta, podés configurar eso acá. or_log_in_with: O iniciar sesión con - privacy_policy_agreement_html: Leí y acepto la política de privacidad progress: confirm: Confirmar correo electrónico details: Tus detalles diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 51a59d93c8..f05626399a 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1132,7 +1132,6 @@ es-MX: migrate_account: Mudarse a otra cuenta migrate_account_html: Si deseas redireccionar esta cuenta a otra distinta, puedes configurarlo aquí. or_log_in_with: O inicia sesión con - privacy_policy_agreement_html: He leído y acepto la política de privacidad progress: confirm: Confirmar dirección de correo details: Tus detalles diff --git a/config/locales/es.yml b/config/locales/es.yml index 48177a20a6..bc0d816ef2 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1132,7 +1132,6 @@ es: migrate_account: Mudarse a otra cuenta migrate_account_html: Si deseas redireccionar esta cuenta a otra distinta, puedes configurarlo aquí. or_log_in_with: O inicia sesión con - privacy_policy_agreement_html: He leído y acepto la política de privacidad progress: confirm: Confirmar dirección de correo details: Tus detalles diff --git a/config/locales/et.yml b/config/locales/et.yml index 759006f728..2f182d08b2 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -1117,7 +1117,6 @@ et: migrate_account: Teisele kontole ära kolimine migrate_account_html: Kui soovid konto siit ära kolida, saad seda teha siin. or_log_in_with: Või logi sisse koos - privacy_policy_agreement_html: Olen tutvunud isikuandmete kaitse põhimõtetega ja nõustun nendega progress: confirm: E-posti kinnitamine details: Sinu üksikasjad diff --git a/config/locales/eu.yml b/config/locales/eu.yml index bb0d122ca1..8ca53d492c 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1041,7 +1041,6 @@ eu: migrate_account: Migratu beste kontu batera migrate_account_html: Kontu hau beste batera birbideratu nahi baduzu, hemen konfiguratu dezakezu. or_log_in_with: Edo hasi saioa honekin - privacy_policy_agreement_html: Pribatutasun politika irakurri dut eta ados nago progress: details: Zure xehetasunak review: Gure berrikuspena diff --git a/config/locales/fa.yml b/config/locales/fa.yml index 00b7497da8..341bb4cf54 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -978,7 +978,6 @@ fa: migrate_account: نقل مکان به یک حساب دیگر migrate_account_html: اگر می‌خواهید این حساب را به حساب دیگری منتقل کنید، این‌جا را کلیک کنید. or_log_in_with: یا ورود به وسیلهٔ - privacy_policy_agreement_html: سیاست محرمانگی را خوانده و پذیرفته‌ام progress: confirm: تأیید رایانامه details: جزئیات شما diff --git a/config/locales/fi.yml b/config/locales/fi.yml index be75536eb6..308d364612 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1132,7 +1132,6 @@ fi: migrate_account: Muuta toiseen tiliin migrate_account_html: Jos haluat ohjata tämän tilin toiseen, voit asettaa toisen tilin tästä. or_log_in_with: Tai käytä kirjautumiseen - privacy_policy_agreement_html: Olen lukenut ja hyväksyn tietosuojakäytännön progress: confirm: Vahvista sähköpostiosoite details: Omat tietosi diff --git a/config/locales/fo.yml b/config/locales/fo.yml index e5d0c3aa5c..00ffed90bb 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -1132,7 +1132,6 @@ fo: migrate_account: Flyt til eina aðra kontu migrate_account_html: Ynskir tú at víðaribeina hesa kontuna til eina aðra, so kanst tú seta tað upp her. or_log_in_with: Ella innrita við - privacy_policy_agreement_html: Eg havi lisið og taki undir við privatlívspolitikkinum progress: confirm: Vátta teldupost details: Tínir smálutir diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index 991a4d669b..5cfd12d0be 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -1135,7 +1135,6 @@ fr-CA: migrate_account: Déménager vers un compte différent migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le configurer ici. or_log_in_with: Ou authentifiez-vous avec - privacy_policy_agreement_html: J’ai lu et j’accepte la politique de confidentialité progress: confirm: Confirmation de l'adresse mail details: Vos infos diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9a8837d81f..2593c60f06 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1135,7 +1135,6 @@ fr: migrate_account: Déménager vers un compte différent migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le configurer ici. or_log_in_with: Ou authentifiez-vous avec - privacy_policy_agreement_html: J’ai lu et j’accepte la politique de confidentialité progress: confirm: Confirmation de l'adresse mail details: Vos infos diff --git a/config/locales/fy.yml b/config/locales/fy.yml index 85b45eef79..5e9d328edc 100644 --- a/config/locales/fy.yml +++ b/config/locales/fy.yml @@ -1117,7 +1117,6 @@ fy: migrate_account: Nei in oar account ferhúzje migrate_account_html: Wannear’t jo dizze account nei in oare account trochferwize wolle, kinne jo dit hjir ynstelle. or_log_in_with: Of oanmelde mei - privacy_policy_agreement_html: Ik haw it privacybelied lêzen en gean dêrmei akkoard progress: confirm: E-mailadres werhelje details: Jo gegevens diff --git a/config/locales/ga.yml b/config/locales/ga.yml index 6e3bc30b7a..1da40dc1c3 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -1186,7 +1186,6 @@ ga: migrate_account: Bog chuig cuntas eile migrate_account_html: Más mian leat an cuntas seo a atreorú chuig ceann eile, is féidir leat é a chumrú anseo. or_log_in_with: Nó logáil isteach le - privacy_policy_agreement_html: Léigh mé agus aontaím leis an polasaí príobháideachais progress: confirm: Deimhnigh ríomhphost details: Do chuid sonraí diff --git a/config/locales/gd.yml b/config/locales/gd.yml index c21dd9d439..850143567f 100644 --- a/config/locales/gd.yml +++ b/config/locales/gd.yml @@ -1168,7 +1168,6 @@ gd: migrate_account: Imrich gu cunntas eile migrate_account_html: Nam bu mhiann leat an cunntas seo ath-stiùireadh gu fear eile, ’s urrainn dhut a rèiteachadh an-seo. or_log_in_with: No clàraich a-steach le - privacy_policy_agreement_html: Leugh mi is tha mi ag aontachadh ris a’ phoileasaidh prìobhaideachd progress: confirm: Dearbh am post-d details: Am fiosrachadh agad diff --git a/config/locales/gl.yml b/config/locales/gl.yml index ab0afe9a2a..c1de161be1 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -1132,7 +1132,6 @@ gl: migrate_account: Mover a unha conta diferente migrate_account_html: Se queres redirixir esta conta hacia outra diferente, podes facelo aquí. or_log_in_with: Ou accede con - privacy_policy_agreement_html: Lin e acepto a política de privacidade progress: confirm: Confirmar correo details: Detalles diff --git a/config/locales/he.yml b/config/locales/he.yml index c54b244310..0ec1cd4620 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1168,7 +1168,6 @@ he: migrate_account: מעבר לחשבון אחר migrate_account_html: אם ברצונך להכווין את החשבון לעבר חשבון אחר, ניתן להגדיר זאת כאן. or_log_in_with: או התחבר באמצעות - privacy_policy_agreement_html: קראתי והסכמתי למדיניות הפרטיות progress: confirm: אימות כתובת הדואל details: הפרטים שלך diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 70dd230251..bc298e7559 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1132,7 +1132,6 @@ hu: migrate_account: Felhasználói fiók költöztetése migrate_account_html: Ha át szeretnéd irányítani ezt a fiókodat egy másikra, akkor itt állíthatod be. or_log_in_with: Vagy jelentkezz be ezzel - privacy_policy_agreement_html: Elolvastam és egyetértek az adatvédemi nyilatkozattal progress: confirm: E-mail megerősítése details: Saját adatok diff --git a/config/locales/hy.yml b/config/locales/hy.yml index 751d6af196..dfbbe73e2a 100644 --- a/config/locales/hy.yml +++ b/config/locales/hy.yml @@ -462,7 +462,6 @@ hy: logout: Դուրս գալ migrate_account: Տեղափոխուել այլ հաշիւ or_log_in_with: Կամ մուտք գործել օգտագործելով՝ - privacy_policy_agreement_html: Ես կարդացել եւ ընդունել եմ գաղնիութեան քաղաքականութիւնը progress: details: Ձեր տուեալները review: Վաւերացում diff --git a/config/locales/ia.yml b/config/locales/ia.yml index 902f9be744..2f8c3ee982 100644 --- a/config/locales/ia.yml +++ b/config/locales/ia.yml @@ -1132,7 +1132,6 @@ ia: migrate_account: Migrar a un altere conto migrate_account_html: Si tu vole rediriger iste conto a un altere, tu pote configurar lo hic. or_log_in_with: O aperi session con - privacy_policy_agreement_html: Io ha legite e accepta le politica de confidentialitate progress: confirm: Confirmar e-mail details: Tu detalios diff --git a/config/locales/id.yml b/config/locales/id.yml index 1b7205bb91..03d25a6e53 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -903,7 +903,6 @@ id: migrate_account: Pindah ke akun berbeda migrate_account_html: Jika Anda ingin mengalihkan akun ini ke akun lain, Anda dapat mengaturnya di sini. or_log_in_with: Atau masuk dengan - privacy_policy_agreement_html: Saya telah membaca dan menerima kebijakan privasi providers: cas: CAS saml: SAML diff --git a/config/locales/ie.yml b/config/locales/ie.yml index af35e1c82e..4e154b3114 100644 --- a/config/locales/ie.yml +++ b/config/locales/ie.yml @@ -1039,7 +1039,6 @@ ie: migrate_account: Mover te a un conto diferent migrate_account_html: Si tu vole redirecter ti-ci conto a un altri, tu posse configurar it ci. or_log_in_with: O intrar med - privacy_policy_agreement_html: Yo leet e consenti li politica pri privatie progress: details: Tui detallies review: Nor revise diff --git a/config/locales/io.yml b/config/locales/io.yml index 36d09c4056..c89d920f30 100644 --- a/config/locales/io.yml +++ b/config/locales/io.yml @@ -1014,7 +1014,6 @@ io: migrate_account: Transferez a diferanta konto migrate_account_html: Se vu volas ridirektar ca konto a diferanto, vu povas ajustar hike. or_log_in_with: O eniras per - privacy_policy_agreement_html: Me lektis e konsentis privatesguidilo progress: details: Vua detali review: Nia revuo diff --git a/config/locales/is.yml b/config/locales/is.yml index dd850b748a..d0d0ac5914 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -1136,7 +1136,6 @@ is: migrate_account: Færa á annan notandaaðgang migrate_account_html: Ef þú vilt endurbeina þessum aðgangi á einhvern annan, geturðu stillt það hér. or_log_in_with: Eða skráðu inn með - privacy_policy_agreement_html: Ég hef lesið og samþykkt persónuverndarstefnuna progress: confirm: Staðfesta tölvupóstfang details: Nánari upplýsingar þínar diff --git a/config/locales/it.yml b/config/locales/it.yml index 28b0900fa6..a062abc202 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1134,7 +1134,6 @@ it: migrate_account: Sposta ad un account differente migrate_account_html: Se vuoi che questo account sia reindirizzato a uno diverso, puoi configurarlo qui. or_log_in_with: Oppure accedi con - privacy_policy_agreement_html: Ho letto e accetto l'informativa sulla privacy progress: confirm: Conferma l'e-mail details: I tuoi dettagli diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 50ef37b4e1..1e0270b811 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1114,7 +1114,6 @@ ja: migrate_account: 別のアカウントに引っ越す migrate_account_html: 引っ越し先を明記したい場合はこちらで設定できます。 or_log_in_with: または次のサービスでログイン - privacy_policy_agreement_html: プライバシーポリシーを読み、同意します progress: confirm: メールアドレスの確認 details: ユーザー情報 diff --git a/config/locales/kab.yml b/config/locales/kab.yml index e4f5dddbe2..314029a89e 100644 --- a/config/locales/kab.yml +++ b/config/locales/kab.yml @@ -495,7 +495,6 @@ kab: logout: Ffeɣ migrate_account: Gujj ɣer umiḍan nniḍen or_log_in_with: Neɣ eqqen s - privacy_policy_agreement_html: Ɣriɣ yerna qebleɣ tasertit n tbaḍnit progress: confirm: Sentem imayl details: Isalli-inek diff --git a/config/locales/ko.yml b/config/locales/ko.yml index a0da1275ed..692d3638ef 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1116,7 +1116,6 @@ ko: migrate_account: 계정 옮기기 migrate_account_html: 이 계정을 다른 계정으로 리디렉션 하길 원하는 경우 여기에서 설정할 수 있습니다. or_log_in_with: 다른 방법으로 로그인 하려면 - privacy_policy_agreement_html: 개인정보처리방침을 읽고 동의합니다 progress: confirm: 이메일 확인 details: 세부사항 diff --git a/config/locales/ku.yml b/config/locales/ku.yml index 32d52f3923..06f3e32f20 100644 --- a/config/locales/ku.yml +++ b/config/locales/ku.yml @@ -916,7 +916,6 @@ ku: migrate_account: Livandin bo ajimêreke din migrate_account_html: Ku tu dixwazî ev ajimêr li ajimêreke cuda beralî bikî, tu dikarî ji vir de saz bike. or_log_in_with: An têketinê bike bi riya - privacy_policy_agreement_html: Min Politîka taybetiyê xwend û dipejirînim providers: cas: CAS saml: SAML diff --git a/config/locales/lad.yml b/config/locales/lad.yml index b206b9de69..939a8826b2 100644 --- a/config/locales/lad.yml +++ b/config/locales/lad.yml @@ -1097,7 +1097,6 @@ lad: migrate_account: Transferate a otro kuento migrate_account_html: Si keres readresar este kuento a otra distinta, puedes konfigurarlo aki. or_log_in_with: O konektate kon tu kuento kon - privacy_policy_agreement_html: Tengo meldado i acheto la politika de privasita progress: confirm: Konfirma posta details: Tus detalyos diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 09fad59410..7c8bb6898a 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -795,7 +795,6 @@ lt: migrate_account: Persikelti prie kitos paskyros migrate_account_html: Jei nori šią paskyrą nukreipti į kitą, gali sukonfigūruoti ją čia. or_log_in_with: Arba prisijungti su - privacy_policy_agreement_html: Perskaičiau ir sutinku su privatumo politika progress: details: Tavo duomenys review: Mūsų peržiūra diff --git a/config/locales/lv.yml b/config/locales/lv.yml index 90ff8b6bcd..2102e84c99 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -1110,7 +1110,6 @@ lv: migrate_account: Pāriešana uz citu kontu migrate_account_html: Ja vēlies novirzīt šo kontu uz citu, tu vari to konfigurēt šeit. or_log_in_with: Vai piesakies ar - privacy_policy_agreement_html: Esmu izlasījis un piekrītu privātuma politikai progress: confirm: Apstiprināt e-pasta adresi details: Tavi dati diff --git a/config/locales/ms.yml b/config/locales/ms.yml index 461496fe07..94cbca57fd 100644 --- a/config/locales/ms.yml +++ b/config/locales/ms.yml @@ -1001,7 +1001,6 @@ ms: migrate_account: Pindah kepada akaun lain migrate_account_html: Jika anda ingin mengubah hala akaun ini kepada akaun lain, anda boleh konfigurasikannya di sini. or_log_in_with: Atau daftar masuk dengan - privacy_policy_agreement_html: Saya telah membaca dan bersetuju menerima dasar privasi progress: details: Maklumat anda review: Ulasan kami diff --git a/config/locales/my.yml b/config/locales/my.yml index 342510bc48..cba06d15f4 100644 --- a/config/locales/my.yml +++ b/config/locales/my.yml @@ -994,7 +994,6 @@ my: migrate_account: အခြားအကောင့်တစ်ခုသို့ ရွှေ့ရန် migrate_account_html: ဤအကောင့်ကို အခြားအကောင့်သို့ ပြန်ညွှန်းလိုပါက ဤနေရာတွင် စီစဉ်သတ်မှတ်နိုင်သည်။ or_log_in_with: သို့မဟုတ် အကောင့်ဖြင့် ဝင်ရောက်ပါ - privacy_policy_agreement_html: ကိုယ်ရေးအချက်အလက်မူဝါဒ ကို ဖတ်ပြီး သဘောတူလိုက်ပါပြီ progress: details: သင့်အသေးစိတ်အချက်အလက်များ review: ကျွန်ုပ်တို့၏သုံးသပ်ချက် diff --git a/config/locales/nl.yml b/config/locales/nl.yml index fd0f7bb1f0..3a5eaee1c3 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1132,7 +1132,6 @@ nl: migrate_account: Naar een ander account verhuizen migrate_account_html: Wanneer je dit account naar een ander account wilt doorverwijzen, kun je dit hier instellen. or_log_in_with: Of inloggen met - privacy_policy_agreement_html: Ik heb het privacybeleid gelezen en ga daarmee akkoord progress: confirm: E-mailadres bevestigen details: Jouw gegevens diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 83803fb0f6..5d3ead4290 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1132,7 +1132,6 @@ nn: migrate_account: Flytt til ein annan konto migrate_account_html: Om du vil visa denne kontoen til ein anna, kan du skipe det her. or_log_in_with: Eller logg inn med - privacy_policy_agreement_html: Jeg har lest og godtar retningslinjer for personvern progress: confirm: Stadfest e-post details: Opplysingane dine diff --git a/config/locales/no.yml b/config/locales/no.yml index 5b66bde947..2529fafb4a 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -1033,7 +1033,6 @@ migrate_account: Flytt til en annen konto migrate_account_html: Hvis du ønsker å henvise denne kontoen til en annen, kan du konfigurere det her. or_log_in_with: Eller logg inn med - privacy_policy_agreement_html: Jeg har lest og godtar retningslinjer for personvern progress: details: Dine opplysninger review: Vår gjennomgang diff --git a/config/locales/oc.yml b/config/locales/oc.yml index a67f55c6b7..258348d41c 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -481,7 +481,6 @@ oc: migrate_account: Mudar endacòm mai migrate_account_html: Se volètz mandar los visitors d’aqueste compte a un autre, podètz o configurar aquí. or_log_in_with: O autentificatz-vos amb - privacy_policy_agreement_html: Ai legit e accepti la politica de confidencialitat providers: cas: CAS saml: SAML diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 02a1fc85dc..f1dfbe4f7d 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1168,7 +1168,6 @@ pl: migrate_account: Przenieś konto migrate_account_html: Jeżeli chcesz skonfigurować przekierowanie z obecnego konta na inne, możesz zrobić to tutaj. or_log_in_with: Lub zaloguj się z użyciem - privacy_policy_agreement_html: Przeczytałem/am i akceptuję politykę prywatności progress: confirm: Potwierdź adres e-mail details: Twoje dane diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index f79a56e148..7255967172 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1132,7 +1132,6 @@ pt-BR: migrate_account: Mudar-se para outra conta migrate_account_html: Se você quer redirecionar essa conta para uma outra você pode configurar isso aqui. or_log_in_with: Ou entre com - privacy_policy_agreement_html: Eu li e concordo com a política de privacidade progress: confirm: Confirmar e-mail details: Suas informações diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index b4ad361367..f739c93394 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -1113,7 +1113,6 @@ pt-PT: migrate_account: Mudar para uma conta diferente migrate_account_html: Se deseja redirecionar esta conta para uma outra pode configurar isso aqui. or_log_in_with: Ou iniciar sessão com - privacy_policy_agreement_html: Eu li e concordo com a política de privacidade progress: confirm: Confirmar e-mail details: Os seus dados diff --git a/config/locales/ru.yml b/config/locales/ru.yml index d68c338588..486d87f003 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1168,7 +1168,6 @@ ru: migrate_account: Перенос учётной записи migrate_account_html: Завели новую учётную запись? Перенаправьте подписчиков на неё — настройте перенаправление тут. or_log_in_with: Или войти с помощью - privacy_policy_agreement_html: Мной прочитана и принята политика конфиденциальности progress: confirm: Подтвердите электронную почту details: Ваши данные diff --git a/config/locales/sco.yml b/config/locales/sco.yml index ad97aa194b..209b761851 100644 --- a/config/locales/sco.yml +++ b/config/locales/sco.yml @@ -909,7 +909,6 @@ sco: migrate_account: Uise a different accoont migrate_account_html: Gin ye'r wantin fir tae redireck this accoont tae a different ane, ye kin configure it here. or_log_in_with: Or log in wi - privacy_policy_agreement_html: A'v read an A agree tae the privacy policy providers: cas: CAS saml: SAML diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index b6b0481368..3a052ad626 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -130,6 +130,17 @@ en: show_application: You will always be able to see which app published your post regardless. tag: name: You can only change the casing of the letters, for example, to make it more readable + terms_of_service: + changelog: Can be structured with Markdown syntax. + text: Can be structured with Markdown syntax. + terms_of_service_generator: + admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests. + arbitration_address: Can be the same as Physical address above, or “N/A” if using email + arbitration_website: Can be a web form, or “N/A” if using email + dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view. + dmca_email: Can be the same email used for “Email address for legal notices” above + domain: Unique identification of the online service you are providing. + jurisdiction: List the country where whoever pays the bills lives. If it’s a company or other entity, list the country where it’s incorporated, and the city, region, territory or state as appropriate. user: chosen_languages: When checked, only posts in selected languages will be displayed in public timelines role: The role controls which permissions the user has. @@ -319,6 +330,17 @@ en: name: Hashtag trendable: Allow this hashtag to appear under trends usable: Allow posts to use this hashtag locally + terms_of_service: + changelog: What's changed? + text: Terms of Service + terms_of_service_generator: + admin_email: Email address for legal notices + arbitration_address: Physical address for arbitration notices + arbitration_website: Website for submitting arbitration notices + dmca_address: Physical address for DMCA/copyright notices + dmca_email: Email address for DMCA/copyright notices + domain: Domain + jurisdiction: Legal jurisdiction user: role: Role time_zone: Time zone diff --git a/config/locales/sl.yml b/config/locales/sl.yml index a82e32756b..24aa67e7ae 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -1141,7 +1141,6 @@ sl: migrate_account: Premakni se na drug račun migrate_account_html: Če želite ta račun preusmeriti na drugega, ga lahko nastavite tukaj. or_log_in_with: Ali se prijavite z - privacy_policy_agreement_html: Prebral_a sem in se strinjam s pravilnikom o zasebnosti. progress: confirm: Potrdi e-pošto details: Vaši podatki diff --git a/config/locales/sq.yml b/config/locales/sq.yml index 5abaf3b575..f6a7d34cff 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -1126,7 +1126,6 @@ sq: migrate_account: Kaloni në një tjetër llogari migrate_account_html: Nëse doni ta ridrejtoni këtë llogari te një tjetër, këtë mund ta formësoni këtu. or_log_in_with: Ose bëni hyrjen me - privacy_policy_agreement_html: I kam lexuar dhe pajtohem me rregullat e privatësisë progress: confirm: Ripohoni email-in details: Hollësitë tuaja diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 766552307d..700e588550 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -1060,7 +1060,6 @@ sr-Latn: migrate_account: Premeštanje u drugi nalog migrate_account_html: Ako želite da preusmerite ovaj nalog na neki drugi, možete to podesiti ovde. or_log_in_with: Ili se prijavite sa - privacy_policy_agreement_html: Pročitao/-la sam i saglasan/-a sam sa politikom privatnosti progress: details: Vaši detalji review: Naš pregled diff --git a/config/locales/sr.yml b/config/locales/sr.yml index f61addafb6..28abe3b46e 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -1090,7 +1090,6 @@ sr: migrate_account: Премештање у други налог migrate_account_html: Ако желите да преусмерите овај налог на неки други, можете то подесити овде. or_log_in_with: Или се пријавите са - privacy_policy_agreement_html: Прочитао/-ла сам и сагласан/-а сам са политиком приватности progress: details: Ваши детаљи review: Наш преглед diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 3f0b05477f..f01ef42d0b 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -1132,7 +1132,6 @@ sv: migrate_account: Flytta till ett annat konto migrate_account_html: Om du vill omdirigera detta konto till ett annat, kan du konfigurera det här. or_log_in_with: Eller logga in med - privacy_policy_agreement_html: Jag har läst och godkänner integritetspolicyn progress: confirm: Bekräfta e-postadress details: Dina uppgifter diff --git a/config/locales/th.yml b/config/locales/th.yml index e3ed3989ff..4f0a6e98bd 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -1114,7 +1114,6 @@ th: migrate_account: ย้ายไปยังบัญชีอื่น migrate_account_html: หากคุณต้องการเปลี่ยนเส้นทางบัญชีนี้ไปยังบัญชีอื่น คุณสามารถ กำหนดค่าบัญชีที่นี่ or_log_in_with: หรือเข้าสู่ระบบด้วย - privacy_policy_agreement_html: ฉันได้อ่านและเห็นด้วยกับ นโยบายความเป็นส่วนตัว progress: confirm: ยืนยันอีเมล details: รายละเอียดของคุณ diff --git a/config/locales/tr.yml b/config/locales/tr.yml index e8b31bd1b4..e947964339 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1132,7 +1132,6 @@ tr: migrate_account: Farklı bir hesaba taşıyın migrate_account_html: Bu hesabı başka bir hesaba yönlendirmek istiyorsan, buradan yapılandırabilirsin. or_log_in_with: 'Veya şununla oturum açın:' - privacy_policy_agreement_html: Gizlilik politikasını okudum ve kabul ettim progress: confirm: E-postanızı onaylayın details: Ayrıntılarınız diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 4f2996714d..000bac6170 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1153,7 +1153,6 @@ uk: migrate_account: Переїхати на інший обліковий запис migrate_account_html: Якщо ви бажаєте переспрямувати цей обліковий запис на інший, ви можете налаштувати це тут. or_log_in_with: Або увійдіть з - privacy_policy_agreement_html: Мною прочитано і я погоджуюся з політикою приватності progress: confirm: Підтвердити електронну адресу details: Ваші дані diff --git a/config/locales/vi.yml b/config/locales/vi.yml index c06cccfe81..c718e94457 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1114,7 +1114,6 @@ vi: migrate_account: Chuyển sang tài khoản khác migrate_account_html: Nếu bạn muốn bỏ tài khoản này để dùng một tài khoản khác, bạn có thể thiết lập tại đây. or_log_in_with: Hoặc đăng nhập bằng - privacy_policy_agreement_html: Tôi đã đọc và đồng ý chính sách bảo mật progress: confirm: Xác nhận email details: Điền thông tin diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 65c96c05e6..681de6186f 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1114,7 +1114,6 @@ zh-CN: migrate_account: 迁移到另一个账户 migrate_account_html: 如果你希望引导他人关注另一个账号,请点击这里进行设置。 or_log_in_with: 或通过外部服务登录 - privacy_policy_agreement_html: 我已阅读并同意 隐私政策 progress: confirm: 确认邮箱 details: 你的详细信息 diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 15b08c883e..7278058dce 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -1021,7 +1021,6 @@ zh-HK: migrate_account: 轉移到另一個帳號 migrate_account_html: 想要將這個帳號指向另一個帳號可到這裡設定。 or_log_in_with: 或登入於 - privacy_policy_agreement_html: 我已閱讀且同意私隱政策 progress: details: 你的資料 review: 我們的審核 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 7e9cd1fd51..a2bcf41a02 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1116,7 +1116,6 @@ zh-TW: migrate_account: 轉移至另一個帳號 migrate_account_html: 如果您希望引導他人跟隨另一個帳號,請至這裡設定。 or_log_in_with: 或透過其他方式登入 - privacy_policy_agreement_html: 我已閱讀且同意 隱私權政策 progress: confirm: 驗證電子郵件地址 details: 您的個人資料 diff --git a/config/navigation.rb b/config/navigation.rb index bdda569092..de9e530f58 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -66,6 +66,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :admin, safe_join([material_symbol('manufacturing'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) && !self_destruct } do |s| s.item :dashboard, safe_join([material_symbol('speed'), t('admin.dashboard.title')]), admin_dashboard_path, if: -> { current_user.can?(:view_dashboard) } s.item :settings, safe_join([material_symbol('manufacturing'), t('admin.settings.title')]), admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings} + s.item :terms_of_service, safe_join([material_symbol('description'), t('admin.terms_of_service.title')]), admin_terms_of_service_index_path, highlights_on: %r{/admin/terms_of_service}, if: -> { current_user.can?(:manage_rules) } s.item :rules, safe_join([material_symbol('gavel'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}, if: -> { current_user.can?(:manage_rules) } s.item :warning_presets, safe_join([material_symbol('warning'), t('admin.warning_presets.title')]), admin_warning_presets_path, highlights_on: %r{/admin/warning_presets}, if: -> { current_user.can?(:manage_settings) } s.item :roles, safe_join([material_symbol('contact_mail'), t('admin.roles.title')]), admin_roles_path, highlights_on: %r{/admin/roles}, if: -> { current_user.can?(:manage_roles) } diff --git a/config/routes.rb b/config/routes.rb index b4c9b6918b..3909dd1b77 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -201,8 +201,9 @@ Rails.application.routes.draw do get '/about', to: 'about#show' get '/about/more', to: redirect('/about') - get '/privacy-policy', to: 'privacy#show', as: :privacy_policy - get '/terms', to: redirect('/privacy-policy') + get '/privacy-policy', to: 'privacy#show', as: :privacy_policy + get '/terms-of-service', to: 'terms_of_service#show', as: :terms_of_service + get '/terms', to: redirect('/terms-of-service') match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false match '*unmatched_route', via: :all, to: 'application#raise_not_found', format: false diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 2afe570236..30778e8bb7 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -33,6 +33,18 @@ namespace :admin do resources :action_logs, only: [:index] resources :warning_presets, except: [:new, :show] + namespace :terms_of_service do + resource :generate, only: [:show, :create] + resource :history, only: [:show] + resource :draft, only: [:show, :update] + end + + resources :terms_of_service, only: [:index, :create, :update] do + resource :preview, only: [:show], module: :terms_of_service + resource :test, only: [:create], module: :terms_of_service + resource :distribution, only: [:create], module: :terms_of_service + end + resources :announcements, except: [:show] do member do post :publish diff --git a/config/routes/api.rb b/config/routes/api.rb index 86e41a2abe..34a267b35d 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -116,6 +116,7 @@ namespace :api, format: false do resources :rules, only: [:index] resources :domain_blocks, only: [:index] resource :privacy_policy, only: [:show] + resource :terms_of_service, only: [:show] resource :extended_description, only: [:show] resource :translation_languages, only: [:show] resource :languages, only: [:show] diff --git a/config/templates/terms-of-service.md b/config/templates/terms-of-service.md new file mode 100644 index 0000000000..0b279c5386 --- /dev/null +++ b/config/templates/terms-of-service.md @@ -0,0 +1,274 @@ +## Introduction + +These terms of service (the "Terms") cover your access and use of Server +Operator’s ("Administrator", "we", or "us") instance, located at %{domain} (the +"Instance"). These Terms apply solely to your use of the Instance as operated +by the Administrator. Please note that we have no affiliation with Mastodon +gGmbH (“Mastodon”) and these Terms do not contain any representations or +warranties or other promises from Mastodon about your use of the Instance. If +you would like to contact us for any reason, please direct all questions, +comments, concerns and notices to us by following the instructions provided in +the Notice section below. + +Please read these Terms carefully before using the Instance as they contain +important information about your interactions with the Instance. We may have +other policies that apply to your use of the Instance and that are incorporated +into these Terms. You should also read these policies before using the Instance. + +## Age Requirements and Responsibility of Parents and Legal Guardians + +By accessing the Instance, you signify that you are at least thirteen years old +and that you meet the minimum age required by the laws in your country. If you +are old enough to access the Instance in your country, but are not old enough to +have the legal authority to consent to our Terms, please ask your parent or +legal guardian to read these Terms with you, as they must agree to the Terms on +your behalf. If you are a parent or legal guardian who has accepted these terms +on your child’s behalf, these terms apply to you and you are responsible for +your child’s activities on the Instance. + +## Prohibited Uses + +You are fully responsible for your activities while using the Instance, +including any content, information or other materials you post or upload to the +Instance, and you bear all risks associated with use of the Instance. By +agreeing to these Terms, you agree to comply with all applicable federal, state, +and local laws and regulations in connection with your use of the Instance. You +also agree not to use the Instance to engage in any prohibited conduct, or to +assist any other person or entity in engaging in any prohibited conduct. + +We reserve the right (but do not have the obligation) in our sole discretion to: +(1) monitor the Instance for violations of these Terms; (2) take appropriate +legal action against anyone who uses or accesses the Instance in a manner that +we believe violates the law or these Terms, including without limitation, +reporting such user to law enforcement authorities; (3) deny access to the +Instance or any features of the Instance to anyone who violates these Terms or +who we believe interferes with the ability of others to enjoy our Instance or +infringes the rights of others; and (4) otherwise manage the Instance in a +manner designed to protect our rights and property and to facilitate the proper +functioning of the Instance. + +You are prohibited from using the Instance for the commission of harmful or +illegal activities. Accordingly, you may not, or assist any other person to (or +attempt to): + +- Violate these Terms or other policies and terms posted on, or otherwise +- applicable to, the Instance; Upload any material, program, or software that +- contains any virus, worm, spyware, Trojan horse or other program or code +- designed to interrupt, destroy or limit the functionality of the Instance, +- launch a denial of service attack, or in any other way attempt to interfere +- with the functioning and availability of the Instance; Except as may be the +- result of standard search engine or Internet browser usage, use, launch, +- develop, or distribute any automated system, including without limitation, any +- spider, robot, cheat utility, scraper, offline reader, or any data mining or +- similar data gathering extraction tools to access the Instance, or use or +- launch any unauthorized script or other software; Interfere with, disable, +- vandalize or disrupt the Instance or servers or networks connected to the +- Instance; Hack into, penetrate, disable, or otherwise circumvent the security +- measures of the Instance or servers or networks connected to the Instance; or +- otherwise use the Instance in any way that violates any applicable national, +- federal, state, local or international law or regulation. + +## Intellectual Property + +The Instance contains content provided by its users, including you, such as +text, photos, videos, audio, links, and streams (“Content”). When you submit +Content to the Instance, you represent and warrant that you have all of the +rights, power, and authority necessary to grant the rights to the Content +contained within these Terms. Because you alone are responsible for the Content +that you submit to the Instance, you may expose yourself to liability from third +parties if you post or share such Content without all necessary rights. + +You retain all ownership rights you have in the Content that you submit to the +Instance, but you grant us a limited, non-exclusive, irrevocable, transferable, +royalty-free, perpetual license to use, copy, store, display, share, distribute, +communicate and transfer the Content in ways that are consistent with your use +of the Instance. To the fullest extent possible, you agree to waive or promise +not to assert against the Administrator all moral rights you may have in the +Content to the extent those rights are necessary for the Administrator to host +the Content on the Instance. + +## DMCA Copyright Infringement Notice + +We have implemented the procedures described in the Digital Millennium Copyright +Act of 1998 ("DMCA"), 17 U.S.C. § 512 , regarding the reporting of alleged +copyright infringement and the removal of or disabling access to infringing +material. If you have a good faith belief that copyrighted material on the +Instance is being used in a way that infringes a copyright over which you are +authorized to act, you may make a Notice of Infringing Material. If you have a +good faith belief that copyrighted material that was removed or access to which +was disabled was a result of a mistake or misidentification, then you may make a +Notice of Counter-Notification. + +Before serving a Notice of Infringing Material or Counter-Notification, you may +wish to contact a lawyer to better understand your rights and obligations under +the DMCA and other applicable laws. For example, if your Notice or +Counter-Notifications fails to comply with all requirements of sections +512(c)(3) or 512(g)(3), respectively, your Notice or Counter-Notification may +not be effective. + +### Termination of Repeat Infringers + +We will terminate or disable your use of the Instance in appropriate +circumstances if you are deemed by us to be a repeat copyright infringer. + +### Notices and Counter-Notifications must be sent to: + +DMCA Agent: Copyright Manager + +Address: %{dmca_address} + +Email: %{dmca_email} + +## Disclaimer + +Administrator reserves the right in our sole discretion to modify or +discontinue, temporarily or permanently, the Instance (or any part thereof) with +or without notice to you. You agree that Administrator will not be liable to +you or to any third party for any modification or discontinuance of the +Instance, except as set forth in the "Limitation of Liability" section below. + +You understand that we are not responsible for any activities or legal +consequences of your use of the Instance. Users are responsible for using the +Instance in compliance with all applicable laws and regulations of the +jurisdictions in which such users are domiciled, reside, or are located at the +time of such access or use, as well as these Terms. Any violation of these +Terms may result in the suspension or termination by us, in our sole discretion, +of your access to and use of the Instance. + +## Limitation of Liability + +In no event will Administrator’s total liability to you for all damages, losses, +or causes of action exceed one hundred dollars ($100). If you are dissatisfied +with the Instance or with these Terms, your sole remedy is to discontinue your +use of the Instance. + +## Links to and From Other Websites + +You may gain access to other websites and Instances via links on the Instance.  +These Terms apply to the Instance only and do not apply to Mastodon, other +Instances, or other parties' websites. Similarly, you may have come to the +Instance via a link from another website or Instance. The terms of use of other +websites and Instances do not apply to the Instance. Administrator assumes no +responsibility for any terms of use or material outside of the Instance accessed +via any link. You are free to establish a hypertext link to the Instance so +long as the link does not state or imply any sponsorship of your website, +instance or service by Administrator or the Instance. Unless expressly agreed +to by us in writing, reference to any of our products, services, processes or +other information, by trade name, trademark, logo, or otherwise by you or any +third party does not constitute or imply endorsement, sponsorship or +recommendation thereof by us. You may not, without our prior written +permission, scrape the Instance or incorporate into another website or other +service any of our material, content or intellectual property, unless you are +otherwise permitted by us to do so in accordance with a license or subject to +separate terms. + +## Dispute Resolution by Binding Arbitration + +### Agreement to Arbitrate: + +This Dispute Resolution by Binding Arbitration section is referred to in these +Terms as the “Arbitration Agreement.” You and the Instance agree that any and +all disputes, claims, demands, or causes of action (“Claims”) that have arisen +or may arise between you and us, whether arising out of or relating to these +Terms, the website, or any aspect of the relationship or transactions between +us, will be resolved exclusively through final and binding arbitration before a +neutral arbitrator, rather than in a court by a judge or jury, in accordance +with the terms of this Arbitration Agreement, except that, where available, you +or we may (but are not required to) assert individual Claims in small claims +court if such Claims are within the scope of such court’s jurisdiction. Further, +this Arbitration Agreement does not preclude you from bringing issues to the +attention of federal, state/provincial, or local agencies, and such agencies +can, if the law allows, seek relief against us on your behalf. You agree that, +by entering into these Terms, you and we are each waiving the right to a trial +by jury or to participate in a class action and that our respective rights will +be determined by a neutral arbitrator, not a judge or jury. + +### Prohibition of Class and Representative Actions and Non-Individualized + +### Relief + +You and we agree that each of us may bring claims against the other only on an +individual basis and not as a plaintiff or class member in any purported class +or representative action or proceeding. + +### Pre-Arbitration Dispute Resolution + +Before commencing any arbitration (or suit in small claims court, if available), +you agree to provide the Instance with a written notice of Claim, and the +Instance agrees to provide you with a written notice of Claim to the extent +reasonably possible based on the availability of your contact information to the +Instance (“Notice”). The Notice to the Instance shall be sent to +%{arbitration_website} with a paper copy to %{arbitration_address}. Where the +Instance has your contact information, the Instance will send its Notice to you +using the last email address we have on file for you if you have provided us +with an email address (each, a “Notice Address”). The Notice must (i) describe +the nature and basis of the Claim in sufficient detail to evaluate the merits of +the claiming party’s Claim and (ii) set forth the specific relief sought, +including the amount of money (if any) that is demanded and the means by which +the demanding party calculated the claimed amount. Both parties agree that they +will attempt to resolve a Claim through informal negotiation within sixty (60) +calendar days from the date the Notice is received. If the Claim is not resolved +within sixty (60) calendar days after the Notice is received, you or we may +commence an arbitration proceeding. Each party agrees that %{jurisdiction} +courts may enter injunctive relief to enforce the pre-filing requirements of +this paragraph, including an injunction to stay an arbitration that has been +commenced in violation of this paragraph. + +### Arbitration Procedures + +The relevant arbitration rules of %{jurisdiction} fully applies to the +Arbitration Agreement. The arbitration will be conducted by a neutral arbitrator +in accordance with %{jurisdiction} rules (the “Rules”), as modified by this +Arbitration Agreement. If there is any inconsistency between any term of the +Rules and any term of this Arbitration Agreement, the applicable terms of this +Arbitration Agreement will control. The arbitrator must also follow the +provisions of these Terms as a court would. Except as set forth above, all +issues are for the arbitrator to decide, including, but not limited to, +threshold issues relating to the scope, enforceability, and arbitrability of +this Arbitration Agreement and issues relating to (a) whether the terms of these +Terms (or any aspect thereof) are enforceable, unconscionable, or illusory and +(b) any defense to arbitration, including waiver, delay, laches, or estoppel. +Regardless of the manner in which the arbitration is conducted, the arbitrator +will issue a reasoned written decision sufficient to explain the essential +findings and conclusions on which the award is based. Payment of all filing, +administration and arbitrator fees (collectively, the “Arbitration Fees”) will +be governed by the Rules unless otherwise provided in this Arbitration +Agreement. + +### Small Claims Court + +Subject to applicable jurisdictional requirements, either party may elect to +pursue a Claim in a local small claims court rather than through arbitration so +long as the matter remains in a small claims court and proceeds only on an +individual basis. + +## Choice of Law  + +Any and all claims related to or arising out of your use of, or access to the +Instance shall be governed by internal substantive laws of New York in all +respects, without regard for the jurisdiction or forum in which you are +domiciled, reside, or located at the time of such access or use. + +## Waiver and Severability  + +If you do not comply with a portion of these Terms and we do not take action +right away, this does not mean we are giving up any of our rights under these +Terms. If any part of these Terms is determined to be invalid or unenforceable +by a court of competent jurisdiction or arbitrator, the remainder of the Terms +shall be enforced to the maximum extent permitted by law. + +## Notices + +All notices to Administrator under these Terms, unless otherwise specified shall +be sent to %{admin_email}. Service of any notice will be deemed given on the +date of receipt delivered by email. + +## Changes to these Terms + +We may change or modify these Terms by posting a revised version on the +Instance, or by otherwise providing notice to you, and will state at the top of +the revised Terms the date they were last revised. Changes will not apply +retroactively and will become effective no earlier than fourteen (14) calendar +days after they are posted, except for changes addressing changes made for legal +reasons, which will be effective immediately. Your continued use of the +Instance after any change means you agree to the new Terms. diff --git a/db/migrate/20241123224956_create_terms_of_services.rb b/db/migrate/20241123224956_create_terms_of_services.rb new file mode 100644 index 0000000000..dda2b0647c --- /dev/null +++ b/db/migrate/20241123224956_create_terms_of_services.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateTermsOfServices < ActiveRecord::Migration[7.2] + def change + create_table :terms_of_services do |t| + t.text :text, null: false, default: '' + t.text :changelog, null: false, default: '' + t.datetime :published_at + t.datetime :notification_sent_at + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 889e914aa6..d28bc5efca 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1103,6 +1103,15 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_05_163118) do t.index "lower((name)::text) text_pattern_ops", name: "index_tags_on_name_lower_btree", unique: true end + create_table "terms_of_services", force: :cascade do |t| + t.text "text", default: "", null: false + t.text "changelog", default: "", null: false + t.datetime "published_at" + t.datetime "notification_sent_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "tombstones", force: :cascade do |t| t.bigint "account_id" t.string "uri", null: false diff --git a/spec/controllers/admin/terms_of_service/distributions_controller_spec.rb b/spec/controllers/admin/terms_of_service/distributions_controller_spec.rb new file mode 100644 index 0000000000..b6d436a26f --- /dev/null +++ b/spec/controllers/admin/terms_of_service/distributions_controller_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::TermsOfService::DistributionsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) } + + before do + sign_in user, scope: :user + end + + describe 'POST #create' do + it 'returns http success' do + post :create, params: { terms_of_service_id: terms_of_service.id } + + expect(response).to redirect_to(admin_terms_of_service_index_path) + end + end +end diff --git a/spec/controllers/admin/terms_of_service/drafts_controller_spec.rb b/spec/controllers/admin/terms_of_service/drafts_controller_spec.rb new file mode 100644 index 0000000000..d0a12f8b3e --- /dev/null +++ b/spec/controllers/admin/terms_of_service/drafts_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::TermsOfService::DraftsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/terms_of_service/generates_controller_spec.rb b/spec/controllers/admin/terms_of_service/generates_controller_spec.rb new file mode 100644 index 0000000000..1f33376de0 --- /dev/null +++ b/spec/controllers/admin/terms_of_service/generates_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::TermsOfService::GeneratesController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/terms_of_service/histories_controller_spec.rb b/spec/controllers/admin/terms_of_service/histories_controller_spec.rb new file mode 100644 index 0000000000..d11ea2cd33 --- /dev/null +++ b/spec/controllers/admin/terms_of_service/histories_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::TermsOfService::HistoriesController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/terms_of_service/previews_controller_spec.rb b/spec/controllers/admin/terms_of_service/previews_controller_spec.rb new file mode 100644 index 0000000000..5d923c9f30 --- /dev/null +++ b/spec/controllers/admin/terms_of_service/previews_controller_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::TermsOfService::PreviewsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show, params: { terms_of_service_id: terms_of_service.id } + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/terms_of_service/tests_controller_spec.rb b/spec/controllers/admin/terms_of_service/tests_controller_spec.rb new file mode 100644 index 0000000000..281c4d28c5 --- /dev/null +++ b/spec/controllers/admin/terms_of_service/tests_controller_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::TermsOfService::TestsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:terms_of_service) { Fabricate(:terms_of_service, notification_sent_at: nil) } + + before do + sign_in user, scope: :user + end + + describe 'POST #create' do + it 'returns http success' do + post :create, params: { terms_of_service_id: terms_of_service.id } + + expect(response).to redirect_to(admin_terms_of_service_preview_path(terms_of_service)) + end + end +end diff --git a/spec/controllers/admin/terms_of_service_controller_spec.rb b/spec/controllers/admin/terms_of_service_controller_spec.rb new file mode 100644 index 0000000000..b7fdb90446 --- /dev/null +++ b/spec/controllers/admin/terms_of_service_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::TermsOfServiceController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/fabricators/terms_of_service_fabricator.rb b/spec/fabricators/terms_of_service_fabricator.rb new file mode 100644 index 0000000000..2b0cfabcfb --- /dev/null +++ b/spec/fabricators/terms_of_service_fabricator.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Fabricator(:terms_of_service) do + text { Faker::Lorem.paragraph } + changelog { Faker::Lorem.paragraph } + published_at { Time.zone.now } + notification_sent_at { Time.zone.now } +end diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 2722538e1a..e677a24df2 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -98,4 +98,9 @@ class UserMailerPreview < ActionMailer::Preview def failed_2fa UserMailer.failed_2fa(User.first, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc) end + + # Preview this email at http://localhost:3000/rails/mailers/user_mailer/terms_of_service_changed + def terms_of_service_changed + UserMailer.terms_of_service_changed(User.first, TermsOfService.live.first) + end end diff --git a/spec/models/terms_of_service_spec.rb b/spec/models/terms_of_service_spec.rb new file mode 100644 index 0000000000..d32ba4e642 --- /dev/null +++ b/spec/models/terms_of_service_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TermsOfService do + describe '#scope_for_notification' do + subject { terms_of_service.scope_for_notification } + + let(:published_at) { Time.now.utc } + let(:terms_of_service) { Fabricate(:terms_of_service, published_at: published_at) } + let(:user_before) { Fabricate(:user, created_at: published_at - 2.days) } + let(:user_before_unconfirmed) { Fabricate(:user, created_at: published_at - 2.days, confirmed_at: nil) } + let(:user_before_suspended) { Fabricate(:user, created_at: published_at - 2.days) } + let(:user_after) { Fabricate(:user, created_at: published_at + 1.hour) } + + before do + user_before_suspended.account.suspend! + user_before_unconfirmed + user_before + user_after + end + + it 'includes only users created before the terms of service were published' do + expect(subject.pluck(:id)).to match_array(user_before.id) + end + end +end diff --git a/spec/requests/api/v1/instances/terms_of_services_spec.rb b/spec/requests/api/v1/instances/terms_of_services_spec.rb new file mode 100644 index 0000000000..5feb49f48d --- /dev/null +++ b/spec/requests/api/v1/instances/terms_of_services_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Terms of Service' do + describe 'GET /api/v1/instance/terms_of_service' do + before do + Fabricate(:terms_of_service) + end + + it 'returns http success' do + get api_v1_instance_terms_of_service_path + + expect(response) + .to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + + expect(response.parsed_body) + .to be_present + .and include(:content) + end + end +end