From 1335083bedd1dd563cfa9c54e69abc189ba3ec7b Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Thu, 18 Jan 2024 19:40:25 +0100 Subject: [PATCH 01/84] Streaming: replace npmlog with pino & pino-http (#27828) --- streaming/.eslintrc.js | 11 ++ streaming/index.js | 334 ++++++++++++++++++++---------------- streaming/logging.js | 119 +++++++++++++ streaming/package.json | 5 +- yarn.lock | 376 +++++++++++++++++++++++++++++------------ 5 files changed, 593 insertions(+), 252 deletions(-) create mode 100644 streaming/logging.js diff --git a/streaming/.eslintrc.js b/streaming/.eslintrc.js index 5e2d233c68..188ebb512d 100644 --- a/streaming/.eslintrc.js +++ b/streaming/.eslintrc.js @@ -15,7 +15,18 @@ module.exports = defineConfig({ ecmaVersion: 2021, }, rules: { + // In the streaming server we need to delete some variables to ensure + // garbage collection takes place on the values referenced by those objects; + // The alternative is to declare the variable as nullable, but then we need + // to assert it's in existence before every use, which becomes much harder + // to maintain. + 'no-delete-var': 'off', + + // The streaming server is written in commonjs, not ESM for now: 'import/no-commonjs': 'off', + + // This overrides the base configuration for this rule to pick up + // dependencies for the streaming server from the correct package.json file. 'import/no-extraneous-dependencies': [ 'error', { diff --git a/streaming/index.js b/streaming/index.js index c8124fcc0f..aa75a08b7f 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -10,12 +10,11 @@ const dotenv = require('dotenv'); const express = require('express'); const Redis = require('ioredis'); const { JSDOM } = require('jsdom'); -const log = require('npmlog'); const pg = require('pg'); const dbUrlToConfig = require('pg-connection-string').parse; -const uuid = require('uuid'); const WebSocket = require('ws'); +const { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } = require('./logging'); const { setupMetrics } = require('./metrics'); const { isTruthy } = require("./utils"); @@ -28,15 +27,30 @@ dotenv.config({ path: path.resolve(__dirname, path.join('..', dotenvFile)) }); -log.level = process.env.LOG_LEVEL || 'verbose'; +initializeLogLevel(process.env, environment); + +/** + * Declares the result type for accountFromToken / accountFromRequest. + * + * Note: This is here because jsdoc doesn't like importing types that + * are nested in functions + * @typedef ResolvedAccount + * @property {string} accessTokenId + * @property {string[]} scopes + * @property {string} accountId + * @property {string[]} chosenLanguages + * @property {string} deviceId + */ /** * @param {Object.} config */ const createRedisClient = async (config) => { const { redisParams, redisUrl } = config; + // @ts-ignore const client = new Redis(redisUrl, redisParams); - client.on('error', (err) => log.error('Redis Client Error!', err)); + // @ts-ignore + client.on('error', (err) => logger.error({ err }, 'Redis Client Error!')); return client; }; @@ -61,12 +75,12 @@ const parseJSON = (json, req) => { */ if (req) { if (req.accountId) { - log.warn(req.requestId, `Error parsing message from user ${req.accountId}: ${err}`); + req.log.error({ err }, `Error parsing message from user ${req.accountId}`); } else { - log.silly(req.requestId, `Error parsing message from ${req.remoteAddress}: ${err}`); + req.log.error({ err }, `Error parsing message from ${req.remoteAddress}`); } } else { - log.warn(`Error parsing message from redis: ${err}`); + logger.error({ err }, `Error parsing message from redis`); } return null; } @@ -105,6 +119,7 @@ const pgConfigFromEnv = (env) => { baseConfig.password = env.DB_PASS; } } else { + // @ts-ignore baseConfig = pgConfigs[environment]; if (env.DB_SSLMODE) { @@ -149,6 +164,7 @@ const redisConfigFromEnv = (env) => { // redisParams.path takes precedence over host and port. if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) { + // @ts-ignore redisParams.path = env.REDIS_URL.slice(7); } @@ -195,6 +211,7 @@ const startServer = async () => { app.set('trust proxy', process.env.TRUSTED_PROXY_IP ? process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) : 'loopback,uniquelocal'); + app.use(httpLogger); app.use(cors()); // Handle eventsource & other http requests: @@ -202,32 +219,37 @@ const startServer = async () => { // Handle upgrade requests: server.on('upgrade', async function handleUpgrade(request, socket, head) { + // Setup the HTTP logger, since websocket upgrades don't get the usual http + // logger. This decorates the `request` object. + attachWebsocketHttpLogger(request); + + request.log.info("HTTP Upgrade Requested"); + /** @param {Error} err */ const onSocketError = (err) => { - log.error(`Error with websocket upgrade: ${err}`); + request.log.error({ error: err }, err.message); }; socket.on('error', onSocketError); - // Authenticate: - try { - await accountFromRequest(request); - } catch (err) { - log.error(`Error authenticating request: ${err}`); + /** @type {ResolvedAccount} */ + let resolvedAccount; + try { + resolvedAccount = await accountFromRequest(request); + } catch (err) { // Unfortunately for using the on('upgrade') setup, we need to manually // write a HTTP Response to the Socket to close the connection upgrade // attempt, so the following code is to handle all of that. const statusCode = err.status ?? 401; - /** @type {Record} */ + /** @type {Record} */ const headers = { 'Connection': 'close', 'Content-Type': 'text/plain', 'Content-Length': 0, 'X-Request-Id': request.id, - // TODO: Send the error message via header so it can be debugged in - // developer tools + 'X-Error-Message': err.status ? err.toString() : 'An unexpected error occurred' }; // Ensure the socket is closed once we've finished writing to it: @@ -238,15 +260,28 @@ const startServer = async () => { // Write the HTTP response manually: socket.end(`HTTP/1.1 ${statusCode} ${http.STATUS_CODES[statusCode]}\r\n${Object.keys(headers).map((key) => `${key}: ${headers[key]}`).join('\r\n')}\r\n\r\n`); + // Finally, log the error: + request.log.error({ + err, + res: { + statusCode, + headers + } + }, err.toString()); + return; } + // Remove the error handler, wss.handleUpgrade has its own: + socket.removeListener('error', onSocketError); + wss.handleUpgrade(request, socket, head, function done(ws) { - // Remove the error handler: - socket.removeListener('error', onSocketError); + request.log.info("Authenticated request & upgraded to WebSocket connection"); + + const wsLogger = createWebsocketLogger(request, resolvedAccount); // Start the connection: - wss.emit('connection', ws, request); + wss.emit('connection', ws, request, wsLogger); }); }); @@ -273,9 +308,9 @@ const startServer = async () => { // When checking metrics in the browser, the favicon is requested this // prevents the request from falling through to the API Router, which would // error for this endpoint: - app.get('/favicon.ico', (req, res) => res.status(404).end()); + app.get('/favicon.ico', (_req, res) => res.status(404).end()); - app.get('/api/v1/streaming/health', (req, res) => { + app.get('/api/v1/streaming/health', (_req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); }); @@ -285,7 +320,7 @@ const startServer = async () => { res.set('Content-Type', metrics.register.contentType); res.end(await metrics.register.metrics()); } catch (ex) { - log.error(ex); + req.log.error(ex); res.status(500).end(); } }); @@ -319,7 +354,7 @@ const startServer = async () => { const callbacks = subs[channel]; - log.silly(`New message on channel ${redisPrefix}${channel}`); + logger.debug(`New message on channel ${redisPrefix}${channel}`); if (!callbacks) { return; @@ -343,17 +378,16 @@ const startServer = async () => { * @param {SubscriptionListener} callback */ const subscribe = (channel, callback) => { - log.silly(`Adding listener for ${channel}`); + logger.debug(`Adding listener for ${channel}`); subs[channel] = subs[channel] || []; if (subs[channel].length === 0) { - log.verbose(`Subscribe ${channel}`); + logger.debug(`Subscribe ${channel}`); redisSubscribeClient.subscribe(channel, (err, count) => { if (err) { - log.error(`Error subscribing to ${channel}`); - } - else { + logger.error(`Error subscribing to ${channel}`); + } else if (typeof count === 'number') { redisSubscriptions.set(count); } }); @@ -367,7 +401,7 @@ const startServer = async () => { * @param {SubscriptionListener} callback */ const unsubscribe = (channel, callback) => { - log.silly(`Removing listener for ${channel}`); + logger.debug(`Removing listener for ${channel}`); if (!subs[channel]) { return; @@ -376,12 +410,11 @@ const startServer = async () => { subs[channel] = subs[channel].filter(item => item !== callback); if (subs[channel].length === 0) { - log.verbose(`Unsubscribe ${channel}`); + logger.debug(`Unsubscribe ${channel}`); redisSubscribeClient.unsubscribe(channel, (err, count) => { if (err) { - log.error(`Error unsubscribing to ${channel}`); - } - else { + logger.error(`Error unsubscribing to ${channel}`); + } else if (typeof count === 'number') { redisSubscriptions.set(count); } }); @@ -390,45 +423,13 @@ const startServer = async () => { }; /** - * @param {any} req - * @param {any} res - * @param {function(Error=): void} next - */ - const setRequestId = (req, res, next) => { - req.requestId = uuid.v4(); - res.header('X-Request-Id', req.requestId); - - next(); - }; - - /** - * @param {any} req - * @param {any} res - * @param {function(Error=): void} next - */ - const setRemoteAddress = (req, res, next) => { - req.remoteAddress = req.connection.remoteAddress; - - next(); - }; - - /** - * @param {any} req + * @param {http.IncomingMessage & ResolvedAccount} req * @param {string[]} necessaryScopes * @returns {boolean} */ const isInScope = (req, necessaryScopes) => req.scopes.some(scope => necessaryScopes.includes(scope)); - /** - * @typedef ResolvedAccount - * @property {string} accessTokenId - * @property {string[]} scopes - * @property {string} accountId - * @property {string[]} chosenLanguages - * @property {string} deviceId - */ - /** * @param {string} token * @param {any} req @@ -441,6 +442,7 @@ const startServer = async () => { return; } + // @ts-ignore client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => { done(); @@ -451,6 +453,7 @@ const startServer = async () => { if (result.rows.length === 0) { err = new Error('Invalid access token'); + // @ts-ignore err.status = 401; reject(err); @@ -485,6 +488,7 @@ const startServer = async () => { if (!authorization && !accessToken) { const err = new Error('Missing access token'); + // @ts-ignore err.status = 401; reject(err); @@ -529,15 +533,16 @@ const startServer = async () => { }; /** - * @param {any} req + * @param {http.IncomingMessage & ResolvedAccount} req + * @param {import('pino').Logger} logger * @param {string|undefined} channelName * @returns {Promise.} */ - const checkScopes = (req, channelName) => new Promise((resolve, reject) => { - log.silly(req.requestId, `Checking OAuth scopes for ${channelName}`); + const checkScopes = (req, logger, channelName) => new Promise((resolve, reject) => { + logger.debug(`Checking OAuth scopes for ${channelName}`); // When accessing public channels, no scopes are needed - if (PUBLIC_CHANNELS.includes(channelName)) { + if (channelName && PUBLIC_CHANNELS.includes(channelName)) { resolve(); return; } @@ -564,6 +569,7 @@ const startServer = async () => { } const err = new Error('Access token does not cover required scopes'); + // @ts-ignore err.status = 401; reject(err); @@ -577,38 +583,40 @@ const startServer = async () => { /** * @param {any} req * @param {SystemMessageHandlers} eventHandlers - * @returns {function(object): void} + * @returns {SubscriptionListener} */ const createSystemMessageListener = (req, eventHandlers) => { return message => { + if (!message?.event) { + return; + } + const { event } = message; - log.silly(req.requestId, `System message for ${req.accountId}: ${event}`); + req.log.debug(`System message for ${req.accountId}: ${event}`); if (event === 'kill') { - log.verbose(req.requestId, `Closing connection for ${req.accountId} due to expired access token`); + req.log.debug(`Closing connection for ${req.accountId} due to expired access token`); eventHandlers.onKill(); } else if (event === 'filters_changed') { - log.verbose(req.requestId, `Invalidating filters cache for ${req.accountId}`); + req.log.debug(`Invalidating filters cache for ${req.accountId}`); req.cachedFilters = null; } }; }; /** - * @param {any} req - * @param {any} res + * @param {http.IncomingMessage & ResolvedAccount} req + * @param {http.OutgoingMessage} res */ const subscribeHttpToSystemChannel = (req, res) => { const accessTokenChannelId = `timeline:access_token:${req.accessTokenId}`; const systemChannelId = `timeline:system:${req.accountId}`; const listener = createSystemMessageListener(req, { - onKill() { res.end(); }, - }); res.on('close', () => { @@ -641,13 +649,14 @@ const startServer = async () => { // the connection, as there's nothing to stream back if (!channelName) { const err = new Error('Unknown channel requested'); + // @ts-ignore err.status = 400; next(err); return; } - accountFromRequest(req).then(() => checkScopes(req, channelName)).then(() => { + accountFromRequest(req).then(() => checkScopes(req, req.log, channelName)).then(() => { subscribeHttpToSystemChannel(req, res); }).then(() => { next(); @@ -663,22 +672,28 @@ const startServer = async () => { * @param {function(Error=): void} next */ const errorMiddleware = (err, req, res, next) => { - log.error(req.requestId, err.toString()); + req.log.error({ err }, err.toString()); if (res.headersSent) { next(err); return; } - res.writeHead(err.status || 500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: err.status ? err.toString() : 'An unexpected error occurred' })); + const hasStatusCode = Object.hasOwnProperty.call(err, 'status'); + // @ts-ignore + const statusCode = hasStatusCode ? err.status : 500; + const errorMessage = hasStatusCode ? err.toString() : 'An unexpected error occurred'; + + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: errorMessage })); }; /** - * @param {array} arr + * @param {any[]} arr * @param {number=} shift * @returns {string} */ + // @ts-ignore const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', '); /** @@ -695,6 +710,7 @@ const startServer = async () => { return; } + // @ts-ignore client.query('SELECT id, account_id FROM lists WHERE id = $1 LIMIT 1', [listId], (err, result) => { done(); @@ -709,34 +725,43 @@ const startServer = async () => { }); /** - * @param {string[]} ids - * @param {any} req + * @param {string[]} channelIds + * @param {http.IncomingMessage & ResolvedAccount} req + * @param {import('pino').Logger} log * @param {function(string, string): void} output * @param {undefined | function(string[], SubscriptionListener): void} attachCloseHandler * @param {'websocket' | 'eventsource'} destinationType * @param {boolean=} needsFiltering * @returns {SubscriptionListener} */ - const streamFrom = (ids, req, output, attachCloseHandler, destinationType, needsFiltering = false) => { - const accountId = req.accountId || req.remoteAddress; - - log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}`); + const streamFrom = (channelIds, req, log, output, attachCloseHandler, destinationType, needsFiltering = false) => { + log.info({ channelIds }, `Starting stream`); + /** + * @param {string} event + * @param {object|string} payload + */ const transmit = (event, payload) => { // TODO: Replace "string"-based delete payloads with object payloads: const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload; messagesSent.labels({ type: destinationType }).inc(1); - log.silly(req.requestId, `Transmitting for ${accountId}: ${event} ${encodedPayload}`); + log.debug({ event, payload }, `Transmitting ${event} to ${req.accountId}`); + output(event, encodedPayload); }; // The listener used to process each message off the redis subscription, // message here is an object with an `event` and `payload` property. Some // events also include a queued_at value, but this is being removed shortly. + /** @type {SubscriptionListener} */ const listener = message => { + if (!message?.event || !message?.payload) { + return; + } + const { event, payload } = message; // Streaming only needs to apply filtering to some channels and only to @@ -759,7 +784,7 @@ const startServer = async () => { // Filter based on language: if (Array.isArray(req.chosenLanguages) && payload.language !== null && req.chosenLanguages.indexOf(payload.language) === -1) { - log.silly(req.requestId, `Message ${payload.id} filtered by language (${payload.language})`); + log.debug(`Message ${payload.id} filtered by language (${payload.language})`); return; } @@ -770,6 +795,7 @@ const startServer = async () => { } // Filter based on domain blocks, blocks, mutes, or custom filters: + // @ts-ignore const targetAccountIds = [payload.account.id].concat(payload.mentions.map(item => item.id)); const accountDomain = payload.account.acct.split('@')[1]; @@ -781,6 +807,7 @@ const startServer = async () => { } const queries = [ + // @ts-ignore client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) @@ -793,10 +820,13 @@ const startServer = async () => { ]; if (accountDomain) { + // @ts-ignore queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain])); } + // @ts-ignore if (!payload.filtered && !req.cachedFilters) { + // @ts-ignore queries.push(client.query('SELECT filter.id AS id, filter.phrase AS title, filter.context AS context, filter.expires_at AS expires_at, filter.action AS filter_action, keyword.keyword AS keyword, keyword.whole_word AS whole_word FROM custom_filter_keywords keyword JOIN custom_filters filter ON keyword.custom_filter_id = filter.id WHERE filter.account_id = $1 AND (filter.expires_at IS NULL OR filter.expires_at > NOW())', [req.accountId])); } @@ -819,9 +849,11 @@ const startServer = async () => { // Handling for constructing the custom filters and caching them on the request // TODO: Move this logic out of the message handling lifecycle + // @ts-ignore if (!req.cachedFilters) { const filterRows = values[accountDomain ? 2 : 1].rows; + // @ts-ignore req.cachedFilters = filterRows.reduce((cache, filter) => { if (cache[filter.id]) { cache[filter.id].keywords.push([filter.keyword, filter.whole_word]); @@ -851,7 +883,9 @@ const startServer = async () => { // needs to be done in a separate loop as the database returns one // filterRow per keyword, so we need all the keywords before // constructing the regular expression + // @ts-ignore Object.keys(req.cachedFilters).forEach((key) => { + // @ts-ignore req.cachedFilters[key].regexp = new RegExp(req.cachedFilters[key].keywords.map(([keyword, whole_word]) => { let expr = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -872,13 +906,16 @@ const startServer = async () => { // Apply cachedFilters against the payload, constructing a // `filter_results` array of FilterResult entities + // @ts-ignore if (req.cachedFilters) { const status = payload; // TODO: Calculate searchableContent in Ruby on Rails: + // @ts-ignore const searchableContent = ([status.spoiler_text || '', status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); const searchableTextContent = JSDOM.fragment(searchableContent).textContent; const now = new Date(); + // @ts-ignore const filter_results = Object.values(req.cachedFilters).reduce((results, cachedFilter) => { // Check the filter hasn't expired before applying: if (cachedFilter.expires_at !== null && cachedFilter.expires_at < now) { @@ -926,12 +963,12 @@ const startServer = async () => { }); }; - ids.forEach(id => { + channelIds.forEach(id => { subscribe(`${redisPrefix}${id}`, listener); }); if (typeof attachCloseHandler === 'function') { - attachCloseHandler(ids.map(id => `${redisPrefix}${id}`), listener); + attachCloseHandler(channelIds.map(id => `${redisPrefix}${id}`), listener); } return listener; @@ -943,8 +980,6 @@ const startServer = async () => { * @returns {function(string, string): void} */ const streamToHttp = (req, res) => { - const accountId = req.accountId || req.remoteAddress; - const channelName = channelNameFromPath(req); connectedClients.labels({ type: 'eventsource' }).inc(); @@ -963,7 +998,8 @@ const startServer = async () => { const heartbeat = setInterval(() => res.write(':thump\n'), 15000); req.on('close', () => { - log.verbose(req.requestId, `Ending stream for ${accountId}`); + req.log.info({ accountId: req.accountId }, `Ending stream`); + // We decrement these counters here instead of in streamHttpEnd as in that // method we don't have knowledge of the channel names connectedClients.labels({ type: 'eventsource' }).dec(); @@ -1007,15 +1043,15 @@ const startServer = async () => { */ const streamToWs = (req, ws, streamName) => (event, payload) => { if (ws.readyState !== ws.OPEN) { - log.error(req.requestId, 'Tried writing to closed socket'); + req.log.error('Tried writing to closed socket'); return; } const message = JSON.stringify({ stream: streamName, event, payload }); - ws.send(message, (/** @type {Error} */ err) => { + ws.send(message, (/** @type {Error|undefined} */ err) => { if (err) { - log.error(req.requestId, `Failed to send to websocket: ${err}`); + req.log.error({err}, `Failed to send to websocket`); } }); }; @@ -1032,20 +1068,19 @@ const startServer = async () => { app.use(api); - api.use(setRequestId); - api.use(setRemoteAddress); - api.use(authenticationMiddleware); api.use(errorMiddleware); api.get('/api/v1/streaming/*', (req, res) => { + // @ts-ignore channelNameToIds(req, channelNameFromPath(req), req.query).then(({ channelIds, options }) => { const onSend = streamToHttp(req, res); const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds)); - streamFrom(channelIds, req, onSend, onEnd, 'eventsource', options.needsFiltering); + // @ts-ignore + streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering); }).catch(err => { - log.verbose(req.requestId, 'Subscription error:', err.toString()); + res.log.info({ err }, 'Subscription error:', err.toString()); httpNotFound(res); }); }); @@ -1197,6 +1232,7 @@ const startServer = async () => { break; case 'list': + // @ts-ignore authorizeListAccess(params.list, req).then(() => { resolve({ channelIds: [`timeline:list:${params.list}`], @@ -1218,9 +1254,9 @@ const startServer = async () => { * @returns {string[]} */ const streamNameFromChannelName = (channelName, params) => { - if (channelName === 'list') { + if (channelName === 'list' && params.list) { return [channelName, params.list]; - } else if (['hashtag', 'hashtag:local'].includes(channelName)) { + } else if (['hashtag', 'hashtag:local'].includes(channelName) && params.tag) { return [channelName, params.tag]; } else { return [channelName]; @@ -1229,8 +1265,9 @@ const startServer = async () => { /** * @typedef WebSocketSession - * @property {WebSocket} websocket - * @property {http.IncomingMessage} request + * @property {WebSocket & { isAlive: boolean}} websocket + * @property {http.IncomingMessage & ResolvedAccount} request + * @property {import('pino').Logger} logger * @property {Object.} subscriptions */ @@ -1240,8 +1277,8 @@ const startServer = async () => { * @param {StreamParams} params * @returns {void} */ - const subscribeWebsocketToChannel = ({ socket, request, subscriptions }, channelName, params) => { - checkScopes(request, channelName).then(() => channelNameToIds(request, channelName, params)).then(({ + const subscribeWebsocketToChannel = ({ websocket, request, logger, subscriptions }, channelName, params) => { + checkScopes(request, logger, channelName).then(() => channelNameToIds(request, channelName, params)).then(({ channelIds, options, }) => { @@ -1249,9 +1286,9 @@ const startServer = async () => { return; } - const onSend = streamToWs(request, socket, streamNameFromChannelName(channelName, params)); + const onSend = streamToWs(request, websocket, streamNameFromChannelName(channelName, params)); const stopHeartbeat = subscriptionHeartbeat(channelIds); - const listener = streamFrom(channelIds, request, onSend, undefined, 'websocket', options.needsFiltering); + const listener = streamFrom(channelIds, request, logger, onSend, undefined, 'websocket', options.needsFiltering); connectedChannels.labels({ type: 'websocket', channel: channelName }).inc(); @@ -1261,14 +1298,17 @@ const startServer = async () => { stopHeartbeat, }; }).catch(err => { - log.verbose(request.requestId, 'Subscription error:', err.toString()); - socket.send(JSON.stringify({ error: err.toString() })); + logger.error({ err }, 'Subscription error'); + websocket.send(JSON.stringify({ error: err.toString() })); }); }; - - const removeSubscription = (subscriptions, channelIds, request) => { - log.verbose(request.requestId, `Ending stream from ${channelIds.join(', ')} for ${request.accountId}`); + /** + * @param {WebSocketSession} session + * @param {string[]} channelIds + */ + const removeSubscription = ({ request, logger, subscriptions }, channelIds) => { + logger.info({ channelIds, accountId: request.accountId }, `Ending stream`); const subscription = subscriptions[channelIds.join(';')]; @@ -1292,16 +1332,17 @@ const startServer = async () => { * @param {StreamParams} params * @returns {void} */ - const unsubscribeWebsocketFromChannel = ({ socket, request, subscriptions }, channelName, params) => { + const unsubscribeWebsocketFromChannel = (session, channelName, params) => { + const { websocket, request, logger } = session; + channelNameToIds(request, channelName, params).then(({ channelIds }) => { - removeSubscription(subscriptions, channelIds, request); + removeSubscription(session, channelIds); }).catch(err => { - log.verbose(request.requestId, 'Unsubscribe error:', err); + logger.error({err}, 'Unsubscribe error'); // If we have a socket that is alive and open still, send the error back to the client: - // FIXME: In other parts of the code ws === socket - if (socket.isAlive && socket.readyState === socket.OPEN) { - socket.send(JSON.stringify({ error: "Error unsubscribing from channel" })); + if (websocket.isAlive && websocket.readyState === websocket.OPEN) { + websocket.send(JSON.stringify({ error: "Error unsubscribing from channel" })); } }); }; @@ -1309,16 +1350,14 @@ const startServer = async () => { /** * @param {WebSocketSession} session */ - const subscribeWebsocketToSystemChannel = ({ socket, request, subscriptions }) => { + const subscribeWebsocketToSystemChannel = ({ websocket, request, subscriptions }) => { const accessTokenChannelId = `timeline:access_token:${request.accessTokenId}`; const systemChannelId = `timeline:system:${request.accountId}`; const listener = createSystemMessageListener(request, { - onKill() { - socket.close(); + websocket.close(); }, - }); subscribe(`${redisPrefix}${accessTokenChannelId}`, listener); @@ -1355,18 +1394,15 @@ const startServer = async () => { /** * @param {WebSocket & { isAlive: boolean }} ws - * @param {http.IncomingMessage} req + * @param {http.IncomingMessage & ResolvedAccount} req + * @param {import('pino').Logger} log */ - function onConnection(ws, req) { + function onConnection(ws, req, log) { // Note: url.parse could throw, which would terminate the connection, so we // increment the connected clients metric straight away when we establish // the connection, without waiting: connectedClients.labels({ type: 'websocket' }).inc(); - // Setup request properties: - req.requestId = uuid.v4(); - req.remoteAddress = ws._socket.remoteAddress; - // Setup connection keep-alive state: ws.isAlive = true; ws.on('pong', () => { @@ -1377,8 +1413,9 @@ const startServer = async () => { * @type {WebSocketSession} */ const session = { - socket: ws, + websocket: ws, request: req, + logger: log, subscriptions: {}, }; @@ -1386,27 +1423,30 @@ const startServer = async () => { const subscriptions = Object.keys(session.subscriptions); subscriptions.forEach(channelIds => { - removeSubscription(session.subscriptions, channelIds.split(';'), req); + removeSubscription(session, channelIds.split(';')); }); // Decrement the metrics for connected clients: connectedClients.labels({ type: 'websocket' }).dec(); - // ensure garbage collection: - session.socket = null; - session.request = null; - session.subscriptions = {}; + // We need to delete the session object as to ensure it correctly gets + // garbage collected, without doing this we could accidentally hold on to + // references to the websocket, the request, and the logger, causing + // memory leaks. + // + // @ts-ignore + delete session; }); // Note: immediately after the `error` event is emitted, the `close` event // is emitted. As such, all we need to do is log the error here. - ws.on('error', (err) => { - log.error('websocket', err.toString()); + ws.on('error', (/** @type {Error} */ err) => { + log.error(err); }); ws.on('message', (data, isBinary) => { if (isBinary) { - log.warn('websocket', 'Received binary data, closing connection'); + log.warn('Received binary data, closing connection'); ws.close(1003, 'The mastodon streaming server does not support binary messages'); return; } @@ -1441,18 +1481,20 @@ const startServer = async () => { setInterval(() => { wss.clients.forEach(ws => { + // @ts-ignore if (ws.isAlive === false) { ws.terminate(); return; } + // @ts-ignore ws.isAlive = false; ws.ping('', false); }); }, 30000); attachServerWithConfig(server, address => { - log.warn(`Streaming API now listening on ${address}`); + logger.info(`Streaming API now listening on ${address}`); }); const onExit = () => { @@ -1460,8 +1502,10 @@ const startServer = async () => { process.exit(0); }; + /** @param {Error} err */ const onError = (err) => { - log.error(err); + logger.error(err); + server.close(); process.exit(0); }; @@ -1485,7 +1529,7 @@ const attachServerWithConfig = (server, onSuccess) => { } }); } else { - server.listen(+process.env.PORT || 4000, process.env.BIND || '127.0.0.1', () => { + server.listen(+(process.env.PORT || 4000), process.env.BIND || '127.0.0.1', () => { if (onSuccess) { onSuccess(`${server.address().address}:${server.address().port}`); } diff --git a/streaming/logging.js b/streaming/logging.js new file mode 100644 index 0000000000..64ee474875 --- /dev/null +++ b/streaming/logging.js @@ -0,0 +1,119 @@ +const { pino } = require('pino'); +const { pinoHttp, stdSerializers: pinoHttpSerializers } = require('pino-http'); +const uuid = require('uuid'); + +/** + * Generates the Request ID for logging and setting on responses + * @param {http.IncomingMessage} req + * @param {http.ServerResponse} [res] + * @returns {import("pino-http").ReqId} + */ +function generateRequestId(req, res) { + if (req.id) { + return req.id; + } + + req.id = uuid.v4(); + + // Allow for usage with WebSockets: + if (res) { + res.setHeader('X-Request-Id', req.id); + } + + return req.id; +} + +/** + * Request log sanitizer to prevent logging access tokens in URLs + * @param {http.IncomingMessage} req + */ +function sanitizeRequestLog(req) { + const log = pinoHttpSerializers.req(req); + if (typeof log.url === 'string' && log.url.includes('access_token')) { + // Doorkeeper uses SecureRandom.urlsafe_base64 per RFC 6749 / RFC 6750 + log.url = log.url.replace(/(access_token)=([a-zA-Z0-9\-_]+)/gi, '$1=[Redacted]'); + } + return log; +} + +const logger = pino({ + name: "streaming", + // Reformat the log level to a string: + formatters: { + level: (label) => { + return { + level: label + }; + }, + }, + redact: { + paths: [ + 'req.headers["sec-websocket-key"]', + // Note: we currently pass the AccessToken via the websocket subprotocol + // field, an anti-pattern, but this ensures it doesn't end up in logs. + 'req.headers["sec-websocket-protocol"]', + 'req.headers.authorization', + 'req.headers.cookie', + 'req.query.access_token' + ] + } +}); + +const httpLogger = pinoHttp({ + logger, + genReqId: generateRequestId, + serializers: { + req: sanitizeRequestLog + } +}); + +/** + * Attaches a logger to the request object received by http upgrade handlers + * @param {http.IncomingMessage} request + */ +function attachWebsocketHttpLogger(request) { + generateRequestId(request); + + request.log = logger.child({ + req: sanitizeRequestLog(request), + }); +} + +/** + * Creates a logger instance for the Websocket connection to use. + * @param {http.IncomingMessage} request + * @param {import('./index.js').ResolvedAccount} resolvedAccount + */ +function createWebsocketLogger(request, resolvedAccount) { + // ensure the request.id is always present. + generateRequestId(request); + + return logger.child({ + req: { + id: request.id + }, + account: { + id: resolvedAccount.accountId ?? null + } + }); +} + +exports.logger = logger; +exports.httpLogger = httpLogger; +exports.attachWebsocketHttpLogger = attachWebsocketHttpLogger; +exports.createWebsocketLogger = createWebsocketLogger; + +/** + * Initializes the log level based on the environment + * @param {Object} env + * @param {string} environment + */ +exports.initializeLogLevel = function initializeLogLevel(env, environment) { + if (env.LOG_LEVEL && Object.keys(logger.levels.values).includes(env.LOG_LEVEL)) { + logger.level = env.LOG_LEVEL; + } else if (environment === 'development') { + logger.level = 'debug'; + } else { + logger.level = 'info'; + } +}; diff --git a/streaming/package.json b/streaming/package.json index 149055ca1b..52a9979702 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -21,9 +21,10 @@ "express": "^4.18.2", "ioredis": "^5.3.2", "jsdom": "^23.0.0", - "npmlog": "^7.0.1", "pg": "^8.5.0", "pg-connection-string": "^2.6.0", + "pino": "^8.17.2", + "pino-http": "^9.0.0", "prom-client": "^15.0.0", "uuid": "^9.0.0", "ws": "^8.12.1" @@ -31,11 +32,11 @@ "devDependencies": { "@types/cors": "^2.8.16", "@types/express": "^4.17.17", - "@types/npmlog": "^7.0.0", "@types/pg": "^8.6.6", "@types/uuid": "^9.0.0", "@types/ws": "^8.5.9", "eslint-define-config": "^2.0.0", + "pino-pretty": "^10.3.1", "typescript": "^5.0.4" }, "optionalDependencies": { diff --git a/yarn.lock b/yarn.lock index 6f8381db0e..75586ac497 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2536,7 +2536,6 @@ __metadata: dependencies: "@types/cors": "npm:^2.8.16" "@types/express": "npm:^4.17.17" - "@types/npmlog": "npm:^7.0.0" "@types/pg": "npm:^8.6.6" "@types/uuid": "npm:^9.0.0" "@types/ws": "npm:^8.5.9" @@ -2547,9 +2546,11 @@ __metadata: express: "npm:^4.18.2" ioredis: "npm:^5.3.2" jsdom: "npm:^23.0.0" - npmlog: "npm:^7.0.1" pg: "npm:^8.5.0" pg-connection-string: "npm:^2.6.0" + pino: "npm:^8.17.2" + pino-http: "npm:^9.0.0" + pino-pretty: "npm:^10.3.1" prom-client: "npm:^15.0.0" typescript: "npm:^5.0.4" utf-8-validate: "npm:^6.0.3" @@ -3338,15 +3339,6 @@ __metadata: languageName: node linkType: hard -"@types/npmlog@npm:^7.0.0": - version: 7.0.0 - resolution: "@types/npmlog@npm:7.0.0" - dependencies: - "@types/node": "npm:*" - checksum: e94cb1d7dc6b1251d58d0a3cbf0c5b9e9b7c7649774cf816b9277fc10e1a09e65f2854357c4972d04d477f8beca3c8accb5e8546d594776e59e35ddfee79aff2 - languageName: node - linkType: hard - "@types/object-assign@npm:^4.0.30": version: 4.0.33 resolution: "@types/object-assign@npm:4.0.33" @@ -3791,6 +3783,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:6.9.1": + version: 6.9.1 + resolution: "@typescript-eslint/scope-manager@npm:6.9.1" + dependencies: + "@typescript-eslint/types": "npm:6.9.1" + "@typescript-eslint/visitor-keys": "npm:6.9.1" + checksum: 53fa7c3813d22b119e464f9b6d7d23407dfe103ee8ad2dcacf9ad6d656fda20e2bb3346df39e62b0e6b6ce71572ce5838071c5d2cca6daa4e0ce117ff22eafe5 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:6.19.0": version: 6.19.0 resolution: "@typescript-eslint/type-utils@npm:6.19.0" @@ -3815,6 +3817,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:6.9.1": + version: 6.9.1 + resolution: "@typescript-eslint/types@npm:6.9.1" + checksum: 4ba21ba18e256da210a4caedfbc5d4927cf8cb4f2c4d74f8ccc865576f3659b974e79119d3c94db2b68a4cec9cd687e43971d355450b7082d6d1736a5dd6db85 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:6.19.0": version: 6.19.0 resolution: "@typescript-eslint/typescript-estree@npm:6.19.0" @@ -3834,7 +3843,25 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:6.19.0, @typescript-eslint/utils@npm:^6.5.0": +"@typescript-eslint/typescript-estree@npm:6.9.1": + version: 6.9.1 + resolution: "@typescript-eslint/typescript-estree@npm:6.9.1" + dependencies: + "@typescript-eslint/types": "npm:6.9.1" + "@typescript-eslint/visitor-keys": "npm:6.9.1" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + semver: "npm:^7.5.4" + ts-api-utils: "npm:^1.0.1" + peerDependenciesMeta: + typescript: + optional: true + checksum: 850b1865a90107879186c3f2969968a2c08fc6fcc56d146483c297cf5be376e33d505ac81533ba8e8103ca4d2edfea7d21b178de9e52217f7ee2922f51a445fa + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:6.19.0": version: 6.19.0 resolution: "@typescript-eslint/utils@npm:6.19.0" dependencies: @@ -3851,6 +3878,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^6.5.0": + version: 6.9.1 + resolution: "@typescript-eslint/utils@npm:6.9.1" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@types/json-schema": "npm:^7.0.12" + "@types/semver": "npm:^7.5.0" + "@typescript-eslint/scope-manager": "npm:6.9.1" + "@typescript-eslint/types": "npm:6.9.1" + "@typescript-eslint/typescript-estree": "npm:6.9.1" + semver: "npm:^7.5.4" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: 3d329d54c3d155ed29e2b456a602aef76bda1b88dfcf847145849362e4ddefabe5c95de236de750d08d5da9bedcfb2131bdfd784ce4eb87cf82728f0b6662033 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:6.19.0": version: 6.19.0 resolution: "@typescript-eslint/visitor-keys@npm:6.19.0" @@ -3861,6 +3905,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:6.9.1": + version: 6.9.1 + resolution: "@typescript-eslint/visitor-keys@npm:6.9.1" + dependencies: + "@typescript-eslint/types": "npm:6.9.1" + eslint-visitor-keys: "npm:^3.4.1" + checksum: ac5f375a177add30489e5b63cafa8d82a196b33624bb36418422ebe0d7973b3ba550dc7e0dda36ea75a94cf9b200b4fb5f5fb4d77c027fd801201c1a269d343b + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -4324,13 +4378,6 @@ __metadata: languageName: node linkType: hard -"aproba@npm:^1.0.3 || ^2.0.0": - version: 2.0.0 - resolution: "aproba@npm:2.0.0" - checksum: d06e26384a8f6245d8c8896e138c0388824e259a329e0c9f196b4fa533c82502a6fd449586e3604950a0c42921832a458bb3aa0aa9f0ba449cfd4f50fd0d09b5 - languageName: node - linkType: hard - "are-docs-informative@npm:^0.0.2": version: 0.0.2 resolution: "are-docs-informative@npm:0.0.2" @@ -4338,16 +4385,6 @@ __metadata: languageName: node linkType: hard -"are-we-there-yet@npm:^4.0.0": - version: 4.0.0 - resolution: "are-we-there-yet@npm:4.0.0" - dependencies: - delegates: "npm:^1.0.0" - readable-stream: "npm:^4.1.0" - checksum: 760008e32948e9f738c5a288792d187e235fee0f170e042850bc7ff242f2a499f3f2874d6dd43ac06f5d9f5306137bc51bbdd4ae0bb11379c58b01678e0f684d - languageName: node - linkType: hard - "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -4669,6 +4706,13 @@ __metadata: languageName: node linkType: hard +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: e329a6665512736a9bbb073e1761b4ec102f7926cce35037753146a9db9c8104f5044c1662e4a863576ce544fb8be27cd2be6bc8c1a40147d03f31eb1cfb6e8a + languageName: node + linkType: hard + "autoprefixer@npm:^10.4.14": version: 10.4.17 resolution: "autoprefixer@npm:10.4.17" @@ -5763,15 +5807,6 @@ __metadata: languageName: node linkType: hard -"color-support@npm:^1.1.3": - version: 1.1.3 - resolution: "color-support@npm:1.1.3" - bin: - color-support: bin.js - checksum: 8ffeaa270a784dc382f62d9be0a98581db43e11eee301af14734a6d089bd456478b1a8b3e7db7ca7dc5b18a75f828f775c44074020b51c05fc00e6d0992b1cc6 - languageName: node - linkType: hard - "colord@npm:^2.9.1, colord@npm:^2.9.3": version: 2.9.3 resolution: "colord@npm:2.9.3" @@ -5779,7 +5814,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.20": +"colorette@npm:^2.0.20, colorette@npm:^2.0.7": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 @@ -5911,13 +5946,6 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.1.0": - version: 1.1.0 - resolution: "console-control-strings@npm:1.1.0" - checksum: 7ab51d30b52d461412cd467721bb82afe695da78fff8f29fe6f6b9cbaac9a2328e27a22a966014df9532100f6dd85370460be8130b9c677891ba36d96a343f50 - languageName: node - linkType: hard - "constants-browserify@npm:^1.0.0": version: 1.0.0 resolution: "constants-browserify@npm:1.0.0" @@ -6445,6 +6473,13 @@ __metadata: languageName: node linkType: hard +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: e2023b905e8cfe2eb8444fb558562b524807a51cdfe712570f360f873271600b5c94aebffaf11efb285e2c072264a7cf243eadb68f3eba0f8cc85fb86cd25df6 + languageName: node + linkType: hard + "debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -6680,13 +6715,6 @@ __metadata: languageName: node linkType: hard -"delegates@npm:^1.0.0": - version: 1.0.0 - resolution: "delegates@npm:1.0.0" - checksum: ba05874b91148e1db4bf254750c042bf2215febd23a6d3cda2e64896aef79745fbd4b9996488bd3cafb39ce19dbce0fd6e3b6665275638befffe1c9b312b91b5 - languageName: node - linkType: hard - "denque@npm:^2.1.0": version: 2.1.0 resolution: "denque@npm:2.1.0" @@ -7952,6 +7980,13 @@ __metadata: languageName: node linkType: hard +"fast-copy@npm:^3.0.0": + version: 3.0.1 + resolution: "fast-copy@npm:3.0.1" + checksum: a8310dbcc4c94ed001dc3e0bbc3c3f0491bb04e6c17163abe441a54997ba06cdf1eb532c2f05e54777c6f072c84548c23ef0ecd54665cd611be1d42f37eca258 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -7993,6 +8028,20 @@ __metadata: languageName: node linkType: hard +"fast-redact@npm:^3.1.1": + version: 3.3.0 + resolution: "fast-redact@npm:3.3.0" + checksum: d81562510681e9ba6404ee5d3838ff5257a44d2f80937f5024c099049ff805437d0fae0124458a7e87535cc9dcf4de305bb075cab8f08d6c720bbc3447861b4e + languageName: node + linkType: hard + +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: d90ec1c963394919828872f21edaa3ad6f1dddd288d2bd4e977027afff09f5db40f94e39536d4646f7e01761d704d72d51dce5af1b93717f3489ef808f5f4e4d + languageName: node + linkType: hard + "fastest-levenshtein@npm:^1.0.16": version: 1.0.16 resolution: "fastest-levenshtein@npm:1.0.16" @@ -8407,22 +8456,6 @@ __metadata: languageName: node linkType: hard -"gauge@npm:^5.0.0": - version: 5.0.1 - resolution: "gauge@npm:5.0.1" - dependencies: - aproba: "npm:^1.0.3 || ^2.0.0" - color-support: "npm:^1.1.3" - console-control-strings: "npm:^1.1.0" - has-unicode: "npm:^2.0.1" - signal-exit: "npm:^4.0.1" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - wide-align: "npm:^1.1.5" - checksum: 845f9a2534356cd0e9c1ae590ed471bbe8d74c318915b92a34e8813b8d3441ca8e0eb0fa87a48081e70b63b84d398c5e66a13b8e8040181c10b9d77e9fe3287f - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -8771,13 +8804,6 @@ __metadata: languageName: node linkType: hard -"has-unicode@npm:^2.0.1": - version: 2.0.1 - resolution: "has-unicode@npm:2.0.1" - checksum: ebdb2f4895c26bb08a8a100b62d362e49b2190bcfd84b76bc4be1a3bd4d254ec52d0dd9f2fbcc093fc5eb878b20c52146f9dfd33e2686ed28982187be593b47c - languageName: node - linkType: hard - "has-value@npm:^0.3.1": version: 0.3.1 resolution: "has-value@npm:0.3.1" @@ -8854,6 +8880,13 @@ __metadata: languageName: node linkType: hard +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 054c0e2e9ae2231c85ab5e04f75109b9d068ffcc54e58fb22079822a5ace8ff3d02c66fd45379c902ad5ab825e5d2e1451fcc2f7eab1eb49e7d488133ba4cacb + languageName: node + linkType: hard + "history@npm:^4.10.1, history@npm:^4.9.0": version: 4.10.1 resolution: "history@npm:4.10.1" @@ -9320,7 +9353,7 @@ __metadata: languageName: node linkType: hard -"intl-messageformat@npm:10.5.10, intl-messageformat@npm:^10.3.5": +"intl-messageformat@npm:10.5.10": version: 10.5.10 resolution: "intl-messageformat@npm:10.5.10" dependencies: @@ -9332,6 +9365,18 @@ __metadata: languageName: node linkType: hard +"intl-messageformat@npm:^10.3.5": + version: 10.5.8 + resolution: "intl-messageformat@npm:10.5.8" + dependencies: + "@formatjs/ecma402-abstract": "npm:1.18.0" + "@formatjs/fast-memoize": "npm:2.2.0" + "@formatjs/icu-messageformat-parser": "npm:2.7.3" + tslib: "npm:^2.4.0" + checksum: 1d2854aae8471ec48165ca265760d6c5b1814eca831c88db698eb29b5ed20bee21ca8533090c9d28d9c6f1d844dda210b0bc58a2e036446158fae0845e5eed4f + languageName: node + linkType: hard + "invariant@npm:^2.2.2, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" @@ -10570,6 +10615,13 @@ __metadata: languageName: node linkType: hard +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae + languageName: node + linkType: hard + "jpeg-autorotate@npm:^7.1.1": version: 7.1.1 resolution: "jpeg-autorotate@npm:7.1.1" @@ -11966,18 +12018,6 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^7.0.1": - version: 7.0.1 - resolution: "npmlog@npm:7.0.1" - dependencies: - are-we-there-yet: "npm:^4.0.0" - console-control-strings: "npm:^1.1.0" - gauge: "npm:^5.0.0" - set-blocking: "npm:^2.0.0" - checksum: d4e6a2aaa7b5b5d2e2ed8f8ac3770789ca0691a49f3576b6a8c97d560a4c3305d2c233a9173d62be737e6e4506bf9e89debd6120a3843c1d37315c34f90fef71 - languageName: node - linkType: hard - "nth-check@npm:^1.0.2": version: 1.0.2 resolution: "nth-check@npm:1.0.2" @@ -12150,6 +12190,13 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: faea2e1c9d696ecee919026c32be8d6a633a7ac1240b3b87e944a380e8a11dc9c95c4a1f8fb0568de7ab8db3823e790f12bda45296b1d111e341aad3922a0570 + languageName: node + linkType: hard + "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -12717,6 +12764,80 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^1.0.0, pino-abstract-transport@npm:v1.1.0": + version: 1.1.0 + resolution: "pino-abstract-transport@npm:1.1.0" + dependencies: + readable-stream: "npm:^4.0.0" + split2: "npm:^4.0.0" + checksum: 6e9b9d5a2c0a37f91ecaf224d335daae1ae682b1c79a05b06ef9e0f0a5d289f8e597992217efc857796dae6f1067e9b4882f95c6228ff433ddc153532cae8aca + languageName: node + linkType: hard + +"pino-http@npm:^9.0.0": + version: 9.0.0 + resolution: "pino-http@npm:9.0.0" + dependencies: + get-caller-file: "npm:^2.0.5" + pino: "npm:^8.17.1" + pino-std-serializers: "npm:^6.2.2" + process-warning: "npm:^3.0.0" + checksum: 05496cb76cc9908658e50c4620fbdf7b0b5d99fb529493d601c3e4635b0bf7ce12b8a8eed7b5b520089f643b099233d61dd71f7cdfad8b66e59b9b81d79b6512 + languageName: node + linkType: hard + +"pino-pretty@npm:^10.3.1": + version: 10.3.1 + resolution: "pino-pretty@npm:10.3.1" + dependencies: + colorette: "npm:^2.0.7" + dateformat: "npm:^4.6.3" + fast-copy: "npm:^3.0.0" + fast-safe-stringify: "npm:^2.1.1" + help-me: "npm:^5.0.0" + joycon: "npm:^3.1.1" + minimist: "npm:^1.2.6" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^1.0.0" + pump: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + secure-json-parse: "npm:^2.4.0" + sonic-boom: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + bin: + pino-pretty: bin.js + checksum: 6964fba5acc7a9f112e4c6738d602e123daf16cb5f6ddc56ab4b6bb05059f28876d51da8f72358cf1172e95fa12496b70465431a0836df693c462986d050686b + languageName: node + linkType: hard + +"pino-std-serializers@npm:^6.0.0, pino-std-serializers@npm:^6.2.2": + version: 6.2.2 + resolution: "pino-std-serializers@npm:6.2.2" + checksum: 8f1c7f0f0d8f91e6c6b5b2a6bfb48f06441abeb85f1c2288319f736f9c6d814fbeebe928d2314efc2ba6018fa7db9357a105eca9fc99fc1f28945a8a8b28d3d5 + languageName: node + linkType: hard + +"pino@npm:^8.17.1, pino@npm:^8.17.2": + version: 8.17.2 + resolution: "pino@npm:8.17.2" + dependencies: + atomic-sleep: "npm:^1.0.0" + fast-redact: "npm:^3.1.1" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:v1.1.0" + pino-std-serializers: "npm:^6.0.0" + process-warning: "npm:^3.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^3.7.0" + thread-stream: "npm:^2.0.0" + bin: + pino: bin.js + checksum: 9e55af6cd9d1833a4dbe64924fc73163295acd3c988a9c7db88926669f2574ab7ec607e8487b6dd71dbdad2d7c1c1aac439f37e59233f37220b1a9d88fa2ce01 + languageName: node + linkType: hard + "pirates@npm:^4.0.4": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -13319,6 +13440,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^3.0.0": + version: 3.0.0 + resolution: "process-warning@npm:3.0.0" + checksum: 60f3c8ddee586f0706c1e6cb5aa9c86df05774b9330d792d7c8851cf0031afd759d665404d07037e0b4901b55c44a423f07bdc465c63de07d8d23196bb403622 + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -13496,6 +13624,13 @@ __metadata: languageName: node linkType: hard +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4 + languageName: node + linkType: hard + "raf@npm:^3.1.0": version: 3.4.1 resolution: "raf@npm:3.4.1" @@ -13991,15 +14126,16 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^4.1.0": - version: 4.4.0 - resolution: "readable-stream@npm:4.4.0" +"readable-stream@npm:^4.0.0": + version: 4.4.2 + resolution: "readable-stream@npm:4.4.2" dependencies: abort-controller: "npm:^3.0.0" buffer: "npm:^6.0.3" events: "npm:^3.3.0" process: "npm:^0.11.10" - checksum: 83f5a11285e5ebefb7b22a43ea77a2275075639325b4932a328a1fb0ee2475b83b9cc94326724d71c6aa3b60fa87e2b16623530b1cac34f3825dcea0996fdbe4 + string_decoder: "npm:^1.3.0" + checksum: cf7cc8daa2b57872d120945a20a1458c13dcb6c6f352505421115827b18ac4df0e483ac1fe195cb1f5cd226e1073fc55b92b569269d8299e8530840bcdbba40c languageName: node linkType: hard @@ -14023,6 +14159,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 23eea5623642f0477412ef8b91acd3969015a1501ed34992ada0e3af521d3c865bb2fe4cdbfec5fe4b505f6d1ef6a03e5c3652520837a8c3b53decff7e74b6a0 + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -14568,6 +14711,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.3.1": + version: 2.4.3 + resolution: "safe-stable-stringify@npm:2.4.3" + checksum: 81dede06b8f2ae794efd868b1e281e3c9000e57b39801c6c162267eb9efda17bd7a9eafa7379e1f1cacd528d4ced7c80d7460ad26f62ada7c9e01dec61b2e768 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -14681,6 +14831,13 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.4.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 + languageName: node + linkType: hard + "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -15084,6 +15241,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^3.0.0, sonic-boom@npm:^3.7.0": + version: 3.7.0 + resolution: "sonic-boom@npm:3.7.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 57a3d560efb77f4576db111168ee2649c99e7869fda6ce0ec2a4e5458832d290ba58d74b073ddb5827d9a30f96d23cff79157993d919e1a6d5f28d8b6391c7f0 + languageName: node + linkType: hard + "source-list-map@npm:^2.0.0": version: 2.0.1 resolution: "source-list-map@npm:2.0.1" @@ -15242,7 +15408,7 @@ __metadata: languageName: node linkType: hard -"split2@npm:^4.1.0": +"split2@npm:^4.0.0, split2@npm:^4.1.0": version: 4.2.0 resolution: "split2@npm:4.2.0" checksum: b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 @@ -15407,7 +15573,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -15500,7 +15666,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1": +"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: @@ -16046,6 +16212,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^2.0.0": + version: 2.4.1 + resolution: "thread-stream@npm:2.4.1" + dependencies: + real-require: "npm:^0.2.0" + checksum: ce29265810b9550ce896726301ff006ebfe96b90292728f07cfa4c379740585583046e2a8018afc53aca66b18fed12b33a84f3883e7ebc317185f6682898b8f8 + languageName: node + linkType: hard + "thunky@npm:^1.0.2": version: 1.1.0 resolution: "thunky@npm:1.1.0" @@ -17283,15 +17458,6 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.5": - version: 1.1.5 - resolution: "wide-align@npm:1.1.5" - dependencies: - string-width: "npm:^1.0.2 || 2 || 3 || 4" - checksum: 1d9c2a3e36dfb09832f38e2e699c367ef190f96b82c71f809bc0822c306f5379df87bab47bed27ea99106d86447e50eb972d3c516c2f95782807a9d082fbea95 - languageName: node - linkType: hard - "wildcard@npm:^2.0.0": version: 2.0.1 resolution: "wildcard@npm:2.0.1" From 244182ad63f93a9617011a197095078322215a74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 19:42:07 +0100 Subject: [PATCH 02/84] Update dependency rdf-normalize to v0.7.0 (#26769) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6cf0504b51..c8042fcb8d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -600,8 +600,8 @@ GEM rdf (3.3.1) bcp47_spec (~> 0.2) link_header (~> 0.0, >= 0.0.8) - rdf-normalize (0.6.1) - rdf (~> 3.2) + rdf-normalize (0.7.0) + rdf (~> 3.3) rdoc (6.6.2) psych (>= 4.0.0) redcarpet (3.6.0) From 5ae3bae586d5344b406cf63c91247d94489f2e22 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 19:43:03 +0100 Subject: [PATCH 03/84] Update dependency sass to v1.70.0 (#28799) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 75586ac497..b2afdc049f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14751,15 +14751,15 @@ __metadata: linkType: hard "sass@npm:^1.62.1": - version: 1.69.7 - resolution: "sass@npm:1.69.7" + version: 1.70.0 + resolution: "sass@npm:1.70.0" dependencies: chokidar: "npm:>=3.0.0 <4.0.0" immutable: "npm:^4.0.0" source-map-js: "npm:>=0.6.2 <2.0.0" bin: sass: sass.js - checksum: 773d0938e7d4ff3972d3fda3132f34fe98a2f712e028a58e28fecd615434795eff3266eddc38d5e13f03b90c0d6360d0e737b30bff2949a47280c64a18e0fb18 + checksum: 7c309ee1c096d591746d122da9f1ebd65b4c4b3a60c2cc0ec720fd98fe1205fa8b44c9f563d113b9fdfeb25af1e32ec9b3e048bd4b8e05d267f020953bd7baf0 languageName: node linkType: hard From 1480573c83f580a3a7eb5fef61ddbba69242032f Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 18 Jan 2024 20:39:30 -0500 Subject: [PATCH 04/84] Add `Account.auditable` scope, fix N+1 in admin/action_logs#index (#28812) --- .../admin/action_logs_controller.rb | 2 +- app/models/account.rb | 1 + app/models/admin/action_log_filter.rb | 2 +- spec/models/account_spec.rb | 19 +++++++++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb index 37a00ad225..8b8e83fde7 100644 --- a/app/controllers/admin/action_logs_controller.rb +++ b/app/controllers/admin/action_logs_controller.rb @@ -6,7 +6,7 @@ module Admin def index authorize :audit_log, :index? - @auditable_accounts = Account.where(id: Admin::ActionLog.select('distinct account_id')).select(:id, :username) + @auditable_accounts = Account.auditable.select(:id, :username) end private diff --git a/app/models/account.rb b/app/models/account.rb index c17de682e3..2fdfc2d514 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -126,6 +126,7 @@ class Account < ApplicationRecord scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) } + scope :auditable, -> { where(id: Admin::ActionLog.select(:account_id).distinct) } scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) } scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) } scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) } diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index d413cb386d..f581af74e8 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -72,7 +72,7 @@ class Admin::ActionLogFilter end def results - scope = latest_action_logs.includes(:target) + scope = latest_action_logs.includes(:target, :account) params.each do |key, value| next if key.to_s == 'page' diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index d360d934d6..8488ccea45 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -835,6 +835,25 @@ RSpec.describe Account do end describe 'scopes' do + describe 'auditable' do + let!(:alice) { Fabricate :account } + let!(:bob) { Fabricate :account } + + before do + 2.times { Fabricate :action_log, account: alice } + end + + it 'returns distinct accounts with action log records' do + results = described_class.auditable + + expect(results.size) + .to eq(1) + expect(results) + .to include(alice) + .and not_include(bob) + end + end + describe 'alphabetic' do it 'sorts by alphabetic order of domain and username' do matches = [ From de09176ab9f04ac64b3ea5f877fa0895bf55e2eb Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 19 Jan 2024 10:18:21 +0100 Subject: [PATCH 05/84] Retry 401 errors on replies fetching (#28788) Co-authored-by: Claire --- app/helpers/jsonld_helper.rb | 12 ++++++------ app/services/activitypub/fetch_replies_service.rb | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index ce3ff094f6..b3d0d032c4 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -155,7 +155,7 @@ module JsonLdHelper end end - def fetch_resource(uri, id, on_behalf_of = nil) + def fetch_resource(uri, id, on_behalf_of = nil, request_options: {}) unless id json = fetch_resource_without_id_validation(uri, on_behalf_of) @@ -164,14 +164,14 @@ module JsonLdHelper uri = json['id'] end - json = fetch_resource_without_id_validation(uri, on_behalf_of) + json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options) json.present? && json['id'] == uri ? json : nil end - def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {}) on_behalf_of ||= Account.representative - build_request(uri, on_behalf_of).perform do |response| + build_request(uri, on_behalf_of, options: request_options).perform do |response| raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error body_to_json(response.body_with_limit) if response.code == 200 @@ -204,8 +204,8 @@ module JsonLdHelper response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) end - def build_request(uri, on_behalf_of = nil) - Request.new(:get, uri).tap do |request| + def build_request(uri, on_behalf_of = nil, options: {}) + Request.new(:get, uri, **options).tap do |request| request.on_behalf_of(on_behalf_of) if on_behalf_of request.add_headers('Accept' => 'application/activity+json, application/ld+json') end diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index b5c7759ec5..a9dd327e96 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -37,7 +37,20 @@ class ActivityPub::FetchRepliesService < BaseService return unless @allow_synchronous_requests return if non_matching_uri_hosts?(@account.uri, collection_or_uri) - fetch_resource_without_id_validation(collection_or_uri, nil, true) + # NOTE: For backward compatibility reasons, Mastodon signs outgoing + # queries incorrectly by default. + # + # While this is relevant for all URLs with query strings, this is + # the only code path where this happens in practice. + # + # Therefore, retry with correct signatures if this fails. + begin + fetch_resource_without_id_validation(collection_or_uri, nil, true) + rescue Mastodon::UnexpectedResponseError => e + raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present? + + fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true }) + end end def filtered_replies From fd64817fbe658514e2753c2e03e13624719f4e41 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 19 Jan 2024 04:19:48 -0500 Subject: [PATCH 06/84] Fix `Rails/WhereExists` cop in app/lib/status_cache_hydrator (#28808) --- .rubocop_todo.yml | 1 - app/lib/status_cache_hydrator.rb | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 87120daef2..31cefa0320 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -80,7 +80,6 @@ Rails/WhereExists: - 'app/lib/activitypub/activity/create.rb' - 'app/lib/delivery_failure_tracker.rb' - 'app/lib/feed_manager.rb' - - 'app/lib/status_cache_hydrator.rb' - 'app/lib/suspicious_sign_in_detector.rb' - 'app/models/poll.rb' - 'app/models/session_activation.rb' diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index 45b50cb379..34f6199ec0 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -26,11 +26,11 @@ class StatusCacheHydrator def hydrate_non_reblog_payload(empty_payload, account_id) empty_payload.tap do |payload| - payload[:favourited] = Favourite.where(account_id: account_id, status_id: @status.id).exists? - payload[:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.id).exists? - payload[:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.conversation_id).exists? - payload[:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.id).exists? - payload[:pinned] = StatusPin.where(account_id: account_id, status_id: @status.id).exists? if @status.account_id == account_id + payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.id) + payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.id) + payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.conversation_id) + payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.id) + payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.id) if @status.account_id == account_id payload[:filtered] = mapped_applied_custom_filter(account_id, @status) if payload[:poll] @@ -51,11 +51,11 @@ class StatusCacheHydrator # used to create the status, we need to hydrate it here too payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id - payload[:reblog][:favourited] = Favourite.where(account_id: account_id, status_id: @status.reblog_of_id).exists? - payload[:reblog][:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.reblog_of_id).exists? - payload[:reblog][:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.reblog.conversation_id).exists? - payload[:reblog][:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.reblog_of_id).exists? - payload[:reblog][:pinned] = StatusPin.where(account_id: account_id, status_id: @status.reblog_of_id).exists? if @status.reblog.account_id == account_id + payload[:reblog][:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.reblog_of_id) + payload[:reblog][:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.reblog_of_id) + payload[:reblog][:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.reblog.conversation_id) + payload[:reblog][:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.reblog_of_id) + payload[:reblog][:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.reblog_of_id) if @status.reblog.account_id == account_id payload[:reblog][:filtered] = payload[:filtered] if payload[:reblog][:poll] From 6dc97321a3b780226f50098e31cf96cbbd7a3156 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:20:16 +0000 Subject: [PATCH 07/84] Update dependency intl-messageformat to v10.5.10 (#28809) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 78 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 76 deletions(-) diff --git a/yarn.lock b/yarn.lock index b2afdc049f..8b55377235 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3783,16 +3783,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:6.9.1": - version: 6.9.1 - resolution: "@typescript-eslint/scope-manager@npm:6.9.1" - dependencies: - "@typescript-eslint/types": "npm:6.9.1" - "@typescript-eslint/visitor-keys": "npm:6.9.1" - checksum: 53fa7c3813d22b119e464f9b6d7d23407dfe103ee8ad2dcacf9ad6d656fda20e2bb3346df39e62b0e6b6ce71572ce5838071c5d2cca6daa4e0ce117ff22eafe5 - languageName: node - linkType: hard - "@typescript-eslint/type-utils@npm:6.19.0": version: 6.19.0 resolution: "@typescript-eslint/type-utils@npm:6.19.0" @@ -3817,13 +3807,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:6.9.1": - version: 6.9.1 - resolution: "@typescript-eslint/types@npm:6.9.1" - checksum: 4ba21ba18e256da210a4caedfbc5d4927cf8cb4f2c4d74f8ccc865576f3659b974e79119d3c94db2b68a4cec9cd687e43971d355450b7082d6d1736a5dd6db85 - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:6.19.0": version: 6.19.0 resolution: "@typescript-eslint/typescript-estree@npm:6.19.0" @@ -3843,25 +3826,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:6.9.1": - version: 6.9.1 - resolution: "@typescript-eslint/typescript-estree@npm:6.9.1" - dependencies: - "@typescript-eslint/types": "npm:6.9.1" - "@typescript-eslint/visitor-keys": "npm:6.9.1" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" - peerDependenciesMeta: - typescript: - optional: true - checksum: 850b1865a90107879186c3f2969968a2c08fc6fcc56d146483c297cf5be376e33d505ac81533ba8e8103ca4d2edfea7d21b178de9e52217f7ee2922f51a445fa - languageName: node - linkType: hard - -"@typescript-eslint/utils@npm:6.19.0": +"@typescript-eslint/utils@npm:6.19.0, @typescript-eslint/utils@npm:^6.5.0": version: 6.19.0 resolution: "@typescript-eslint/utils@npm:6.19.0" dependencies: @@ -3878,23 +3843,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^6.5.0": - version: 6.9.1 - resolution: "@typescript-eslint/utils@npm:6.9.1" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@types/json-schema": "npm:^7.0.12" - "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:6.9.1" - "@typescript-eslint/types": "npm:6.9.1" - "@typescript-eslint/typescript-estree": "npm:6.9.1" - semver: "npm:^7.5.4" - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - checksum: 3d329d54c3d155ed29e2b456a602aef76bda1b88dfcf847145849362e4ddefabe5c95de236de750d08d5da9bedcfb2131bdfd784ce4eb87cf82728f0b6662033 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:6.19.0": version: 6.19.0 resolution: "@typescript-eslint/visitor-keys@npm:6.19.0" @@ -3905,16 +3853,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:6.9.1": - version: 6.9.1 - resolution: "@typescript-eslint/visitor-keys@npm:6.9.1" - dependencies: - "@typescript-eslint/types": "npm:6.9.1" - eslint-visitor-keys: "npm:^3.4.1" - checksum: ac5f375a177add30489e5b63cafa8d82a196b33624bb36418422ebe0d7973b3ba550dc7e0dda36ea75a94cf9b200b4fb5f5fb4d77c027fd801201c1a269d343b - languageName: node - linkType: hard - "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -9353,7 +9291,7 @@ __metadata: languageName: node linkType: hard -"intl-messageformat@npm:10.5.10": +"intl-messageformat@npm:10.5.10, intl-messageformat@npm:^10.3.5": version: 10.5.10 resolution: "intl-messageformat@npm:10.5.10" dependencies: @@ -9365,18 +9303,6 @@ __metadata: languageName: node linkType: hard -"intl-messageformat@npm:^10.3.5": - version: 10.5.8 - resolution: "intl-messageformat@npm:10.5.8" - dependencies: - "@formatjs/ecma402-abstract": "npm:1.18.0" - "@formatjs/fast-memoize": "npm:2.2.0" - "@formatjs/icu-messageformat-parser": "npm:2.7.3" - tslib: "npm:^2.4.0" - checksum: 1d2854aae8471ec48165ca265760d6c5b1814eca831c88db698eb29b5ed20bee21ca8533090c9d28d9c6f1d844dda210b0bc58a2e036446158fae0845e5eed4f - languageName: node - linkType: hard - "invariant@npm:^2.2.2, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" From 6a1c9987220b0e6537e4c31fd2e812f498d93858 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:21:07 +0100 Subject: [PATCH 08/84] Update dependency kt-paperclip to v7.2.2 (#28813) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c8042fcb8d..93931d8724 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.1.5) + bigdecimal (3.1.6) bindata (2.4.15) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) @@ -398,12 +398,12 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kt-paperclip (7.2.1) + kt-paperclip (7.2.2) activemodel (>= 4.2.0) activesupport (>= 4.2.0) marcel (~> 1.0.1) mime-types - terrapin (~> 0.6.0) + terrapin (>= 0.6.0, < 2.0) language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) From 86cc88c21627bef221f461749189c91d446f3902 Mon Sep 17 00:00:00 2001 From: HTeuMeuLeu Date: Fri, 19 Jan 2024 10:23:59 +0100 Subject: [PATCH 09/84] Fix banner image not showing in follow emails (#28814) --- app/javascript/styles/mailer.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss index bd220bb1a8..a2cbb494b4 100644 --- a/app/javascript/styles/mailer.scss +++ b/app/javascript/styles/mailer.scss @@ -100,9 +100,8 @@ table + p { border-top-right-radius: 12px; height: 140px; vertical-align: bottom; - background-color: #f3f2f5; - background-position: center; - background-size: cover; + background-position: center !important; + background-size: cover !important; } .email-account-banner-inner-td { From 329911b0a31016c313b433c1068308236736598f Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 19 Jan 2024 04:32:41 -0500 Subject: [PATCH 10/84] Migrate controller->request spec for api/v1/follow* (#28811) --- .../accounts/follower_accounts_controller.rb | 2 +- .../accounts/following_accounts_controller.rb | 2 +- .../v1/accounts/follower_accounts_spec.rb} | 19 +++++++++---------- .../v1/accounts/following_accounts_spec.rb} | 19 +++++++++---------- 4 files changed, 20 insertions(+), 22 deletions(-) rename spec/{controllers/api/v1/accounts/follower_accounts_controller_spec.rb => requests/api/v1/accounts/follower_accounts_spec.rb} (69%) rename spec/{controllers/api/v1/accounts/following_accounts_controller_spec.rb => requests/api/v1/accounts/following_accounts_spec.rb} (69%) diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 21b1095f18..d6a5a7176d 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController return [] if hide_results? scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id scope.merge(paginated_follows).to_a end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 1db521f79c..b8578ef539 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController return [] if hide_results? scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id scope.merge(paginated_follows).to_a end diff --git a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb b/spec/requests/api/v1/accounts/follower_accounts_spec.rb similarity index 69% rename from spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb rename to spec/requests/api/v1/accounts/follower_accounts_spec.rb index 510a47566b..7ff92d6a48 100644 --- a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb +++ b/spec/requests/api/v1/accounts/follower_accounts_spec.rb @@ -2,11 +2,11 @@ require 'rails_helper' -describe Api::V1::Accounts::FollowerAccountsController do - render_views - +describe 'API V1 Accounts FollowerAccounts' do let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'read:accounts' } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } let(:account) { Fabricate(:account) } let(:alice) { Fabricate(:account) } let(:bob) { Fabricate(:account) } @@ -14,12 +14,11 @@ describe Api::V1::Accounts::FollowerAccountsController do before do alice.follow!(account) bob.follow!(account) - allow(controller).to receive(:doorkeeper_token) { token } end - describe 'GET #index' do + describe 'GET /api/v1/accounts/:acount_id/followers' do it 'returns accounts following the given account', :aggregate_failures do - get :index, params: { account_id: account.id, limit: 2 } + get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers expect(response).to have_http_status(200) expect(body_as_json.size).to eq 2 @@ -28,7 +27,7 @@ describe Api::V1::Accounts::FollowerAccountsController do it 'does not return blocked users', :aggregate_failures do user.account.block!(bob) - get :index, params: { account_id: account.id, limit: 2 } + get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers expect(response).to have_http_status(200) expect(body_as_json.size).to eq 1 @@ -41,7 +40,7 @@ describe Api::V1::Accounts::FollowerAccountsController do end it 'hides results' do - get :index, params: { account_id: account.id, limit: 2 } + get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers expect(body_as_json.size).to eq 0 end end @@ -51,7 +50,7 @@ describe Api::V1::Accounts::FollowerAccountsController do it 'returns all accounts, including muted accounts' do account.mute!(bob) - get :index, params: { account_id: account.id, limit: 2 } + get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers expect(body_as_json.size).to eq 2 expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) diff --git a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb b/spec/requests/api/v1/accounts/following_accounts_spec.rb similarity index 69% rename from spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb rename to spec/requests/api/v1/accounts/following_accounts_spec.rb index a7d07a6bec..b343a48654 100644 --- a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb +++ b/spec/requests/api/v1/accounts/following_accounts_spec.rb @@ -2,11 +2,11 @@ require 'rails_helper' -describe Api::V1::Accounts::FollowingAccountsController do - render_views - +describe 'API V1 Accounts FollowingAccounts' do let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'read:accounts' } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } let(:account) { Fabricate(:account) } let(:alice) { Fabricate(:account) } let(:bob) { Fabricate(:account) } @@ -14,12 +14,11 @@ describe Api::V1::Accounts::FollowingAccountsController do before do account.follow!(alice) account.follow!(bob) - allow(controller).to receive(:doorkeeper_token) { token } end - describe 'GET #index' do + describe 'GET /api/v1/accounts/:account_id/following' do it 'returns accounts followed by the given account', :aggregate_failures do - get :index, params: { account_id: account.id, limit: 2 } + get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers expect(response).to have_http_status(200) expect(body_as_json.size).to eq 2 @@ -28,7 +27,7 @@ describe Api::V1::Accounts::FollowingAccountsController do it 'does not return blocked users', :aggregate_failures do user.account.block!(bob) - get :index, params: { account_id: account.id, limit: 2 } + get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers expect(response).to have_http_status(200) expect(body_as_json.size).to eq 1 @@ -41,7 +40,7 @@ describe Api::V1::Accounts::FollowingAccountsController do end it 'hides results' do - get :index, params: { account_id: account.id, limit: 2 } + get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers expect(body_as_json.size).to eq 0 end end @@ -51,7 +50,7 @@ describe Api::V1::Accounts::FollowingAccountsController do it 'returns all accounts, including muted accounts' do account.mute!(bob) - get :index, params: { account_id: account.id, limit: 2 } + get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers expect(body_as_json.size).to eq 2 expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) From 4ec7d7d98911f5047e9da9004748ea5900f975d7 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 19 Jan 2024 04:35:58 -0500 Subject: [PATCH 11/84] Fix `Rails/WhereExists` cop in REST::TagSerializer model (#28790) --- .rubocop_todo.yml | 1 - app/serializers/rest/tag_serializer.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 31cefa0320..0cebf37b57 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -86,7 +86,6 @@ Rails/WhereExists: - 'app/models/status.rb' - 'app/policies/status_policy.rb' - 'app/serializers/rest/announcement_serializer.rb' - - 'app/serializers/rest/tag_serializer.rb' - 'app/services/activitypub/fetch_remote_status_service.rb' - 'app/services/vote_service.rb' - 'app/validators/reaction_validator.rb' diff --git a/app/serializers/rest/tag_serializer.rb b/app/serializers/rest/tag_serializer.rb index 7801e77d1f..017b572718 100644 --- a/app/serializers/rest/tag_serializer.rb +++ b/app/serializers/rest/tag_serializer.rb @@ -19,7 +19,7 @@ class REST::TagSerializer < ActiveModel::Serializer if instance_options && instance_options[:relationships] instance_options[:relationships].following_map[object.id] || false else - TagFollow.where(tag_id: object.id, account_id: current_user.account_id).exists? + TagFollow.exists?(tag_id: object.id, account_id: current_user.account_id) end end From 163db814c2b3cf544b78e427e7f7bbd99b94a025 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:41:03 +0100 Subject: [PATCH 12/84] Update dependency react-redux to v9.1.0 (#28717) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Renaud Chaput --- app/javascript/mastodon/store/typed_functions.ts | 5 ++--- yarn.lock | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/store/typed_functions.ts b/app/javascript/mastodon/store/typed_functions.ts index 46a10b8b47..4859b82651 100644 --- a/app/javascript/mastodon/store/typed_functions.ts +++ b/app/javascript/mastodon/store/typed_functions.ts @@ -1,12 +1,11 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import type { TypedUseSelectorHook } from 'react-redux'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useDispatch, useSelector } from 'react-redux'; import type { AppDispatch, RootState } from './store'; -export const useAppDispatch: () => AppDispatch = useDispatch; -export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); export const createAppAsyncThunk = createAsyncThunk.withTypes<{ state: RootState; diff --git a/yarn.lock b/yarn.lock index 8b55377235..35abcf80b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13799,8 +13799,8 @@ __metadata: linkType: hard "react-redux@npm:^9.0.4": - version: 9.0.4 - resolution: "react-redux@npm:9.0.4" + version: 9.1.0 + resolution: "react-redux@npm:9.1.0" dependencies: "@types/use-sync-external-store": "npm:^0.0.3" use-sync-external-store: "npm:^1.0.0" @@ -13816,7 +13816,7 @@ __metadata: optional: true redux: optional: true - checksum: 23af10014b129aeb051de729bde01de21175170b860deefb7ad83483feab5816253f770a4cea93333fc22a53ac9ac699b27f5c3705c388dab53dbcb2906a571a + checksum: 53161b5dc4d109020fbc42d26906ace92fed9ba1d7ab6274af60e9c0684583d20d1c8ec6d58601ac7b833c6468a652bbf3d4a102149d1793cb8a28b05b042f73 languageName: node linkType: hard From 9cd17020bc6aa966bdf26787a6ce4c9d2204c5e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:41:57 +0000 Subject: [PATCH 13/84] New Crowdin Translations (automated) (#28798) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/vi.json | 2 +- config/locales/devise.fi.yml | 9 +++++++++ config/locales/devise.hu.yml | 9 +++++++++ config/locales/devise.ko.yml | 9 +++++++++ config/locales/fi.yml | 6 ++++++ config/locales/hu.yml | 6 ++++++ config/locales/ko.yml | 6 ++++++ config/locales/sk.yml | 2 ++ 8 files changed, 48 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 9de043bb20..c623caa3fb 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -358,7 +358,7 @@ "keyboard_shortcuts.my_profile": "mở hồ sơ của bạn", "keyboard_shortcuts.notifications": "mở thông báo", "keyboard_shortcuts.open_media": "mở ảnh hoặc video", - "keyboard_shortcuts.pinned": "mở những tút đã ghim", + "keyboard_shortcuts.pinned": "Open pinned posts list", "keyboard_shortcuts.profile": "mở trang của người đăng tút", "keyboard_shortcuts.reply": "trả lời", "keyboard_shortcuts.requests": "mở danh sách yêu cầu theo dõi", diff --git a/config/locales/devise.fi.yml b/config/locales/devise.fi.yml index bedf8a56f6..ac7a57c6f8 100644 --- a/config/locales/devise.fi.yml +++ b/config/locales/devise.fi.yml @@ -47,14 +47,19 @@ fi: subject: 'Mastodon: ohjeet salasanan vaihtoon' title: Salasanan vaihto two_factor_disabled: + explanation: Olet nyt mahdollistanut sisäänkirjautumisen pelkästään sähköpostiosoitteella ja salasanalla. subject: 'Mastodon: kaksivaiheinen todennus poistettu käytöstä' + subtitle: Kaksivaiheinen tunnistautuminen käyttäjätilillesi on poistettu käytöstä. title: 2-vaiheinen todennus pois käytöstä two_factor_enabled: + explanation: Sisäänkirjautuminen edellyttää liitetyn TOTP-sovelluksen luomaa aikarajattua kertatunnuslukua. subject: 'Mastodon: kaksivaiheinen todennus otettu käyttöön' + subtitle: Kaksivaiheinen kirjautuminen tilillesi on määritetty käyttöön. title: 2-vaiheinen todennus käytössä two_factor_recovery_codes_changed: explanation: Uudet palautuskoodit on nyt luotu ja vanhat on mitätöity. subject: 'Mastodon: kaksivaiheisen todennuksen palautuskoodit luotiin uudelleen' + subtitle: Aiemmat palautuskoodit on mitätöity, ja korvaavat uudet koodit on luotu. title: 2-vaiheisen todennuksen palautuskoodit vaihdettiin unlock_instructions: subject: 'Mastodon: lukituksen poistamisen ohjeet' @@ -68,9 +73,13 @@ fi: subject: 'Mastodon: suojausavain poistettu' title: Yksi suojausavaimistasi on poistettu webauthn_disabled: + explanation: Turva-avaimin kirjautuminen tilillesi on kytketty pois käytöstä. + extra: Olet nyt mahdollistanut sisäänkirjautumisen käyttäjätilillesi pelkästään palveluun liitetyn TOTP-sovelluksen luomalla aikarajoitteisella kertatunnusluvulla. subject: 'Mastodon: Todennus suojausavaimilla poistettu käytöstä' title: Suojausavaimet poistettu käytöstä webauthn_enabled: + explanation: Turva-avainkirjautuminen käyttäjätilillesi on otettu käyttöön. + extra: Voit nyt kirjautua sisään käyttäen turva-avaintasi. subject: 'Mastodon: Todennus suojausavaimella on otettu käyttöön' title: Suojausavaimet käytössä omniauth_callbacks: diff --git a/config/locales/devise.hu.yml b/config/locales/devise.hu.yml index 522ac66ad3..fea56ab24a 100644 --- a/config/locales/devise.hu.yml +++ b/config/locales/devise.hu.yml @@ -47,14 +47,19 @@ hu: subject: 'Mastodon: Jelszóvisszaállítási utasítások' title: Jelszó visszaállítása two_factor_disabled: + explanation: A bejelentkezés most már csupán email címmel és jelszóval lehetséges. subject: Kétlépcsős azonosítás kikapcsolva + subtitle: A kétlépcsős hitelesítés a fiókodhoz ki lett kapcsolva. title: Kétlépcsős hitelesítés kikapcsolva two_factor_enabled: + explanation: Egy párosított TOTP appal generált tokenre lesz szükség a bejelentkezéshez. subject: 'Mastodon: Kétlépcsős azonosítás engedélyezve' + subtitle: A kétlépcsős hitelesítés a fiókodhoz aktiválva lett. title: Kétlépcsős hitelesítés engedélyezve two_factor_recovery_codes_changed: explanation: A korábbi helyreállítási kódok letiltásra és újragenerálásra kerültek. subject: 'Mastodon: Kétlépcsős helyreállítási kódok újból előállítva' + subtitle: A korábbi helyreállítási kódokat letiltottuk, és újakat generáltunk. title: A kétlépcsős kódok megváltoztak unlock_instructions: subject: 'Mastodon: Feloldási utasítások' @@ -68,9 +73,13 @@ hu: subject: 'Mastodon: A biztonsági kulcs törlésre került' title: Az egyik biztonsági kulcsodat törölték webauthn_disabled: + explanation: A biztonsági kulcsokkal történő hitelesítés a fiókodhoz ki lett kapcsolva. + extra: A bejelentkezés most már csak TOTP app által generált tokennel lehetséges. subject: 'Mastodon: A biztonsági kulccsal történő hitelesítés letiltásra került' title: A biztonsági kulcsok letiltásra kerültek webauthn_enabled: + explanation: A biztonsági kulcsokkal történő hitelesítés a fiókodhoz aktiválva lett. + extra: A biztonsági kulcsodat mostantól lehet bejelentkezésre használni. subject: 'Mastodon: A biztonsági kulcsos hitelesítés engedélyezésre került' title: A biztonsági kulcsok engedélyezésre kerültek omniauth_callbacks: diff --git a/config/locales/devise.ko.yml b/config/locales/devise.ko.yml index 88865aec58..0c848e4bac 100644 --- a/config/locales/devise.ko.yml +++ b/config/locales/devise.ko.yml @@ -47,14 +47,19 @@ ko: subject: 'Mastodon: 암호 재설정 설명' title: 암호 재설정 two_factor_disabled: + explanation: 이제 이메일과 암호만 이용해서 로그인이 가능합니다. subject: '마스토돈: 이중 인증 비활성화' + subtitle: 계정에 대한 2단계 인증이 비활성화되었습니다. title: 2FA 비활성화 됨 two_factor_enabled: + explanation: 로그인 하기 위해서는 짝이 되는 TOTP 앱에서 생성한 토큰이 필요합니다. subject: '마스토돈: 이중 인증 활성화' + subtitle: 계정에 대한 2단계 인증이 활성화되었습니다. title: 2FA 활성화 됨 two_factor_recovery_codes_changed: explanation: 이전 복구 코드가 무효화되고 새 코드가 생성되었습니다 subject: '마스토돈: 이중 인증 복구 코드 재생성 됨' + subtitle: 이전 복구 코드가 무효화되고 새 코드가 생성되었습니다. title: 2FA 복구 코드 변경됨 unlock_instructions: subject: '마스토돈: 잠금 해제 방법' @@ -68,9 +73,13 @@ ko: subject: '마스토돈: 보안 키 삭제' title: 보안 키가 삭제되었습니다 webauthn_disabled: + explanation: 계정의 보안 키 인증이 비활성화되었습니다 + extra: 이제 TOTP 앱에서 생성한 토큰을 통해서만 로그인 가능합니다. subject: '마스토돈: 보안 키를 이용한 인증이 비활성화 됨' title: 보안 키 비활성화 됨 webauthn_enabled: + explanation: 계정에 대한 보안키 인증이 활성화되었습니다. + extra: 로그인시 보안키가 사용됩니다. subject: '마스토돈: 보안 키 인증 활성화 됨' title: 보안 키 활성화 됨 omniauth_callbacks: diff --git a/config/locales/fi.yml b/config/locales/fi.yml index a719f3496f..26fe6b7f02 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1608,6 +1608,7 @@ fi: unknown_browser: Tuntematon selain weibo: Weibo current_session: Nykyinen istunto + date: Päiväys description: "%{browser} alustalla %{platform}" explanation: Nämä verkkoselaimet ovat tällä hetkellä kirjautuneena Mastodon-tilillesi. ip: IP-osoite @@ -1774,14 +1775,19 @@ fi: webauthn: Suojausavaimet user_mailer: appeal_approved: + action: Tilin asetukset explanation: Valitus tiliäsi koskevasta varoituksesta %{strike_date} jonka lähetit %{appeal_date} on hyväksytty. Tilisi on jälleen hyvässä kunnossa. subject: Valituksesi %{date} on hyväksytty + subtitle: Tilisi on jälleen normaalissa tilassa. title: Valitus hyväksytty appeal_rejected: explanation: Valitus tiliäsi koskevasta varoituksesta %{strike_date} jonka lähetit %{appeal_date} on hylätty. subject: Valituksesi %{date} on hylätty + subtitle: Vetoomuksesi on hylätty. title: Valitus hylätty backup_ready: + explanation: Olet pyytänyt täysvarmuuskopion Mastodon-tilistäsi. + extra: Se on nyt valmis ladattavaksi! subject: Arkisto on valmiina ladattavaksi title: Arkiston tallennus suspicious_sign_in: diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 536af8b6b5..7cfd7d80e2 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1608,6 +1608,7 @@ hu: unknown_browser: Ismeretlen böngésző weibo: Weibo current_session: Jelenlegi munkamenet + date: Dátum description: "%{browser} az alábbi platformon: %{platform}" explanation: Jelenleg az alábbi böngészőkkel vagy bejelentkezve a fiókodba. ip: IP @@ -1774,14 +1775,19 @@ hu: webauthn: Biztonsági kulcsok user_mailer: appeal_approved: + action: Fiók Beállításai explanation: A fiókod %{appeal_date}-i fellebbezése, mely a %{strike_date}-i vétségeddel kapcsolatos, jóváhagyásra került. A fiókod megint makulátlan. subject: A %{date}-i fellebbezésedet jóváhagyták + subtitle: A fiókod ismét használható állapotban van. title: Fellebbezés jóváhagyva appeal_rejected: explanation: A %{appeal_date}-i fellebbezésed, amely a fiókod %{strike_date}-i vétségével kapcsolatos, elutasításra került. subject: A %{date}-i fellebbezésedet visszautasították + subtitle: A fellebbezésedet visszautasították. title: Fellebbezés visszautasítva backup_ready: + explanation: A Mastodon fiókod teljes biztonsági mentését kérted. + extra: Már letöltésre kész! subject: Az adataidról készült archív letöltésre kész title: Archiválás suspicious_sign_in: diff --git a/config/locales/ko.yml b/config/locales/ko.yml index b0eadc0504..b85b9b5861 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1584,6 +1584,7 @@ ko: unknown_browser: 알 수 없는 브라우저 weibo: 웨이보 current_session: 현재 세션 + date: 날짜 description: "%{platform}의 %{browser}" explanation: 내 마스토돈 계정에 로그인되어 있는 웹 브라우저 목록입니다. ip: IP @@ -1744,14 +1745,19 @@ ko: webauthn: 보안 키 user_mailer: appeal_approved: + action: 계정 설정 explanation: "%{strike_date}에 일어난 중재결정에 대한 소명을 %{appeal_date}에 작성했으며 승낙되었습니다. 당신의 계정은 정상적인 상태로 돌아왔습니다." subject: 귀하가 %{date}에 작성한 소명이 승낙되었습니다 + subtitle: 계정이 다시 정상적인 상태입니다. title: 소명이 받아들여짐 appeal_rejected: explanation: "%{strike_date}에 일어난 중재결정에 대한 소명을 %{appeal_date}에 작성했지만 반려되었습니다." subject: "%{date}에 작성한 소명이 반려되었습니다." + subtitle: 소명이 기각되었습니다. title: 이의 제기가 거절되었습니다 backup_ready: + explanation: 마스토돈 계정에 대한 전체 백업을 요청했습니다 + extra: 다운로드 할 준비가 되었습니다! subject: 아카이브를 다운로드할 수 있습니다 title: 아카이브 테이크아웃 suspicious_sign_in: diff --git a/config/locales/sk.yml b/config/locales/sk.yml index fdd64b5bb7..89f456a205 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -633,6 +633,7 @@ sk: documentation_link: Zisti viac release_notes: Poznámky k vydaniu title: Dostupné aktualizácie + type: Druh types: major: Hlavné vydanie patch: Opravné vydanie - opravy a jednoducho uplatniteľné zmeny @@ -641,6 +642,7 @@ sk: account: Autor application: Aplikácia back_to_account: Späť na účet + back_to_report: Späť na stránku hlásenia batch: remove_from_report: Vymaž z hlásenia report: Hlásenie From 6a5d70e146c0bc15e965b802f3711f3b7c145169 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 19 Jan 2024 06:20:20 -0500 Subject: [PATCH 14/84] Update pre_migration_check postgres version requirement (#28800) --- lib/tasks/db.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index 3bc526bd21..4208c2ae4b 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -16,8 +16,8 @@ namespace :db do end task pre_migration_check: :environment do - version = ActiveRecord::Base.connection.select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i - abort 'This version of Mastodon requires PostgreSQL 9.5 or newer. Please update PostgreSQL before updating Mastodon' if version < 90_500 + version = ActiveRecord::Base.connection.database_version + abort 'This version of Mastodon requires PostgreSQL 12.0 or newer. Please update PostgreSQL before updating Mastodon.' if version < 120_000 end Rake::Task['db:migrate'].enhance(['db:pre_migration_check']) From 5fc4ae7c5f9667b335e63f97cacaa1efe5f7a6d5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 19 Jan 2024 06:22:23 -0500 Subject: [PATCH 15/84] Move privacy policy into markdown file (#28699) --- .rubocop_todo.yml | 1 - app/models/privacy_policy.rb | 61 +------------- config/templates/privacy-policy.md | 128 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 61 deletions(-) create mode 100644 config/templates/privacy-policy.md diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0cebf37b57..a2ee32d284 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -135,7 +135,6 @@ Style/FetchEnvVar: # AllowedMethods: redirect Style/FormatStringToken: Exclude: - - 'app/models/privacy_policy.rb' - 'config/initializers/devise.rb' - 'lib/paperclip/color_extractor.rb' diff --git a/app/models/privacy_policy.rb b/app/models/privacy_policy.rb index 36cbf18822..c0d6e1b76d 100644 --- a/app/models/privacy_policy.rb +++ b/app/models/privacy_policy.rb @@ -1,66 +1,7 @@ # frozen_string_literal: true class PrivacyPolicy < ActiveModelSerializers::Model - DEFAULT_PRIVACY_POLICY = <<~TXT - This privacy policy describes how %{domain} ("%{domain}", "we", "us") collects, protects and uses the personally identifiable information you may provide through the %{domain} website or its API. The policy also describes the choices available to you regarding our use of your personal information and how you can access and update this information. This policy does not apply to the practices of companies that %{domain} does not own or control, or to individuals that %{domain} does not employ or manage. - - # What information do we collect? - - - **Basic account information**: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly. - - **Posts, following and other public information**: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public. - - **Direct and followers-only posts**: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it's important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. **Please keep in mind that the operators of the server and any receiving server may view such messages**, and that recipients may screenshot, copy or otherwise re-share them. **Do not share any sensitive information over Mastodon.** - - **IPs and other metadata**: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server. - - # What do we use your information for? - - Any of the information we collect from you may be used in the following ways: - - - To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline. - - To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations. - - The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions. - - # How do we protect your information? - - We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account. - - # What is our data retention policy? - - We will make a good faith effort to: - - - Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days. - - Retain the IP addresses associated with registered users no more than 12 months. - - You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image. - - You may irreversibly delete your account at any time. - - # Do we use cookies? - - Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. - - We use cookies to understand and save your preferences for future visits. - - # Do we disclose any information to outside parties? - - We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. - - Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this. - - When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password. - - # Site usage by children - - If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site. - - If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. - - Law requirements can be different if this server is in another jurisdiction. - - ___ - - This document is CC-BY-SA. Originally adapted from the [Discourse privacy policy](https://github.com/discourse/discourse). - TXT - + DEFAULT_PRIVACY_POLICY = Rails.root.join('config', 'templates', 'privacy-policy.md').read DEFAULT_UPDATED_AT = DateTime.new(2022, 10, 7).freeze attributes :updated_at, :text diff --git a/config/templates/privacy-policy.md b/config/templates/privacy-policy.md new file mode 100644 index 0000000000..9e042af80a --- /dev/null +++ b/config/templates/privacy-policy.md @@ -0,0 +1,128 @@ +This privacy policy describes how %{domain}s ("%{domain}s", "we", "us") +collects, protects and uses the personally identifiable information you may +provide through the %{domain}s website or its API. The policy also +describes the choices available to you regarding our use of your personal +information and how you can access and update this information. This policy +does not apply to the practices of companies that %{domain}s does not own +or control, or to individuals that %{domain}s does not employ or manage. + +# What information do we collect? + +- **Basic account information**: If you register on this server, you may be + asked to enter a username, an e-mail address and a password. You may also + enter additional profile information such as a display name and biography, and + upload a profile picture and header image. The username, display name, + biography, profile picture and header image are always listed publicly. +- **Posts, following and other public information**: The list of people you + follow is listed publicly, the same is true for your followers. When you + submit a message, the date and time is stored as well as the application you + submitted the message from. Messages may contain media attachments, such as + pictures and videos. Public and unlisted posts are available publicly. When + you feature a post on your profile, that is also publicly available + information. Your posts are delivered to your followers, in some cases it + means they are delivered to different servers and copies are stored there. + When you delete posts, this is likewise delivered to your followers. The + action of reblogging or favouriting another post is always public. +- **Direct and followers-only posts**: All posts are stored and processed on the + server. Followers-only posts are delivered to your followers and users who are + mentioned in them, and direct posts are delivered only to users mentioned in + them. In some cases it means they are delivered to different servers and + copies are stored there. We make a good faith effort to limit the access to + those posts only to authorized persons, but other servers may fail to do so. + Therefore it's important to review servers your followers belong to. You may + toggle an option to approve and reject new followers manually in the settings. + **Please keep in mind that the operators of the server and any receiving + server may view such messages**, and that recipients may screenshot, copy or + otherwise re-share them. **Do not share any sensitive information over + Mastodon.** +- **IPs and other metadata**: When you log in, we record the IP address you log + in from, as well as the name of your browser application. All the logged in + sessions are available for your review and revocation in the settings. The + latest IP address used is stored for up to 12 months. We also may retain + server logs which include the IP address of every request to our server. + +# What do we use your information for? + +Any of the information we collect from you may be used in the following ways: + +- To provide the core functionality of Mastodon. You can only interact with + other people's content and post your own content when you are logged in. For + example, you may follow other people to view their combined posts in your own + personalized home timeline. +- To aid moderation of the community, for example comparing your IP address with + other known ones to determine ban evasion or other violations. +- The email address you provide may be used to send you information, + notifications about other people interacting with your content or sending you + messages, and to respond to inquiries, and/or other requests or questions. + +# How do we protect your information? + +We implement a variety of security measures to maintain the safety of your +personal information when you enter, submit, or access your personal +information. Among other things, your browser session, as well as the traffic +between your applications and the API, are secured with SSL, and your password +is hashed using a strong one-way algorithm. You may enable two-factor +authentication to further secure access to your account. + +# What is our data retention policy? + +We will make a good faith effort to: + +- Retain server logs containing the IP address of all requests to this server, + in so far as such logs are kept, no more than 90 days. +- Retain the IP addresses associated with registered users no more than 12 + months. + +You can request and download an archive of your content, including your posts, +media attachments, profile picture, and header image. + +You may irreversibly delete your account at any time. + +# Do we use cookies? + +Yes. Cookies are small files that a site or its service provider transfers to +your computer's hard drive through your Web browser (if you allow). These +cookies enable the site to recognize your browser and, if you have a registered +account, associate it with your registered account. + +We use cookies to understand and save your preferences for future visits. + +# Do we disclose any information to outside parties? + +We do not sell, trade, or otherwise transfer to outside parties your personally +identifiable information. This does not include trusted third parties who assist +us in operating our site, conducting our business, or servicing you, so long as +those parties agree to keep this information confidential. We may also release +your information when we believe release is appropriate to comply with the law, +enforce our site policies, or protect ours or others rights, property, or +safety. + +Your public content may be downloaded by other servers in the network. Your +public and followers-only posts are delivered to the servers where your +followers reside, and direct messages are delivered to the servers of the +recipients, in so far as those followers or recipients reside on a different +server than this. + +When you authorize an application to use your account, depending on the scope of +permissions you approve, it may access your public profile information, your +following list, your followers, your lists, all your posts, and your favourites. +Applications can never access your e-mail address or password. + +# Site usage by children + +If this server is in the EU or the EEA: Our site, products and services are all +directed to people who are at least 16 years old. If you are under the age of +16, per the requirements of the GDPR (General Data Protection Regulation) do not +use this site. + +If this server is in the USA: Our site, products and services are all directed +to people who are at least 13 years old. If you are under the age of 13, per the +requirements of COPPA (Children's Online Privacy Protection Act) do not use this +site. + +Law requirements can be different if this server is in another jurisdiction. + +--- + +This document is CC-BY-SA. Originally adapted from the [Discourse privacy +policy](https://github.com/discourse/discourse). From 3593ee2e36284de71be8dc74c1772de7a7e1a7e3 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Jan 2024 13:19:49 +0100 Subject: [PATCH 16/84] Add rate-limit of TOTP authentication attempts at controller level (#28801) --- app/controllers/auth/sessions_controller.rb | 22 +++++++++++++++++++ .../auth/two_factor_authentication_concern.rb | 5 +++++ config/locales/en.yml | 1 + .../auth/sessions_controller_spec.rb | 20 +++++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 148ad53755..6bc48a7804 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Auth::SessionsController < Devise::SessionsController + include Redisable + + MAX_2FA_ATTEMPTS_PER_HOUR = 10 + layout 'auth' skip_before_action :check_self_destruct! @@ -130,9 +134,23 @@ class Auth::SessionsController < Devise::SessionsController session.delete(:attempt_user_updated_at) end + def clear_2fa_attempt_from_user(user) + redis.del(second_factor_attempts_key(user)) + end + + def check_second_factor_rate_limits(user) + attempts, = redis.multi do |multi| + multi.incr(second_factor_attempts_key(user)) + multi.expire(second_factor_attempts_key(user), 1.hour) + end + + attempts >= MAX_2FA_ATTEMPTS_PER_HOUR + end + def on_authentication_success(user, security_measure) @on_authentication_success_called = true + clear_2fa_attempt_from_user(user) clear_attempt_from_session user.update_sign_in!(new_sign_in: true) @@ -164,4 +182,8 @@ class Auth::SessionsController < Devise::SessionsController user_agent: request.user_agent ) end + + def second_factor_attempts_key(user) + "2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}" + end end diff --git a/app/controllers/concerns/auth/two_factor_authentication_concern.rb b/app/controllers/concerns/auth/two_factor_authentication_concern.rb index effdb8d21c..404164751a 100644 --- a/app/controllers/concerns/auth/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/auth/two_factor_authentication_concern.rb @@ -66,6 +66,11 @@ module Auth::TwoFactorAuthenticationConcern end def authenticate_with_two_factor_via_otp(user) + if check_second_factor_rate_limits(user) + flash.now[:alert] = I18n.t('users.rate_limited') + return prompt_for_two_factor(user) + end + if valid_otp_attempt?(user) on_authentication_success(user, :otp) else diff --git a/config/locales/en.yml b/config/locales/en.yml index 78820c3b59..89ca0ad72c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1844,6 +1844,7 @@ en: go_to_sso_account_settings: Go to your identity provider's account settings invalid_otp_token: Invalid two-factor code otp_lost_help_html: If you lost access to both, you may get in touch with %{email} + rate_limited: Too many authentication attempts, try again later. seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available. signed_in_as: 'Signed in as:' verification: diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index e3f2b278bd..d238626c9d 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -262,6 +262,26 @@ RSpec.describe Auth::SessionsController do end end + context 'when repeatedly using an invalid TOTP code before using a valid code' do + before do + stub_const('Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR', 2) + end + + it 'does not log the user in' do + # Travel to the beginning of an hour to avoid crossing rate-limit buckets + travel_to '2023-12-20T10:00:00Z' + + Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR.times do + post :create, params: { user: { otp_attempt: '1234' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } + expect(controller.current_user).to be_nil + end + + post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } + expect(controller.current_user).to be_nil + expect(flash[:alert]).to match I18n.t('users.rate_limited') + end + end + context 'when using a valid OTP' do before do post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } From cf2a2ed71c63cf113bd3569c237e8cebe00162bb Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Jan 2024 13:43:10 +0100 Subject: [PATCH 17/84] Fix processing of compacted single-item JSON-LD collections (#28816) --- .../fetch_featured_collection_service.rb | 4 +-- .../activitypub/fetch_replies_service.rb | 4 +-- .../synchronize_followers_service.rb | 4 +-- app/services/keys/query_service.rb | 2 +- .../fetch_featured_collection_service_spec.rb | 34 +++++++++++++++++-- .../activitypub/fetch_replies_service_spec.rb | 12 +++++++ 6 files changed, 51 insertions(+), 9 deletions(-) diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index d2bae08a0e..89c3a1b6c0 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -23,9 +23,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService case collection['type'] when 'Collection', 'CollectionPage' - collection['items'] + as_array(collection['items']) when 'OrderedCollection', 'OrderedCollectionPage' - collection['orderedItems'] + as_array(collection['orderedItems']) end end diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index a9dd327e96..e2ecdef165 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -26,9 +26,9 @@ class ActivityPub::FetchRepliesService < BaseService case collection['type'] when 'Collection', 'CollectionPage' - collection['items'] + as_array(collection['items']) when 'OrderedCollection', 'OrderedCollectionPage' - collection['orderedItems'] + as_array(collection['orderedItems']) end end diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb index 7ccc917309..f51d671a00 100644 --- a/app/services/activitypub/synchronize_followers_service.rb +++ b/app/services/activitypub/synchronize_followers_service.rb @@ -59,9 +59,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService case collection['type'] when 'Collection', 'CollectionPage' - collection['items'] + as_array(collection['items']) when 'OrderedCollection', 'OrderedCollectionPage' - collection['orderedItems'] + as_array(collection['orderedItems']) end end diff --git a/app/services/keys/query_service.rb b/app/services/keys/query_service.rb index 14c9d9205b..33e13293f3 100644 --- a/app/services/keys/query_service.rb +++ b/app/services/keys/query_service.rb @@ -69,7 +69,7 @@ class Keys::QueryService < BaseService return if json['items'].blank? - @devices = json['items'].map do |device| + @devices = as_array(json['items']).map do |device| Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim']) end rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e diff --git a/spec/services/activitypub/fetch_featured_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collection_service_spec.rb index a98108cea3..b9e95b825f 100644 --- a/spec/services/activitypub/fetch_featured_collection_service_spec.rb +++ b/spec/services/activitypub/fetch_featured_collection_service_spec.rb @@ -31,7 +31,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do } end - let(:status_json_pinned_unknown_unreachable) do + let(:status_json_pinned_unknown_reachable) do { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', @@ -75,7 +75,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known)) stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined)) stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404) - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_unreachable)) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null)) subject.call(actor, note: true, hashtag: false) @@ -115,6 +115,21 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do end it_behaves_like 'sets pinned posts' + + context 'when there is a single item, with the array compacted away' do + let(:items) { 'https://example.com/account/pinned/unknown-reachable' } + + before do + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) + subject.call(actor, note: true, hashtag: false) + end + + it 'sets expected posts as pinned posts' do + expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( + 'https://example.com/account/pinned/unknown-reachable' + ) + end + end end context 'when the endpoint is a paginated Collection' do @@ -136,6 +151,21 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do end it_behaves_like 'sets pinned posts' + + context 'when there is a single item, with the array compacted away' do + let(:items) { 'https://example.com/account/pinned/unknown-reachable' } + + before do + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) + subject.call(actor, note: true, hashtag: false) + end + + it 'sets expected posts as pinned posts' do + expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( + 'https://example.com/account/pinned/unknown-reachable' + ) + end + end end end end diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb index d7716dd4ef..a76b996c20 100644 --- a/spec/services/activitypub/fetch_replies_service_spec.rb +++ b/spec/services/activitypub/fetch_replies_service_spec.rb @@ -34,6 +34,18 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do describe '#call' do context 'when the payload is a Collection with inlined replies' do + context 'when there is a single reply, with the array compacted away' do + let(:items) { 'http://example.com/self-reply-1' } + + it 'queues the expected worker' do + allow(FetchReplyWorker).to receive(:push_bulk) + + subject.call(status, payload) + + expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1']) + end + end + context 'when passing the collection itself' do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) From 93957daa500502520ba4d7c8fc9d7918c99d1cdb Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Jan 2024 19:52:59 +0100 Subject: [PATCH 18/84] Fix error when processing remote files with unusually long names (#28823) --- lib/paperclip/response_with_limit_adapter.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/paperclip/response_with_limit_adapter.rb b/lib/paperclip/response_with_limit_adapter.rb index deb89717a4..ff7a938abb 100644 --- a/lib/paperclip/response_with_limit_adapter.rb +++ b/lib/paperclip/response_with_limit_adapter.rb @@ -16,7 +16,7 @@ module Paperclip private def cache_current_values - @original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data' + @original_filename = truncated_filename @tempfile = copy_to_tempfile(@target) @content_type = ContentTypeDetector.new(@tempfile.path).detect @size = File.size(@tempfile) @@ -43,6 +43,13 @@ module Paperclip source.response.connection.close end + def truncated_filename + filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data' + extension = File.extname(filename) + basename = File.basename(filename, extension) + [basename[...20], extension[..4]].compact_blank.join + end + def filename_from_content_disposition disposition = @target.response.headers['content-disposition'] disposition&.match(/filename="([^"]*)"/)&.captures&.first From 9f8e3cca9a6764018ccef2bc48b5d9a867e3a4e3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 21 Jan 2024 08:44:46 +0100 Subject: [PATCH 19/84] Fix duplicate and missing keys in search popout component in web UI (#28834) --- app/javascript/mastodon/actions/search.js | 7 +++++- .../features/compose/components/search.jsx | 22 ++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 38a089b486..a34a490e76 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -179,6 +179,11 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => { export const clickSearchResult = (q, type) => (dispatch, getState) => { const previous = getState().getIn(['search', 'recent']); + + if (previous.some(x => x.get('q') === q && x.get('type') === type)) { + return; + } + const me = getState().getIn(['meta', 'me']); const current = previous.add(fromJS({ type, q })).takeLast(4); @@ -207,4 +212,4 @@ export const hydrateSearch = () => (dispatch, getState) => { if (history !== null) { dispatch(updateSearchHistory(history)); } -}; \ No newline at end of file +}; diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index 0bcc41b929..ca02c23fc4 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -62,14 +62,14 @@ class Search extends PureComponent { }; defaultOptions = [ - { label: <>has: , action: e => { e.preventDefault(); this._insertText('has:'); } }, - { label: <>is: , action: e => { e.preventDefault(); this._insertText('is:'); } }, - { label: <>language: , action: e => { e.preventDefault(); this._insertText('language:'); } }, - { label: <>from: , action: e => { e.preventDefault(); this._insertText('from:'); } }, - { label: <>before: , action: e => { e.preventDefault(); this._insertText('before:'); } }, - { label: <>during: , action: e => { e.preventDefault(); this._insertText('during:'); } }, - { label: <>after: , action: e => { e.preventDefault(); this._insertText('after:'); } }, - { label: <>in: , action: e => { e.preventDefault(); this._insertText('in:'); } } + { key: 'prompt-has', label: <>has: , action: e => { e.preventDefault(); this._insertText('has:'); } }, + { key: 'prompt-is', label: <>is: , action: e => { e.preventDefault(); this._insertText('is:'); } }, + { key: 'prompt-language', label: <>language: , action: e => { e.preventDefault(); this._insertText('language:'); } }, + { key: 'prompt-from', label: <>from: , action: e => { e.preventDefault(); this._insertText('from:'); } }, + { key: 'prompt-before', label: <>before: , action: e => { e.preventDefault(); this._insertText('before:'); } }, + { key: 'prompt-during', label: <>during: , action: e => { e.preventDefault(); this._insertText('during:'); } }, + { key: 'prompt-after', label: <>after: , action: e => { e.preventDefault(); this._insertText('after:'); } }, + { key: 'prompt-in', label: <>in: , action: e => { e.preventDefault(); this._insertText('in:'); } } ]; setRef = c => { @@ -262,6 +262,8 @@ class Search extends PureComponent { const { recent } = this.props; return recent.toArray().map(search => ({ + key: `${search.get('type')}/${search.get('q')}`, + label: labelForRecentSearch(search), action: () => this.handleRecentSearchClick(search), @@ -346,8 +348,8 @@ class Search extends PureComponent {

- {recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => ( - From 3fbf01918f8dfe166c15032bdb782ee6a2d339d1 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Mon, 22 Jan 2024 11:02:26 +0100 Subject: [PATCH 20/84] Streaming: Move more methods to the utils from the main file (#28825) --- streaming/index.js | 42 +---------------------------------------- streaming/utils.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/streaming/index.js b/streaming/index.js index aa75a08b7f..78b049723f 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -16,7 +16,7 @@ const WebSocket = require('ws'); const { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } = require('./logging'); const { setupMetrics } = require('./metrics'); -const { isTruthy } = require("./utils"); +const { isTruthy, normalizeHashtag, firstParam } = require("./utils"); const environment = process.env.NODE_ENV || 'development'; @@ -1110,34 +1110,6 @@ const startServer = async () => { return arr; }; - /** - * See app/lib/ascii_folder.rb for the canon definitions - * of these constants - */ - const NON_ASCII_CHARS = 'ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšſŢţŤťŦŧÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž'; - const EQUIVALENT_ASCII_CHARS = 'AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz'; - - /** - * @param {string} str - * @returns {string} - */ - const foldToASCII = str => { - const regex = new RegExp(NON_ASCII_CHARS.split('').join('|'), 'g'); - - return str.replace(regex, match => { - const index = NON_ASCII_CHARS.indexOf(match); - return EQUIVALENT_ASCII_CHARS[index]; - }); - }; - - /** - * @param {string} str - * @returns {string} - */ - const normalizeHashtag = str => { - return foldToASCII(str.normalize('NFKC').toLowerCase()).replace(/[^\p{L}\p{N}_\u00b7\u200c]/gu, ''); - }; - /** * @param {any} req * @param {string} name @@ -1380,18 +1352,6 @@ const startServer = async () => { connectedChannels.labels({ type: 'websocket', channel: 'system' }).inc(2); }; - /** - * @param {string|string[]} arrayOrString - * @returns {string} - */ - const firstParam = arrayOrString => { - if (Array.isArray(arrayOrString)) { - return arrayOrString[0]; - } else { - return arrayOrString; - } - }; - /** * @param {WebSocket & { isAlive: boolean }} ws * @param {http.IncomingMessage & ResolvedAccount} req diff --git a/streaming/utils.js b/streaming/utils.js index ad8dd4889f..7b87a1d14c 100644 --- a/streaming/utils.js +++ b/streaming/utils.js @@ -20,3 +20,50 @@ const isTruthy = value => value && !FALSE_VALUES.includes(value); exports.isTruthy = isTruthy; + + +/** + * See app/lib/ascii_folder.rb for the canon definitions + * of these constants + */ +const NON_ASCII_CHARS = 'ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšſŢţŤťŦŧÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž'; +const EQUIVALENT_ASCII_CHARS = 'AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz'; + +/** + * @param {string} str + * @returns {string} + */ +function foldToASCII(str) { + const regex = new RegExp(NON_ASCII_CHARS.split('').join('|'), 'g'); + + return str.replace(regex, function(match) { + const index = NON_ASCII_CHARS.indexOf(match); + return EQUIVALENT_ASCII_CHARS[index]; + }); +} + +exports.foldToASCII = foldToASCII; + +/** + * @param {string} str + * @returns {string} + */ +function normalizeHashtag(str) { + return foldToASCII(str.normalize('NFKC').toLowerCase()).replace(/[^\p{L}\p{N}_\u00b7\u200c]/gu, ''); +} + +exports.normalizeHashtag = normalizeHashtag; + +/** + * @param {string|string[]} arrayOrString + * @returns {string} + */ +function firstParam(arrayOrString) { + if (Array.isArray(arrayOrString)) { + return arrayOrString[0]; + } else { + return arrayOrString; + } +} + +exports.firstParam = firstParam; From 62001b5a311fb98d41f546d36faffaaf8e82c4f7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:04:28 +0100 Subject: [PATCH 21/84] Update dependency jsdom to v24 (#28836) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- streaming/package.json | 2 +- yarn.lock | 34 +++++++--------------------------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/streaming/package.json b/streaming/package.json index 52a9979702..3f76e25786 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -20,7 +20,7 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "ioredis": "^5.3.2", - "jsdom": "^23.0.0", + "jsdom": "^24.0.0", "pg": "^8.5.0", "pg-connection-string": "^2.6.0", "pino": "^8.17.2", diff --git a/yarn.lock b/yarn.lock index 35abcf80b0..34e0d526e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,17 +42,6 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/dom-selector@npm:^2.0.1": - version: 2.0.1 - resolution: "@asamuzakjp/dom-selector@npm:2.0.1" - dependencies: - bidi-js: "npm:^1.0.3" - css-tree: "npm:^2.3.1" - is-potential-custom-element-name: "npm:^1.0.1" - checksum: 232895f16f2f9dfc637764df2529084d16e1c122057766a79b16e1d40808e09fffae28c0f0cc8376f8a1564a85dba9d4b2f140a9a0b65f4f95c960192b797037 - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5": version: 7.23.5 resolution: "@babel/code-frame@npm:7.23.5" @@ -2545,7 +2534,7 @@ __metadata: eslint-define-config: "npm:^2.0.0" express: "npm:^4.18.2" ioredis: "npm:^5.3.2" - jsdom: "npm:^23.0.0" + jsdom: "npm:^24.0.0" pg: "npm:^8.5.0" pg-connection-string: "npm:^2.6.0" pino: "npm:^8.17.2" @@ -4948,15 +4937,6 @@ __metadata: languageName: node linkType: hard -"bidi-js@npm:^1.0.3": - version: 1.0.3 - resolution: "bidi-js@npm:1.0.3" - dependencies: - require-from-string: "npm:^2.0.2" - checksum: fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1 - languageName: node - linkType: hard - "big-integer@npm:^1.6.44": version: 1.6.51 resolution: "big-integer@npm:1.6.51" @@ -10646,11 +10626,10 @@ __metadata: languageName: node linkType: hard -"jsdom@npm:^23.0.0": - version: 23.2.0 - resolution: "jsdom@npm:23.2.0" +"jsdom@npm:^24.0.0": + version: 24.0.0 + resolution: "jsdom@npm:24.0.0" dependencies: - "@asamuzakjp/dom-selector": "npm:^2.0.1" cssstyle: "npm:^4.0.1" data-urls: "npm:^5.0.0" decimal.js: "npm:^10.4.3" @@ -10659,6 +10638,7 @@ __metadata: http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.2" is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.7" parse5: "npm:^7.1.2" rrweb-cssom: "npm:^0.6.0" saxes: "npm:^6.0.0" @@ -10676,7 +10656,7 @@ __metadata: peerDependenciesMeta: canvas: optional: true - checksum: b062af50f7be59d914ba75236b7817c848ef3cd007aea1d6b8020a41eb263b7d5bd2652298106e9756b56892f773d990598778d02adab7d0d0d8e58726fc41d3 + checksum: 7b35043d7af39ad6dcaef0fa5679d8c8a94c6c9b6cc4a79222b7c9987d57ab7150c50856684ae56b473ab28c7d82aec0fb7ca19dcbd4c3f46683c807d717a3af languageName: node linkType: hard @@ -11962,7 +11942,7 @@ __metadata: languageName: node linkType: hard -"nwsapi@npm:^2.2.2": +"nwsapi@npm:^2.2.2, nwsapi@npm:^2.2.7": version: 2.2.7 resolution: "nwsapi@npm:2.2.7" checksum: 44be198adae99208487a1c886c0a3712264f7bbafa44368ad96c003512fed2753d4e22890ca1e6edb2690c3456a169f2a3c33bfacde1905cf3bf01c7722464db From 9ff9849381e1c28b23920b72112d97994bb73ac1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:07:44 +0100 Subject: [PATCH 22/84] Update dependency core-js to v3.35.1 (#28831) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 34e0d526e2..67c03e104f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5939,9 +5939,9 @@ __metadata: linkType: hard "core-js@npm:^3.30.2": - version: 3.35.0 - resolution: "core-js@npm:3.35.0" - checksum: 1d545ff4406f2afa5e681f44b45ed5f7f119d158b380234d5aa7787ce7e47fc7a635b98b74c28c766ba8191e3db8c2316ad6ab4ff1ddecbc3fd618413a52c29c + version: 3.35.1 + resolution: "core-js@npm:3.35.1" + checksum: ebc8e22c36d13bcf2140cbc1d8ad65d1b08192bff4c43ade70c72eac103cb4dcfbc521f2b1ad1c74881b0a4353e64986537893ae4f07888e49228340efa13ae6 languageName: node linkType: hard From a83aeccac9583528f50e24166ef9d42b147fb26d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:08:06 +0000 Subject: [PATCH 23/84] Update dependency dotenv to v16.3.2 (#28824) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 67c03e104f..5272d4167f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6901,9 +6901,9 @@ __metadata: linkType: hard "dotenv@npm:^16.0.3": - version: 16.3.1 - resolution: "dotenv@npm:16.3.1" - checksum: b95ff1bbe624ead85a3cd70dbd827e8e06d5f05f716f2d0cbc476532d54c7c9469c3bc4dd93ea519f6ad711cb522c00ac9a62b6eb340d5affae8008facc3fbd7 + version: 16.3.2 + resolution: "dotenv@npm:16.3.2" + checksum: a87d62cef0810b670cb477db1a24a42a093b6b428c9e65c185ce1d6368ad7175234b13547718ba08da18df43faae4f814180cc0366e11be1ded2277abc4dd22e languageName: node linkType: hard From e078d0048cd84a3bb33be569628cfbcf18999e2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:08:22 +0100 Subject: [PATCH 24/84] Update dependency @types/react to v18.2.48 (#28839) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5272d4167f..aca2278f9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3533,13 +3533,13 @@ __metadata: linkType: hard "@types/react@npm:*, @types/react@npm:16 || 17 || 18, @types/react@npm:>=16.9.11, @types/react@npm:^18.2.7": - version: 18.2.47 - resolution: "@types/react@npm:18.2.47" + version: 18.2.48 + resolution: "@types/react@npm:18.2.48" dependencies: "@types/prop-types": "npm:*" "@types/scheduler": "npm:*" csstype: "npm:^3.0.2" - checksum: e98ea1827fe60636d0f7ce206397159a29fc30613fae43e349e32c10ad3c0b7e0ed2ded2f3239e07bd5a3cba8736b6114ba196acccc39905ca4a06f56a8d2841 + checksum: 7e89f18ea2928b1638f564b156d692894dcb9352a7e0a807873c97e858abe1f23dbd165a25dd088a991344e973fdeef88ba5724bfb64504b74072cbc9c220c3a languageName: node linkType: hard From 9620b21259f4bb91862e954e16b90d566991670a Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Mon, 22 Jan 2024 11:11:47 +0000 Subject: [PATCH 25/84] docs: update FEDERATION.md to more closely follow FEP conventions. (#28838) --- FEDERATION.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/FEDERATION.md b/FEDERATION.md index e3721d7241..2819fa935a 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -1,19 +1,35 @@ -## ActivityPub federation in Mastodon +# Federation + +## Supported federation protocols and standards + +- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server) +- [WebFinger](https://webfinger.net/) +- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) +- [NodeInfo](https://nodeinfo.diaspora.software/) + +## Supported FEPs + +- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) +- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) +- [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md) +- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md) + +## ActivityPub in Mastodon Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all. -Supported vocabulary: https://docs.joinmastodon.org/spec/activitypub/ +- [Supported ActivityPub vocabulary](https://docs.joinmastodon.org/spec/activitypub/) ### Required extensions -#### Webfinger +#### WebFinger In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`). This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings. As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger. -More information and examples are available at: https://docs.joinmastodon.org/spec/webfinger/ +- [WebFinger information and examples](https://docs.joinmastodon.org/spec/webfinger/) #### HTTP Signatures @@ -21,11 +37,13 @@ In order to authenticate activities, Mastodon relies on HTTP Signatures, signing Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server. -More information on HTTP Signatures, as well as examples, can be found here: https://docs.joinmastodon.org/spec/security/#http +- [HTTP Signatures information and examples](https://docs.joinmastodon.org/spec/security/#http) ### Optional extensions -- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld -- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/ -- Followers collection synchronization: https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md -- Search indexing consent for actors: https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md +- [Linked-Data Signatures](https://docs.joinmastodon.org/spec/security/#ld) +- [Bearcaps](https://docs.joinmastodon.org/spec/bearcaps/) + +### Additional documentation + +- [Mastodon documentation](https://docs.joinmastodon.org/) From 76e383ea1e416d2a034d4327ed711eb01d106634 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:52:26 +0100 Subject: [PATCH 26/84] New Crowdin Translations (automated) (#28827) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/ast.json | 10 ++-- app/javascript/mastodon/locales/ko.json | 2 +- app/javascript/mastodon/locales/lad.json | 4 ++ app/javascript/mastodon/locales/th.json | 2 +- config/locales/ast.yml | 1 + config/locales/bg.yml | 1 + config/locales/ca.yml | 1 + config/locales/da.yml | 1 + config/locales/de.yml | 1 + config/locales/devise.fi.yml | 16 +++--- config/locales/devise.ie.yml | 2 + config/locales/devise.ja.yml | 6 +-- config/locales/devise.lad.yml | 8 +++ config/locales/devise.nn.yml | 9 ++++ config/locales/devise.no.yml | 9 ++++ config/locales/devise.sl.yml | 9 ++++ config/locales/devise.th.yml | 11 +++- config/locales/doorkeeper.ia.yml | 9 ++++ config/locales/eo.yml | 19 +++++++ config/locales/es-AR.yml | 1 + config/locales/es-MX.yml | 1 + config/locales/es.yml | 1 + config/locales/eu.yml | 1 + config/locales/fi.yml | 3 +- config/locales/fo.yml | 1 + config/locales/gl.yml | 1 + config/locales/he.yml | 1 + config/locales/hu.yml | 1 + config/locales/ie.yml | 2 + config/locales/is.yml | 1 + config/locales/it.yml | 1 + config/locales/ja.yml | 1 + config/locales/ko.yml | 1 + config/locales/lad.yml | 40 ++++++++++++++ config/locales/nl.yml | 1 + config/locales/nn.yml | 7 +++ config/locales/no.yml | 69 +++++++++++++----------- config/locales/pl.yml | 1 + config/locales/pt-PT.yml | 1 + config/locales/simple_form.no.yml | 4 +- config/locales/sk.yml | 2 + config/locales/sl.yml | 1 + config/locales/sr-Latn.yml | 1 + config/locales/sr.yml | 1 + config/locales/sv.yml | 1 + config/locales/th.yml | 11 +++- config/locales/tr.yml | 1 + config/locales/uk.yml | 1 + config/locales/vi.yml | 1 + config/locales/zh-CN.yml | 1 + config/locales/zh-HK.yml | 1 + config/locales/zh-TW.yml | 1 + 52 files changed, 231 insertions(+), 53 deletions(-) diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index 4b555c4829..1467f8891e 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -116,7 +116,6 @@ "compose_form.publish_form": "Artículu nuevu", "compose_form.publish_loud": "¡{publish}!", "compose_form.save_changes": "Guardar los cambeos", - "compose_form.spoiler.unmarked": "Text is not hidden", "confirmation_modal.cancel": "Encaboxar", "confirmations.block.block_and_report": "Bloquiar ya informar", "confirmations.block.confirm": "Bloquiar", @@ -146,6 +145,7 @@ "dismissable_banner.community_timeline": "Esta seición contién los artículos públicos más actuales de los perfiles agospiaos nel dominiu {domain}.", "dismissable_banner.dismiss": "Escartar", "dismissable_banner.explore_tags": "Esta seición contién les etiquetes del fediversu que tán ganando popularidá güei. Les etiquetes más usaes polos perfiles apaecen no cimero.", + "dismissable_banner.public_timeline": "Esta seición contién los artículos más nuevos de les persones na web social que les persones de {domain} siguen.", "embed.instructions": "Empotra esti artículu nel to sitiu web pente la copia del códigu d'abaxo.", "embed.preview": "Va apaecer asina:", "emoji_button.activity": "Actividá", @@ -155,6 +155,7 @@ "emoji_button.not_found": "Nun s'atoparon fustaxes que concasen", "emoji_button.objects": "Oxetos", "emoji_button.people": "Persones", + "emoji_button.recent": "D'usu frecuente", "emoji_button.search": "Buscar…", "emoji_button.search_results": "Resultaos de la busca", "emoji_button.symbols": "Símbolos", @@ -217,7 +218,6 @@ "hashtag.column_header.tag_mode.any": "o {additional}", "hashtag.column_header.tag_mode.none": "ensin {additional}", "hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia", - "hashtag.column_settings.tag_toggle": "Include additional tags in this column", "hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}", "hashtag.follow": "Siguir a la etiqueta", "hashtag.unfollow": "Dexar de siguir a la etiqueta", @@ -259,7 +259,6 @@ "keyboard_shortcuts.reply": "Responder a un artículu", "keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu", "keyboard_shortcuts.search": "Enfocar la barra de busca", - "keyboard_shortcuts.spoilers": "to show/hide CW field", "keyboard_shortcuts.start": "Abrir la columna «Entamar»", "keyboard_shortcuts.toggle_sensitivity": "Amosar/anubrir el conteníu multimedia", "keyboard_shortcuts.toot": "Comenzar un artículu nuevu", @@ -412,12 +411,16 @@ "search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}", "search.quick_action.status_search": "Artículos que concasen con {x}", "search.search_or_paste": "Busca o apiega una URL", + "search_popout.language_code": "códigu de llingua ISO", "search_popout.quick_actions": "Aiciones rápides", "search_popout.recent": "Busques de recién", + "search_popout.specific_date": "data específica", + "search_popout.user": "perfil", "search_results.accounts": "Perfiles", "search_results.all": "Too", "search_results.hashtags": "Etiquetes", "search_results.nothing_found": "Nun se pudo atopar nada con esos términos de busca", + "search_results.see_all": "Ver too", "search_results.statuses": "Artículos", "search_results.title": "Busca de: {q}", "server_banner.introduction": "{domain} ye parte de la rede social descentralizada que tien la teunoloxía de {mastodon}.", @@ -460,6 +463,7 @@ "status.replied_to": "En rempuesta a {name}", "status.reply": "Responder", "status.replyAll": "Responder al filu", + "status.report": "Informar de @{name}", "status.sensitive_warning": "Conteníu sensible", "status.show_filter_reason": "Amosar de toes toes", "status.show_less": "Amosar menos", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 264781baa3..70ce6611d6 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -683,7 +683,7 @@ "status.show_more": "펼치기", "status.show_more_all": "모두 펼치기", "status.show_original": "원본 보기", - "status.title.with_attachments": "{user} 님이 {attachmentCount, plural, one {첨부} other {{attachmentCount}개 첨부}}하여 게시", + "status.title.with_attachments": "{user} 님이 {attachmentCount, plural, one {첨부파일} other {{attachmentCount}개의 첨부파일}}과 함께 게시함", "status.translate": "번역", "status.translated_from_with": "{provider}에 의해 {lang}에서 번역됨", "status.uncached_media_warning": "마리보기 허용되지 않음", diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json index 2a911483de..8fde687427 100644 --- a/app/javascript/mastodon/locales/lad.json +++ b/app/javascript/mastodon/locales/lad.json @@ -328,6 +328,7 @@ "interaction_modal.on_another_server": "En otro sirvidor", "interaction_modal.on_this_server": "En este sirvidor", "interaction_modal.sign_in": "No estas konektado kon este sirvidor. Ande tyenes tu kuento?", + "interaction_modal.sign_in_hint": "Konsejo: Akel es el sitio adonde te enrejistrates. Si no lo akodras, bushka el mesaj de posta elektronika de bienvenida en tu kuti de arivo. Tambien puedes eskrivir tu nombre de utilizador kompleto (por enshemplo @Mastodon@mastodon.social)", "interaction_modal.title.favourite": "Endika ke te plaze publikasyon de {name}", "interaction_modal.title.follow": "Sige a {name}", "interaction_modal.title.reblog": "Repartaja publikasyon de {name}", @@ -478,6 +479,7 @@ "onboarding.actions.go_to_explore": "Va a los trendes", "onboarding.actions.go_to_home": "Va a tu linya prinsipala", "onboarding.compose.template": "Ke haber, #Mastodon?", + "onboarding.follows.empty": "Malorozamente, no se pueden amostrar rezultados en este momento. Puedes aprovar uzar la bushkeda o navigar por la pajina de eksplorasyon para topar personas a las que segir, o aprovarlo de muevo mas tadre.", "onboarding.follows.title": "Personaliza tu linya prinsipala", "onboarding.profile.discoverable": "Faz ke mi profil apareska en bushkedas", "onboarding.profile.display_name": "Nombre amostrado", @@ -497,7 +499,9 @@ "onboarding.start.title": "Lo logrates!", "onboarding.steps.follow_people.body": "El buto de Mastodon es segir a djente interesante.", "onboarding.steps.follow_people.title": "Personaliza tu linya prinsipala", + "onboarding.steps.publish_status.body": "Puedes introdusirte al mundo con teksto, fotos, videos o anketas {emoji}", "onboarding.steps.publish_status.title": "Eskrive tu primera publikasyon", + "onboarding.steps.setup_profile.body": "Kompleta tu profil para aumentar tus enteraksyones.", "onboarding.steps.setup_profile.title": "Personaliza tu profil", "onboarding.steps.share_profile.body": "Informe a tus amigos komo toparte en Mastodon", "onboarding.steps.share_profile.title": "Partaja tu profil de Mastodon", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index b108e581a4..65f27ef061 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -314,7 +314,7 @@ "home.explore_prompt.body": "ฟีดหน้าแรกของคุณจะมีการผสมผสานของโพสต์จากแฮชแท็กที่คุณได้เลือกติดตาม, ผู้คนที่คุณได้เลือกติดตาม และโพสต์ที่เขาดัน หากนั่นรู้สึกเงียบเกินไป คุณอาจต้องการ:", "home.explore_prompt.title": "นี่คือฐานหน้าแรกของคุณภายใน Mastodon", "home.hide_announcements": "ซ่อนประกาศ", - "home.pending_critical_update.body": "โปรดอัปเดตเซิร์ฟเวอร์ Mastodon ของคุณโดยเร็วที่สุดเท่าที่จะทำได้!", + "home.pending_critical_update.body": "โปรดอัปเดตเซิร์ฟเวอร์ Mastodon ของคุณโดยเร็วที่สุดเท่าที่จะเป็นไปได้!", "home.pending_critical_update.link": "ดูการอัปเดต", "home.pending_critical_update.title": "มีการอัปเดตความปลอดภัยสำคัญพร้อมใช้งาน!", "home.show_announcements": "แสดงประกาศ", diff --git a/config/locales/ast.yml b/config/locales/ast.yml index a32413cb9a..7e5a4c8876 100644 --- a/config/locales/ast.yml +++ b/config/locales/ast.yml @@ -909,6 +909,7 @@ ast: users: follow_limit_reached: Nun pues siguir a más de %{limit} persones invalid_otp_token: El códigu de l'autenticación en dos pasos nun ye válidu + rate_limited: Fixéronse milenta intentos d'autenticación. Volvi tentalo dempués. seamless_external_login: Aniciesti la sesión pente un serviciu esternu, polo que la configuración de la contraseña ya de la direición de corréu electrónicu nun tán disponibles. signed_in_as: 'Aniciesti la sesión como:' verification: diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 377babe22e..58a5cae2fd 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -1843,6 +1843,7 @@ bg: go_to_sso_account_settings: Отидете при настройките на акаунта на своя доставчик на идентичност invalid_otp_token: Невалиден код otp_lost_help_html: Ако загубите достъп до двете, то може да се свържете с %{email} + rate_limited: Премного опити за удостоверяване. Опитайте пак по-късно. seamless_external_login: Влезли сте чрез външна услуга, така че настройките за парола и имейл не са налични. signed_in_as: 'Влезли като:' verification: diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 580c4a3ed9..36ebb9785b 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1840,6 +1840,7 @@ ca: go_to_sso_account_settings: Ves a la configuració del compte del teu proveïdor d'identitat invalid_otp_token: El codi de dos factors no és correcte otp_lost_help_html: Si has perdut l'accés a tots dos pots contactar per %{email} + rate_limited: Excessius intents d'autenticació, torneu-ho a provar més tard. seamless_external_login: Has iniciat sessió via un servei extern per tant els ajustos de contrasenya i correu electrònic no estan disponibles. signed_in_as: 'Sessió iniciada com a:' verification: diff --git a/config/locales/da.yml b/config/locales/da.yml index e09a6eb2f5..58fd723aef 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1843,6 +1843,7 @@ da: go_to_sso_account_settings: Gå til identitetsudbyderens kontoindstillinger invalid_otp_token: Ugyldig tofaktorkode otp_lost_help_html: Har du mistet adgang til begge, kan du kontakte %{email} + rate_limited: For mange godkendelsesforsøg. Prøv igen senere. seamless_external_login: Du er logget ind via en ekstern tjeneste, så adgangskode- og e-mailindstillinger er utilgængelige. signed_in_as: 'Logget ind som:' verification: diff --git a/config/locales/de.yml b/config/locales/de.yml index dc78b188e2..e177c6d2d1 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1843,6 +1843,7 @@ de: go_to_sso_account_settings: Kontoeinstellungen des Identitätsanbieters aufrufen invalid_otp_token: Ungültiger Code der Zwei-Faktor-Authentisierung (2FA) otp_lost_help_html: Wenn du beides nicht mehr weißt, melde dich bitte bei uns unter der E-Mail-Adresse %{email} + rate_limited: Zu viele Authentisierungsversuche. Bitte versuche es später noch einmal. seamless_external_login: Du bist über einen externen Dienst angemeldet, daher sind Passwort- und E-Mail-Einstellungen nicht verfügbar. signed_in_as: 'Angemeldet als:' verification: diff --git a/config/locales/devise.fi.yml b/config/locales/devise.fi.yml index ac7a57c6f8..22fd7ff47b 100644 --- a/config/locales/devise.fi.yml +++ b/config/locales/devise.fi.yml @@ -47,19 +47,19 @@ fi: subject: 'Mastodon: ohjeet salasanan vaihtoon' title: Salasanan vaihto two_factor_disabled: - explanation: Olet nyt mahdollistanut sisäänkirjautumisen pelkästään sähköpostiosoitteella ja salasanalla. + explanation: Sisäänkirjautuminen on nyt mahdollista pelkällä sähköpostiosoitteella ja salasanalla. subject: 'Mastodon: kaksivaiheinen todennus poistettu käytöstä' - subtitle: Kaksivaiheinen tunnistautuminen käyttäjätilillesi on poistettu käytöstä. + subtitle: Kaksivaiheinen todennus on poistettu käytöstä tililtäsi. title: 2-vaiheinen todennus pois käytöstä two_factor_enabled: explanation: Sisäänkirjautuminen edellyttää liitetyn TOTP-sovelluksen luomaa aikarajattua kertatunnuslukua. subject: 'Mastodon: kaksivaiheinen todennus otettu käyttöön' - subtitle: Kaksivaiheinen kirjautuminen tilillesi on määritetty käyttöön. + subtitle: Kaksivaiheinen todennus on otettu käyttöön tilillesi. title: 2-vaiheinen todennus käytössä two_factor_recovery_codes_changed: explanation: Uudet palautuskoodit on nyt luotu ja vanhat on mitätöity. subject: 'Mastodon: kaksivaiheisen todennuksen palautuskoodit luotiin uudelleen' - subtitle: Aiemmat palautuskoodit on mitätöity, ja korvaavat uudet koodit on luotu. + subtitle: Aiemmat palautuskoodit on mitätöity ja tilalle on luotu uudet. title: 2-vaiheisen todennuksen palautuskoodit vaihdettiin unlock_instructions: subject: 'Mastodon: lukituksen poistamisen ohjeet' @@ -73,13 +73,13 @@ fi: subject: 'Mastodon: suojausavain poistettu' title: Yksi suojausavaimistasi on poistettu webauthn_disabled: - explanation: Turva-avaimin kirjautuminen tilillesi on kytketty pois käytöstä. - extra: Olet nyt mahdollistanut sisäänkirjautumisen käyttäjätilillesi pelkästään palveluun liitetyn TOTP-sovelluksen luomalla aikarajoitteisella kertatunnusluvulla. + explanation: Turva-avaimin kirjautuminen on poistettu käytöstä tililtäsi. + extra: Sisäänkirjautuminen on nyt mahdollista pelkällä palveluun liitetyn TOTP-sovelluksen luomalla aikarajoitteisella kertatunnusluvulla. subject: 'Mastodon: Todennus suojausavaimilla poistettu käytöstä' title: Suojausavaimet poistettu käytöstä webauthn_enabled: - explanation: Turva-avainkirjautuminen käyttäjätilillesi on otettu käyttöön. - extra: Voit nyt kirjautua sisään käyttäen turva-avaintasi. + explanation: Turva-avaimella kirjautuminen on otettu käyttöön tilillesi. + extra: Voit nyt kirjautua sisään turva-avaimellasi. subject: 'Mastodon: Todennus suojausavaimella on otettu käyttöön' title: Suojausavaimet käytössä omniauth_callbacks: diff --git a/config/locales/devise.ie.yml b/config/locales/devise.ie.yml index 97cda4e8c6..332c9da456 100644 --- a/config/locales/devise.ie.yml +++ b/config/locales/devise.ie.yml @@ -52,6 +52,7 @@ ie: subtitle: 2-factor autentication por tui conto ha esset desactivisat. title: 2FA desvalidat two_factor_enabled: + explanation: Un clave generat del acuplat TOTP-aplication nu va esser besonat por aperter session. subject: 'Mastodon: 2-factor autentication activat' subtitle: 2-factor autentication ha esset activisat por tui conto. title: 2FA permisset @@ -73,6 +74,7 @@ ie: title: Un ex tui claves de securitá ha esset deletet webauthn_disabled: explanation: Autentication per clave de securitá ha esset desactivisat por tui conto. + extra: Aperter session es nu possibil solmen per li clave generat del acuplat TOTP-aplication. subject: 'Mastodon: Autentication con claves de securitá desactivisat' title: Claves de securitá desactivisat webauthn_enabled: diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml index 9a3ffd9c4d..44a9a31839 100644 --- a/config/locales/devise.ja.yml +++ b/config/locales/devise.ja.yml @@ -49,12 +49,12 @@ ja: two_factor_disabled: explanation: メールアドレスとパスワードのみでログイン可能になりました。 subject: 'Mastodon: 二要素認証が無効になりました' - subtitle: 二要素認証が無効になっています。 + subtitle: 今後、アカウントへのログインに二要素認証を要求しません。 title: 二要素認証が無効化されました two_factor_enabled: explanation: ログインには設定済みのTOTPアプリが生成したトークンが必要です。 subject: 'Mastodon: 二要素認証が有効になりました' - subtitle: 二要素認証が有効になりました。 + subtitle: 今後、アカウントへのログインに二要素認証が必要になります。 title: 二要素認証が有効化されました two_factor_recovery_codes_changed: explanation: 以前のリカバリーコードが無効化され、新しいコードが生成されました。 @@ -73,7 +73,7 @@ ja: subject: 'Mastodon: セキュリティキーが削除されました' title: セキュリティキーが削除されました webauthn_disabled: - explanation: セキュリティキー認証が無効になっています。 + explanation: セキュリティキー認証が無効になりました。 extra: 設定済みのTOTPアプリが生成したトークンのみでログインできるようになりました。 subject: 'Mastodon: セキュリティキー認証が無効になりました' title: セキュリティキーは無効になっています diff --git a/config/locales/devise.lad.yml b/config/locales/devise.lad.yml index bec76d82f9..2b6b8aafb1 100644 --- a/config/locales/devise.lad.yml +++ b/config/locales/devise.lad.yml @@ -47,10 +47,14 @@ lad: subject: 'Mastodon: Instruksyones para reinisyar kod' title: Reinisyar kod two_factor_disabled: + explanation: Agora puedes konektarte kon tu kuento uzando solo tu adreso de posta i kod. subject: 'Mastodon: La autentifikasyon de dos pasos esta inkapasitada' + subtitle: La autentifikasyon en dos pasos para tu kuento tiene sido inkapasitada. title: Autentifikasyon 2FA inkapasitada two_factor_enabled: + explanation: Se rekierira un token djenerado por la aplikasyon TOTP konektada para entrar. subject: 'Mastodon: La autentifikasyon de dos pasos esta kapasitada' + subtitle: La autentifikasyon de dos pasos para tu kuento tiene sido kapasitada. title: Autentifikasyon 2FA aktivada two_factor_recovery_codes_changed: explanation: Los kodiches de rekuperasyon previos tienen sido invalidados i se djeneraron kodiches muevos. @@ -69,9 +73,13 @@ lad: subject: 'Mastodon: Yave de sigurita supremida' title: Una de tus yaves de sigurita tiene sido supremida webauthn_disabled: + explanation: La autentifikasyon kon yaves de sigurita tiene sido inkapasitada para tu kuento. + extra: Agora el inisyo de sesyon solo es posivle utilizando el token djeenerado por la aplikasyon TOTP konektada. subject: 'Mastodon: autentifikasyon kon yaves de sigurita inkapasitada' title: Yaves de sigurita inkapasitadas webauthn_enabled: + explanation: La autentifikasyon kon yave de sigurita tiene sido kapasitada para tu kuento. + extra: Agora tu yave de sigurita puede ser utilizada para konektarte kon tu kuento. subject: 'Mastodon: Autentifikasyon de yave de sigurita aktivada' title: Yaves de sigurita kapasitadas omniauth_callbacks: diff --git a/config/locales/devise.nn.yml b/config/locales/devise.nn.yml index acee9fdcdc..96920d42b5 100644 --- a/config/locales/devise.nn.yml +++ b/config/locales/devise.nn.yml @@ -47,14 +47,19 @@ nn: subject: 'Mastodon: Instuksjonar for å endra passord' title: Attstilling av passord two_factor_disabled: + explanation: Innlogging er nå mulig med kun e-postadresse og passord. subject: 'Mastodon: To-faktor-autentisering deaktivert' + subtitle: To-faktor autentisering for din konto har blitt deaktivert. title: 2FA deaktivert two_factor_enabled: + explanation: En token generert av den sammenkoblede TOTP-appen vil være påkrevd for innlogging. subject: 'Mastodon: To-faktor-autentisering aktivert' + subtitle: Tofaktorautentisering er aktivert for din konto. title: 2FA aktivert two_factor_recovery_codes_changed: explanation: Dei førre gjenopprettingskodane er ugyldige og nye er genererte. subject: 'Mastodon: To-faktor-gjenopprettingskodar har vorte genererte på nytt' + subtitle: De forrige gjenopprettingskodene er gjort ugyldige og nye er generert. title: 2FA-gjenopprettingskodane er endra unlock_instructions: subject: 'Mastodon: Instruksjonar for å opne kontoen igjen' @@ -68,9 +73,13 @@ nn: subject: 'Mastodon: Sikkerheitsnøkkel sletta' title: Ein av sikkerheitsnøklane dine har blitt sletta webauthn_disabled: + explanation: Autentisering med sikkerhetsnøkler er deaktivert for kontoen din. + extra: Innlogging er nå mulig med kun tilgangstoken generert av den sammenkoblede TOTP-appen. subject: 'Mastodon: Autentisering med sikkerheitsnøklar vart skrudd av' title: Sikkerheitsnøklar deaktivert webauthn_enabled: + explanation: Sikkerhetsnøkkelautentisering har blitt aktivert for kontoen din. + extra: Sikkerhetsnøkkelen din kan nå bli brukt for innlogging. subject: 'Mastodon: Sikkerheitsnøkkelsautentisering vart skrudd på' title: Sikkerheitsnøklar aktivert omniauth_callbacks: diff --git a/config/locales/devise.no.yml b/config/locales/devise.no.yml index 0d824da815..961778eaa5 100644 --- a/config/locales/devise.no.yml +++ b/config/locales/devise.no.yml @@ -47,14 +47,19 @@ subject: 'Mastodon: Hvordan nullstille passord' title: Nullstill passord two_factor_disabled: + explanation: Innlogging er nå mulig med kun e-postadresse og passord. subject: 'Mastodon: Tofaktorautentisering deaktivert' + subtitle: To-faktor autentisering for din konto har blitt deaktivert. title: 2FA deaktivert two_factor_enabled: + explanation: En token generert av den sammenkoblede TOTP-appen vil være påkrevd for innlogging. subject: 'Mastodon: Tofaktorautentisering aktivert' + subtitle: Tofaktorautentisering er aktivert for din konto. title: 2FA aktivert two_factor_recovery_codes_changed: explanation: De forrige gjenopprettingskodene er gjort ugyldige og nye er generert. subject: 'Mastodon: Tofaktor-gjenopprettingskoder har blitt generert på nytt' + subtitle: De forrige gjenopprettingskodene er gjort ugyldige og nye er generert. title: 2FA-gjenopprettingskodene ble endret unlock_instructions: subject: 'Mastodon: Instruksjoner for å gjenåpne konto' @@ -68,9 +73,13 @@ subject: 'Mastodon: Sikkerhetsnøkkel slettet' title: En av sikkerhetsnøklene dine har blitt slettet webauthn_disabled: + explanation: Autentisering med sikkerhetsnøkler er deaktivert for kontoen din. + extra: Innlogging er nå mulig med kun tilgangstoken generert av den sammenkoblede TOTP-appen. subject: 'Mastodon: Autentisering med sikkerhetsnøkler ble skrudd av' title: Sikkerhetsnøkler deaktivert webauthn_enabled: + explanation: Sikkerhetsnøkkelautentisering har blitt aktivert for kontoen din. + extra: Sikkerhetsnøkkelen din kan nå bli brukt for innlogging. subject: 'Mastodon: Sikkerhetsnøkkelsautentisering ble skrudd på' title: Sikkerhetsnøkler aktivert omniauth_callbacks: diff --git a/config/locales/devise.sl.yml b/config/locales/devise.sl.yml index 72269e4826..2d567e63f4 100644 --- a/config/locales/devise.sl.yml +++ b/config/locales/devise.sl.yml @@ -47,14 +47,19 @@ sl: subject: 'Mastodon: navodila za ponastavitev gesla' title: Ponastavitev gesla two_factor_disabled: + explanation: Prijava je sedaj mogoče le z uporabo e-poštnega naslova in gesla. subject: 'Mastodon: dvojno preverjanje pristnosti je onemogočeno' + subtitle: Dvo-faktorsko preverjanje pristnosti za vaš račun je bilo onemogočeno. title: 2FA onemogočeno two_factor_enabled: + explanation: Za prijavo bo zahtevan žeton, ustvarjen s povezano aplikacijo TOTP. subject: 'Mastodon: dvojno preverjanje pristnosti je omogočeno' + subtitle: Dvo-faktorsko preverjanje pristnosti za vaš račun je bilo omogočeno. title: 2FA omogočeno two_factor_recovery_codes_changed: explanation: Prejšnje obnovitvene kode so postale neveljavne in ustvarjene so bile nove. subject: 'Mastodon: varnostne obnovitvene kode za dvojno preverjanje pristnosti so ponovno izdelane' + subtitle: Prejšnje kode za obnovitev so bile razveljavljene, ustvarjene pa so bile nove. title: obnovitvene kode 2FA spremenjene unlock_instructions: subject: 'Mastodon: navodila za odklepanje' @@ -68,9 +73,13 @@ sl: subject: 'Mastodon: varnostna koda izbrisana' title: Ena od vaših varnostnih kod je bila izbrisana webauthn_disabled: + explanation: Preverjanje pristnosti z varnostnimi ključi za vaš račun je bilo onemogočeno. + extra: Prijava je sedaj mogoče le z uporabo žetona, ustvarjenega s povezano aplikacijo TOTP. subject: 'Mastodon: overjanje pristnosti z varnosnimi kodami je onemogočeno' title: Varnostne kode onemogočene webauthn_enabled: + explanation: Preverjanje pristnosti z varnostnimi ključi za vaš račun je bilo omogočeno. + extra: Za prijavo sedaj lahko uporabite svoj varnostni ključ. subject: 'Mastodon: preverjanje pristnosti z varnostno kodo je omogočeno' title: Varnostne kode omogočene omniauth_callbacks: diff --git a/config/locales/devise.th.yml b/config/locales/devise.th.yml index 13fdea3fef..40baabcf75 100644 --- a/config/locales/devise.th.yml +++ b/config/locales/devise.th.yml @@ -47,14 +47,19 @@ th: subject: 'Mastodon: คำแนะนำการตั้งรหัสผ่านใหม่' title: การตั้งรหัสผ่านใหม่ two_factor_disabled: + explanation: ตอนนี้สามารถเข้าสู่ระบบได้โดยใช้เพียงที่อยู่อีเมลและรหัสผ่านเท่านั้น subject: 'Mastodon: ปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยแล้ว' + subtitle: ปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยสำหรับบัญชีของคุณแล้ว title: ปิดใช้งาน 2FA แล้ว two_factor_enabled: + explanation: จะต้องใช้โทเคนที่สร้างโดยแอป TOTP ที่จับคู่สำหรับการเข้าสู่ระบบ subject: 'Mastodon: เปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยแล้ว' + subtitle: เปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยสำหรับบัญชีของคุณแล้ว title: เปิดใช้งาน 2FA แล้ว two_factor_recovery_codes_changed: - explanation: ยกเลิกรหัสกู้คืนก่อนหน้านี้และสร้างรหัสใหม่แล้ว + explanation: ยกเลิกรหัสกู้คืนก่อนหน้านี้และสร้างรหัสกู้คืนใหม่แล้ว subject: 'Mastodon: สร้างรหัสกู้คืนสองปัจจัยใหม่แล้ว' + subtitle: ยกเลิกรหัสกู้คืนก่อนหน้านี้และสร้างรหัสกู้คืนใหม่แล้ว title: เปลี่ยนรหัสกู้คืน 2FA แล้ว unlock_instructions: subject: 'Mastodon: คำแนะนำการปลดล็อค' @@ -68,9 +73,13 @@ th: subject: 'Mastodon: ลบกุญแจความปลอดภัยแล้ว' title: ลบหนึ่งในกุญแจความปลอดภัยของคุณแล้ว webauthn_disabled: + explanation: ปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยสำหรับบัญชีของคุณแล้ว + extra: ตอนนี้สามารถเข้าสู่ระบบได้โดยใช้เพียงโทเคนที่สร้างโดยแอป TOTP ที่จับคู่เท่านั้น subject: 'Mastodon: ปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยแล้ว' title: ปิดใช้งานกุญแจความปลอดภัยแล้ว webauthn_enabled: + explanation: เปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยสำหรับบัญชีของคุณแล้ว + extra: ตอนนี้สามารถใช้กุญแจความปลอดภัยของคุณสำหรับการเข้าสู่ระบบ subject: 'Mastodon: เปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยแล้ว' title: เปิดใช้งานกุญแจความปลอดภัยแล้ว omniauth_callbacks: diff --git a/config/locales/doorkeeper.ia.yml b/config/locales/doorkeeper.ia.yml index ec85df24fc..d689354f61 100644 --- a/config/locales/doorkeeper.ia.yml +++ b/config/locales/doorkeeper.ia.yml @@ -17,6 +17,7 @@ ia: index: application: Application delete: Deler + empty: Tu non ha applicationes. name: Nomine new: Nove application show: Monstrar @@ -47,6 +48,7 @@ ia: title: accounts: Contos admin/accounts: Gestion de contos + all: Accesso plen a tu conto de Mastodon bookmarks: Marcapaginas conversations: Conversationes favourites: Favoritos @@ -61,8 +63,15 @@ ia: applications: Applicationes oauth2_provider: Fornitor OAuth2 scopes: + read:favourites: vider tu favoritos + read:lists: vider tu listas + read:notifications: vider tu notificationes + read:statuses: vider tote le messages write:accounts: modificar tu profilo + write:blocks: blocar contos e dominios write:favourites: messages favorite + write:filters: crear filtros write:lists: crear listas + write:media: incargar files de medios write:notifications: rader tu notificationes write:statuses: publicar messages diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 1bcf36700b..beb6aa6d9f 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -309,6 +309,7 @@ eo: unpublish: Malpublikigi unpublished_msg: Anonco sukcese malpublikigita! updated_msg: Anonco sukcese ĝisdatigis! + critical_update_pending: Kritika ĝisdatigo pritraktotas custom_emojis: assign_category: Atribui kategorion by_domain: Domajno @@ -424,6 +425,7 @@ eo: view: Vidi domajna blokado email_domain_blocks: add_new: Aldoni novan + allow_registrations_with_approval: Permesi aliĝojn kun aprobo attempts_over_week: one: "%{count} provo ekde lasta semajno" other: "%{count} registroprovoj ekde lasta semajno" @@ -770,11 +772,21 @@ eo: approved: Bezonas aprobi por aliĝi none: Neniu povas aliĝi open: Iu povas aliĝi + security: + authorized_fetch: Devigi aŭtentigon de frataraj serviloj + title: Agordoj de la servilo site_uploads: delete: Forigi elŝutitan dosieron destroyed_msg: Reteja alŝuto sukcese forigita! software_updates: + critical_update: Kritika — bonvolu ĝisdatiĝi rapide documentation_link: Lerni pli + release_notes: Eldono-notoj + title: Disponeblaj ĝisdatigoj + type: Tipo + types: + major: Ĉefa eldono + minor: Neĉefa eldono statuses: account: Skribanto application: Aplikaĵo @@ -1259,6 +1271,9 @@ eo: overwrite: Anstataŭigi overwrite_long: Anstataŭigi la nunajn registrojn per la novaj preface: Vi povas importi datumojn, kiujn vi eksportis el alia servilo, kiel liston de homoj, kiujn vi sekvas aŭ blokas. + states: + finished: Finita + unconfirmed: Nekonfirmita success: Viaj datumoj estis sukcese alŝutitaj kaj estos traktitaj kiel planite titles: following: Importado de sekvaj kontoj @@ -1528,6 +1543,7 @@ eo: unknown_browser: Nekonata retumilo weibo: Weibo current_session: Nuna seanco + date: Dato description: "%{browser} en %{platform}" explanation: Ĉi tiuj estas la retumiloj nun ensalutintaj al via Mastodon-konto. ip: IP @@ -1693,6 +1709,7 @@ eo: webauthn: Sekurecaj ŝlosiloj user_mailer: appeal_approved: + action: Konto-agordoj explanation: La apelacio de la admono kontra via konto je %{strike_date} pri sendodato %{appeal_date} aprobitas. subject: Via apelacio de %{date} aprobitas title: Apelacio estis aprobita @@ -1701,6 +1718,7 @@ eo: subject: Via apelacio de %{date} estis malaprobita title: Apelacio estis malaprobita backup_ready: + extra: Estas nun preta por elŝuto! subject: Via arkivo estas preta por elŝutado title: Arkiva elŝuto suspicious_sign_in: @@ -1756,6 +1774,7 @@ eo: go_to_sso_account_settings: Iru al la agordoj de la konto de via identeca provizanto invalid_otp_token: Nevalida kodo de dufaktora aŭtentigo otp_lost_help_html: Se vi perdas aliron al ambaŭ, vi povas kontakti %{email} + rate_limited: Estas tro multaj aŭtentigaj provoj, reprovu poste. seamless_external_login: Vi estas ensalutinta per ekstera servo, do pasvortaj kaj retadresaj agordoj ne estas disponeblaj. signed_in_as: 'Salutinta kiel:' verification: diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index 26c18b5feb..0b6e58db59 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -1843,6 +1843,7 @@ es-AR: go_to_sso_account_settings: Andá a la configuración de cuenta de tu proveedor de identidad invalid_otp_token: Código de dos factores no válido otp_lost_help_html: Si perdiste al acceso a ambos, podés ponerte en contacto con %{email} + rate_limited: Demasiados intentos de autenticación; intentá de nuevo más tarde. seamless_external_login: Iniciaste sesión desde un servicio externo, así que la configuración de contraseña y correo electrónico no están disponibles. signed_in_as: 'Iniciaste sesión como:' verification: diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 32178d0b04..11c327bcca 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1843,6 +1843,7 @@ es-MX: go_to_sso_account_settings: Diríjete a la configuración de la cuenta de su proveedor de identidad invalid_otp_token: Código de dos factores incorrecto otp_lost_help_html: Si perdiste al acceso a ambos, puedes ponerte en contancto con %{email} + rate_limited: Demasiados intentos de autenticación, inténtalo de nuevo más tarde. seamless_external_login: Has iniciado sesión desde un servicio externo, así que los ajustes de contraseña y correo no están disponibles. signed_in_as: 'Sesión iniciada como:' verification: diff --git a/config/locales/es.yml b/config/locales/es.yml index 9235b985fb..4dbb76c526 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1843,6 +1843,7 @@ es: go_to_sso_account_settings: Diríjase a la configuración de la cuenta de su proveedor de identidad invalid_otp_token: Código de dos factores incorrecto otp_lost_help_html: Si perdiste al acceso a ambos, puedes ponerte en contancto con %{email} + rate_limited: Demasiados intentos de autenticación, inténtalo de nuevo más tarde. seamless_external_login: Has iniciado sesión desde un servicio externo, así que los ajustes de contraseña y correo no están disponibles. signed_in_as: 'Sesión iniciada como:' verification: diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 4b91f7a524..bfa1f829b6 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1847,6 +1847,7 @@ eu: go_to_sso_account_settings: Jo zure identitate-hornitzaileko kontuaren ezarpenetara invalid_otp_token: Bi faktoreetako kode baliogabea otp_lost_help_html: 'Bietara sarbidea galdu baduzu, jarri kontaktuan hemen: %{email}' + rate_limited: Autentifikazio saiakera gehiegi, saiatu berriro geroago. seamless_external_login: Kanpo zerbitzu baten bidez hasi duzu saioa, beraz pasahitza eta e-mail ezarpenak ez daude eskuragarri. signed_in_as: 'Saioa honela hasita:' verification: diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 26fe6b7f02..9d8974392f 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1786,7 +1786,7 @@ fi: subtitle: Vetoomuksesi on hylätty. title: Valitus hylätty backup_ready: - explanation: Olet pyytänyt täysvarmuuskopion Mastodon-tilistäsi. + explanation: Olet pyytänyt täyden varmuuskopion Mastodon-tilistäsi. extra: Se on nyt valmis ladattavaksi! subject: Arkisto on valmiina ladattavaksi title: Arkiston tallennus @@ -1843,6 +1843,7 @@ fi: go_to_sso_account_settings: Avaa identiteettitarjoajasi tiliasetukset invalid_otp_token: Virheellinen kaksivaiheisen todentamisen koodi otp_lost_help_html: Jos sinulla ei ole pääsyä kumpaankaan, voit ottaa yhteyden osoitteeseen %{email} + rate_limited: Liian monta todennusyritystä. Yritä myöhemmin uudelleen. seamless_external_login: Olet kirjautunut ulkoisen palvelun kautta, joten salasana- ja sähköpostiasetukset eivät ole käytettävissä. signed_in_as: 'Kirjautunut tilillä:' verification: diff --git a/config/locales/fo.yml b/config/locales/fo.yml index 03a525fa5d..dabaf24ba7 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -1843,6 +1843,7 @@ fo: go_to_sso_account_settings: Far til kontustillingarnar hjá samleikaveitaranum hjá tær invalid_otp_token: Ógyldug tvey-stigs koda otp_lost_help_html: Hevur tú mist atgongd til bæði, so kanst tú koma í samband við %{email} + rate_limited: Ov nógvar samgildisroyndir, royn aftur seinni. seamless_external_login: Tú er ritað/ur inn umvegis eina uttanhýsis tænastu, so loyniorð og teldupoststillingar eru ikki tøkar. signed_in_as: 'Ritað/ur inn sum:' verification: diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 1398f6ad0b..3c43a4e23d 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -1843,6 +1843,7 @@ gl: go_to_sso_account_settings: Ir aos axustes da conta no teu provedor de identidade invalid_otp_token: O código do segundo factor non é válido otp_lost_help_html: Se perdes o acceso a ambos, podes contactar con %{email} + rate_limited: Demasiados intentos de autenticación, inténtao máis tarde. seamless_external_login: Accedeches a través dun servizo externo, polo que os axustes de contrasinal e email non están dispoñibles. signed_in_as: 'Rexistrada como:' verification: diff --git a/config/locales/he.yml b/config/locales/he.yml index 2969cf33e8..db57912d89 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1907,6 +1907,7 @@ he: go_to_sso_account_settings: מעבר לאפיוני החשבון שלך בשרת הזהות invalid_otp_token: קוד דו-שלבי שגוי otp_lost_help_html: אם איבדת גישה לשניהם, ניתן ליצור קשר ב-%{email} + rate_limited: יותר מדי ניסיונות אימות, נסו שוב מאוחר יותר. seamless_external_login: את.ה מחובר דרך שירות חיצוני, לכן אפשרויות הסיסמא והדוא"ל לא מאופשרות. signed_in_as: 'מחובר בתור:' verification: diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 7cfd7d80e2..8fce206e9e 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1843,6 +1843,7 @@ hu: go_to_sso_account_settings: Ugrás az azonosítási szolgáltatód fiókbeállításaihoz invalid_otp_token: Érvénytelen ellenőrző kód otp_lost_help_html: Ha mindkettőt elvesztetted, kérhetsz segítséget itt %{email} + rate_limited: Túl sok hiteleítési kísérlet történt. Próbáld újra később. seamless_external_login: Külső szolgáltatáson keresztül jelentkeztél be, így a jelszó és e-mail beállítások nem elérhetőek. signed_in_as: Bejelentkezve mint verification: diff --git a/config/locales/ie.yml b/config/locales/ie.yml index c8cd5d5f8d..c77a8f802d 100644 --- a/config/locales/ie.yml +++ b/config/locales/ie.yml @@ -1786,6 +1786,7 @@ ie: subtitle: Tui apelle ha esset rejectet. title: Apelle rejectet backup_ready: + explanation: Tu petit un complet archive de tui conto de Mastodon. extra: It es ja pret a descargar! subject: Tui archive es pret por descargar title: Descargar archive @@ -1842,6 +1843,7 @@ ie: go_to_sso_account_settings: Ear al parametres de conto de tui provisor de identification invalid_otp_token: Ínvalid 2-factor code otp_lost_help_html: Si tu perdit accesse a ambis, tu posse contacter %{email} + rate_limited: Tro mult de provas de autentication, ples provar denov plu tard. seamless_external_login: Tu ha intrat per un servicie external, dunc parametres pri tui passa-parol e email-adresse ne es disponibil. signed_in_as: 'Session apertet quam:' verification: diff --git a/config/locales/is.yml b/config/locales/is.yml index 9f8d5d42dc..b048d5cb00 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -1847,6 +1847,7 @@ is: go_to_sso_account_settings: Fara í stillingar aðgangsins hjá auðkennisveitunni þinni invalid_otp_token: Ógildur tveggja-þátta kóði otp_lost_help_html: Ef þú hefur misst aðganginn að hvoru tveggja, geturðu sett þig í samband við %{email} + rate_limited: Of margar tilraunir til auðkenningar, prófaðu aftur síðar. seamless_external_login: Innskráning þín er í gegnum utanaðkomandi þjónustu, þannig að stillingar fyrir lykilorð og tölvupóst eru ekki aðgengilegar. signed_in_as: 'Skráð inn sem:' verification: diff --git a/config/locales/it.yml b/config/locales/it.yml index a17fae4804..adcef9559f 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1845,6 +1845,7 @@ it: go_to_sso_account_settings: Vai alle impostazioni dell'account del tuo provider di identità invalid_otp_token: Codice d'accesso non valido otp_lost_help_html: Se perdessi l'accesso ad entrambi, puoi entrare in contatto con %{email} + rate_limited: Troppi tentativi di autenticazione, per favore riprova più tardi. seamless_external_login: Hai effettuato l'accesso tramite un servizio esterno, quindi le impostazioni di password e e-mail non sono disponibili. signed_in_as: 'Hai effettuato l''accesso come:' verification: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index a68426cb53..c966cbe36f 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1813,6 +1813,7 @@ ja: go_to_sso_account_settings: 外部サービスアカウントの設定はこちらで行ってください invalid_otp_token: 二要素認証コードが間違っています otp_lost_help_html: どちらも使用できない場合、%{email}に連絡を取ると解決できるかもしれません + rate_limited: 認証に失敗した回数が多すぎます。時間をおいてからログインしてください。 seamless_external_login: あなたは外部サービスを介してログインしているため、パスワードとメールアドレスの設定は利用できません。 signed_in_as: '下記でログイン中:' verification: diff --git a/config/locales/ko.yml b/config/locales/ko.yml index b85b9b5861..946aa35657 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1813,6 +1813,7 @@ ko: go_to_sso_account_settings: ID 공급자의 계정 설정으로 이동 invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다 otp_lost_help_html: 만약 양쪽 모두를 잃어버렸다면 %{email}을 통해 복구할 수 있습니다 + rate_limited: 너무 많은 인증 시도가 있었습니다, 잠시 후에 시도하세요. seamless_external_login: 외부 서비스를 이용해 로그인했으므로 이메일과 암호는 설정할 수 없습니다. signed_in_as: '다음과 같이 로그인 중:' verification: diff --git a/config/locales/lad.yml b/config/locales/lad.yml index d1247fc781..5a09c4c609 100644 --- a/config/locales/lad.yml +++ b/config/locales/lad.yml @@ -384,6 +384,7 @@ lad: cancel: Anula confirm: Suspende permanent_action: Si kites la suspensyon no restoraras dingunos datos ni relasyones. + preamble_html: Estas a punto de suspender %{domain} i sus subdomenos. remove_all_data: Esto efasara todo el kontenido, multimedia i datos de profiles de los kuentos en este domeno de tu sirvidor. stop_communication: Tu sirvidor deshara de komunikarse kon estos sirvidores. title: Konfirma bloko de domeno para %{domain} @@ -608,6 +609,7 @@ lad: created_at: Raportado delete_and_resolve: Efasa publikasyones forwarded: Reembiado + forwarded_replies_explanation: Este raporto vyene de un utilizador remoto i es sovre kontenido remoto. Tiene sido reembiado a ti porke el kontenido raportado esta en una repuesta a uno de tus utilizadores. forwarded_to: Reembiado a %{domain} mark_as_resolved: Marka komo rezolvido mark_as_sensitive: Marka komo sensivle @@ -712,6 +714,7 @@ lad: manage_users: Administra utilizadores manage_users_description: Permete a los utilizadores ver los peratim de otros utilizadores i realizar aksyones de moderasyon kontra eyos manage_webhooks: Administrar webhooks + manage_webhooks_description: Permite a los utilizadores konfigurar webhooks para evenimientos administrativos view_audit_log: Mostra defter de revisyon view_audit_log_description: Permete a los utilizadores ver una estoria de aksyones administrativas en el sirvidor view_dashboard: Ve pano @@ -738,6 +741,8 @@ lad: branding: preamble: La marka de tu sirvidor lo desferensia de otros sirvidores de la red. Esta enformasyon puede amostrarse por una varieta de entornos, komo en la enterfaz web de Mastodon, en aplikasyones nativas, en previsualizasiones de atadijos en otros sitios internetikos i en aplikasyones de mesajes, etc. Por esta razon, es mijor mantener esta enformasyon klara, breve i konsiza. title: Marka + captcha_enabled: + title: Solisita ke los muevos utilizadores rezolven un CAPTCHA para konfirmar su konto content_retention: preamble: Kontrola komo el kontenido jenerado por el utilizador se magazina en Mastodon. title: Retensyon de kontenido @@ -765,6 +770,9 @@ lad: approved: Se rekiere achetasion para enrejistrarse none: Permete a los utilizadores trokar la konfigurasyon del sitio open: Kualkiera puede enrejistrarse + security: + authorized_fetch_overridden_hint: Agora no puedes trokar esta konfigurasyon dkee esta sovreeskrita por una variable de entorno. + federation_authentication: Forzamyento de autentifikasyon para la federasyon title: Konfigurasyon del sirvidor site_uploads: delete: Efasa dosya kargada @@ -820,8 +828,13 @@ lad: system_checks: database_schema_check: message_html: Ay migrasyones asperando de la baza de datos. Por favor, egzekutalas para asigurarte de ke la aplikasyon fonksiona komo deveria + elasticsearch_health_red: + message_html: El klaster de Elasticsearch no es sano (estado kolorado), funksyones de bushkeda no estan disponivles + elasticsearch_health_yellow: + message_html: El klaster de Elasticsearch no es sano (estado amariyo), es posivle ke keras investigar la razon elasticsearch_preset: action: Ve dokumentasyon + message_html: Tu klaster de Elasticsearch tiene mas ke un nodo, ama Mastodon no esta konfigurado para uzarlos. elasticsearch_preset_single_node: action: Ve dokumentasyon elasticsearch_running_check: @@ -1012,12 +1025,17 @@ lad: auth: apply_for_account: Solisita un kuento captcha_confirmation: + help_html: Si tyenes problemas kon rezolver el CAPTCHA, puedes kontaktarnos en %{email} i podremos ayudarte. + hint_html: Una koza mas! Tenemos ke konfirmar ke eres umano (para evitar spam!). Rezolve el CAPTCHA abasho i klika "Kontinua". title: Kontrolo de sigurita confirmations: + awaiting_review: Tu adreso de posta tiene sido konfirmado! La taifa de %{domain} esta revizando tu enrejistrasyon. Risiviras un meil si acheten tu kuento! awaiting_review_title: Estamos revizando tu enrejistramiento clicking_this_link: klikando en este atadijo login_link: konektate kon kuento proceed_to_login_html: Agora puedes ir a %{login_link}. + redirect_to_app_html: Seras readresado a la aplikasyon %{app_name}. Si esto no afita, aprova %{clicking_this_link} o regresa manualmente a la aplikasyon. + registration_complete: Tu enrejistrasyon en %{domain} ya esta kompletada! welcome_title: Bienvenido, %{name}! wrong_email_hint: Si este adreso de posta es inkorekto, puedes trokarlo en las preferensyas del kuento. delete_account: Efasa kuento @@ -1054,6 +1072,7 @@ lad: rules: accept: Acheta back: Atras + invited_by: 'Puedes adjuntarte a %{domain} grasyas a la envitasyon de:' preamble: Estas son establesidas i aplikadas por los moderadores de %{domain}. preamble_invited: Antes de kontinuar, por favor reviza las reglas del sirvidor establesidas por los moderatores de %{domain}. title: Algunas reglas bazikas. @@ -1078,6 +1097,7 @@ lad: functional: Tu kuento esta kompletamente funksyonal. pending: Tu solisitasyon esta asperando la revizion por muestros administradores. Esto puede tadrar algun tiempo. Arisiviras una posta elektronika si la solisitasyon sea achetada. redirecting_to: Tu kuento se topa inaktivo porke esta siendo readresado a %{acct}. + self_destruct: Deke %{domain} va a serrarse, solo tendras akseso limitado a tu kuento. view_strikes: Ve amonestamientos pasados kontra tu kuento too_fast: Formulario enviado demaziado rapido, aprovalo de muevo. use_security_key: Uza la yave de sigurita @@ -1271,6 +1291,19 @@ lad: merge_long: Manten rejistros egzistentes i adjusta muevos overwrite: Sobreskrive overwrite_long: Mete muevos rejistros en vez de los aktuales + overwrite_preambles: + blocking_html: Estas a punto de substituyir tu lista de blokos por asta %{total_items} kuentos de %{filename}. + bookmarks_html: Estas a punto de substituyir tus markadores por asta %{total_items} publikasyones ke vinyeron de %{filename}. + domain_blocking_html: Estas a punto de substituyir tu lista de blokos de domeno por asta %{total_items} domenos de %{filename}. + following_html: Estas a punto de segir asta %{total_items} kuentos de %{filename} i deshar de segir todos los otros kuentos. + lists_html: Estas a punto de sustituyir tus listas con el kontenido de %{filename}. Asta %{total_items} kuentos seran adjustados a muevas listas. + muting_html: Estas a punto de substituyir tu lista de kuentos silensyados por asta %{total_items} kuentos de %{filename}. + preambles: + blocking_html: Estas a punto de blokar asta %{total_items} kuentos de %{filename}. + bookmarks_html: Estas a punto de adjustar asta %{total_items} publikasyones de %{filename} a tus markadores. + domain_blocking_html: Estas a punto de blokar asta %{total_items} domenos de %{filename}. + following_html: Estas a punto de segir asta %{total_items} kuentos de %{filename}. + muting_html: Estas a punto de silensyar asta %{total_items} kuentos de %{filename}. preface: Puedes importar siertos datos, komo todas las personas a las kualas estas sigiendo o blokando en tu kuento en esta instansya, dizde dosyas eksportadas de otra instansya. recent_imports: Importasyones resyentes states: @@ -1474,7 +1507,9 @@ lad: public_timelines: Linyas de tiempo publikas privacy: privacy: Privasita + reach: Alkanse search: Bushkeda + title: Privasita i alkanse privacy_policy: title: Politika de privasita reactions: @@ -1711,6 +1746,7 @@ lad: action: Preferensyas de kuento explanation: La apelasyon del amonestamiento kontra tu kuento del %{strike_date} ke mandates el %{appeal_date} fue achetada. Tu kuento se topa de muevo en dobro estado. subject: Tu apelasyon del %{date} fue achetada + subtitle: Tu konto de muevo tiene una reputasyon buena. title: Apelasyon achetada appeal_rejected: explanation: La apelasyon del amonestamiento kontra tu kuento del %{strike_date} ke mandates el %{appeal_date} fue refuzada. @@ -1718,6 +1754,7 @@ lad: subtitle: Tu apelasyon fue refuzada. title: Apelasyon refuzada backup_ready: + extra: Agora esta pronto para abashar! subject: Tu dosya esta pronta para abashar title: Abasha dosya suspicious_sign_in: @@ -1773,6 +1810,8 @@ lad: go_to_sso_account_settings: Va a la konfigurasyon de kuento de tu prokurador de identita invalid_otp_token: Kodiche de dos pasos no valido otp_lost_help_html: Si pedriste akseso a los dos, puedes kontaktarte kon %{email} + rate_limited: Demaziadas provas de autentifikasyon, aprova de muevo dempues. + seamless_external_login: Estas konektado por un servisyo eksterno i estonses la konfigurasyon de kod i konto de posta no estan disponivles. signed_in_as: 'Konektado komo:' verification: here_is_how: Ansina es komo @@ -1785,6 +1824,7 @@ lad: success: Tu yave de sigurita fue adjustada kon sukseso. delete: Efasa delete_confirmation: Estas siguro ke keres efasar esta yave de sigurita? + description_html: Si kapasites autentifikasyon kon yave de sigurita, nesesitaras uno de tus yaves de sigurita para konektarte kon tu kuento. destroy: error: Uvo un problem al efasar tu yave de sigurita. Por favor aprova de muevo. success: Tu yave de sigurita fue efasada kon sukseso. diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 9235b99fed..5ffa788a8c 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1843,6 +1843,7 @@ nl: go_to_sso_account_settings: Ga naar de accountinstellingen van je identiteitsprovider invalid_otp_token: Ongeldige tweestaps-toegangscode otp_lost_help_html: Als je toegang tot beiden kwijt bent geraakt, neem dan contact op via %{email} + rate_limited: Te veel authenticatiepogingen, probeer het later opnieuw. seamless_external_login: Je bent ingelogd via een externe dienst, daarom zijn wachtwoorden en e-mailinstellingen niet beschikbaar. signed_in_as: 'Ingelogd als:' verification: diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 914ee7fb04..626252be00 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1608,6 +1608,7 @@ nn: unknown_browser: Ukjend nettlesar weibo: Weibo current_session: Noverande økt + date: Dato description: "%{browser} på %{platform}" explanation: Desse nettlesarane er logga inn på Mastodon-kontoen din. ip: IP-adresse @@ -1774,14 +1775,19 @@ nn: webauthn: Sikkerhetsnøkler user_mailer: appeal_approved: + action: Kontoinnstillinger explanation: Apellen på prikken mot din kontor på %{strike_date} som du la inn på %{appeal_date} har blitt godkjend. Din konto er nok ein gong i god stand. subject: Din klage fra %{date} er godkjent + subtitle: Kontoen din er tilbake i god stand. title: Anke godkjend appeal_rejected: explanation: Klagen på advarselen mot din konto den %{strike_date} som du sendte inn den %{appeal_date} har blitt avvist. subject: Din klage fra %{date} er avvist + subtitle: Anken din har blitt avvist. title: Anke avvist backup_ready: + explanation: Du etterspurte en fullstendig sikkerhetskopi av din Mastodon-konto. + extra: Den er nå klar for nedlasting! subject: Arkivet ditt er klart til å lastes ned title: Nedlasting av arkiv suspicious_sign_in: @@ -1837,6 +1843,7 @@ nn: go_to_sso_account_settings: Gå til kontoinnstillingane hjå identitetsleverandøren din invalid_otp_token: Ugyldig tostegskode otp_lost_help_html: Hvis du mistet tilgangen til begge deler, kan du komme i kontakt med %{email} + rate_limited: For mange autentiseringsforsøk, prøv igjen seinare. seamless_external_login: Du er logga inn gjennom eit eksternt reiskap, so passord og e-postinstillingar er ikkje tilgjengelege. signed_in_as: 'Logga inn som:' verification: diff --git a/config/locales/no.yml b/config/locales/no.yml index 61cc89181e..d90aa5bab5 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -229,7 +229,7 @@ update_status: Oppdater statusen update_user_role: Oppdater rolle actions: - approve_appeal_html: "%{name} godkjente klagen på modereringa fra %{target}" + approve_appeal_html: "%{name} godkjente anken på moderering fra %{target}" approve_user_html: "%{name} godkjente registrering fra %{target}" assigned_to_self_report_html: "%{name} tildelte rapport %{target} til seg selv" change_email_user_html: "%{name} endret e-postadressen til brukeren %{target}" @@ -266,7 +266,7 @@ enable_user_html: "%{name} aktiverte innlogging for bruker %{target}" memorialize_account_html: "%{name} endret %{target}s konto til en minneside" promote_user_html: "%{name} forfremmet bruker %{target}" - reject_appeal_html: "%{name} avviste moderasjonsavgjørelsesklagen fra %{target}" + reject_appeal_html: "%{name} avviste anken på moderering fra %{target}" reject_user_html: "%{name} avslo registrering fra %{target}" remove_avatar_user_html: "%{name} fjernet %{target} sitt profilbilde" reopen_report_html: "%{name} gjenåpnet rapporten %{target}" @@ -372,8 +372,8 @@ website: Nettside disputes: appeals: - empty: Ingen klager funnet. - title: Klager + empty: Ingen anker funnet. + title: Anker domain_allows: add_new: Hvitelist domene created_msg: Domenet har blitt hvitelistet @@ -692,8 +692,8 @@ invite_users_description: Lar brukere invitere nye personer til serveren manage_announcements: Behandle Kunngjøringer manage_announcements_description: Lar brukere endre kunngjøringer på serveren - manage_appeals: Behandle klager - manage_appeals_description: Lar brukere gjennomgå klager mot modereringsaktiviteter + manage_appeals: Behandle anker + manage_appeals_description: Lar brukere gjennomgå anker mot modereringsaktiviteter manage_blocks: Behandle Blokker manage_blocks_description: Lar brukere blokkere e-postleverandører og IP-adresser manage_custom_emojis: Administrer egendefinerte Emojier @@ -829,8 +829,8 @@ sensitive: "%{name} merket %{target}s konto som følsom" silence: "%{name} begrenset %{target}s konto" suspend: "%{name} suspenderte %{target}s konto" - appeal_approved: Klage tatt til følge - appeal_pending: Klage behandles + appeal_approved: Anket + appeal_pending: Anke behandles appeal_rejected: Anke avvist system_checks: database_schema_check: @@ -975,9 +975,9 @@ sensitive: å merke kontoen sin som følsom silence: for å begrense deres konto suspend: for å avslutte kontoen - body: "%{target} klager på en moderasjonsbeslutning av %{action_taken_by} fra %{date}, noe som var %{type}. De skrev:" - next_steps: Du kan godkjenne klagen for å angre på moderasjonsvedtaket eller ignorere det. - subject: "%{username} klager på en moderasjonsbeslutning for %{instance}" + body: "%{target} anker en moderasjonsbeslutning av %{action_taken_by} fra %{date}, noe som var %{type}. De skrev:" + next_steps: Du kan godkjenne anken for å angre på moderasjonsvedtaket eller ignorere det. + subject: "%{username} anker en moderasjonsbeslutning for %{instance}" new_critical_software_updates: body: Nye kritiske versjoner av Mastodon har blitt utgitt, det kan være fordelaktig å oppdatere så snart som mulig! subject: Kritiske Mastodon-oppdateringer er tilgjengelige for %{instance}! @@ -1161,19 +1161,19 @@ disputes: strikes: action_taken: Handling utført - appeal: Klage - appeal_approved: Denne advarselens klage ble tatt til følge og er ikke lenger gyldig - appeal_rejected: Klagen ble avvist - appeal_submitted_at: Klage levert - appealed_msg: Din klage har blitt levert. Du får beskjed om den blir godkjent. + appeal: Anke + appeal_approved: Denne advarselens anke ble tatt til følge og er ikke lenger gyldig + appeal_rejected: Anken ble avvist + appeal_submitted_at: Anke levert + appealed_msg: Anken din har blitt levert. Du får beskjed om den blir godkjent. appeals: - submit: Lever klage - approve_appeal: Godkjenn klage + submit: Lever anke + approve_appeal: Godkjenn anke associated_report: Tilhørende rapport created_at: Datert description_html: Dette er tiltakene mot din konto og advarsler som har blitt sent til deg av %{instance}-personalet. recipient: Adressert til - reject_appeal: Avvis klage + reject_appeal: Avvis anke status: 'Innlegg #%{id}' status_removed: Innlegg allerede fjernet fra systemet title: "%{action} fra %{date}" @@ -1185,9 +1185,9 @@ sensitive: Merking av konto som sensitiv silence: Begrensning av konto suspend: Suspensjon av konto - your_appeal_approved: Din klage har blitt godkjent - your_appeal_pending: Du har levert en klage - your_appeal_rejected: Din klage har blitt avvist + your_appeal_approved: Anken din har blitt godkjent + your_appeal_pending: Du har levert en anke + your_appeal_rejected: Anken din har blitt avvist domain_validator: invalid_domain: er ikke et gyldig domenenavn edit_profile: @@ -1608,6 +1608,7 @@ unknown_browser: Ukjent Nettleser weibo: Weibo current_session: Nåværende økt + date: Dato description: "%{browser} på %{platform}" explanation: Dette er nettlesere som er pålogget på din Mastodon-konto akkurat nå. ip: IP-adresse @@ -1740,7 +1741,7 @@ sensitive_content: Følsomt innhold strikes: errors: - too_late: Det er for sent å klage på denne advarselen + too_late: Det er for sent å anke denne advarselen tags: does_not_match_previous_name: samsvarer ikke med det forrige navnet themes: @@ -1774,14 +1775,19 @@ webauthn: Sikkerhetsnøkler user_mailer: appeal_approved: - explanation: Klagen på advarselen mot din konto den %{strike_date} som du sendte inn den %{appeal_date} har blitt godkjent. Din konto er nok en gang i god stand. - subject: Din klage fra %{date} er godkjent - title: Klage godkjent + action: Kontoinnstillinger + explanation: Anken på advarselen mot din konto den %{strike_date} som du sendte inn den %{appeal_date} har blitt godkjent. Din konto er nok en gang i god stand. + subject: Anken din fra %{date} er godkjent + subtitle: Kontoen din er tilbake i god stand. + title: Anke godkjent appeal_rejected: - explanation: Klagen på advarselen mot din konto den %{strike_date} som du sendte inn den %{appeal_date} har blitt avvist. - subject: Din klage fra %{date} er avvist - title: Klage avvist + explanation: Anken på advarselen mot din konto den %{strike_date} som du sendte inn den %{appeal_date} har blitt avvist. + subject: Anken din fra %{date} er avvist + subtitle: Anken din har blitt avvist. + title: Anke avvist backup_ready: + explanation: Du etterspurte en fullstendig sikkerhetskopi av din Mastodon-konto. + extra: Den er nå klar for nedlasting! subject: Arkivet ditt er klart til å lastes ned title: Nedlasting av arkiv suspicious_sign_in: @@ -1792,8 +1798,8 @@ subject: Din konto ble tatt i bruk fra en ny IP-adresse title: En ny pålogging warning: - appeal: Lever en klage - appeal_description: Hvis du mener dette er feil, kan du sende inn en klage til personalet i %{instance}. + appeal: Lever en anke + appeal_description: Hvis du mener dette er feil, kan du sende inn en anke til personalet i %{instance}. categories: spam: Søppelpost violation: Innholdet bryter følgende retningslinjer for fellesskapet @@ -1837,6 +1843,7 @@ go_to_sso_account_settings: Gå til din identitetsleverandørs kontoinnstillinger invalid_otp_token: Ugyldig to-faktorkode otp_lost_help_html: Hvis du mistet tilgangen til begge deler, kan du komme i kontakt med %{email} + rate_limited: For mange autentiseringsforsøk, prøv igjen senere. seamless_external_login: Du er logget inn via en ekstern tjeneste, så passord og e-post innstillinger er ikke tilgjengelige. signed_in_as: 'Innlogget som:' verification: diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 8a973b71c7..4d8fde8f4f 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1907,6 +1907,7 @@ pl: go_to_sso_account_settings: Przejdź do ustawień konta dostawcy tożsamości invalid_otp_token: Kod uwierzytelniający jest niepoprawny otp_lost_help_html: Jeżeli utracisz dostęp do obu, możesz skontaktować się z %{email} + rate_limited: Zbyt wiele prób uwierzytelnienia. Spróbuj ponownie później. seamless_external_login: Zalogowano z użyciem zewnętrznej usługi, więc ustawienia hasła i adresu e-mail nie są dostępne. signed_in_as: 'Zalogowano jako:' verification: diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index fc1e3e6367..2e077b37a8 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -1843,6 +1843,7 @@ pt-PT: go_to_sso_account_settings: Ir para as definições de conta do seu fornecedor de identidade invalid_otp_token: Código de autenticação inválido otp_lost_help_html: Se perdeu o acesso a ambos, pode entrar em contacto com %{email} + rate_limited: Demasiadas tentativas de autenticação, tente novamente mais tarde. seamless_external_login: Tu estás ligado via um serviço externo. Por isso, as configurações da palavra-passe e do e-mail não estão disponíveis. signed_in_as: 'Registado como:' verification: diff --git a/config/locales/simple_form.no.yml b/config/locales/simple_form.no.yml index ca2020e21e..7651792212 100644 --- a/config/locales/simple_form.no.yml +++ b/config/locales/simple_form.no.yml @@ -36,7 +36,7 @@ starts_at: Valgfritt. I tilfellet din kunngjøring er bundet til en spesifikk tidsramme text: Du kan bruke innlegg-syntaks. Vennligst vær oppmerksom på plassen som kunngjøringen vil ta opp på brukeren sin skjerm appeal: - text: Du kan kun klage på en advarsel en gang + text: Du kan kun anke en advarsel en gang defaults: autofollow: Folk som lager en konto gjennom invitasjonen, vil automatisk følge deg avatar: PNG, GIF eller JPG. Maksimalt %{size}. Vil bli nedskalert til %{dimensions}px @@ -282,7 +282,7 @@ sign_up_requires_approval: Begrens påmeldinger severity: Oppføring notification_emails: - appeal: Noen klager på en moderator sin avgjørelse + appeal: Noen anker en moderator sin avgjørelse digest: Send sammendrag på e-post favourite: Send e-post når noen setter din status som favoritt follow: Send e-post når noen følger deg diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 89f456a205..c639bbe1a6 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -732,6 +732,7 @@ sk: new_appeal: actions: none: varovanie + silence: obmedziť ich účet new_pending_account: body: Podrobnosti o novom účte sú uvedené nižšie. Môžeš túto registračnú požiadavku buď prijať, alebo zamietnúť. subject: Nový účet očakáva preverenie na %{instance} (%{username}) @@ -1279,6 +1280,7 @@ sk: follow_limit_reached: Nemôžeš následovať viac ako %{limit} ľudí invalid_otp_token: Neplatný kód pre dvojfaktorovú autentikáciu otp_lost_help_html: Pokiaľ si stratil/a prístup k obom, môžeš dať vedieť %{email} + rate_limited: Príliš veľa pokusov o overenie, skús to znova neskôr. seamless_external_login: Si prihlásená/ý cez externú službu, takže nastavenia hesla a emailu ti niesú prístupné. signed_in_as: 'Prihlásená/ý ako:' verification: diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 1a0afe034f..ba707f49eb 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -1907,6 +1907,7 @@ sl: go_to_sso_account_settings: Pojdite na nastavitve svojega računa ponudnika identitete invalid_otp_token: Neveljavna dvofaktorska koda otp_lost_help_html: Če ste izgubili dostop do obeh, stopite v stik z %{email} + rate_limited: Preveč poskusov preverjanja pristnosti, poskusite kasneje. seamless_external_login: Prijavljeni ste prek zunanje storitve, tako da nastavitve gesla in e-pošte niso na voljo. signed_in_as: 'Vpisani kot:' verification: diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index fc1239bedf..39c9f2f873 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -1875,6 +1875,7 @@ sr-Latn: go_to_sso_account_settings: Idite na podešavanja naloga svog dobavljača identiteta invalid_otp_token: Neispravni dvofaktorski kod otp_lost_help_html: Ako izgubite pristup za oba, možete stupiti u kontakt sa %{email} + rate_limited: Previše pokušaja autentifikacije, pokušajte ponovo kasnije. seamless_external_login: Prijavljeni ste putem spoljašnje usluge, tako da lozinka i podešavanja E-pošte nisu dostupni. signed_in_as: 'Prijavljen/a kao:' verification: diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 4e5e58c859..0cf35c14cc 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -1875,6 +1875,7 @@ sr: go_to_sso_account_settings: Идите на подешавања налога свог добављача идентитета invalid_otp_token: Неисправни двофакторски код otp_lost_help_html: Ако изгубите приступ за оба, можете ступити у контакт са %{email} + rate_limited: Превише покушаја аутентификације, покушајте поново касније. seamless_external_login: Пријављени сте путем спољашње услуге, тако да лозинка и подешавања Е-поште нису доступни. signed_in_as: 'Пријављен/а као:' verification: diff --git a/config/locales/sv.yml b/config/locales/sv.yml index d4657e9743..3a82f29d2f 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -1842,6 +1842,7 @@ sv: go_to_sso_account_settings: Gå till din identitetsleverantörs kontoinställningar invalid_otp_token: Ogiltig tvåfaktorskod otp_lost_help_html: Om du förlorat åtkomst till båda kan du komma i kontakt med %{email} + rate_limited: För många autentiseringsförsök, försök igen senare. seamless_external_login: Du är inloggad via en extern tjänst, inställningar för lösenord och e-post är därför inte tillgängliga. signed_in_as: 'Inloggad som:' verification: diff --git a/config/locales/th.yml b/config/locales/th.yml index 7bea8f9de8..ac5cfbacf5 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -847,7 +847,7 @@ th: message_html: ไม่มีกระบวนการ Sidekiq ที่กำลังทำงานสำหรับคิว %{value} โปรดตรวจทานการกำหนดค่า Sidekiq ของคุณ software_version_critical_check: action: ดูการอัปเดตที่พร้อมใช้งาน - message_html: มีการอัปเดต Mastodon สำคัญพร้อมใช้งาน โปรดอัปเดตโดยเร็วที่สุดเท่าที่จะทำได้ + message_html: มีการอัปเดต Mastodon สำคัญพร้อมใช้งาน โปรดอัปเดตโดยเร็วที่สุดเท่าที่จะเป็นไปได้ software_version_patch_check: action: ดูการอัปเดตที่พร้อมใช้งาน message_html: มีการอัปเดต Mastodon ที่แก้ไขข้อบกพร่องพร้อมใช้งาน @@ -961,7 +961,7 @@ th: next_steps: คุณสามารถอนุมัติการอุทธรณ์เพื่อเลิกทำการตัดสินใจในการควบคุม หรือเพิกเฉยต่อการอุทธรณ์ subject: "%{username} กำลังอุทธรณ์การตัดสินใจในการควบคุมใน %{instance}" new_critical_software_updates: - body: มีการปล่อยรุ่น Mastodon สำคัญใหม่ คุณอาจต้องการอัปเดตโดยเร็วที่สุดเท่าที่จะทำได้! + body: มีการปล่อยรุ่น Mastodon สำคัญใหม่ คุณอาจต้องการอัปเดตโดยเร็วที่สุดเท่าที่จะเป็นไปได้! subject: การอัปเดต Mastodon สำคัญพร้อมใช้งานสำหรับ %{instance}! new_pending_account: body: รายละเอียดของบัญชีใหม่อยู่ด้านล่าง คุณสามารถอนุมัติหรือปฏิเสธใบสมัครนี้ @@ -1582,6 +1582,7 @@ th: unknown_browser: เบราว์เซอร์ที่ไม่รู้จัก weibo: Weibo current_session: เซสชันปัจจุบัน + date: วันที่ description: "%{browser} ใน %{platform}" explanation: นี่คือเว็บเบราว์เซอร์ที่เข้าสู่ระบบบัญชี Mastodon ของคุณในปัจจุบัน ip: IP @@ -1742,14 +1743,19 @@ th: webauthn: กุญแจความปลอดภัย user_mailer: appeal_approved: + action: การตั้งค่าบัญชี explanation: อนุมัติการอุทธรณ์การดำเนินการต่อบัญชีของคุณเมื่อ %{strike_date} ที่คุณได้ส่งเมื่อ %{appeal_date} แล้ว บัญชีของคุณอยู่ในสถานะที่ดีอีกครั้งหนึ่ง subject: อนุมัติการอุทธรณ์ของคุณจาก %{date} แล้ว + subtitle: บัญชีของคุณอยู่ในสถานะที่ดีอีกครั้งหนึ่ง title: อนุมัติการอุทธรณ์แล้ว appeal_rejected: explanation: ปฏิเสธการอุทธรณ์การดำเนินการต่อบัญชีของคุณเมื่อ %{strike_date} ที่คุณได้ส่งเมื่อ %{appeal_date} แล้ว subject: ปฏิเสธการอุทธรณ์ของคุณจาก %{date} แล้ว + subtitle: ปฏิเสธการอุทธรณ์ของคุณแล้ว title: ปฏิเสธการอุทธรณ์แล้ว backup_ready: + explanation: คุณได้ขอข้อมูลสำรองแบบเต็มของบัญชี Mastodon ของคุณ + extra: ตอนนี้ข้อมูลสำรองพร้อมสำหรับการดาวน์โหลดแล้ว! subject: การเก็บถาวรของคุณพร้อมสำหรับการดาวน์โหลดแล้ว title: การส่งออกการเก็บถาวร suspicious_sign_in: @@ -1805,6 +1811,7 @@ th: go_to_sso_account_settings: ไปยังการตั้งค่าบัญชีของผู้ให้บริการข้อมูลประจำตัวของคุณ invalid_otp_token: รหัสสองปัจจัยไม่ถูกต้อง otp_lost_help_html: หากคุณสูญเสียการเข้าถึงทั้งสองอย่าง คุณสามารถติดต่อ %{email} + rate_limited: มีความพยายามในการรับรองความถูกต้องมากเกินไป ลองอีกครั้งในภายหลัง seamless_external_login: คุณได้เข้าสู่ระบบผ่านบริการภายนอก ดังนั้นจึงไม่มีการตั้งค่ารหัสผ่านและอีเมล signed_in_as: 'ลงชื่อเข้าเป็น:' verification: diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 99b5e782ce..3b74c4eaa1 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1843,6 +1843,7 @@ tr: go_to_sso_account_settings: Kimlik sağlayıcı hesap ayarlarına gidin invalid_otp_token: Geçersiz iki adımlı doğrulama kodu otp_lost_help_html: Her ikisine de erişiminizi kaybettiyseniz, %{email} ile irtibata geçebilirsiniz + rate_limited: Çok fazla kimlik doğrulama denemesi. Daha sonra tekrar deneyin. seamless_external_login: Harici bir servis aracılığıyla oturum açtınız, bu nedenle parola ve e-posta ayarları mevcut değildir. signed_in_as: 'Oturum açtı:' verification: diff --git a/config/locales/uk.yml b/config/locales/uk.yml index a80fbf1404..40a858d72a 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1903,6 +1903,7 @@ uk: go_to_sso_account_settings: Перейдіть до налаштувань облікового запису постачальника ідентифікації invalid_otp_token: Введено неправильний код otp_lost_help_html: Якщо ви втратили доступ до обох, ви можете отримати доступ з %{email} + rate_limited: Занадто багато спроб з'єднання. Спробуйте ще раз пізніше. seamless_external_login: Ви увійшли за допомогою зовнішнього сервісу, тому налаштування паролю та електронної пошти недоступні. signed_in_as: 'Ви увійшли як:' verification: diff --git a/config/locales/vi.yml b/config/locales/vi.yml index dabb73a475..3817b18f07 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1811,6 +1811,7 @@ vi: go_to_sso_account_settings: Thiết lập tài khoản nhà cung cấp danh tính invalid_otp_token: Mã xác minh 2 bước không hợp lệ otp_lost_help_html: Nếu bạn mất quyền truy cập vào cả hai, bạn có thể đăng nhập bằng %{email} + rate_limited: Quá nhiều lần thử, vui lòng thử lại sau. seamless_external_login: Bạn đã đăng nhập thông qua một dịch vụ bên ngoài, vì vậy mật khẩu và email không khả dụng. signed_in_as: 'Đăng nhập bằng:' verification: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 6611510b7d..80bb5653ca 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1811,6 +1811,7 @@ zh-CN: go_to_sso_account_settings: 转到您的身份提供商进行账户设置 invalid_otp_token: 输入的双因素认证代码无效 otp_lost_help_html: 如果你不慎丢失了所有的代码,请联系 %{email} 寻求帮助 + rate_limited: 验证尝试次数过多,请稍后再试。 seamless_external_login: 因为你是通过外部服务登录的,所以密码和电子邮件地址设置都不可用。 signed_in_as: 当前登录的账户: verification: diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 4b682f9358..ac32c03e95 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -1811,6 +1811,7 @@ zh-HK: go_to_sso_account_settings: 前往你身份提供者的帳號設定 invalid_otp_token: 雙重認證碼不正確 otp_lost_help_html: 如果這兩者你均無法登入,你可以聯繫 %{email} + rate_limited: 嘗試認證次數太多,請稍後再試。 seamless_external_login: 因為你正在使用第三方服務登入,所以不能設定密碼和電郵。 signed_in_as: 目前登入的帳戶: verification: diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index dd17de7ef1..6662e44cdc 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1813,6 +1813,7 @@ zh-TW: go_to_sso_account_settings: 前往您的身分提供商 (identity provider) 之帳號設定 invalid_otp_token: 兩階段認證碼不正確 otp_lost_help_html: 如果您無法存取這兩者,您可以透過 %{email} 與我們聯繫 + rate_limited: 身份驗證嘗試太多次,請稍後再試。 seamless_external_login: 由於您是由外部系統登入,所以不能設定密碼與電子郵件。 signed_in_as: 目前登入的帳號: verification: From 5efb00ddb8cd8d4d36382a66e048e5c78424f9a1 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 22 Jan 2024 06:55:51 -0500 Subject: [PATCH 27/84] Use ruby version 3.2.3 (#28817) --- .ruby-version | 2 +- Dockerfile | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ruby-version b/.ruby-version index be94e6f53d..b347b11eac 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.2.3 diff --git a/Dockerfile b/Dockerfile index 96f8b5cd27..119c266b89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,15 +7,15 @@ ARG TARGETPLATFORM=${TARGETPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM} -# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"] -ARG RUBY_VERSION="3.2.2" +# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.3"] +ARG RUBY_VERSION="3.2.3" # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] ARG NODE_MAJOR_VERSION="20" # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] ARG DEBIAN_VERSION="bookworm" # Node image to use for base image based on combined variables (ex: 20-bookworm-slim) FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node -# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm) +# Ruby image to use for base image based on combined variables (ex: 3.2.3-slim-bookworm) FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA From 7ecf7f540309a968027ff6ac3874e3643f3fe3e1 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 22 Jan 2024 06:58:54 -0500 Subject: [PATCH 28/84] Move controller->request specs for api/v1/statuses/* (#28818) --- .../favourited_by_accounts_controller.rb | 2 +- .../reblogged_by_accounts_controller.rb | 2 +- .../statuses/favourited_by_accounts_spec.rb} | 60 +++++++++++-------- .../statuses/reblogged_by_accounts_spec.rb} | 53 +++++++++------- 4 files changed, 69 insertions(+), 48 deletions(-) rename spec/{controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb => requests/api/v1/statuses/favourited_by_accounts_spec.rb} (52%) rename spec/{controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb => requests/api/v1/statuses/reblogged_by_accounts_spec.rb} (57%) diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 3cca246ce8..98b69c347f 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas def load_accounts scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? scope.merge(paginated_favourites).to_a end diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index dd3e60846b..aacab5f8f4 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base def load_accounts scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? scope.merge(paginated_statuses).to_a end diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb similarity index 52% rename from spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb rename to spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb index 01816743e5..44296f4c37 100644 --- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb +++ b/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb @@ -2,21 +2,21 @@ require 'rails_helper' -RSpec.describe Api::V1::Statuses::FavouritedByAccountsController do - render_views - - let(:user) { Fabricate(:user) } - let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') } +RSpec.describe 'API V1 Statuses Favourited by Accounts' do + let(:user) { Fabricate(:user) } + let(:scopes) { 'read:accounts' } + # let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } let(:alice) { Fabricate(:account) } let(:bob) { Fabricate(:account) } context 'with an oauth token' do - before do - allow(controller).to receive(:doorkeeper_token) { token } + subject do + get "/api/v1/statuses/#{status.id}/favourited_by", headers: headers, params: { limit: 2 } end - describe 'GET #index' do + describe 'GET /api/v1/statuses/:status_id/favourited_by' do let(:status) { Fabricate(:status, account: user.account) } before do @@ -24,30 +24,38 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController do Favourite.create!(account: bob, status: status) end - it 'returns http success' do - get :index, params: { status_id: status.id, limit: 2 } - expect(response).to have_http_status(200) - expect(response.headers['Link'].links.size).to eq(2) - end + it 'returns http success and accounts who favourited the status' do + subject - it 'returns accounts who favorited the status' do - get :index, params: { status_id: status.id, limit: 2 } - expect(body_as_json.size).to eq 2 - expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) + expect(response) + .to have_http_status(200) + expect(response.headers['Link'].links.size) + .to eq(2) + + expect(body_as_json.size) + .to eq(2) + expect(body_as_json) + .to contain_exactly( + include(id: alice.id.to_s), + include(id: bob.id.to_s) + ) end it 'does not return blocked users' do user.account.block!(bob) - get :index, params: { status_id: status.id, limit: 2 } - expect(body_as_json.size).to eq 1 - expect(body_as_json[0][:id]).to eq alice.id.to_s + + subject + + expect(body_as_json.size) + .to eq 1 + expect(body_as_json.first[:id]).to eq(alice.id.to_s) end end end context 'without an oauth token' do - before do - allow(controller).to receive(:doorkeeper_token).and_return(nil) + subject do + get "/api/v1/statuses/#{status.id}/favourited_by", params: { limit: 2 } end context 'with a private status' do @@ -59,7 +67,8 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController do end it 'returns http unauthorized' do - get :index, params: { status_id: status.id } + subject + expect(response).to have_http_status(404) end end @@ -74,7 +83,8 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController do end it 'returns http success' do - get :index, params: { status_id: status.id } + subject + expect(response).to have_http_status(200) end end diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb similarity index 57% rename from spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb rename to spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb index 0d15cca75c..6f99ce9464 100644 --- a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb +++ b/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb @@ -2,21 +2,20 @@ require 'rails_helper' -RSpec.describe Api::V1::Statuses::RebloggedByAccountsController do - render_views - - let(:user) { Fabricate(:user) } - let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') } +RSpec.describe 'API V1 Statuses Reblogged by Accounts' do + let(:user) { Fabricate(:user) } + let(:scopes) { 'read:accounts' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } let(:alice) { Fabricate(:account) } let(:bob) { Fabricate(:account) } context 'with an oauth token' do - before do - allow(controller).to receive(:doorkeeper_token) { token } + subject do + get "/api/v1/statuses/#{status.id}/reblogged_by", headers: headers, params: { limit: 2 } end - describe 'GET #index' do + describe 'GET /api/v1/statuses/:status_id/reblogged_by' do let(:status) { Fabricate(:status, account: user.account) } before do @@ -25,27 +24,37 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController do end it 'returns accounts who reblogged the status', :aggregate_failures do - get :index, params: { status_id: status.id, limit: 2 } + subject - expect(response).to have_http_status(200) - expect(response.headers['Link'].links.size).to eq(2) + expect(response) + .to have_http_status(200) + expect(response.headers['Link'].links.size) + .to eq(2) - expect(body_as_json.size).to eq 2 - expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) + expect(body_as_json.size) + .to eq(2) + expect(body_as_json) + .to contain_exactly( + include(id: alice.id.to_s), + include(id: bob.id.to_s) + ) end it 'does not return blocked users' do user.account.block!(bob) - get :index, params: { status_id: status.id, limit: 2 } - expect(body_as_json.size).to eq 1 - expect(body_as_json[0][:id]).to eq alice.id.to_s + + subject + + expect(body_as_json.size) + .to eq 1 + expect(body_as_json.first[:id]).to eq(alice.id.to_s) end end end context 'without an oauth token' do - before do - allow(controller).to receive(:doorkeeper_token).and_return(nil) + subject do + get "/api/v1/statuses/#{status.id}/reblogged_by", params: { limit: 2 } end context 'with a private status' do @@ -57,7 +66,8 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController do end it 'returns http unauthorized' do - get :index, params: { status_id: status.id } + subject + expect(response).to have_http_status(404) end end @@ -72,7 +82,8 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController do end it 'returns http success' do - get :index, params: { status_id: status.id } + subject + expect(response).to have_http_status(200) end end From 18004bf22723b677345f417b24729c7e17dac36e Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 22 Jan 2024 08:55:37 -0500 Subject: [PATCH 29/84] Add `Account.matches_uri_prefix` scope and use in activitypub/followers_synchronizations controller (#28820) --- .../followers_synchronizations_controller.rb | 2 +- app/models/account.rb | 1 + spec/models/account_spec.rb | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 976caa3445..d2942104e5 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -24,7 +24,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro end def set_items - @items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%", false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri) + @items = @account.followers.matches_uri_prefix(uri_prefix).pluck(:uri) end def collection_presenter diff --git a/app/models/account.rb b/app/models/account.rb index 2fdfc2d514..05e1f943ca 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -123,6 +123,7 @@ class Account < ApplicationRecord scope :bots, -> { where(actor_type: %w(Application Service)) } scope :groups, -> { where(actor_type: 'Group') } scope :alphabetic, -> { order(domain: :asc, username: :asc) } + scope :matches_uri_prefix, ->(value) { where(arel_table[:uri].matches("#{sanitize_sql_like(value)}/%", false, true)).or(where(uri: value)) } scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) } diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 8488ccea45..7ef5ca94cc 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -835,6 +835,31 @@ RSpec.describe Account do end describe 'scopes' do + describe 'matches_uri_prefix' do + let!(:alice) { Fabricate :account, domain: 'host.example', uri: 'https://host.example/user/a' } + let!(:bob) { Fabricate :account, domain: 'top-level.example', uri: 'https://top-level.example' } + + it 'returns accounts which start with the value' do + results = described_class.matches_uri_prefix('https://host.example') + + expect(results.size) + .to eq(1) + expect(results) + .to include(alice) + .and not_include(bob) + end + + it 'returns accounts which equal the value' do + results = described_class.matches_uri_prefix('https://top-level.example') + + expect(results.size) + .to eq(1) + expect(results) + .to include(bob) + .and not_include(alice) + end + end + describe 'auditable' do let!(:alice) { Fabricate :account } let!(:bob) { Fabricate :account } From e2d9635074ad33cc8144adc434bcd90faae9c424 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 22 Jan 2024 14:55:43 +0100 Subject: [PATCH 30/84] Add notification email on invalid second authenticator (#28822) --- app/controllers/auth/sessions_controller.rb | 5 ++++ app/mailers/user_mailer.rb | 12 ++++++++++ app/views/user_mailer/failed_2fa.html.haml | 24 +++++++++++++++++++ app/views/user_mailer/failed_2fa.text.erb | 15 ++++++++++++ config/locales/en.yml | 6 +++++ .../auth/sessions_controller_spec.rb | 20 +++++++++++++--- spec/mailers/previews/user_mailer_preview.rb | 5 ++++ spec/mailers/user_mailer_spec.rb | 18 ++++++++++++++ 8 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 app/views/user_mailer/failed_2fa.html.haml create mode 100644 app/views/user_mailer/failed_2fa.text.erb diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 6bc48a7804..962b78de65 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -181,6 +181,11 @@ class Auth::SessionsController < Devise::SessionsController ip: request.remote_ip, user_agent: request.user_agent ) + + # Only send a notification email every hour at most + return if redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour, get: true).present? + + UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! end def second_factor_attempts_key(user) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 432b851b5e..3b1a085cb8 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -191,6 +191,18 @@ class UserMailer < Devise::Mailer end end + def failed_2fa(user, remote_ip, user_agent, timestamp) + @resource = user + @remote_ip = remote_ip + @user_agent = user_agent + @detection = Browser.new(user_agent) + @timestamp = timestamp.to_time.utc + + I18n.with_locale(locale) do + mail subject: default_i18n_subject + end + end + private def default_devise_subject diff --git a/app/views/user_mailer/failed_2fa.html.haml b/app/views/user_mailer/failed_2fa.html.haml new file mode 100644 index 0000000000..e1da35ce06 --- /dev/null +++ b/app/views/user_mailer/failed_2fa.html.haml @@ -0,0 +1,24 @@ += content_for :heading do + = render 'application/mailer/heading', heading_title: t('user_mailer.failed_2fa.title'), heading_subtitle: t('user_mailer.failed_2fa.explanation'), heading_image_url: frontend_asset_url('images/mailer-new/heading/login.png') +%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.failed_2fa.details' + %p + %strong #{t('sessions.ip')}: + = @remote_ip + %br/ + %strong #{t('sessions.browser')}: + %span{ title: @user_agent } + = t 'sessions.description', + browser: t("sessions.browsers.#{@detection.id}", default: @detection.id.to_s), + platform: t("sessions.platforms.#{@detection.platform.id}", default: @detection.platform.id.to_s) + %br/ + %strong #{t('sessions.date')}: + = l(@timestamp.in_time_zone(@resource.time_zone.presence), format: :with_time_zone) + = render 'application/mailer/button', text: t('settings.account_settings'), url: edit_user_registration_url + %p= t 'user_mailer.failed_2fa.further_actions_html', + action: link_to(t('user_mailer.suspicious_sign_in.change_password'), edit_user_registration_url) diff --git a/app/views/user_mailer/failed_2fa.text.erb b/app/views/user_mailer/failed_2fa.text.erb new file mode 100644 index 0000000000..c1dbf7d929 --- /dev/null +++ b/app/views/user_mailer/failed_2fa.text.erb @@ -0,0 +1,15 @@ +<%= t 'user_mailer.failed_2fa.title' %> + +=== + +<%= t 'user_mailer.failed_2fa.explanation' %> + +<%= t 'user_mailer.failed_2fa.details' %> + +<%= t('sessions.ip') %>: <%= @remote_ip %> +<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %> +<%= l(@timestamp.in_time_zone(@resource.time_zone.presence), format: :with_time_zone) %> + +<%= t 'user_mailer.failed_2fa.further_actions_html', action: t('user_mailer.suspicious_sign_in.change_password') %> + +=> <%= edit_user_registration_url %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 89ca0ad72c..83eaaa4552 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1791,6 +1791,12 @@ en: extra: It's now ready for download! subject: Your archive is ready for download title: Archive takeout + failed_2fa: + details: 'Here are details of the sign-in attempt:' + explanation: Someone has tried to sign in to your account but provided an invalid second authentication factor. + further_actions_html: If this wasn't you, we recommend that you %{action} immediately as it may be compromised. + subject: Second factor authentication failure + title: Failed second factor authentication suspicious_sign_in: change_password: change your password details: 'Here are details of the sign-in:' diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index d238626c9d..b663f55afa 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -265,21 +265,35 @@ RSpec.describe Auth::SessionsController do context 'when repeatedly using an invalid TOTP code before using a valid code' do before do stub_const('Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR', 2) + + # Travel to the beginning of an hour to avoid crossing rate-limit buckets + travel_to '2023-12-20T10:00:00Z' end it 'does not log the user in' do - # Travel to the beginning of an hour to avoid crossing rate-limit buckets - travel_to '2023-12-20T10:00:00Z' - Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR.times do post :create, params: { user: { otp_attempt: '1234' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } expect(controller.current_user).to be_nil end post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } + expect(controller.current_user).to be_nil expect(flash[:alert]).to match I18n.t('users.rate_limited') end + + it 'sends a suspicious sign-in mail', :sidekiq_inline do + Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR.times do + post :create, params: { user: { otp_attempt: '1234' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } + expect(controller.current_user).to be_nil + end + + post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } + + expect(UserMailer.deliveries.size).to eq(1) + expect(UserMailer.deliveries.first.to.first).to eq(user.email) + expect(UserMailer.deliveries.first.subject).to eq(I18n.t('user_mailer.failed_2fa.subject')) + end end context 'when using a valid OTP' do diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 098c9cd901..2722538e1a 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -93,4 +93,9 @@ class UserMailerPreview < ActionMailer::Preview def suspicious_sign_in UserMailer.suspicious_sign_in(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/failed_2fa + 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 end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 4a43928248..404b834702 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -135,6 +135,24 @@ describe UserMailer do 'user_mailer.suspicious_sign_in.subject' end + describe '#failed_2fa' do + let(:ip) { '192.168.0.1' } + let(:agent) { 'NCSA_Mosaic/2.0 (Windows 3.1)' } + let(:timestamp) { Time.now.utc } + let(:mail) { described_class.failed_2fa(receiver, ip, agent, timestamp) } + + it 'renders failed 2FA notification' do + receiver.update!(locale: nil) + + expect(mail) + .to be_present + .and(have_body_text(I18n.t('user_mailer.failed_2fa.explanation'))) + end + + include_examples 'localized subject', + 'user_mailer.failed_2fa.subject' + end + describe '#appeal_approved' do let(:appeal) { Fabricate(:appeal, account: receiver.account, approved_at: Time.now.utc) } let(:mail) { described_class.appeal_approved(receiver, appeal) } From 1d381c16644e2f7c8eaa80199acf69a3d86851e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:41:03 +0100 Subject: [PATCH 31/84] [Glitch] Update dependency react-redux to v9.1.0 Port 163db814c2b3cf544b78e427e7f7bbd99b94a025 to glitch-soc Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Renaud Chaput Signed-off-by: Claire --- app/javascript/flavours/glitch/store/typed_functions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/javascript/flavours/glitch/store/typed_functions.ts b/app/javascript/flavours/glitch/store/typed_functions.ts index 46a10b8b47..4859b82651 100644 --- a/app/javascript/flavours/glitch/store/typed_functions.ts +++ b/app/javascript/flavours/glitch/store/typed_functions.ts @@ -1,12 +1,11 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import type { TypedUseSelectorHook } from 'react-redux'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useDispatch, useSelector } from 'react-redux'; import type { AppDispatch, RootState } from './store'; -export const useAppDispatch: () => AppDispatch = useDispatch; -export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); export const createAppAsyncThunk = createAsyncThunk.withTypes<{ state: RootState; From 72bae7e17091535b5a7648501e1f8593231df97f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 21 Jan 2024 08:44:46 +0100 Subject: [PATCH 32/84] [Glitch] Fix duplicate and missing keys in search popout component in web UI Port 9f8e3cca9a6764018ccef2bc48b5d9a867e3a4e3 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/actions/search.js | 7 +++++- .../features/compose/components/search.jsx | 22 ++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index 5bb3aa3a79..7e54740d52 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -170,6 +170,11 @@ export const openURL = routerHistory => (dispatch, getState) => { export const clickSearchResult = (q, type) => (dispatch, getState) => { const previous = getState().getIn(['search', 'recent']); + + if (previous.some(x => x.get('q') === q && x.get('type') === type)) { + return; + } + const me = getState().getIn(['meta', 'me']); const current = previous.add(fromJS({ type, q })).takeLast(4); @@ -198,4 +203,4 @@ export const hydrateSearch = () => (dispatch, getState) => { if (history !== null) { dispatch(updateSearchHistory(history)); } -}; \ No newline at end of file +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/search.jsx b/app/javascript/flavours/glitch/features/compose/components/search.jsx index 70c24d0a42..fa995663f0 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/search.jsx @@ -63,14 +63,14 @@ class Search extends PureComponent { }; defaultOptions = [ - { label: <>has: , action: e => { e.preventDefault(); this._insertText('has:'); } }, - { label: <>is: , action: e => { e.preventDefault(); this._insertText('is:'); } }, - { label: <>language: , action: e => { e.preventDefault(); this._insertText('language:'); } }, - { label: <>from: , action: e => { e.preventDefault(); this._insertText('from:'); } }, - { label: <>before: , action: e => { e.preventDefault(); this._insertText('before:'); } }, - { label: <>during: , action: e => { e.preventDefault(); this._insertText('during:'); } }, - { label: <>after: , action: e => { e.preventDefault(); this._insertText('after:'); } }, - { label: <>in: , action: e => { e.preventDefault(); this._insertText('in:'); } } + { key: 'prompt-has', label: <>has: , action: e => { e.preventDefault(); this._insertText('has:'); } }, + { key: 'prompt-is', label: <>is: , action: e => { e.preventDefault(); this._insertText('is:'); } }, + { key: 'prompt-language', label: <>language: , action: e => { e.preventDefault(); this._insertText('language:'); } }, + { key: 'prompt-from', label: <>from: , action: e => { e.preventDefault(); this._insertText('from:'); } }, + { key: 'prompt-before', label: <>before: , action: e => { e.preventDefault(); this._insertText('before:'); } }, + { key: 'prompt-during', label: <>during: , action: e => { e.preventDefault(); this._insertText('during:'); } }, + { key: 'prompt-after', label: <>after: , action: e => { e.preventDefault(); this._insertText('after:'); } }, + { key: 'prompt-in', label: <>in: , action: e => { e.preventDefault(); this._insertText('in:'); } } ]; setRef = c => { @@ -263,6 +263,8 @@ class Search extends PureComponent { const { recent } = this.props; return recent.toArray().map(search => ({ + key: `${search.get('type')}/${search.get('q')}`, + label: labelForRecentSearch(search), action: () => this.handleRecentSearchClick(search), @@ -347,8 +349,8 @@ class Search extends PureComponent {

- {recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => ( - From 67f54c4e75aeaa78ba72e10603b43a713929fcbd Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 23 Jan 2024 04:06:53 -0500 Subject: [PATCH 33/84] Fix `Rails/WhereExists` cop in app/validators (#28854) --- .rubocop_todo.yml | 2 -- app/validators/reaction_validator.rb | 2 +- app/validators/vote_validator.rb | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index a2ee32d284..4d2f11ff7b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -88,8 +88,6 @@ Rails/WhereExists: - 'app/serializers/rest/announcement_serializer.rb' - 'app/services/activitypub/fetch_remote_status_service.rb' - 'app/services/vote_service.rb' - - 'app/validators/reaction_validator.rb' - - 'app/validators/vote_validator.rb' - 'app/workers/move_worker.rb' - 'lib/tasks/tests.rake' - 'spec/models/account_spec.rb' diff --git a/app/validators/reaction_validator.rb b/app/validators/reaction_validator.rb index 4ed3376e8b..89d83de5a2 100644 --- a/app/validators/reaction_validator.rb +++ b/app/validators/reaction_validator.rb @@ -19,7 +19,7 @@ class ReactionValidator < ActiveModel::Validator end def new_reaction?(reaction) - !reaction.announcement.announcement_reactions.where(name: reaction.name).exists? + !reaction.announcement.announcement_reactions.exists?(name: reaction.name) end def limit_reached?(reaction) diff --git a/app/validators/vote_validator.rb b/app/validators/vote_validator.rb index fa2bd223dc..e725b4c0b8 100644 --- a/app/validators/vote_validator.rb +++ b/app/validators/vote_validator.rb @@ -35,7 +35,7 @@ class VoteValidator < ActiveModel::Validator if vote.persisted? account_votes_on_same_poll(vote).where(choice: vote.choice).where.not(poll_votes: { id: vote }).exists? else - account_votes_on_same_poll(vote).where(choice: vote.choice).exists? + account_votes_on_same_poll(vote).exists?(choice: vote.choice) end end From defe5f407600e9259f6b7c0683b8d56a0349b3d9 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 23 Jan 2024 04:07:22 -0500 Subject: [PATCH 34/84] Fix `Rails/WhereExists` cop in lib/tasks (#28852) --- .rubocop_todo.yml | 1 - lib/tasks/tests.rake | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4d2f11ff7b..c0fb7a5ce2 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -89,7 +89,6 @@ Rails/WhereExists: - 'app/services/activitypub/fetch_remote_status_service.rb' - 'app/services/vote_service.rb' - 'app/workers/move_worker.rb' - - 'lib/tasks/tests.rake' - 'spec/models/account_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb' - 'spec/services/purge_domain_service_spec.rb' diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index c3a9dbfd73..45f055e218 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -24,7 +24,7 @@ namespace :tests do exit(1) end - if Account.where(domain: Rails.configuration.x.local_domain).exists? + if Account.exists?(domain: Rails.configuration.x.local_domain) puts 'Faux remote accounts not properly cleaned up' exit(1) end From b0207d77579e8179b683fe56711a83f5d2fb0909 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 23 Jan 2024 04:10:11 -0500 Subject: [PATCH 35/84] Add coverage for `Tag.recently_used` scope (#28850) --- app/models/tag.rb | 4 +++- spec/models/tag_spec.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index 46e55d74f9..f2168ae904 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -39,6 +39,8 @@ class Tag < ApplicationRecord HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]\u0E47-\u0E4E#{HASHTAG_SEPARATORS}]/ + RECENT_STATUS_LIMIT = 1000 + validates :name, presence: true, format: { with: HASHTAG_NAME_RE } validates :display_name, format: { with: HASHTAG_NAME_RE } validate :validate_name_change, if: -> { !new_record? && name_changed? } @@ -53,7 +55,7 @@ class Tag < ApplicationRecord scope :not_trendable, -> { where(trendable: false) } scope :recently_used, lambda { |account| joins(:statuses) - .where(statuses: { id: account.statuses.select(:id).limit(1000) }) + .where(statuses: { id: account.statuses.select(:id).limit(RECENT_STATUS_LIMIT) }) .group(:id).order(Arel.sql('count(*) desc')) } scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 6177b7a25a..69aaeed0af 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -100,6 +100,38 @@ RSpec.describe Tag do end end + describe '.recently_used' do + let(:account) { Fabricate(:account) } + let(:other_person_status) { Fabricate(:status) } + let(:out_of_range) { Fabricate(:status, account: account) } + let(:older_in_range) { Fabricate(:status, account: account) } + let(:newer_in_range) { Fabricate(:status, account: account) } + let(:unused_tag) { Fabricate(:tag) } + let(:used_tag_one) { Fabricate(:tag) } + let(:used_tag_two) { Fabricate(:tag) } + let(:used_tag_on_out_of_range) { Fabricate(:tag) } + + before do + stub_const 'Tag::RECENT_STATUS_LIMIT', 2 + + other_person_status.tags << used_tag_one + + out_of_range.tags << used_tag_on_out_of_range + + older_in_range.tags << used_tag_one + older_in_range.tags << used_tag_two + + newer_in_range.tags << used_tag_one + end + + it 'returns tags used by account within last X statuses ordered most used first' do + results = described_class.recently_used(account) + + expect(results) + .to eq([used_tag_one, used_tag_two]) + end + end + describe '.find_normalized' do it 'returns tag for a multibyte case-insensitive name' do upcase_string = 'abcABCabcABCやゆよ' From d03fe2bdee45b895a907aa484128cbe96c44b847 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 23 Jan 2024 04:31:59 -0500 Subject: [PATCH 36/84] N+1 fixes for CLI maintenance command (#28847) --- lib/mastodon/cli/maintenance.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index e2ea866152..73012812fd 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -275,7 +275,7 @@ module Mastodon::CLI def deduplicate_users_process_email ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row| - users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).to_a + users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).includes(:account).to_a ref_user = users.shift say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow say "e-mail will be disabled for the following accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow @@ -289,7 +289,7 @@ module Mastodon::CLI def deduplicate_users_process_confirmation_token ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row| - users = User.where(id: row['ids'].split(',')).order(created_at: :desc).to_a.drop(1) + users = User.where(id: row['ids'].split(',')).order(created_at: :desc).includes(:account).to_a.drop(1) say "Unsetting confirmation token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow users.each do |user| @@ -313,7 +313,7 @@ module Mastodon::CLI def deduplicate_users_process_password_token ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row| - users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).to_a.drop(1) + users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).includes(:account).to_a.drop(1) say "Unsetting password reset token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow users.each do |user| @@ -591,7 +591,7 @@ module Mastodon::CLI end def deduplicate_local_accounts!(scope) - accounts = scope.order(id: :desc).to_a + accounts = scope.order(id: :desc).includes(:account_stat, :user).to_a say "Multiple local accounts were found for username '#{accounts.first.username}'.", :yellow say 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.', :yellow From 78ee1453f99bbdd1411349ac5b84833ff7b9e6cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:11:37 +0100 Subject: [PATCH 37/84] New Crowdin Translations (automated) (#28857) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/ca.json | 4 ++-- app/javascript/mastodon/locales/pt-BR.json | 2 ++ config/locales/bg.yml | 5 +++++ config/locales/ca.yml | 15 ++++++++++++--- config/locales/de.yml | 8 +++++++- config/locales/devise.ca.yml | 8 ++++---- config/locales/es-AR.yml | 6 ++++++ config/locales/es-MX.yml | 2 ++ config/locales/es.yml | 2 ++ config/locales/et.yml | 7 +++++++ config/locales/eu.yml | 6 ++++++ config/locales/fi.yml | 6 ++++++ config/locales/fo.yml | 6 ++++++ config/locales/fy.yml | 1 + config/locales/he.yml | 6 ++++++ config/locales/hu.yml | 6 ++++++ config/locales/is.yml | 6 ++++++ config/locales/it.yml | 6 ++++++ config/locales/ko.yml | 2 ++ config/locales/lad.yml | 2 ++ config/locales/lt.yml | 7 +++++++ config/locales/nl.yml | 5 +++++ config/locales/nn.yml | 6 ++++++ config/locales/no.yml | 6 ++++++ config/locales/pl.yml | 6 ++++++ config/locales/pt-BR.yml | 7 +++++++ config/locales/pt-PT.yml | 6 ++++++ config/locales/sr-Latn.yml | 6 ++++++ config/locales/sr.yml | 6 ++++++ config/locales/sv.yml | 3 +++ config/locales/tr.yml | 6 ++++++ config/locales/zh-CN.yml | 6 ++++++ config/locales/zh-HK.yml | 6 ++++++ config/locales/zh-TW.yml | 6 ++++++ 34 files changed, 178 insertions(+), 10 deletions(-) diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 290b364a52..7d1049a30f 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -150,7 +150,7 @@ "compose_form.poll.duration": "Durada de l'enquesta", "compose_form.poll.option_placeholder": "Opció {number}", "compose_form.poll.remove_option": "Elimina aquesta opció", - "compose_form.poll.switch_to_multiple": "Canvia l’enquesta per a permetre diverses opcions", + "compose_form.poll.switch_to_multiple": "Canvia l’enquesta per a permetre múltiples opcions", "compose_form.poll.switch_to_single": "Canvia l’enquesta per a permetre una única opció", "compose_form.publish": "Tut", "compose_form.publish_form": "Nou tut", @@ -607,7 +607,7 @@ "search.quick_action.status_search": "Tuts coincidint amb {x}", "search.search_or_paste": "Cerca o escriu l'URL", "search_popout.full_text_search_disabled_message": "No disponible a {domain}.", - "search_popout.full_text_search_logged_out_message": "Només disponible en iniciar la sessió.", + "search_popout.full_text_search_logged_out_message": "Només disponible amb la sessió iniciada.", "search_popout.language_code": "Codi de llengua ISO", "search_popout.options": "Opcions de cerca", "search_popout.quick_actions": "Accions ràpides", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 482cc8ee73..b8e18e1229 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -32,6 +32,7 @@ "account.featured_tags.last_status_never": "Sem publicações", "account.featured_tags.title": "Hashtags em destaque de {name}", "account.follow": "Seguir", + "account.follow_back": "Seguir de volta", "account.followers": "Seguidores", "account.followers.empty": "Nada aqui.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", @@ -52,6 +53,7 @@ "account.mute_notifications_short": "Silenciar notificações", "account.mute_short": "Silenciar", "account.muted": "Silenciado", + "account.mutual": "Mútuo", "account.no_bio": "Nenhuma descrição fornecida.", "account.open_original_page": "Abrir a página original", "account.posts": "Toots", diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 58a5cae2fd..c3eaa7e4c2 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -1790,6 +1790,11 @@ bg: extra: Вече е готово за теглене! subject: Вашият архив е готов за изтегляне title: Сваляне на архива + failed_2fa: + details: 'Ето подробности на опита за влизане:' + explanation: Някой се опита да влезе в акаунта ви, но предостави невалиден втори фактор за удостоверяване. + subject: Неуспешен втори фактор за удостоверяване + title: Провал на втория фактор за удостоверяване suspicious_sign_in: change_password: промяна на паролата ви details: 'Ето подробности при вход:' diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 36ebb9785b..38ef976b83 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -425,7 +425,7 @@ ca: view: Veure el bloqueig del domini email_domain_blocks: add_new: Afegir nou - allow_registrations_with_approval: Registre permès amb validació + allow_registrations_with_approval: Permet els registres amb validació attempts_over_week: one: "%{count} intent en la darrera setmana" other: "%{count} intents de registre en la darrera setmana" @@ -1046,6 +1046,7 @@ ca: clicking_this_link: en clicar aquest enllaç login_link: inici de sessió proceed_to_login_html: Ara pots passar a %{login_link}. + redirect_to_app_html: Se us hauria d'haver redirigit a l'app %{app_name}. Si això no ha passat, intenteu %{clicking_this_link} o torneu manualment a l'app. registration_complete: La teva inscripció a %{domain} ja és completa. welcome_title: Hola, %{name}! wrong_email_hint: Si aquesta adreça de correu electrònic no és correcte, pots canviar-la en els ajustos del compte. @@ -1109,6 +1110,7 @@ ca: functional: El teu compte està completament operatiu. pending: La vostra sol·licitud està pendent de revisió pel nostre personal. Això pot trigar una mica. Rebreu un correu electrònic quan sigui aprovada. redirecting_to: El teu compte és inactiu perquè actualment està redirigint a %{acct}. + self_destruct: Com que %{domain} tanca, només tindreu accés limitat al vostre compte. view_strikes: Veure accions del passat contra el teu compte too_fast: Formulari enviat massa ràpid, torna a provar-ho. use_security_key: Usa clau de seguretat @@ -1580,6 +1582,7 @@ ca: over_total_limit: Has superat el límit de %{limit} tuts programats too_soon: La data programada ha de ser futura self_destruct: + lead_html: Lamentablement, %{domain} tanca de forma definitiva. Si hi teníeu un compte, no el podreu continuar utilitzant, però podeu demanar una còpia de les vostres dades. title: Aquest servidor tancarà sessions: activity: Última activitat @@ -1784,9 +1787,15 @@ ca: title: Apel·lació rebutjada backup_ready: explanation: Heu demanat una còpia completa de les dades del vostre compte de Mastodon. - extra: Ja us ho podeu baixar + extra: Ja la podeu baixar subject: L'arxiu està preparat per a descàrrega title: Recollida de l'arxiu + failed_2fa: + details: 'Aquests són els detalls de l''intent d''accés:' + explanation: Algú ha intentat accedir al vostre compte però no ha proporcionat un factor de doble autenticació correcte. + further_actions_html: Si no heu estat vosaltres, us recomanem que %{action} immediatament perquè pot estar compromès. + subject: Ha fallat el factor de doble autenticació + title: Ha fallat l'autenticació de doble factor suspicious_sign_in: change_password: canvia la teva contrasenya details: 'Aquest són els detalls de l''inici de sessió:' @@ -1840,7 +1849,7 @@ ca: go_to_sso_account_settings: Ves a la configuració del compte del teu proveïdor d'identitat invalid_otp_token: El codi de dos factors no és correcte otp_lost_help_html: Si has perdut l'accés a tots dos pots contactar per %{email} - rate_limited: Excessius intents d'autenticació, torneu-ho a provar més tard. + rate_limited: Excessius intents d'autenticació, torneu-hi més tard. seamless_external_login: Has iniciat sessió via un servei extern per tant els ajustos de contrasenya i correu electrònic no estan disponibles. signed_in_as: 'Sessió iniciada com a:' verification: diff --git a/config/locales/de.yml b/config/locales/de.yml index e177c6d2d1..9568f698d1 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1790,8 +1790,14 @@ de: extra: Sie ist jetzt zum Herunterladen bereit! subject: Dein persönliches Archiv kann heruntergeladen werden title: Archiv-Download + failed_2fa: + details: 'Details zum Anmeldeversuch:' + explanation: Jemand hat versucht, sich bei deinem Konto anzumelden, aber die Zwei-Faktor-Authentisierung schlug fehl. + further_actions_html: Solltest du das nicht gewesen sein, empfehlen wir dir, sofort %{action}, da dein Konto möglicherweise kompromittiert ist. + subject: Zwei-Faktor-Authentisierung fehlgeschlagen + title: Zwei-Faktor-Authentisierung fehlgeschlagen suspicious_sign_in: - change_password: dein Passwort ändern + change_password: dein Passwort zu ändern details: 'Hier sind die Details zu den Anmeldeversuchen:' explanation: Wir haben eine Anmeldung zu deinem Konto von einer neuen IP-Adresse festgestellt. further_actions_html: Wenn du das nicht warst, empfehlen wir dir schnellstmöglich, %{action} und die Zwei-Faktor-Authentisierung (2FA) für dein Konto zu aktivieren, um es abzusichern. diff --git a/config/locales/devise.ca.yml b/config/locales/devise.ca.yml index 2bf741ee40..3720d3c5f7 100644 --- a/config/locales/devise.ca.yml +++ b/config/locales/devise.ca.yml @@ -49,19 +49,19 @@ ca: subject: 'Mastodon: Instruccions per a reiniciar contrasenya' title: Contrasenya restablerta two_factor_disabled: - explanation: Només es pot accedir amb compte de correu i contrasenya. + explanation: Ara es pot accedir amb només compte de correu i contrasenya. subject: 'Mastodon: Autenticació de doble factor desactivada' subtitle: S'ha deshabilitat l'autenticació de doble factor al vostre compte. title: A2F desactivada two_factor_enabled: - explanation: Per accedir fa falta un token generat per l'aplicació TOTP aparellada. + explanation: Per accedir cal un token generat per l'aplicació TOTP aparellada. subject: 'Mastodon: Autenticació de doble factor activada' subtitle: S'ha habilitat l'autenticació de doble factor al vostre compte. title: A2F activada two_factor_recovery_codes_changed: explanation: Els codis de recuperació anteriors ja no són vàlids i se n'han generat de nous. subject: 'Mastodon: codis de recuperació de doble factor regenerats' - subtitle: S'han invalidat els codis de recuperació anteriors i se n'ha generat de nous. + subtitle: S'han invalidat els codis de recuperació anteriors i se n'han generat de nous. title: Codis de recuperació A2F canviats unlock_instructions: subject: 'Mastodon: Instruccions per a desblocar' @@ -76,7 +76,7 @@ ca: title: Una de les teves claus de seguretat ha estat esborrada webauthn_disabled: explanation: S'ha deshabilitat l'autenticació amb claus de seguretat al vostre compte. - extra: Ara només podeu accedir amb el token generat amb l'aplicació TOTP aparellada. + extra: Ara es pot accedir amb només el token generat amb l'aplicació TOTP aparellada. subject: 'Mastodon: S''ha desactivat l''autenticació amb claus de seguretat' title: Claus de seguretat desactivades webauthn_enabled: diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index 0b6e58db59..cc55d3d3ff 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -1790,6 +1790,12 @@ es-AR: extra: "¡Ya está lista para descargar!" subject: Tu archivo historial está listo para descargar title: Descargar archivo historial + failed_2fa: + details: 'Estos son los detalles del intento de inicio de sesión:' + explanation: Alguien intentó iniciar sesión en tu cuenta pero proporcionó un segundo factor de autenticación no válido. + further_actions_html: Si vos no fuiste, te recomendamos que %{action} inmediatamente, ya que la seguridad de tu cuenta podría estar comprometida. + subject: Fallo de autenticación del segundo factor + title: Fallo en la autenticación del segundo factor suspicious_sign_in: change_password: cambiés tu contraseña details: 'Acá están los detalles del inicio de sesión:' diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 11c327bcca..040d8a9d3c 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1790,6 +1790,8 @@ es-MX: extra: "¡Ya está listo para descargar!" subject: Tu archivo está preparado para descargar title: Descargar archivo + failed_2fa: + details: 'Estos son los detalles del intento de inicio de sesión:' suspicious_sign_in: change_password: cambies tu contraseña details: 'Aquí están los detalles del inicio de sesión:' diff --git a/config/locales/es.yml b/config/locales/es.yml index 4dbb76c526..ffe3eb5b00 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1790,6 +1790,8 @@ es: extra: "¡Ya está listo para descargar!" subject: Tu archivo está preparado para descargar title: Descargar archivo + failed_2fa: + details: 'Estos son los detalles del intento de inicio de sesión:' suspicious_sign_in: change_password: cambies tu contraseña details: 'Aquí están los detalles del inicio de sesión:' diff --git a/config/locales/et.yml b/config/locales/et.yml index 71f49e1abb..f82ee6cb8f 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -1792,6 +1792,12 @@ et: extra: See on nüüd allalaadimiseks valmis! subject: Arhiiv on allalaadimiseks valmis title: Arhiivi väljavõte + failed_2fa: + details: 'Sisenemise üksikasjad:' + explanation: Keegi püüdis Su kontole siseneda, ent sisestas vale teisese autentimisfaktori. + further_actions_html: Kui see polnud Sina, siis soovitame viivitamata %{action}, kuna see võib olla lekkinud. + subject: Kaheastmelise autentimise nurjumine + title: Kaheastmeline autentimine nurjus suspicious_sign_in: change_password: muuta oma salasõna details: 'Sisenemise üksikasjad:' @@ -1848,6 +1854,7 @@ et: go_to_sso_account_settings: Mine oma idenditeedipakkuja kontosätetesse invalid_otp_token: Vale kaheastmeline võti otp_lost_help_html: Kui kaotasid ligipääsu mõlemale, saad võtta ühendust %{email}-iga + rate_limited: Liiga palju autentimise katseid, proovi hiljem uuesti. seamless_external_login: Välise teenuse kaudu sisse logides pole salasõna ja e-posti sätted saadaval. signed_in_as: 'Sisse logitud kasutajana:' verification: diff --git a/config/locales/eu.yml b/config/locales/eu.yml index bfa1f829b6..bd6ea8c832 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1794,6 +1794,12 @@ eu: extra: Deskargatzeko prest! subject: Zure artxiboa deskargatzeko prest dago title: Artxiboa jasotzea + failed_2fa: + details: 'Hemen dituzu saio-hasieraren saiakeraren xehetasunak:' + explanation: Norbait zure kontuan saioa hasten saiatu da, baina bigarren autentifikazioaren faktore baliogabea eman du. + further_actions_html: Ez bazara zu izan, "%{action}" ekintza berehala egitea gomendatzen dugu, kontua arriskarazi daiteke eta. + subject: Autentifikazioaren bigarren faktoreak huts egin du + title: Huts egin duen autentifikazioaren bigarren faktorea suspicious_sign_in: change_password: aldatu pasahitza details: 'Hemen daude saio hasieraren xehetasunak:' diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 9d8974392f..8e61c7b2a0 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1790,6 +1790,12 @@ fi: extra: Se on nyt valmis ladattavaksi! subject: Arkisto on valmiina ladattavaksi title: Arkiston tallennus + failed_2fa: + details: 'Tässä on tiedot kirjautumisyrityksestä:' + explanation: Joku on yrittänyt kirjautua tilillesi, mutta antanut virheellisen kaksivaiheisen todennuksen. + further_actions_html: Jos se et ollut sinä, suosittelemme, että %{action} välittömästi, sillä se on saattanut vaarantua. + subject: Kaksivaiheisen todennuksen virhe + title: Epäonnistunut kaksivaiheinen todennus suspicious_sign_in: change_password: vaihda salasanasi details: 'Tässä on tiedot kirjautumisesta:' diff --git a/config/locales/fo.yml b/config/locales/fo.yml index dabaf24ba7..8e34265313 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -1790,6 +1790,12 @@ fo: extra: Tað er nú klárt at taka niður! subject: Savnið hjá tær er tøkt at taka niður title: Tak savn niður + failed_2fa: + details: 'Her eru smálutirnir í innritanarroyndini:' + explanation: Onkur hevur roynt at rita inn á tína kontu, men gav eitt ógildugt seinna samgildi. + further_actions_html: Um hetta ikki var tú, so skjóta vit upp, at tú %{action} beinan vegin, tí tað kann vera sett í vanda. + subject: Seinna samgildi miseydnaðist + title: Miseydnað seinna samgildi suspicious_sign_in: change_password: broyt loyniorðið hjá tær details: 'Her eru smálutirnir í innritanini:' diff --git a/config/locales/fy.yml b/config/locales/fy.yml index 1d648f4790..f861bc3e4a 100644 --- a/config/locales/fy.yml +++ b/config/locales/fy.yml @@ -1843,6 +1843,7 @@ fy: go_to_sso_account_settings: Gean nei de accountynstellingen fan jo identiteitsprovider invalid_otp_token: Unjildige twa-stapstagongskoade otp_lost_help_html: As jo tagong ta beide kwytrekke binne, nim dan kontakt op fia %{email} + rate_limited: Te folle autentikaasjebesykjen, probearje it letter opnij. seamless_external_login: Jo binne oanmeld fia in eksterne tsjinst, dêrom binne wachtwurden en e-mailynstellingen net beskikber. signed_in_as: 'Oanmeld as:' verification: diff --git a/config/locales/he.yml b/config/locales/he.yml index db57912d89..1f5fd096ac 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1854,6 +1854,12 @@ he: extra: הגיבוי מוכן להורדה! subject: הארכיון שלך מוכן להורדה title: הוצאת ארכיון + failed_2fa: + details: 'הנה פרטי נסיון ההתחברות:' + explanation: פולני אלמוני ניסה להתחבר לחשבונך אך האימות המשני נכשל. + further_actions_html: אם הנסיון לא היה שלך, אנו ממליצים על %{action} באופן מיידי כדי שהחשבון לא יפול קורבן. + subject: נכשל אימות בגורם שני + title: אימות בגורם שני נכשל suspicious_sign_in: change_password: שינוי הסיסמא שלך details: 'הנה פרטי ההתחברות:' diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 8fce206e9e..2870435ea7 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1790,6 +1790,12 @@ hu: extra: Már letöltésre kész! subject: Az adataidról készült archív letöltésre kész title: Archiválás + failed_2fa: + details: 'Itt vannak a bejelentkezési kísérlet részletei:' + explanation: Valaki megpróbált bejelentkezni a fiókodba, de a második hitelesítési lépése érvénytelen volt. + further_actions_html: Ha ez nem te voltál, azt javasoljuk, hogy azonnal %{action}, mivel lehetséges, hogy az rossz kezekbe került. + subject: Második körös hitelesítés sikertelen + title: Sikertelen a második körös hitelesítés suspicious_sign_in: change_password: módosítsd a jelszavad details: 'Itt vannak a bejelentkezés részletei:' diff --git a/config/locales/is.yml b/config/locales/is.yml index b048d5cb00..191383f56c 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -1794,6 +1794,12 @@ is: extra: Það er núna tilbúið til niðurhals! subject: Safnskráin þín er tilbúin til niðurhals title: Taka út í safnskrá + failed_2fa: + details: 'Hér eru nánari upplýsingar um innskráningartilraunina:' + explanation: Einhver reyndi að skrá sig inn á aðganginn þinn en gaf upp ógild gögn seinna þrepi auðkenningar. + further_actions_html: Ef þetta varst ekki þú, þá mælum við eindregið með því að þú %{action} samstundis, þar sem það gæti verið berskjaldað. + subject: Bilun í seinna þrepi auðkenningar + title: Seinna þrep auðkenningar brást suspicious_sign_in: change_password: breytir lykilorðinu þínu details: 'Hér eru nánari upplýsingar um innskráninguna:' diff --git a/config/locales/it.yml b/config/locales/it.yml index adcef9559f..89ff071f36 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1792,6 +1792,12 @@ it: extra: Ora è pronto per il download! subject: Il tuo archivio è pronto per essere scaricato title: Esportazione archivio + failed_2fa: + details: 'Questi sono i dettagli del tentativo di accesso:' + explanation: Qualcuno ha tentato di accedere al tuo account ma ha fornito un secondo fattore di autenticazione non valido. + further_actions_html: Se non eri tu, ti consigliamo di %{action} immediatamente poiché potrebbe essere compromesso. + subject: Errore di autenticazione del secondo fattore + title: Autenticazione del secondo fattore non riuscita suspicious_sign_in: change_password: cambiare la tua password details: 'Questi sono i dettagli del tentativo di accesso:' diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 946aa35657..b3c786e265 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1760,6 +1760,8 @@ ko: extra: 다운로드 할 준비가 되었습니다! subject: 아카이브를 다운로드할 수 있습니다 title: 아카이브 테이크아웃 + failed_2fa: + details: '로그인 시도에 대한 상세 정보입니다:' suspicious_sign_in: change_password: 암호 변경 details: '로그인에 대한 상세 정보입니다:' diff --git a/config/locales/lad.yml b/config/locales/lad.yml index 5a09c4c609..be5d2d21bd 100644 --- a/config/locales/lad.yml +++ b/config/locales/lad.yml @@ -1757,6 +1757,8 @@ lad: extra: Agora esta pronto para abashar! subject: Tu dosya esta pronta para abashar title: Abasha dosya + failed_2fa: + details: 'Aki estan los peratim de las provas de koneksyon kon tu kuento:' suspicious_sign_in: change_password: troka tu kod details: 'Aki estan los peratim de la koneksyon kon tu kuento:' diff --git a/config/locales/lt.yml b/config/locales/lt.yml index f3715fd2ee..ba8b53fdc9 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -559,6 +559,12 @@ lt: extra: Jį jau galima atsisiųsti! subject: Jūsų archyvas paruoštas parsisiuntimui title: Archyvas išimtas + failed_2fa: + details: 'Štai išsami informacija apie bandymą prisijungti:' + explanation: Kažkas bandė prisijungti prie tavo paskyros, bet nurodė netinkamą antrąjį tapatybės nustatymo veiksnį. + further_actions_html: Jei tai buvo ne tu, rekomenduojame nedelsiant imtis %{action}, nes jis gali būti pažeistas. + subject: Antrojo veiksnio tapatybės nustatymas nesėkmingai + title: Nepavyko atlikti antrojo veiksnio tapatybės nustatymo warning: subject: disable: Jūsų paskyra %{acct} buvo užšaldyta @@ -584,6 +590,7 @@ lt: go_to_sso_account_settings: Eik į savo tapatybės teikėjo paskyros nustatymus invalid_otp_token: Netinkamas dviejų veiksnių kodas otp_lost_help_html: Jei praradai prieigą prie abiejų, gali susisiek su %{email} + rate_limited: Per daug tapatybės nustatymo bandymų. Bandyk dar kartą vėliau. seamless_external_login: Esi prisijungęs (-usi) per išorinę paslaugą, todėl slaptažodžio ir el. pašto nustatymai nepasiekiami. signed_in_as: 'Prisijungta kaip:' verification: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 5ffa788a8c..2d27f9165d 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1790,6 +1790,11 @@ nl: extra: Het staat nu klaar om te worden gedownload! subject: Jouw archief staat klaar om te worden gedownload title: Archief ophalen + failed_2fa: + details: 'Hier zijn details van de aanmeldpoging:' + explanation: Iemand heeft geprobeerd om in te loggen op uw account maar heeft een ongeldige tweede verificatiefactor opgegeven. + subject: Tweede factor authenticatiefout + title: Tweestapsverificatie mislukt suspicious_sign_in: change_password: je wachtwoord te wijzigen details: 'Hier zijn de details van inlogpoging:' diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 626252be00..95eed49785 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1790,6 +1790,12 @@ nn: extra: Den er nå klar for nedlasting! subject: Arkivet ditt er klart til å lastes ned title: Nedlasting av arkiv + failed_2fa: + details: 'Her er detaljane om innloggingsforsøket:' + explanation: Nokon har prøvd å logge inn på kontoen din, men brukte ein ugyldig andre-autentiseringsfaktor. + further_actions_html: Om dette ikkje var deg, rår me deg til å %{action} med éin gong, då det kan vere kompomittert. + subject: To-faktor-autentiseringsfeil + title: Mislukka to-faktor-autentisering suspicious_sign_in: change_password: endre passord details: 'Her er påloggingsdetaljane:' diff --git a/config/locales/no.yml b/config/locales/no.yml index d90aa5bab5..7ece8564fc 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -1790,6 +1790,12 @@ extra: Den er nå klar for nedlasting! subject: Arkivet ditt er klart til å lastes ned title: Nedlasting av arkiv + failed_2fa: + details: 'Her er detaljer om påloggingsforsøket:' + explanation: Noen har prøvd å logge på kontoen din, men ga en ugyldig andre-autentiseringsfaktor. + further_actions_html: Hvis dette ikke var deg, anbefaler vi at du %{action} umiddelbart fordi det kan ha blitt kompromittert. + subject: Andre-autentiseringsfaktorfeil + title: Mislykket andre-autentiseringsfaktor suspicious_sign_in: change_password: endre passord details: 'Her er detaljer om påloggingen:' diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 4d8fde8f4f..6718f1994b 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1854,6 +1854,12 @@ pl: extra: Gotowe do pobrania! subject: Twoje archiwum jest gotowe do pobrania title: Odbiór archiwum + failed_2fa: + details: 'Oto szczegóły próby logowania:' + explanation: Ktoś próbował zalogować się na twoje konto, ale nie przeszedł drugiego etapu autoryzacji. + further_actions_html: Jeśli to nie ty, polecamy natychmiastowo %{action}, bo może ono być narażone. + subject: Błąd drugiego etapu uwierzytelniania + title: Nieudane uwierzytelnienie w drugim etapie suspicious_sign_in: change_password: zmień hasło details: 'Oto szczegóły logowania:' diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 47ad0ac448..c1a47c0161 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1789,6 +1789,12 @@ pt-BR: extra: Agora está pronto para baixar! subject: Seu arquivo está pronto para ser baixado title: Baixar arquivo + failed_2fa: + details: 'Aqui estão os detalhes da tentativa de acesso:' + explanation: Alguém tentou entrar em sua conta, mas forneceu um segundo fator de autenticação inválido. + further_actions_html: Se não foi você, recomendamos que %{action} imediatamente, pois ela pode ser comprometida. + subject: Falha na autenticação do segundo fator + title: Falha na autenticação do segundo fator suspicious_sign_in: change_password: Altere sua senha details: 'Aqui estão os detalhes do acesso:' @@ -1842,6 +1848,7 @@ pt-BR: go_to_sso_account_settings: Vá para as configurações de conta do seu provedor de identidade invalid_otp_token: Código de dois fatores inválido otp_lost_help_html: Se você perder o acesso à ambos, você pode entrar em contato com %{email} + rate_limited: Muitas tentativas de autenticação; tente novamente mais tarde. seamless_external_login: Você entrou usando um serviço externo, então configurações de e-mail e senha não estão disponíveis. signed_in_as: 'Entrou como:' verification: diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index 2e077b37a8..268531718d 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -1790,6 +1790,12 @@ pt-PT: extra: Está pronta para transferir! subject: O seu arquivo está pronto para descarregar title: Arquivo de ficheiros + failed_2fa: + details: 'Aqui estão os detalhes da tentativa de entrada:' + explanation: Alguém tentou entrar em sua conta mas forneceu um segundo fator de autenticação inválido. + further_actions_html: Se não foi você, recomendamos que %{action} imediatamente, pois pode ter sido comprometido. + subject: Falha na autenticação do segundo fator + title: Falha na autenticação do segundo fator suspicious_sign_in: change_password: alterar a sua palavra-passe details: 'Eis os pormenores do início de sessão:' diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 39c9f2f873..9cb555c943 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -1822,6 +1822,12 @@ sr-Latn: extra: Sada je spremno za preuzimanje! subject: Vaša arhiva je spremna za preuzimanje title: Izvoz arhive + failed_2fa: + details: 'Evo detalja o pokušaju prijavljivanja:' + explanation: Neko je pokušao da se prijavi na vaš nalog ali je dao nevažeći drugi faktor autentifikacije. + further_actions_html: Ako to niste bili vi, preporučujemo vam da odmah %{action} jer može biti ugrožena. + subject: Neuspeh drugog faktora autentifikacije + title: Nije uspeo drugi faktor autentifikacije suspicious_sign_in: change_password: promenite svoju lozinku details: 'Evo detalja o prijavi:' diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 0cf35c14cc..e1c2e992ed 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -1822,6 +1822,12 @@ sr: extra: Сада је спремно за преузимање! subject: Ваша архива је спремна за преузимање title: Извоз архиве + failed_2fa: + details: 'Ево детаља о покушају пријављивања:' + explanation: Неко је покушао да се пријави на ваш налог али је дао неважећи други фактор аутентификације. + further_actions_html: Ако то нисте били ви, препоручујемо вам да одмах %{action} јер може бити угрожена. + subject: Неуспех другог фактора аутентификације + title: Није успео други фактор аутентификације suspicious_sign_in: change_password: промените своју лозинку details: 'Ево детаља о пријави:' diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 3a82f29d2f..c9000d50fc 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -1789,6 +1789,9 @@ sv: extra: Nu redo för nedladdning! subject: Ditt arkiv är klart för nedladdning title: Arkivuttagning + failed_2fa: + further_actions_html: Om detta inte var du, rekommenderar vi att du %{action} omedelbart eftersom ditt konto kan ha äventyrats. + title: Misslyckad tvåfaktorsautentisering suspicious_sign_in: change_password: Ändra ditt lösenord details: 'Här är inloggningsdetaljerna:' diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 3b74c4eaa1..fa84d2a96d 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1790,6 +1790,12 @@ tr: extra: Şimdi indirebilirsiniz! subject: Arşiviniz indirilmeye hazır title: Arşiv paketlemesi + failed_2fa: + details: 'Oturum açma denemesinin ayrıntıları şöyledir:' + explanation: Birisi hesabınızda oturum açmaya çalıştı ancak hatalı bir iki aşamalı doğrulama kodu kullandı. + further_actions_html: Eğer bu kişi siz değilseniz, hemen %{action} yapmanızı öneriyoruz çünkü hesabınız ifşa olmuş olabilir. + subject: İki aşamalı doğrulama başarısızlığı + title: Başarısız iki aşamalı kimlik doğrulama suspicious_sign_in: change_password: parolanızı değiştirin details: 'Oturum açma ayrıntıları şöyledir:' diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 80bb5653ca..272787ce25 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1758,6 +1758,12 @@ zh-CN: extra: 现在它可以下载了! subject: 你的存档已经准备完毕 title: 存档导出 + failed_2fa: + details: 以下是该次登录尝试的详情: + explanation: 有人试图登录到您的账户,但提供了无效的辅助认证因子。 + further_actions_html: 如果这不是您所为,您的密码可能已经泄露,建议您立即 %{action} 。 + subject: 辅助认证失败 + title: 辅助认证失败 suspicious_sign_in: change_password: 更改密码 details: 以下是该次登录的详细信息: diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index ac32c03e95..0c39aa8c0b 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -1758,6 +1758,12 @@ zh-HK: extra: 現在可以下載了! subject: 你的備份檔已可供下載 title: 檔案匯出 + failed_2fa: + details: 以下是嘗試登入的細節: + explanation: 有人嘗試登入你的帳號,但沒有通過雙重認證。 + further_actions_html: 如果這不是你,我們建議你立刻%{action},因為你的帳號或已遭到侵害。 + subject: 雙重認證失敗 + title: 雙重認證失敗 suspicious_sign_in: change_password: 更改你的密碼 details: 以下是登入的細節: diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 6662e44cdc..8726ea72a4 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1760,6 +1760,12 @@ zh-TW: extra: 準備好下載了! subject: 您的備份檔已可供下載 title: 檔案匯出 + failed_2fa: + details: 以下是該登入嘗試之詳細資訊: + explanation: 有人嘗試登入您的帳號,但提供了無效的第二個驗證因子。 + further_actions_html: 若這並非您所為,我們建議您立刻 %{action},因為其可能已被入侵。 + subject: 第二因子驗證失敗 + title: 第二因子身份驗證失敗 suspicious_sign_in: change_password: 變更密碼 details: 以下是該登入之詳細資訊: From ceade78182e882f3a045d4c6c748743bfc0b8f5e Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 23 Jan 2024 06:41:34 -0500 Subject: [PATCH 38/84] Fix `Rails/WhereExists` cop in app/services (#28853) --- .rubocop_todo.yml | 2 -- app/services/activitypub/fetch_remote_status_service.rb | 2 +- app/services/vote_service.rb | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c0fb7a5ce2..bef79e451a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -86,8 +86,6 @@ Rails/WhereExists: - 'app/models/status.rb' - 'app/policies/status_policy.rb' - 'app/serializers/rest/announcement_serializer.rb' - - 'app/services/activitypub/fetch_remote_status_service.rb' - - 'app/services/vote_service.rb' - 'app/workers/move_worker.rb' - 'spec/models/account_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb' diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index a491b32b26..e3a9b60b56 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -44,7 +44,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService # If we fetched a status that already exists, then we need to treat the # activity as an update rather than create - activity_json['type'] = 'Update' if equals_or_includes_any?(activity_json['type'], %w(Create)) && Status.where(uri: object_uri, account_id: actor.id).exists? + activity_json['type'] = 'Update' if equals_or_includes_any?(activity_json['type'], %w(Create)) && Status.exists?(uri: object_uri, account_id: actor.id) with_redis do |redis| discoveries = redis.incr("status_discovery_per_request:#{@request_id}") diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb index 3e92a1690a..878350388b 100644 --- a/app/services/vote_service.rb +++ b/app/services/vote_service.rb @@ -19,7 +19,7 @@ class VoteService < BaseService already_voted = true with_redis_lock("vote:#{@poll.id}:#{@account.id}") do - already_voted = @poll.votes.where(account: @account).exists? + already_voted = @poll.votes.exists?(account: @account) ApplicationRecord.transaction do @choices.each do |choice| From c0e8e457abee9d6f6dd20338bdaac90cb9e1f7bc Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 23 Jan 2024 06:41:54 -0500 Subject: [PATCH 39/84] Eager loading fixes for `api/` controllers (#28848) --- .../api/v1/accounts/follower_accounts_controller.rb | 2 +- .../api/v1/accounts/following_accounts_controller.rb | 2 +- app/controllers/api/v1/blocks_controller.rb | 2 +- app/controllers/api/v1/directories_controller.rb | 2 +- app/controllers/api/v1/endorsements_controller.rb | 2 +- app/controllers/api/v1/follow_requests_controller.rb | 2 +- app/controllers/api/v1/lists/accounts_controller.rb | 4 ++-- app/controllers/api/v1/mutes_controller.rb | 2 +- .../api/v1/statuses/favourited_by_accounts_controller.rb | 2 +- .../api/v1/statuses/reblogged_by_accounts_controller.rb | 2 +- app/controllers/api/v2/filters_controller.rb | 2 +- app/models/account_suggestions.rb | 2 +- app/models/report.rb | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index d6a5a7176d..f60181f1eb 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -30,7 +30,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def default_accounts - Account.includes(:active_relationships, :account_stat).references(:active_relationships) + Account.includes(:active_relationships, :account_stat, :user).references(:active_relationships) end def paginated_follows diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index b8578ef539..3ab8c1efd6 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -30,7 +30,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def default_accounts - Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) + Account.includes(:passive_relationships, :account_stat, :user).references(:passive_relationships) end def paginated_follows diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index 06a8bfa891..0934622f88 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -17,7 +17,7 @@ class Api::V1::BlocksController < Api::BaseController end def paginated_blocks - @paginated_blocks ||= Block.eager_load(target_account: :account_stat) + @paginated_blocks ||= Block.eager_load(target_account: [:account_stat, :user]) .joins(:target_account) .merge(Account.without_suspended) .where(account: current_account) diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index e79b20ce42..6c540404ea 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -27,7 +27,7 @@ class Api::V1::DirectoriesController < Api::BaseController scope.merge!(local_account_scope) if local_accounts? scope.merge!(account_exclusion_scope) if current_account scope.merge!(account_domain_block_scope) if current_account && !local_accounts? - end + end.includes(:account_stat, user: :role) end def local_accounts? diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index 46e3fcd647..2216a9860d 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController end def endorsed_accounts - current_account.endorsed_accounts.includes(:account_stat).without_suspended + current_account.endorsed_accounts.includes(:account_stat, :user).without_suspended end def insert_pagination_headers diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index ee717ebbcc..87f6df5f94 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController end def default_accounts - Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) + Account.without_suspended.includes(:follow_requests, :account_stat, :user).references(:follow_requests) end def paginated_follow_requests diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index 8e12cb7b65..0604ad60fc 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController def load_accounts if unlimited? - @list.accounts.without_suspended.includes(:account_stat).all + @list.accounts.without_suspended.includes(:account_stat, :user).all else - @list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + @list.accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) end end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 555485823c..2fb685ac39 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -17,7 +17,7 @@ class Api::V1::MutesController < Api::BaseController end def paginated_mutes - @paginated_mutes ||= Mute.eager_load(:target_account) + @paginated_mutes ||= Mute.eager_load(target_account: [:account_stat, :user]) .joins(:target_account) .merge(Account.without_suspended) .where(account: current_account) diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 98b69c347f..069ad37cb2 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas def default_accounts Account .without_suspended - .includes(:favourites, :account_stat) + .includes(:favourites, :account_stat, :user) .references(:favourites) .where(favourites: { status_id: @status.id }) end diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index aacab5f8f4..b8a997518d 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -19,7 +19,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base end def default_accounts - Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) + Account.without_suspended.includes(:statuses, :account_stat, :user).references(:statuses) end def paginated_statuses diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 2fcdeeae45..09d4813f34 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -35,7 +35,7 @@ class Api::V2::FiltersController < Api::BaseController private def set_filters - @filters = current_account.custom_filters.includes(:keywords) + @filters = current_account.custom_filters.includes(:keywords, :statuses) end def set_filter diff --git a/app/models/account_suggestions.rb b/app/models/account_suggestions.rb index d62176c7ca..25c8b04d50 100644 --- a/app/models/account_suggestions.rb +++ b/app/models/account_suggestions.rb @@ -29,7 +29,7 @@ class AccountSuggestions # a complicated query on this end. account_ids = account_ids_with_sources[offset, limit] - accounts_map = Account.where(id: account_ids.map(&:first)).includes(:account_stat).index_by(&:id) + accounts_map = Account.where(id: account_ids.map(&:first)).includes(:account_stat, :user).index_by(&:id) account_ids.filter_map do |(account_id, source)| next unless accounts_map.key?(account_id) diff --git a/app/models/report.rb b/app/models/report.rb index 126701b3d6..38da26d7b7 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -41,7 +41,7 @@ class Report < ApplicationRecord scope :unresolved, -> { where(action_taken_at: nil) } scope :resolved, -> { where.not(action_taken_at: nil) } - scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) } + scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with([:account_stat, { user: [:invite_request, :invite, :ips] }])) } # A report is considered local if the reporter is local delegate :local?, to: :account From 61a0ec69fcf5565a96c2cc53167f74e6ff0391d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:44:50 +0100 Subject: [PATCH 40/84] chore(deps): update devdependencies (non-major) (#28840) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Renaud Chaput --- .devcontainer/codespaces/devcontainer.json | 18 ++++----- .devcontainer/devcontainer.json | 16 ++++---- jsconfig.json | 4 +- streaming/tsconfig.json | 4 +- tsconfig.json | 8 ++-- yarn.lock | 46 +++++++++++----------- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json index ca9156fdaa..b32e4026d2 100644 --- a/.devcontainer/codespaces/devcontainer.json +++ b/.devcontainer/codespaces/devcontainer.json @@ -5,7 +5,7 @@ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "features": { - "ghcr.io/devcontainers/features/sshd:1": {} + "ghcr.io/devcontainers/features/sshd:1": {}, }, "runServices": ["app", "db", "redis"], @@ -15,16 +15,16 @@ "portsAttributes": { "3000": { "label": "web", - "onAutoForward": "notify" + "onAutoForward": "notify", }, "4000": { "label": "stream", - "onAutoForward": "silent" - } + "onAutoForward": "silent", + }, }, "otherPortsAttributes": { - "onAutoForward": "silent" + "onAutoForward": "silent", }, "remoteEnv": { @@ -33,7 +33,7 @@ "STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev", "DISABLE_FORGERY_REQUEST_PROTECTION": "true", "ES_ENABLED": "", - "LIBRE_TRANSLATE_ENDPOINT": "" + "LIBRE_TRANSLATE_ENDPOINT": "", }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", @@ -43,7 +43,7 @@ "customizations": { "vscode": { "settings": {}, - "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] - } - } + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"], + }, + }, } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fa8d6542c1..ed71235b3b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "features": { - "ghcr.io/devcontainers/features/sshd:1": {} + "ghcr.io/devcontainers/features/sshd:1": {}, }, "forwardPorts": [3000, 4000], @@ -14,17 +14,17 @@ "3000": { "label": "web", "onAutoForward": "notify", - "requireLocalPort": true + "requireLocalPort": true, }, "4000": { "label": "stream", "onAutoForward": "silent", - "requireLocalPort": true - } + "requireLocalPort": true, + }, }, "otherPortsAttributes": { - "onAutoForward": "silent" + "onAutoForward": "silent", }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", @@ -34,7 +34,7 @@ "customizations": { "vscode": { "settings": {}, - "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] - } - } + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"], + }, + }, } diff --git a/jsconfig.json b/jsconfig.json index d52816a98b..7b710de83c 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -11,7 +11,7 @@ "noEmit": true, "resolveJsonModule": true, "strict": false, - "target": "ES2022" + "target": "ES2022", }, - "exclude": ["**/build/*", "**/node_modules/*", "**/public/*", "**/vendor/*"] + "exclude": ["**/build/*", "**/node_modules/*", "**/public/*", "**/vendor/*"], } diff --git a/streaming/tsconfig.json b/streaming/tsconfig.json index f7bb711b9b..a0cf68ef90 100644 --- a/streaming/tsconfig.json +++ b/streaming/tsconfig.json @@ -6,7 +6,7 @@ "moduleResolution": "node", "noUnusedParameters": false, "tsBuildInfoFile": "../tmp/cache/streaming/tsconfig.tsbuildinfo", - "paths": {} + "paths": {}, }, - "include": ["./*.js", "./.eslintrc.js"] + "include": ["./*.js", "./.eslintrc.js"], } diff --git a/tsconfig.json b/tsconfig.json index a193ea35f2..dc71fc4a9c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,12 +15,12 @@ "paths": { "mastodon": ["app/javascript/mastodon"], "mastodon/*": ["app/javascript/mastodon/*"], - "@/*": ["app/javascript/*"] - } + "@/*": ["app/javascript/*"], + }, }, "include": [ "app/javascript/mastodon", "app/javascript/packs", - "app/javascript/types" - ] + "app/javascript/types", + ], } diff --git a/yarn.lock b/yarn.lock index aca2278f9f..61f699d19e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1538,7 +1538,7 @@ __metadata: languageName: node linkType: hard -"@csstools/css-parser-algorithms@npm:^2.4.0": +"@csstools/css-parser-algorithms@npm:^2.5.0": version: 2.5.0 resolution: "@csstools/css-parser-algorithms@npm:2.5.0" peerDependencies: @@ -1547,14 +1547,14 @@ __metadata: languageName: node linkType: hard -"@csstools/css-tokenizer@npm:^2.2.2": +"@csstools/css-tokenizer@npm:^2.2.3": version: 2.2.3 resolution: "@csstools/css-tokenizer@npm:2.2.3" checksum: 557266ec52e8b36c19008a5bbd7151effba085cdd6d68270c01afebf914981caac698eda754b2a530a8a9947a3dd70e3f3a39a5e037c4170bb2a055a92754acb languageName: node linkType: hard -"@csstools/media-query-list-parser@npm:^2.1.6": +"@csstools/media-query-list-parser@npm:^2.1.7": version: 2.1.7 resolution: "@csstools/media-query-list-parser@npm:2.1.7" peerDependencies: @@ -1777,8 +1777,8 @@ __metadata: linkType: hard "@formatjs/cli@npm:^6.1.1": - version: 6.2.4 - resolution: "@formatjs/cli@npm:6.2.4" + version: 6.2.6 + resolution: "@formatjs/cli@npm:6.2.6" peerDependencies: vue: ^3.3.4 peerDependenciesMeta: @@ -1786,7 +1786,7 @@ __metadata: optional: true bin: formatjs: bin/formatjs - checksum: 3f6bbbc633a3a6ebd4e6fcfc3a9f889bc044043452cbc8f81abcaee97aaef991a778ae785d3b9d21ecc5f55b147eb0009b44520bb895fe244b4c14a36d9b05bd + checksum: f8b0bc45c72b83437f0dc91a2d3ea513852c11bfd8eedbc2f255b19552f153bccb4d38fcd281f897ca60d0dfddf2b99de22e5a87cb1e173ca11df88c61cde2e4 languageName: node linkType: hard @@ -11330,10 +11330,10 @@ __metadata: languageName: node linkType: hard -"meow@npm:^13.0.0": - version: 13.0.0 - resolution: "meow@npm:13.0.0" - checksum: fab0f91578154c048e792a81704f3f28099ffff900f364df8a85f6e770a57e1c124859a25e186186e149dad30692c7893af0dfd71517bea343bfe5d749b1fa04 +"meow@npm:^13.1.0": + version: 13.1.0 + resolution: "meow@npm:13.1.0" + checksum: 2dac9dbf99a17ce29618fe5919072a9b28e2aedb9547f9b1f15d046d5501dd6c14fe1f35f7a5665d0ee7111c98c4d359fcf3f985463ec5896dd50177363f442d languageName: node linkType: hard @@ -13200,7 +13200,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.32": +"postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.33": version: 8.4.33 resolution: "postcss@npm:8.4.33" dependencies: @@ -13295,11 +13295,11 @@ __metadata: linkType: hard "prettier@npm:^3.0.0": - version: 3.2.2 - resolution: "prettier@npm:3.2.2" + version: 3.2.4 + resolution: "prettier@npm:3.2.4" bin: prettier: bin/prettier.cjs - checksum: e84d0d2a4ce2b88ee1636904effbdf68b59da63d9f887128f2ed5382206454185432e7c0a9578bc4308bc25d099cfef47fd0b9c211066777854e23e65e34044d + checksum: 88dfeb78ac6096522c9a5b81f1413d875f568420d9bb6a5e5103527912519b993f2bcdcac311fcff5718d5869671d44e4f85827d3626f3a6ce32b9abc65d88e0 languageName: node linkType: hard @@ -15785,12 +15785,12 @@ __metadata: linkType: hard "stylelint@npm:^16.0.2": - version: 16.1.0 - resolution: "stylelint@npm:16.1.0" + version: 16.2.0 + resolution: "stylelint@npm:16.2.0" dependencies: - "@csstools/css-parser-algorithms": "npm:^2.4.0" - "@csstools/css-tokenizer": "npm:^2.2.2" - "@csstools/media-query-list-parser": "npm:^2.1.6" + "@csstools/css-parser-algorithms": "npm:^2.5.0" + "@csstools/css-tokenizer": "npm:^2.2.3" + "@csstools/media-query-list-parser": "npm:^2.1.7" "@csstools/selector-specificity": "npm:^3.0.1" balanced-match: "npm:^2.0.0" colord: "npm:^2.9.3" @@ -15810,14 +15810,14 @@ __metadata: is-plain-object: "npm:^5.0.0" known-css-properties: "npm:^0.29.0" mathml-tag-names: "npm:^2.1.3" - meow: "npm:^13.0.0" + meow: "npm:^13.1.0" micromatch: "npm:^4.0.5" normalize-path: "npm:^3.0.0" picocolors: "npm:^1.0.0" - postcss: "npm:^8.4.32" + postcss: "npm:^8.4.33" postcss-resolve-nested-selector: "npm:^0.1.1" postcss-safe-parser: "npm:^7.0.0" - postcss-selector-parser: "npm:^6.0.13" + postcss-selector-parser: "npm:^6.0.15" postcss-value-parser: "npm:^4.2.0" resolve-from: "npm:^5.0.0" string-width: "npm:^4.2.3" @@ -15828,7 +15828,7 @@ __metadata: write-file-atomic: "npm:^5.0.1" bin: stylelint: bin/stylelint.mjs - checksum: 765eea0b07319d1e7989502c07b8b5794938e5a8542bec00990b09ec10c3f7006891689930099e948d06c9ef9982066edb98b1ea64a435138a6b0f0905eb2b87 + checksum: 6fdf0451833c11b18c9aa502f687febd6881a912ac94f39d509b894b0f74ccb636f3dac2991c69cc82dc6190731cc2fa48e307fed477d2a0fce57067cd22b572 languageName: node linkType: hard From 01ce9df88008cee705b7e02a4581802afa07c3df Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 24 Jan 2024 08:03:30 +0100 Subject: [PATCH 41/84] Fix search form re-rendering spuriously in web UI (#28876) --- .../features/compose/containers/search_container.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/compose/containers/search_container.js b/app/javascript/mastodon/features/compose/containers/search_container.js index 758b6b07db..616b91369c 100644 --- a/app/javascript/mastodon/features/compose/containers/search_container.js +++ b/app/javascript/mastodon/features/compose/containers/search_container.js @@ -1,3 +1,4 @@ +import { createSelector } from '@reduxjs/toolkit'; import { connect } from 'react-redux'; import { @@ -12,10 +13,15 @@ import { import Search from '../components/search'; +const getRecentSearches = createSelector( + state => state.getIn(['search', 'recent']), + recent => recent.reverse(), +); + const mapStateToProps = state => ({ value: state.getIn(['search', 'value']), submitted: state.getIn(['search', 'submitted']), - recent: state.getIn(['search', 'recent']).reverse(), + recent: getRecentSearches(state), }); const mapDispatchToProps = dispatch => ({ From 5b1eb09d546120cb456990e15a740d994011013f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 24 Jan 2024 10:38:10 +0100 Subject: [PATCH 42/84] Add annual reports for accounts (#28693) --- .../api/v1/annual_reports_controller.rb | 30 +++++++++ app/lib/annual_report.rb | 43 +++++++++++++ app/lib/annual_report/archetype.rb | 49 +++++++++++++++ .../commonly_interacted_with_accounts.rb | 22 +++++++ .../annual_report/most_reblogged_accounts.rb | 22 +++++++ app/lib/annual_report/most_used_apps.rb | 22 +++++++ app/lib/annual_report/percentiles.rb | 62 +++++++++++++++++++ app/lib/annual_report/source.rb | 16 +++++ app/lib/annual_report/time_series.rb | 30 +++++++++ app/lib/annual_report/top_hashtags.rb | 22 +++++++ app/lib/annual_report/top_statuses.rb | 21 +++++++ app/lib/annual_report/type_distribution.rb | 20 ++++++ app/models/generated_annual_report.rb | 37 +++++++++++ app/presenters/annual_reports_presenter.rb | 23 +++++++ .../rest/annual_report_serializer.rb | 5 ++ .../rest/annual_reports_serializer.rb | 7 +++ app/workers/generate_annual_report_worker.rb | 11 ++++ app/workers/scheduler/indexing_scheduler.rb | 2 + config/routes/api.rb | 6 ++ ...1033014_create_generated_annual_reports.rb | 17 +++++ db/schema.rb | 14 ++++- 21 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/annual_reports_controller.rb create mode 100644 app/lib/annual_report.rb create mode 100644 app/lib/annual_report/archetype.rb create mode 100644 app/lib/annual_report/commonly_interacted_with_accounts.rb create mode 100644 app/lib/annual_report/most_reblogged_accounts.rb create mode 100644 app/lib/annual_report/most_used_apps.rb create mode 100644 app/lib/annual_report/percentiles.rb create mode 100644 app/lib/annual_report/source.rb create mode 100644 app/lib/annual_report/time_series.rb create mode 100644 app/lib/annual_report/top_hashtags.rb create mode 100644 app/lib/annual_report/top_statuses.rb create mode 100644 app/lib/annual_report/type_distribution.rb create mode 100644 app/models/generated_annual_report.rb create mode 100644 app/presenters/annual_reports_presenter.rb create mode 100644 app/serializers/rest/annual_report_serializer.rb create mode 100644 app/serializers/rest/annual_reports_serializer.rb create mode 100644 app/workers/generate_annual_report_worker.rb create mode 100644 db/migrate/20240111033014_create_generated_annual_reports.rb diff --git a/app/controllers/api/v1/annual_reports_controller.rb b/app/controllers/api/v1/annual_reports_controller.rb new file mode 100644 index 0000000000..9bc8e68ac2 --- /dev/null +++ b/app/controllers/api/v1/annual_reports_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::V1::AnnualReportsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index + before_action :require_user! + before_action :set_annual_report, except: :index + + def index + with_read_replica do + @presenter = AnnualReportsPresenter.new(GeneratedAnnualReport.where(account_id: current_account.id).pending) + @relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id) + end + + render json: @presenter, + serializer: REST::AnnualReportsSerializer, + relationships: @relationships + end + + def read + @annual_report.view! + render_empty + end + + private + + def set_annual_report + @annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id]) + end +end diff --git a/app/lib/annual_report.rb b/app/lib/annual_report.rb new file mode 100644 index 0000000000..cf4297f2a4 --- /dev/null +++ b/app/lib/annual_report.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class AnnualReport + include DatabaseHelper + + SOURCES = [ + AnnualReport::Archetype, + AnnualReport::TypeDistribution, + AnnualReport::TopStatuses, + AnnualReport::MostUsedApps, + AnnualReport::CommonlyInteractedWithAccounts, + AnnualReport::TimeSeries, + AnnualReport::TopHashtags, + AnnualReport::MostRebloggedAccounts, + AnnualReport::Percentiles, + ].freeze + + SCHEMA = 1 + + def initialize(account, year) + @account = account + @year = year + end + + def generate + return if GeneratedAnnualReport.exists?(account: @account, year: @year) + + GeneratedAnnualReport.create( + account: @account, + year: @year, + schema_version: SCHEMA, + data: data + ) + end + + private + + def data + with_read_replica do + SOURCES.each_with_object({}) { |klass, hsh| hsh.merge!(klass.new(@account, @year).generate) } + end + end +end diff --git a/app/lib/annual_report/archetype.rb b/app/lib/annual_report/archetype.rb new file mode 100644 index 0000000000..ea9ef366df --- /dev/null +++ b/app/lib/annual_report/archetype.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class AnnualReport::Archetype < AnnualReport::Source + # Average number of posts (including replies and reblogs) made by + # each active user in a single year (2023) + AVERAGE_PER_YEAR = 113 + + def generate + { + archetype: archetype, + } + end + + private + + def archetype + if (standalone_count + replies_count + reblogs_count) < AVERAGE_PER_YEAR + :lurker + elsif reblogs_count > (standalone_count * 2) + :booster + elsif polls_count > (standalone_count * 0.1) # standalone_count includes posts with polls + :pollster + elsif replies_count > (standalone_count * 2) + :replier + else + :oracle + end + end + + def polls_count + @polls_count ||= base_scope.where.not(poll_id: nil).count + end + + def reblogs_count + @reblogs_count ||= base_scope.where.not(reblog_of_id: nil).count + end + + def replies_count + @replies_count ||= base_scope.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count + end + + def standalone_count + @standalone_count ||= base_scope.without_replies.without_reblogs.count + end + + def base_scope + @account.statuses.where(id: year_as_snowflake_range) + end +end diff --git a/app/lib/annual_report/commonly_interacted_with_accounts.rb b/app/lib/annual_report/commonly_interacted_with_accounts.rb new file mode 100644 index 0000000000..af5e854c22 --- /dev/null +++ b/app/lib/annual_report/commonly_interacted_with_accounts.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AnnualReport::CommonlyInteractedWithAccounts < AnnualReport::Source + SET_SIZE = 40 + + def generate + { + commonly_interacted_with_accounts: commonly_interacted_with_accounts.map do |(account_id, count)| + { + account_id: account_id, + count: count, + } + end, + } + end + + private + + def commonly_interacted_with_accounts + @account.statuses.reorder(nil).where(id: year_as_snowflake_range).where.not(in_reply_to_account_id: @account.id).group(:in_reply_to_account_id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('in_reply_to_account_id, count(*) AS total')) + end +end diff --git a/app/lib/annual_report/most_reblogged_accounts.rb b/app/lib/annual_report/most_reblogged_accounts.rb new file mode 100644 index 0000000000..e3e8a7c90b --- /dev/null +++ b/app/lib/annual_report/most_reblogged_accounts.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AnnualReport::MostRebloggedAccounts < AnnualReport::Source + SET_SIZE = 10 + + def generate + { + most_reblogged_accounts: most_reblogged_accounts.map do |(account_id, count)| + { + account_id: account_id, + count: count, + } + end, + } + end + + private + + def most_reblogged_accounts + @account.statuses.reorder(nil).where(id: year_as_snowflake_range).where.not(reblog_of_id: nil).joins(reblog: :account).group('accounts.id').having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('accounts.id, count(*) as total')) + end +end diff --git a/app/lib/annual_report/most_used_apps.rb b/app/lib/annual_report/most_used_apps.rb new file mode 100644 index 0000000000..85ff1ff86e --- /dev/null +++ b/app/lib/annual_report/most_used_apps.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AnnualReport::MostUsedApps < AnnualReport::Source + SET_SIZE = 10 + + def generate + { + most_used_apps: most_used_apps.map do |(name, count)| + { + name: name, + count: count, + } + end, + } + end + + private + + def most_used_apps + @account.statuses.reorder(nil).where(id: year_as_snowflake_range).joins(:application).group('oauth_applications.name').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('oauth_applications.name, count(*) as total')) + end +end diff --git a/app/lib/annual_report/percentiles.rb b/app/lib/annual_report/percentiles.rb new file mode 100644 index 0000000000..9fe4698ee5 --- /dev/null +++ b/app/lib/annual_report/percentiles.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class AnnualReport::Percentiles < AnnualReport::Source + def generate + { + percentiles: { + followers: (total_with_fewer_followers / (total_with_any_followers + 1.0)) * 100, + statuses: (total_with_fewer_statuses / (total_with_any_statuses + 1.0)) * 100, + }, + } + end + + private + + def followers_gained + @followers_gained ||= @account.passive_relationships.where("date_part('year', follows.created_at) = ?", @year).count + end + + def statuses_created + @statuses_created ||= @account.statuses.where(id: year_as_snowflake_range).count + end + + def total_with_fewer_followers + @total_with_fewer_followers ||= Follow.find_by_sql([<<~SQL.squish, { year: @year, comparison: followers_gained }]).first.total + WITH tmp0 AS ( + SELECT follows.target_account_id + FROM follows + INNER JOIN accounts ON accounts.id = follows.target_account_id + WHERE date_part('year', follows.created_at) = :year + AND accounts.domain IS NULL + GROUP BY follows.target_account_id + HAVING COUNT(*) < :comparison + ) + SELECT count(*) AS total + FROM tmp0 + SQL + end + + def total_with_fewer_statuses + @total_with_fewer_statuses ||= Status.find_by_sql([<<~SQL.squish, { comparison: statuses_created, min_id: year_as_snowflake_range.first, max_id: year_as_snowflake_range.last }]).first.total + WITH tmp0 AS ( + SELECT statuses.account_id + FROM statuses + INNER JOIN accounts ON accounts.id = statuses.account_id + WHERE statuses.id BETWEEN :min_id AND :max_id + AND accounts.domain IS NULL + GROUP BY statuses.account_id + HAVING count(*) < :comparison + ) + SELECT count(*) AS total + FROM tmp0 + SQL + end + + def total_with_any_followers + @total_with_any_followers ||= Follow.where("date_part('year', follows.created_at) = ?", @year).joins(:target_account).merge(Account.local).count('distinct follows.target_account_id') + end + + def total_with_any_statuses + @total_with_any_statuses ||= Status.where(id: year_as_snowflake_range).joins(:account).merge(Account.local).count('distinct statuses.account_id') + end +end diff --git a/app/lib/annual_report/source.rb b/app/lib/annual_report/source.rb new file mode 100644 index 0000000000..1ccb622676 --- /dev/null +++ b/app/lib/annual_report/source.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AnnualReport::Source + attr_reader :account, :year + + def initialize(account, year) + @account = account + @year = year + end + + protected + + def year_as_snowflake_range + (Mastodon::Snowflake.id_at(DateTime.new(year, 1, 1))..Mastodon::Snowflake.id_at(DateTime.new(year, 12, 31))) + end +end diff --git a/app/lib/annual_report/time_series.rb b/app/lib/annual_report/time_series.rb new file mode 100644 index 0000000000..a144bac0d1 --- /dev/null +++ b/app/lib/annual_report/time_series.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class AnnualReport::TimeSeries < AnnualReport::Source + def generate + { + time_series: (1..12).map do |month| + { + month: month, + statuses: statuses_per_month[month] || 0, + following: following_per_month[month] || 0, + followers: followers_per_month[month] || 0, + } + end, + } + end + + private + + def statuses_per_month + @statuses_per_month ||= @account.statuses.reorder(nil).where(id: year_as_snowflake_range).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h + end + + def following_per_month + @following_per_month ||= @account.active_relationships.where("date_part('year', created_at) = ?", @year).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h + end + + def followers_per_month + @followers_per_month ||= @account.passive_relationships.where("date_part('year', created_at) = ?", @year).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h + end +end diff --git a/app/lib/annual_report/top_hashtags.rb b/app/lib/annual_report/top_hashtags.rb new file mode 100644 index 0000000000..488dacb1b4 --- /dev/null +++ b/app/lib/annual_report/top_hashtags.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AnnualReport::TopHashtags < AnnualReport::Source + SET_SIZE = 40 + + def generate + { + top_hashtags: top_hashtags.map do |(name, count)| + { + name: name, + count: count, + } + end, + } + end + + private + + def top_hashtags + Tag.joins(:statuses).where(statuses: { id: @account.statuses.where(id: year_as_snowflake_range).reorder(nil).select(:id) }).group(:id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('COALESCE(tags.display_name, tags.name), count(*) AS total')) + end +end diff --git a/app/lib/annual_report/top_statuses.rb b/app/lib/annual_report/top_statuses.rb new file mode 100644 index 0000000000..112e5591ce --- /dev/null +++ b/app/lib/annual_report/top_statuses.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AnnualReport::TopStatuses < AnnualReport::Source + def generate + top_reblogs = base_scope.order(reblogs_count: :desc).first&.id + top_favourites = base_scope.where.not(id: top_reblogs).order(favourites_count: :desc).first&.id + top_replies = base_scope.where.not(id: [top_reblogs, top_favourites]).order(replies_count: :desc).first&.id + + { + top_statuses: { + by_reblogs: top_reblogs, + by_favourites: top_favourites, + by_replies: top_replies, + }, + } + end + + def base_scope + @account.statuses.with_public_visibility.joins(:status_stat).where(id: year_as_snowflake_range).reorder(nil) + end +end diff --git a/app/lib/annual_report/type_distribution.rb b/app/lib/annual_report/type_distribution.rb new file mode 100644 index 0000000000..fc12a6f1f4 --- /dev/null +++ b/app/lib/annual_report/type_distribution.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AnnualReport::TypeDistribution < AnnualReport::Source + def generate + { + type_distribution: { + total: base_scope.count, + reblogs: base_scope.where.not(reblog_of_id: nil).count, + replies: base_scope.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count, + standalone: base_scope.without_replies.without_reblogs.count, + }, + } + end + + private + + def base_scope + @account.statuses.where(id: year_as_snowflake_range) + end +end diff --git a/app/models/generated_annual_report.rb b/app/models/generated_annual_report.rb new file mode 100644 index 0000000000..43c97d7108 --- /dev/null +++ b/app/models/generated_annual_report.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: generated_annual_reports +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) not null +# year :integer not null +# data :jsonb not null +# schema_version :integer not null +# viewed_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class GeneratedAnnualReport < ApplicationRecord + belongs_to :account + + scope :pending, -> { where(viewed_at: nil) } + + def viewed? + viewed_at.present? + end + + def view! + update!(viewed_at: Time.now.utc) + end + + def account_ids + data['most_reblogged_accounts'].pluck('account_id') + data['commonly_interacted_with_accounts'].pluck('account_id') + end + + def status_ids + data['top_statuses'].values + end +end diff --git a/app/presenters/annual_reports_presenter.rb b/app/presenters/annual_reports_presenter.rb new file mode 100644 index 0000000000..001e1d37b0 --- /dev/null +++ b/app/presenters/annual_reports_presenter.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class AnnualReportsPresenter + alias read_attribute_for_serialization send + + attr_reader :annual_reports + + def initialize(annual_reports) + @annual_reports = annual_reports + end + + def accounts + @accounts ||= Account.where(id: @annual_reports.flat_map(&:account_ids)).includes(:account_stat, :moved_to_account, user: :role) + end + + def statuses + @statuses ||= Status.where(id: @annual_reports.flat_map(&:status_ids)).with_includes + end + + def self.model_name + @model_name ||= ActiveModel::Name.new(self) + end +end diff --git a/app/serializers/rest/annual_report_serializer.rb b/app/serializers/rest/annual_report_serializer.rb new file mode 100644 index 0000000000..1fb5ddb5c1 --- /dev/null +++ b/app/serializers/rest/annual_report_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class REST::AnnualReportSerializer < ActiveModel::Serializer + attributes :year, :data, :schema_version +end diff --git a/app/serializers/rest/annual_reports_serializer.rb b/app/serializers/rest/annual_reports_serializer.rb new file mode 100644 index 0000000000..ea9572be1b --- /dev/null +++ b/app/serializers/rest/annual_reports_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class REST::AnnualReportsSerializer < ActiveModel::Serializer + has_many :annual_reports, serializer: REST::AnnualReportSerializer + has_many :accounts, serializer: REST::AccountSerializer + has_many :statuses, serializer: REST::StatusSerializer +end diff --git a/app/workers/generate_annual_report_worker.rb b/app/workers/generate_annual_report_worker.rb new file mode 100644 index 0000000000..7094c1ab9c --- /dev/null +++ b/app/workers/generate_annual_report_worker.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class GenerateAnnualReportWorker + include Sidekiq::Worker + + def perform(account_id, year) + AnnualReport.new(Account.find(account_id), year).generate + rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique + true + end +end diff --git a/app/workers/scheduler/indexing_scheduler.rb b/app/workers/scheduler/indexing_scheduler.rb index 5c985e25a0..f52d0141d4 100644 --- a/app/workers/scheduler/indexing_scheduler.rb +++ b/app/workers/scheduler/indexing_scheduler.rb @@ -24,6 +24,8 @@ class Scheduler::IndexingScheduler end end + private + def indexes [AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex] end diff --git a/config/routes/api.rb b/config/routes/api.rb index 0fe9f69abc..853a44e0e1 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -51,6 +51,12 @@ namespace :api, format: false do resources :scheduled_statuses, only: [:index, :show, :update, :destroy] resources :preferences, only: [:index] + resources :annual_reports, only: [:index] do + member do + post :read + end + end + resources :announcements, only: [:index] do scope module: :announcements do resources :reactions, only: [:update, :destroy] diff --git a/db/migrate/20240111033014_create_generated_annual_reports.rb b/db/migrate/20240111033014_create_generated_annual_reports.rb new file mode 100644 index 0000000000..2a755fb14e --- /dev/null +++ b/db/migrate/20240111033014_create_generated_annual_reports.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateGeneratedAnnualReports < ActiveRecord::Migration[7.1] + def change + create_table :generated_annual_reports do |t| + t.belongs_to :account, null: false, foreign_key: { on_cascade: :delete }, index: false + t.integer :year, null: false + t.jsonb :data, null: false + t.integer :schema_version, null: false + t.datetime :viewed_at + + t.timestamps + end + + add_index :generated_annual_reports, [:account_id, :year], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index cbe54c1db7..50f4e7189d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do +ActiveRecord::Schema[7.1].define(version: 2024_01_11_033014) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -516,6 +516,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do t.index ["target_account_id"], name: "index_follows_on_target_account_id" end + create_table "generated_annual_reports", force: :cascade do |t| + t.bigint "account_id", null: false + t.integer "year", null: false + t.jsonb "data", null: false + t.integer "schema_version", null: false + t.datetime "viewed_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "year"], name: "index_generated_annual_reports_on_account_id_and_year", unique: true + end + create_table "identities", force: :cascade do |t| t.string "provider", default: "", null: false t.string "uid", default: "", null: false @@ -1226,6 +1237,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade + add_foreign_key "generated_annual_reports", "accounts" add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade add_foreign_key "invites", "users", on_delete: :cascade From 599bc69503e9ef54655626052c8337cedb8519d0 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 24 Jan 2024 04:57:32 -0500 Subject: [PATCH 43/84] Simplify `AccountSummary.filtered` query generation (#28868) --- app/models/account_summary.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/account_summary.rb b/app/models/account_summary.rb index 0d8835b83c..2a21d09a8b 100644 --- a/app/models/account_summary.rb +++ b/app/models/account_summary.rb @@ -12,9 +12,11 @@ class AccountSummary < ApplicationRecord self.primary_key = :account_id + has_many :follow_recommendation_suppressions, primary_key: :account_id, foreign_key: :account_id, inverse_of: false + scope :safe, -> { where(sensitive: false) } scope :localized, ->(locale) { where(language: locale) } - scope :filtered, -> { joins(arel_table.join(FollowRecommendationSuppression.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:account_id].eq(FollowRecommendationSuppression.arel_table[:account_id])).join_sources).where(FollowRecommendationSuppression.arel_table[:id].eq(nil)) } + scope :filtered, -> { where.missing(:follow_recommendation_suppressions) } def self.refresh Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false) From 9d413cbaf83f1a030a250806302c1a21435559d7 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 24 Jan 2024 04:57:49 -0500 Subject: [PATCH 44/84] Fix `Rails/WhereExists` cop in app/models (#28863) --- .rubocop_todo.yml | 3 --- app/models/poll.rb | 2 +- app/models/session_activation.rb | 2 +- app/models/status.rb | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index bef79e451a..b62dfa72a8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -81,9 +81,6 @@ Rails/WhereExists: - 'app/lib/delivery_failure_tracker.rb' - 'app/lib/feed_manager.rb' - 'app/lib/suspicious_sign_in_detector.rb' - - 'app/models/poll.rb' - - 'app/models/session_activation.rb' - - 'app/models/status.rb' - 'app/policies/status_policy.rb' - 'app/serializers/rest/announcement_serializer.rb' - 'app/workers/move_worker.rb' diff --git a/app/models/poll.rb b/app/models/poll.rb index 37149c3d86..cc4184f80a 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -57,7 +57,7 @@ class Poll < ApplicationRecord end def voted?(account) - account.id == account_id || votes.where(account: account).exists? + account.id == account_id || votes.exists?(account: account) end def own_votes(account) diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index 7f5f0d9a9a..c67180d3ba 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -41,7 +41,7 @@ class SessionActivation < ApplicationRecord class << self def active?(id) - id && where(session_id: id).exists? + id && exists?(session_id: id) end def activate(**options) diff --git a/app/models/status.rb b/app/models/status.rb index 9a2169f995..e3d41cceda 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -263,7 +263,7 @@ class Status < ApplicationRecord end def reported? - @reported ||= Report.where(target_account: account).unresolved.where('? = ANY(status_ids)', id).exists? + @reported ||= Report.where(target_account: account).unresolved.exists?(['? = ANY(status_ids)', id]) end def emojis From a34d27c18f936b63fa1d595b42b69df618793a69 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:06:56 +0100 Subject: [PATCH 45/84] New Crowdin Translations (automated) (#28875) Co-authored-by: GitHub Actions --- config/locales/ar.yml | 1 + config/locales/bg.yml | 1 + config/locales/da.yml | 6 ++++++ config/locales/devise.ru.yml | 9 +++++++++ config/locales/devise.sq.yml | 9 +++++++++ config/locales/es-MX.yml | 4 ++++ config/locales/es.yml | 4 ++++ config/locales/fy.yml | 6 ++++++ config/locales/gl.yml | 6 ++++++ config/locales/ru.yml | 14 ++++++++++++++ config/locales/simple_form.sk.yml | 1 + config/locales/sk.yml | 3 +++ config/locales/sq.yml | 13 +++++++++++++ config/locales/tr.yml | 2 +- config/locales/vi.yml | 6 ++++++ 15 files changed, 84 insertions(+), 1 deletion(-) diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 3c8c643fe7..e6d653c674 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -1934,6 +1934,7 @@ ar: go_to_sso_account_settings: انتقل إلى إعدادات حساب مزود الهوية الخاص بك invalid_otp_token: رمز المصادقة بخطوتين غير صالح otp_lost_help_html: إن فقدتَهُما ، يمكنك الاتصال بـ %{email} + rate_limited: عدد محاولات التحقق كثير جدًا، يرجى المحاولة مرة أخرى لاحقًا. seamless_external_login: لقد قمت بتسجيل الدخول عبر خدمة خارجية، إنّ إعدادات الكلمة السرية و البريد الإلكتروني غير متوفرة. signed_in_as: 'تم تسجيل دخولك بصفة:' verification: diff --git a/config/locales/bg.yml b/config/locales/bg.yml index c3eaa7e4c2..b9a3135448 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -1793,6 +1793,7 @@ bg: failed_2fa: details: 'Ето подробности на опита за влизане:' explanation: Някой се опита да влезе в акаунта ви, но предостави невалиден втори фактор за удостоверяване. + further_actions_html: Ако не бяхте вие, то препоръчваме да направите %{action} незабавно, тъй като може да се злепостави. subject: Неуспешен втори фактор за удостоверяване title: Провал на втория фактор за удостоверяване suspicious_sign_in: diff --git a/config/locales/da.yml b/config/locales/da.yml index 58fd723aef..d92d001905 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1790,6 +1790,12 @@ da: extra: Sikkerhedskopien kan nu downloades! subject: Dit arkiv er klar til download title: Arkiv download + failed_2fa: + details: 'Her er detaljerne om login-forsøget:' + explanation: Nogen har forsøgt at logge ind på kontoen, men har angivet en ugyldig anden godkendelsesfaktor. + further_actions_html: Var dette ikke dig, anbefales det straks at %{action}, da den kan være kompromitteret. + subject: Anden faktor godkendelsesfejl + title: Fejlede på anden faktor godkendelse suspicious_sign_in: change_password: ændrer din adgangskode details: 'Her er nogle detaljer om login-forsøget:' diff --git a/config/locales/devise.ru.yml b/config/locales/devise.ru.yml index ccbd13438d..9dd418f2cd 100644 --- a/config/locales/devise.ru.yml +++ b/config/locales/devise.ru.yml @@ -47,14 +47,19 @@ ru: subject: 'Mastodon: Инструкция по сбросу пароля' title: Сброс пароля two_factor_disabled: + explanation: Вход в систему теперь возможен только с использованием адреса электронной почты и пароля. subject: 'Mastodon: Двухфакторная авторизация отключена' + subtitle: Двухфакторная аутентификация для вашей учетной записи была отключена. title: 2ФА отключена two_factor_enabled: + explanation: Для входа в систему потребуется токен, сгенерированный сопряженным приложением TOTP. subject: 'Mastodon: Настроена двухфакторная авторизация' + subtitle: Для вашей учетной записи была включена двухфакторная аутентификация. title: 2ФА включена two_factor_recovery_codes_changed: explanation: Предыдущие резервные коды были аннулированы и созданы новые. subject: 'Mastodon: Резервные коды двуфакторной авторизации обновлены' + subtitle: Предыдущие коды восстановления были аннулированы и сгенерированы новые. title: Коды восстановления 2FA изменены unlock_instructions: subject: 'Mastodon: Инструкция по разблокировке' @@ -68,9 +73,13 @@ ru: subject: 'Мастодон: Ключ Безопасности удален' title: Один из ваших защитных ключей был удален webauthn_disabled: + explanation: Аутентификация с помощью ключей безопасности была отключена для вашей учетной записи. + extra: Теперь вход в систему возможен только с использованием токена, сгенерированного сопряженным приложением TOTP. subject: 'Мастодон: Аутентификация с ключами безопасности отключена' title: Ключи безопасности отключены webauthn_enabled: + explanation: Для вашей учетной записи включена аутентификация по ключу безопасности. + extra: Теперь ваш ключ безопасности можно использовать для входа в систему. subject: 'Мастодон: Включена аутентификация по ключу безопасности' title: Ключи безопасности включены omniauth_callbacks: diff --git a/config/locales/devise.sq.yml b/config/locales/devise.sq.yml index 7cea2f8e2e..32136a0baa 100644 --- a/config/locales/devise.sq.yml +++ b/config/locales/devise.sq.yml @@ -47,14 +47,19 @@ sq: subject: 'Mastodon: Udhëzime ricaktimi fjalëkalimi' title: Ricaktim fjalëkalimi two_factor_disabled: + explanation: Hyrja tanimë është e mundshme duke përdorur vetëm adresë email dhe fjalëkalim. subject: 'Mastodon: U çaktivizua mirëfilltësimi dyfaktorësh' + subtitle: Mirëfilltësimi dyfaktorësh për llogarinë tuaj është çaktivizuar. title: 2FA u çaktivizua two_factor_enabled: + explanation: Për të kryer hyrjen do të kërkohet doemos një token i prodhuar nga aplikacioni TOTP i çiftuar. subject: 'Mastodon: U aktivizua mirëfilltësimi dyfaktorësh' + subtitle: Për llogarinë tuaj është aktivizuar mirëfilltësmi dyfaktorësh. title: 2FA u aktivizua two_factor_recovery_codes_changed: explanation: Kodet e dikurshëm të rikthimit janë bërë të pavlefshëm dhe janë prodhuar të rinj. subject: 'Mastodon: U riprodhuan kode rikthimi dyfaktorësh' + subtitle: Kodet e dikurshëm të rikthimit janë bërë të pavlefshëm dhe janë prodhuar të rinj. title: Kodet e rikthimit 2FA u ndryshuan unlock_instructions: subject: 'Mastodon: Udhëzime shkyçjeje' @@ -68,9 +73,13 @@ sq: subject: 'Mastodon: Fshirje kyçi sigurie' title: Një nga kyçet tuaj të sigurisë është fshirë webauthn_disabled: + explanation: Mirëfilltësimi me kyçe sigurie është çaktivizuar për llogarinë tuaj. + extra: Hyrjet tani janë të mundshme vetëm duke përdorur token-in e prodhuar nga aplikacioni TOTP i çiftuar. subject: 'Mastodon: U çaktivizua mirëfilltësimi me kyçe sigurie' title: U çaktivizuan kyçe sigurie webauthn_enabled: + explanation: Mirëfilltësimi me kyçe sigurie është aktivizuar për këtë llogari. + extra: Kyçi juaj i sigurisë tanimë mund të përdoret për hyrje. subject: 'Mastodon: U aktivizua mirëfilltësim me kyçe sigurie' title: U aktivizuan kyçe sigurie omniauth_callbacks: diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 040d8a9d3c..b84fb7cf96 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1792,6 +1792,10 @@ es-MX: title: Descargar archivo failed_2fa: details: 'Estos son los detalles del intento de inicio de sesión:' + explanation: Alguien ha intentado iniciar sesión en tu cuenta pero proporcionó un segundo factor de autenticación inválido. + further_actions_html: Si no fuiste tú, se recomienda %{action} inmediatamente ya que puede estar comprometido. + subject: Fallo de autenticación de segundo factor + title: Falló la autenticación de segundo factor suspicious_sign_in: change_password: cambies tu contraseña details: 'Aquí están los detalles del inicio de sesión:' diff --git a/config/locales/es.yml b/config/locales/es.yml index ffe3eb5b00..95816d6bcb 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1792,6 +1792,10 @@ es: title: Descargar archivo failed_2fa: details: 'Estos son los detalles del intento de inicio de sesión:' + explanation: Alguien ha intentado iniciar sesión en tu cuenta pero proporcionó un segundo factor de autenticación inválido. + further_actions_html: Si no fuiste tú, se recomienda %{action} inmediatamente ya que puede estar comprometida. + subject: Fallo de autenticación del segundo factor + title: Fallo en la autenticación del segundo factor suspicious_sign_in: change_password: cambies tu contraseña details: 'Aquí están los detalles del inicio de sesión:' diff --git a/config/locales/fy.yml b/config/locales/fy.yml index f861bc3e4a..c59ad72725 100644 --- a/config/locales/fy.yml +++ b/config/locales/fy.yml @@ -1790,6 +1790,12 @@ fy: extra: It stiet no klear om download te wurden! subject: Jo argyf stiet klear om download te wurden title: Argyf ophelje + failed_2fa: + details: 'Hjir binne de details fan de oanmeldbesykjen:' + explanation: Ien hat probearre om oan te melden op jo account, mar hat in ûnjildige twaddeferifikaasjefaktor opjûn. + further_actions_html: As jo dit net wiene, rekommandearje wy jo oan daliks %{action}, omdat it kompromitearre wêze kin. + subject: Twaddefaktorautentikaasjeflater + title: Twastapsferifikaasje mislearre suspicious_sign_in: change_password: wizigje jo wachtwurd details: 'Hjir binne de details fan oanmeldbesykjen:' diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 3c43a4e23d..087ed2ec76 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -1790,6 +1790,12 @@ gl: extra: Está preparada para descargala! subject: O teu ficheiro xa está preparado para descargar title: Leve o ficheiro + failed_2fa: + details: 'Detalles do intento de acceso:' + explanation: Alguén intentou acceder á túa conta mais fíxoo cun segundo factor de autenticación non válido. + further_actions_html: Se non foches ti, recomendámosche %{action} inmediatamente xa que a conta podería estar en risco. + subject: Fallo co segundo factor de autenticación + title: Fallou o segundo factor de autenticación suspicious_sign_in: change_password: cambia o teu contrasinal details: 'Estos son os detalles do acceso:' diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 2644275c37..24edbdc75e 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -439,6 +439,7 @@ ru: view: Посмотреть доменные блокировки email_domain_blocks: add_new: Добавить новую + allow_registrations_with_approval: Разрешить регистрацию с одобрением attempts_over_week: few: "%{count} попытки за последнюю неделю" many: "%{count} попыток за последнюю неделю" @@ -1659,6 +1660,7 @@ ru: unknown_browser: Неизвестный браузер weibo: Weibo current_session: Текущая сессия + date: Дата description: "%{browser} на %{platform}" explanation: Здесь отображаются все браузеры, с которых выполнен вход в вашу учётную запись. Авторизованные приложения находятся в секции «Приложения». ip: IP @@ -1837,16 +1839,27 @@ ru: webauthn: Ключи безопасности user_mailer: appeal_approved: + action: Настройки аккаунта explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись снова на хорошем счету. subject: Ваше обжалование от %{date} была одобрено + subtitle: Ваш аккаунт снова с хорошей репутацией. title: Обжалование одобрено appeal_rejected: explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись восстановлена. subject: Ваше обжалование от %{date} отклонено + subtitle: Ваша апелляция отклонена. title: Обжалование отклонено backup_ready: + explanation: Вы запросили полное резервное копирование вашей учетной записи Mastodon. + extra: Теперь он готов к загрузке! subject: Ваш архив готов к загрузке title: Архив ваших данных готов + failed_2fa: + details: 'Вот подробности попытки регистрации:' + explanation: Кто-то пытался войти в вашу учетную запись, но указал неверный второй фактор аутентификации. + further_actions_html: Если это не вы, мы рекомендуем %{action} немедленно принять меры, так как он может быть скомпрометирован. + subject: Сбой двухфакторной аутентификации + title: Сбой двухфакторной аутентификации suspicious_sign_in: change_password: сменить пароль details: 'Подробности о новом входе:' @@ -1900,6 +1913,7 @@ ru: go_to_sso_account_settings: Перейти к настройкам сторонних аккаунтов учетной записи invalid_otp_token: Введен неверный код двухфакторной аутентификации otp_lost_help_html: Если Вы потеряли доступ к обоим, свяжитесь с %{email} + rate_limited: Слишком много попыток аутентификации, повторите попытку позже. seamless_external_login: Вы залогинены через сторонний сервис, поэтому настройки e-mail и пароля недоступны. signed_in_as: 'Выполнен вход под именем:' verification: diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml index e13a05835f..614812a3a9 100644 --- a/config/locales/simple_form.sk.yml +++ b/config/locales/simple_form.sk.yml @@ -60,6 +60,7 @@ sk: fields: name: Označenie value: Obsah + unlocked: Automaticky prijímaj nových nasledovateľov account_alias: acct: Adresa starého účtu account_migration: diff --git a/config/locales/sk.yml b/config/locales/sk.yml index c639bbe1a6..e83ae348f6 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -430,6 +430,7 @@ sk: dashboard: instance_accounts_dimension: Najsledovanejšie účty instance_accounts_measure: uložené účty + instance_followers_measure: naši nasledovatelia tam instance_follows_measure: ich sledovatelia tu instance_languages_dimension: Najpopulárnejšie jazyky instance_media_attachments_measure: uložené mediálne prílohy @@ -1257,6 +1258,8 @@ sk: extra: Teraz je pripravená na stiahnutie! subject: Tvoj archív je pripravený na stiahnutie title: Odber archívu + failed_2fa: + details: 'Tu sú podrobnosti o pokuse o prihlásenie:' warning: subject: disable: Tvoj účet %{acct} bol zamrazený diff --git a/config/locales/sq.yml b/config/locales/sq.yml index 1693db7f31..d6e6925c70 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -1604,6 +1604,7 @@ sq: unknown_browser: Shfletues i Panjohur weibo: Weibo current_session: Sesioni i tanishëm + date: Datë description: "%{browser} në %{platform}" explanation: Këta janë shfletuesit e përdorur tani për hyrje te llogaria juaj Mastodon. ip: IP @@ -1770,16 +1771,27 @@ sq: webauthn: Kyçe sigurie user_mailer: appeal_approved: + action: Rregullime Llogarie explanation: Apelimi i paralajmërimit kundër llogarisë tuaj më %{strike_date}, të cilin e parashtruar më %{appeal_date} është miratuar. Llogaria juaj është sërish në pozita të mira. subject: Apelimi juaj i datës %{date} u miratua + subtitle: Llogaria juaj edhe një herë është e shëndetshme. title: Apelimi u miratua appeal_rejected: explanation: Apelimi i paralajmërimit kundër llogarisë tuaj më %{strike_date}, të cilin e parashtruar më %{appeal_date}, u hodh poshtë. subject: Apelimi juaj prej %{date} është hedhur poshtë + subtitle: Apelimi juaj është hedhur poshtë. title: Apelimi u hodh poshtë backup_ready: + explanation: Kërkuat një kopjeruajtje të plotë të llogarisë tuaj Mastodon. + extra: Tani është gati për shkarkim! subject: Arkivi juaj është gati për shkarkim title: Marrje arkivi me vete + failed_2fa: + details: 'Ja hollësitë e përpjekjes për hyrje:' + explanation: Dikush ka provuar të hyjë në llogarinë tuaj, por dha faktor të dytë mirëfilltësimi. + further_actions_html: Nëse s’qetë ju, rekomandojmë të %{action} menjëherë, ngaqë mund të jetë komprometua. + subject: Dështim faktori të dytë mirëfilltësimesh + title: Dështoi mirëfilltësimi me faktor të dytë suspicious_sign_in: change_password: ndryshoni fjalëkalimin tuaj details: 'Ja hollësitë për hyrjen:' @@ -1833,6 +1845,7 @@ sq: go_to_sso_account_settings: Kaloni te rregullime llogarie te shërbimi juaj i identitetit invalid_otp_token: Kod dyfaktorësh i pavlefshëm otp_lost_help_html: Nëse humbët hyrjen te të dy, mund të lidheni me %{email} + rate_limited: Shumë përpjekje mirëfilltësimi, riprovoni më vonë. seamless_external_login: Jeni futur përmes një shërbimi të jashtëm, ndaj s’ka rregullime fjalëkalimi dhe email. signed_in_as: 'I futur si:' verification: diff --git a/config/locales/tr.yml b/config/locales/tr.yml index fa84d2a96d..2b5b5ad45b 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1791,7 +1791,7 @@ tr: subject: Arşiviniz indirilmeye hazır title: Arşiv paketlemesi failed_2fa: - details: 'Oturum açma denemesinin ayrıntıları şöyledir:' + details: 'İşte oturum açma girişiminin ayrıntıları:' explanation: Birisi hesabınızda oturum açmaya çalıştı ancak hatalı bir iki aşamalı doğrulama kodu kullandı. further_actions_html: Eğer bu kişi siz değilseniz, hemen %{action} yapmanızı öneriyoruz çünkü hesabınız ifşa olmuş olabilir. subject: İki aşamalı doğrulama başarısızlığı diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 3817b18f07..1ece72e154 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1758,6 +1758,12 @@ vi: extra: Hiện nó đã sẵn sàng tải xuống! subject: Dữ liệu cá nhân của bạn đã sẵn sàng để tải về title: Nhận dữ liệu cá nhân + failed_2fa: + details: 'Chi tiết thông tin đăng nhập:' + explanation: Ai đó đã cố đăng nhập vào tài khoản của bạn nhưng cung cấp yếu tố xác thực thứ hai không hợp lệ. + further_actions_html: Nếu không phải bạn, hãy lập tức %{action} vì có thể có rủi ro. + subject: Xác minh hai bước thất bại + title: Xác minh hai bước thất bại suspicious_sign_in: change_password: đổi mật khẩu của bạn details: 'Chi tiết thông tin đăng nhập:' From a11a2fb052ad2903426e7798040f699b8a1f051c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 24 Jan 2024 05:31:31 -0500 Subject: [PATCH 46/84] Add error classes to api/base errors coverage (#28864) --- spec/controllers/api/base_controller_spec.rb | 30 +++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/spec/controllers/api/base_controller_spec.rb b/spec/controllers/api/base_controller_spec.rb index db1e8777f7..f8e014be2f 100644 --- a/spec/controllers/api/base_controller_spec.rb +++ b/spec/controllers/api/base_controller_spec.rb @@ -12,7 +12,7 @@ describe Api::BaseController do head 200 end - def error + def failure FakeService.new end end @@ -30,7 +30,7 @@ describe Api::BaseController do it 'does not protect from forgery' do ActionController::Base.allow_forgery_protection = true - post 'success' + post :success expect(response).to have_http_status(200) end end @@ -50,47 +50,55 @@ describe Api::BaseController do it 'returns http forbidden for unconfirmed accounts' do user.update(confirmed_at: nil) - post 'success' + post :success expect(response).to have_http_status(403) end it 'returns http forbidden for pending accounts' do user.update(approved: false) - post 'success' + post :success expect(response).to have_http_status(403) end it 'returns http forbidden for disabled accounts' do user.update(disabled: true) - post 'success' + post :success expect(response).to have_http_status(403) end it 'returns http forbidden for suspended accounts' do user.account.suspend! - post 'success' + post :success expect(response).to have_http_status(403) end end describe 'error handling' do before do - routes.draw { get 'error' => 'api/base#error' } + routes.draw { get 'failure' => 'api/base#failure' } end { ActiveRecord::RecordInvalid => 422, - Mastodon::ValidationError => 422, ActiveRecord::RecordNotFound => 404, - Mastodon::UnexpectedResponseError => 503, + ActiveRecord::RecordNotUnique => 422, + Date::Error => 422, HTTP::Error => 503, - OpenSSL::SSL::SSLError => 503, + Mastodon::InvalidParameterError => 400, Mastodon::NotPermittedError => 403, + Mastodon::RaceConditionError => 503, + Mastodon::RateLimitExceededError => 429, + Mastodon::UnexpectedResponseError => 503, + Mastodon::ValidationError => 422, + OpenSSL::SSL::SSLError => 503, + Seahorse::Client::NetworkingError => 503, + Stoplight::Error::RedLight => 503, }.each do |error, code| it "Handles error class of #{error}" do allow(FakeService).to receive(:new).and_raise(error) - get 'error' + get :failure + expect(response).to have_http_status(code) expect(FakeService).to have_received(:new) end From 7a1f087659204e9d0cbba2de37e45b1921cefe20 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 24 Jan 2024 05:32:54 -0500 Subject: [PATCH 47/84] Add `created_before` and `updated_before` scopes to `MediaAttachment` (#28869) --- app/lib/vacuum/media_attachments_vacuum.rb | 10 ++++++++-- app/models/media_attachment.rb | 12 +++++++----- lib/mastodon/cli/statuses.rb | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/lib/vacuum/media_attachments_vacuum.rb b/app/lib/vacuum/media_attachments_vacuum.rb index ab7ea4092f..e558195290 100644 --- a/app/lib/vacuum/media_attachments_vacuum.rb +++ b/app/lib/vacuum/media_attachments_vacuum.rb @@ -27,11 +27,17 @@ class Vacuum::MediaAttachmentsVacuum end def media_attachments_past_retention_period - MediaAttachment.remote.cached.where(MediaAttachment.arel_table[:created_at].lt(@retention_period.ago)).where(MediaAttachment.arel_table[:updated_at].lt(@retention_period.ago)) + MediaAttachment + .remote + .cached + .created_before(@retention_period.ago) + .updated_before(@retention_period.ago) end def orphaned_media_attachments - MediaAttachment.unattached.where(MediaAttachment.arel_table[:created_at].lt(TTL.ago)) + MediaAttachment + .unattached + .created_before(TTL.ago) end def retention_period? diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 1f40e5725e..7ff6a15f5d 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -204,12 +204,14 @@ class MediaAttachment < ApplicationRecord validates :file, presence: true, if: :local? validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? } - scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) } - scope :cached, -> { remote.where.not(file_file_name: nil) } - scope :local, -> { where(remote_url: '') } - scope :ordered, -> { order(id: :asc) } - scope :remote, -> { where.not(remote_url: '') } + scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) } + scope :cached, -> { remote.where.not(file_file_name: nil) } + scope :created_before, ->(value) { where(arel_table[:created_at].lt(value)) } + scope :local, -> { where(remote_url: '') } + scope :ordered, -> { order(id: :asc) } + scope :remote, -> { where.not(remote_url: '') } scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) } + scope :updated_before, ->(value) { where(arel_table[:updated_at].lt(value)) } attr_accessor :skip_download diff --git a/lib/mastodon/cli/statuses.rb b/lib/mastodon/cli/statuses.rb index 7acf3f9b77..48d76e0288 100644 --- a/lib/mastodon/cli/statuses.rb +++ b/lib/mastodon/cli/statuses.rb @@ -120,7 +120,7 @@ module Mastodon::CLI say('Beginning removal of now-orphaned media attachments to free up disk space...') - scope = MediaAttachment.unattached.where('created_at < ?', options[:days].pred.days.ago) + scope = MediaAttachment.unattached.created_before(options[:days].pred.days.ago) processed = 0 removed = 0 progress = create_progress_bar(scope.count) From b19ae521b7d28a76e8e1d8da8157e051e9d8de6c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 24 Jan 2024 11:49:19 +0100 Subject: [PATCH 48/84] Add confirmation when redirecting logged-out requests to permalink (#27792) Co-authored-by: Claire --- .../concerns/web_app_controller_concern.rb | 15 ++++- .../redirect/accounts_controller.rb | 10 +++ app/controllers/redirect/base_controller.rb | 24 +++++++ .../redirect/statuses_controller.rb | 10 +++ .../styles/mastodon/containers.scss | 56 +++++++++++++++++ app/lib/permalink_redirector.rb | 63 +++++++++++-------- app/views/redirects/show.html.haml | 8 +++ config/locales/en.yml | 3 + config/routes.rb | 5 ++ 9 files changed, 165 insertions(+), 29 deletions(-) create mode 100644 app/controllers/redirect/accounts_controller.rb create mode 100644 app/controllers/redirect/base_controller.rb create mode 100644 app/controllers/redirect/statuses_controller.rb create mode 100644 app/views/redirects/show.html.haml diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 5687d6e5b6..b8c909877b 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -21,10 +21,19 @@ module WebAppControllerConcern def redirect_unauthenticated_to_permalinks! return if user_signed_in? && current_account.moved_to_account_id.nil? - redirect_path = PermalinkRedirector.new(request.path).redirect_path - return if redirect_path.blank? + permalink_redirector = PermalinkRedirector.new(request.path) + return if permalink_redirector.redirect_path.blank? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? - redirect_to(redirect_path) + + respond_to do |format| + format.html do + redirect_to(permalink_redirector.redirect_confirmation_path, allow_other_host: false) + end + + format.json do + redirect_to(permalink_redirector.redirect_uri, allow_other_host: true) + end + end end end diff --git a/app/controllers/redirect/accounts_controller.rb b/app/controllers/redirect/accounts_controller.rb new file mode 100644 index 0000000000..98d2cc2b1f --- /dev/null +++ b/app/controllers/redirect/accounts_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Redirect::AccountsController < ApplicationController + private + + def set_resource + @resource = Account.find(params[:id]) + not_found if @resource.local? + end +end diff --git a/app/controllers/redirect/base_controller.rb b/app/controllers/redirect/base_controller.rb new file mode 100644 index 0000000000..90894ec1ed --- /dev/null +++ b/app/controllers/redirect/base_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Redirect::BaseController < ApplicationController + vary_by 'Accept-Language' + + before_action :set_resource + before_action :set_app_body_class + + def show + @redirect_path = ActivityPub::TagManager.instance.url_for(@resource) + + render 'redirects/show', layout: 'application' + end + + private + + def set_app_body_class + @body_classes = 'app-body' + end + + def set_resource + raise NotImplementedError + end +end diff --git a/app/controllers/redirect/statuses_controller.rb b/app/controllers/redirect/statuses_controller.rb new file mode 100644 index 0000000000..37a938c651 --- /dev/null +++ b/app/controllers/redirect/statuses_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Redirect::StatusesController < Redirect::BaseController + private + + def set_resource + @resource = Status.find(params[:id]) + not_found if @resource.local? || !@resource.distributable? + end +end diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 3d646da239..b6e995787d 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -104,3 +104,59 @@ margin-inline-start: 10px; } } + +.redirect { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + font-size: 14px; + line-height: 18px; + + &__logo { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 30px; + + img { + height: 48px; + } + } + + &__message { + text-align: center; + + h1 { + font-size: 17px; + line-height: 22px; + font-weight: 700; + margin-bottom: 30px; + } + + p { + margin-bottom: 30px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: $highlight-text-color; + font-weight: 500; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } + + &__link { + margin-top: 15px; + } +} diff --git a/app/lib/permalink_redirector.rb b/app/lib/permalink_redirector.rb index 0dd37483e2..f551f69db8 100644 --- a/app/lib/permalink_redirector.rb +++ b/app/lib/permalink_redirector.rb @@ -5,17 +5,46 @@ class PermalinkRedirector def initialize(path) @path = path + @object = nil + end + + def object + @object ||= begin + if at_username_status_request? || statuses_status_request? + status = Status.find_by(id: second_segment) + status if status&.distributable? && !status&.local? + elsif at_username_request? + username, domain = first_segment.delete_prefix('@').split('@') + domain = nil if TagManager.instance.local_domain?(domain) + account = Account.find_remote(username, domain) + account unless account&.local? + elsif accounts_request? && record_integer_id_request? + account = Account.find_by(id: second_segment) + account unless account&.local? + end + end end def redirect_path - if at_username_status_request? || statuses_status_request? - find_status_url_by_id(second_segment) - elsif at_username_request? - find_account_url_by_name(first_segment) - elsif accounts_request? && record_integer_id_request? - find_account_url_by_id(second_segment) - elsif @path.start_with?('/deck') - @path.delete_prefix('/deck') + return ActivityPub::TagManager.instance.url_for(object) if object.present? + + @path.delete_prefix('/deck') if @path.start_with?('/deck') + end + + def redirect_uri + return ActivityPub::TagManager.instance.uri_for(object) if object.present? + + @path.delete_prefix('/deck') if @path.start_with?('/deck') + end + + def redirect_confirmation_path + case object.class.name + when 'Account' + redirect_account_path(object.id) + when 'Status' + redirect_status_path(object.id) + else + @path.delete_prefix('/deck') if @path.start_with?('/deck') end end @@ -56,22 +85,4 @@ class PermalinkRedirector def path_segments @path_segments ||= @path.delete_prefix('/deck').delete_prefix('/').split('/') end - - def find_status_url_by_id(id) - status = Status.find_by(id: id) - ActivityPub::TagManager.instance.url_for(status) if status&.distributable? && !status.account.local? - end - - def find_account_url_by_id(id) - account = Account.find_by(id: id) - ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local? - end - - def find_account_url_by_name(name) - username, domain = name.gsub(/\A@/, '').split('@') - domain = nil if TagManager.instance.local_domain?(domain) - account = Account.find_remote(username, domain) - - ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local? - end end diff --git a/app/views/redirects/show.html.haml b/app/views/redirects/show.html.haml new file mode 100644 index 0000000000..0d09387a9c --- /dev/null +++ b/app/views/redirects/show.html.haml @@ -0,0 +1,8 @@ +.redirect + .redirect__logo + = link_to render_logo, root_path + + .redirect__message + %h1= t('redirects.title', instance: site_hostname) + %p= t('redirects.prompt') + %p= link_to @redirect_path, @redirect_path, rel: 'noreferrer noopener' diff --git a/config/locales/en.yml b/config/locales/en.yml index 83eaaa4552..9d739be07f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1547,6 +1547,9 @@ en: errors: limit_reached: Limit of different reactions reached unrecognized_emoji: is not a recognized emoji + redirects: + prompt: If you trust this link, click it to continue. + title: You are leaving %{instance}. relationships: activity: Account activity confirm_follow_selected_followers: Are you sure you want to follow selected followers? diff --git a/config/routes.rb b/config/routes.rb index 85c3b18556..c4f862acaf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -162,6 +162,11 @@ Rails.application.routes.draw do end end + namespace :redirect do + resources :accounts, only: :show + resources :statuses, only: :show + end + resources :media, only: [:show] do get :player end From 41c2af22705d9a8b265f77dae5da960d2eef2150 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 12:50:41 +0100 Subject: [PATCH 49/84] chore(deps): update dependency rubocop to v1.60.1 (#28731) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 93931d8724..54955000b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -504,7 +504,7 @@ GEM orm_adapter (0.5.0) ox (2.14.17) parallel (1.24.0) - parser (3.2.2.4) + parser (3.3.0.5) ast (~> 2.4.1) racc parslet (2.0.0) @@ -610,7 +610,7 @@ GEM redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.8.3) + regexp_parser (2.9.0) reline (0.4.2) io-console (~> 0.5) request_store (1.5.1) @@ -650,11 +650,11 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 8) rspec-support (3.12.1) - rubocop (1.59.0) + rubocop (1.60.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) From 1290fede651b47585de7dbfe4a9b118dc9d59856 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 24 Jan 2024 06:51:09 -0500 Subject: [PATCH 50/84] Fix `Rails/WhereExists` cop in app/lib (#28862) --- .rubocop_todo.yml | 4 ---- app/lib/activitypub/activity/create.rb | 4 ++-- app/lib/delivery_failure_tracker.rb | 2 +- app/lib/feed_manager.rb | 6 +++--- app/lib/suspicious_sign_in_detector.rb | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b62dfa72a8..302c66a16a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -77,10 +77,6 @@ Rails/WhereExists: Exclude: - 'app/controllers/activitypub/inboxes_controller.rb' - 'app/controllers/admin/email_domain_blocks_controller.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/delivery_failure_tracker.rb' - - 'app/lib/feed_manager.rb' - - 'app/lib/suspicious_sign_in_detector.rb' - 'app/policies/status_policy.rb' - 'app/serializers/rest/announcement_serializer.rb' - 'app/workers/move_worker.rb' diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 5a2d33c1fa..62c35d4dd3 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -320,7 +320,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity already_voted = true with_redis_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do - already_voted = poll.votes.where(account: @account).exists? + already_voted = poll.votes.exists?(account: @account) poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri) end @@ -406,7 +406,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return false if local_usernames.empty? - Account.local.where(username: local_usernames).exists? + Account.local.exists?(username: local_usernames) end def tombstone_exists? diff --git a/app/lib/delivery_failure_tracker.rb b/app/lib/delivery_failure_tracker.rb index d938269829..e17b45d667 100644 --- a/app/lib/delivery_failure_tracker.rb +++ b/app/lib/delivery_failure_tracker.rb @@ -28,7 +28,7 @@ class DeliveryFailureTracker end def available? - !UnavailableDomain.where(domain: @host).exists? + !UnavailableDomain.exists?(domain: @host) end def exhausted_deliveries_days diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 53767486ff..38a177e645 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -420,8 +420,8 @@ class FeedManager check_for_blocks = status.active_mentions.pluck(:account_id) check_for_blocks.push(status.in_reply_to_account) if status.reply? && !status.in_reply_to_account_id.nil? - should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) - should_filter ||= status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists? # of if the account is silenced and I'm not following them + should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) + should_filter ||= status.account.silenced? && !Follow.exists?(account_id: receiver_id, target_account_id: status.account_id) # Filter if the account is silenced and I'm not following them should_filter end @@ -434,7 +434,7 @@ class FeedManager if status.reply? && status.in_reply_to_account_id != status.account_id should_filter = status.in_reply_to_account_id != list.account_id should_filter &&= !list.show_followed? - should_filter &&= !(list.show_list? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?) + should_filter &&= !(list.show_list? && ListAccount.exists?(list_id: list.id, account_id: status.in_reply_to_account_id)) return !!should_filter end diff --git a/app/lib/suspicious_sign_in_detector.rb b/app/lib/suspicious_sign_in_detector.rb index 1af5188c65..74f49aa558 100644 --- a/app/lib/suspicious_sign_in_detector.rb +++ b/app/lib/suspicious_sign_in_detector.rb @@ -19,7 +19,7 @@ class SuspiciousSignInDetector end def previously_seen_ip?(request) - @user.ips.where('ip <<= ?', masked_ip(request)).exists? + @user.ips.exists?(['ip <<= ?', masked_ip(request)]) end def freshly_signed_up? From 5a838ceaa9a003bc2e2fdee727d4aa87cd53de4f Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Jan 2024 13:37:43 +0100 Subject: [PATCH 51/84] Use active variants for boost icons and increase icon size (#27924) --- app/javascript/mastodon/components/status_action_bar.jsx | 4 +++- .../mastodon/features/status/components/action_bar.jsx | 4 +++- app/javascript/svg-icons/repeat_active.svg | 4 ++++ app/javascript/svg-icons/repeat_disabled.svg | 0 app/javascript/svg-icons/repeat_private.svg | 0 app/javascript/svg-icons/repeat_private_active.svg | 6 ++++++ 6 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 app/javascript/svg-icons/repeat_active.svg mode change 100755 => 100644 app/javascript/svg-icons/repeat_disabled.svg mode change 100755 => 100644 app/javascript/svg-icons/repeat_private.svg create mode 100644 app/javascript/svg-icons/repeat_private_active.svg diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index bfe77a4900..b111a65385 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -18,8 +18,10 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -366,7 +368,7 @@ class StatusActionBar extends ImmutablePureComponent { if (status.get('reblogged')) { reblogTitle = intl.formatMessage(messages.cancel_reblog_private); - reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon; + reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon; } else if (publicStatus) { reblogTitle = intl.formatMessage(messages.reblog); reblogIconComponent = RepeatIcon; diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 4cb06aac2c..c243a49129 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -17,8 +17,10 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -296,7 +298,7 @@ class ActionBar extends PureComponent { if (status.get('reblogged')) { reblogTitle = intl.formatMessage(messages.cancel_reblog_private); - reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon; + reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon; } else if (publicStatus) { reblogTitle = intl.formatMessage(messages.reblog); reblogIconComponent = RepeatIcon; diff --git a/app/javascript/svg-icons/repeat_active.svg b/app/javascript/svg-icons/repeat_active.svg new file mode 100644 index 0000000000..a5bbb8fc4f --- /dev/null +++ b/app/javascript/svg-icons/repeat_active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/javascript/svg-icons/repeat_disabled.svg b/app/javascript/svg-icons/repeat_disabled.svg old mode 100755 new mode 100644 diff --git a/app/javascript/svg-icons/repeat_private.svg b/app/javascript/svg-icons/repeat_private.svg old mode 100755 new mode 100644 diff --git a/app/javascript/svg-icons/repeat_private_active.svg b/app/javascript/svg-icons/repeat_private_active.svg new file mode 100644 index 0000000000..cf2a05c84e --- /dev/null +++ b/app/javascript/svg-icons/repeat_private_active.svg @@ -0,0 +1,6 @@ + + + + + + From 64993d3f779a6e01c104e1a2024a87c9785bc79d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:37:37 +0100 Subject: [PATCH 52/84] chore(deps): update dependency haml_lint to v0.55.0 (#28856) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 54955000b1..97d2b5f7a0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -319,7 +319,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.53.0) + haml_lint (0.55.0) haml (>= 5.0) parallel (~> 1.10) rainbow From ea5397c3735145ad62374798506f57afa122959d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:55:06 +0100 Subject: [PATCH 53/84] chore(deps): update dependency selenium-webdriver to v4.17.0 (#28858) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 97d2b5f7a0..77d924f948 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -696,7 +696,8 @@ GEM scenic (1.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - selenium-webdriver (4.16.0) + selenium-webdriver (4.17.0) + base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) From 559bbf0aa6ceca1cbf417fcc76ea7ef359e42099 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:51:18 +0100 Subject: [PATCH 54/84] chore(deps): update artifact actions (major) to v4 (major) (#28415) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test-ruby.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index ae25648a0b..346703ced4 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -52,7 +52,7 @@ jobs: run: | tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: matrix.mode == 'test' with: path: |- @@ -117,7 +117,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './' name: ${{ github.sha }} @@ -193,7 +193,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './public' name: ${{ github.sha }} @@ -213,14 +213,14 @@ jobs: - run: bundle exec rake spec:system - name: Archive logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: e2e-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: e2e-screenshots @@ -297,7 +297,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './public' name: ${{ github.sha }} @@ -317,14 +317,14 @@ jobs: - run: bin/rspec --tag search - name: Archive logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: test-search-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: test-search-screenshots From 7019af431d2b91a4d31ede9558bb3a17c3875f37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:51:36 +0100 Subject: [PATCH 55/84] fix(deps): update dependency dotenv to v16.4.0 (#28872) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 61f699d19e..53d2caaed9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6901,9 +6901,9 @@ __metadata: linkType: hard "dotenv@npm:^16.0.3": - version: 16.3.2 - resolution: "dotenv@npm:16.3.2" - checksum: a87d62cef0810b670cb477db1a24a42a093b6b428c9e65c185ce1d6368ad7175234b13547718ba08da18df43faae4f814180cc0366e11be1ded2277abc4dd22e + version: 16.4.0 + resolution: "dotenv@npm:16.4.0" + checksum: 70c3b422cefaffdba300aecd9157668590c3b5e66efb3742b7dec207f85023e5997364f04030fc0393fae52bf3a874979632d289ab4fafc1386ff2c68f2f2e8d languageName: node linkType: hard From 9c5be139806ba323f760ceb24476c1261af2ed41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:51:47 +0100 Subject: [PATCH 56/84] chore(deps): update dependency chewy to v7.5.0 (#28730) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 77d924f948..d573debe4b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -180,7 +180,7 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.4.0) + chewy (7.5.0) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl @@ -445,7 +445,7 @@ GEM mime-types-data (3.2023.1205) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.21.1) msgpack (1.7.2) multi_json (1.15.0) multipart-post (2.3.0) From 38f7f8b9096a3c56c676942730184f01e17bd93d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 24 Jan 2024 12:30:28 -0500 Subject: [PATCH 57/84] Tidy up association declaration in `Instance` model (#28880) --- app/models/instance.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/instance.rb b/app/models/instance.rb index 8f8d87c62a..2dec75d6fe 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -13,12 +13,12 @@ class Instance < ApplicationRecord attr_accessor :failure_days - has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false - with_options foreign_key: :domain, primary_key: :domain, inverse_of: false do belongs_to :domain_block belongs_to :domain_allow - belongs_to :unavailable_domain # skipcq: RB-RL1031 + belongs_to :unavailable_domain + + has_many :accounts, dependent: nil end scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) } From 22e419f95e3b2819891cc0815b270108178d6c54 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 24 Jan 2024 08:03:30 +0100 Subject: [PATCH 58/84] [Glitch] Fix search form re-rendering spuriously in web UI Port 01ce9df88008cee705b7e02a4581802afa07c3df to glitch-soc Signed-off-by: Claire --- .../features/compose/containers/search_container.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/compose/containers/search_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_container.js index 17be30edcc..93b8645ac6 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/search_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/search_container.js @@ -1,3 +1,4 @@ +import { createSelector } from '@reduxjs/toolkit'; import { connect } from 'react-redux'; import { @@ -12,10 +13,15 @@ import { import Search from '../components/search'; +const getRecentSearches = createSelector( + state => state.getIn(['search', 'recent']), + recent => recent.reverse(), +); + const mapStateToProps = state => ({ value: state.getIn(['search', 'value']), submitted: state.getIn(['search', 'submitted']), - recent: state.getIn(['search', 'recent']).reverse(), + recent: getRecentSearches(state), }); const mapDispatchToProps = dispatch => ({ From 9a8293f58d0317e3917b855483a5b2958226f2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?= Date: Thu, 25 Jan 2024 19:37:09 +0900 Subject: [PATCH 59/84] Fix process of receiving posts with bearcaps is not working (#26527) --- app/lib/activitypub/activity/create.rb | 2 +- app/lib/activitypub/parser/status_parser.rb | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 62c35d4dd3..85195f4c39 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -108,7 +108,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def process_status_params - @status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url) + @status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url, object: @object) attachment_ids = process_attachments.take(4).map(&:id) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 45f5fc5bf2..cfc2b8788b 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -4,12 +4,13 @@ class ActivityPub::Parser::StatusParser include JsonLdHelper # @param [Hash] json - # @param [Hash] magic_values - # @option magic_values [String] :followers_collection - def initialize(json, magic_values = {}) - @json = json - @object = json['object'] || json - @magic_values = magic_values + # @param [Hash] options + # @option options [String] :followers_collection + # @option options [Hash] :object + def initialize(json, **options) + @json = json + @object = options[:object] || json['object'] || json + @options = options end def uri @@ -78,7 +79,7 @@ class ActivityPub::Parser::StatusParser :public elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) } :unlisted - elsif audience_to.include?(@magic_values[:followers_collection]) + elsif audience_to.include?(@options[:followers_collection]) :private else :direct From c50274a0acd3679160984bbf410bff46fd76179c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 25 Jan 2024 11:44:25 +0100 Subject: [PATCH 60/84] Fix redirect confirmation for accounts (#28902) --- app/controllers/redirect/accounts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/redirect/accounts_controller.rb b/app/controllers/redirect/accounts_controller.rb index 98d2cc2b1f..713ccf2ca1 100644 --- a/app/controllers/redirect/accounts_controller.rb +++ b/app/controllers/redirect/accounts_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Redirect::AccountsController < ApplicationController +class Redirect::AccountsController < Redirect::BaseController private def set_resource From 0471a780556720338fef29266f2fc2578ab2010c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 25 Jan 2024 12:13:33 +0100 Subject: [PATCH 61/84] Add tests for redirect confirmations (#28903) --- spec/features/redirections_spec.rb | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 spec/features/redirections_spec.rb diff --git a/spec/features/redirections_spec.rb b/spec/features/redirections_spec.rb new file mode 100644 index 0000000000..f73ab58470 --- /dev/null +++ b/spec/features/redirections_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'redirection confirmations' do + let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/foo', url: 'https://example.com/@foo') } + let(:status) { Fabricate(:status, account: account, uri: 'https://example.com/users/foo/statuses/1', url: 'https://example.com/@foo/1') } + + context 'when a logged out user visits a local page for a remote account' do + it 'shows a confirmation page' do + visit "/@#{account.pretty_acct}" + + # It explains about the redirect + expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io')) + + # It features an appropriate link + expect(page).to have_link(account.url, href: account.url) + end + end + + context 'when a logged out user visits a local page for a remote status' do + it 'shows a confirmation page' do + visit "/@#{account.pretty_acct}/#{status.id}" + + # It explains about the redirect + expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io')) + + # It features an appropriate link + expect(page).to have_link(status.url, href: status.url) + end + end +end From 087415d0fe8d9a719eb2df7e915b08f4d4d0360a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 25 Jan 2024 12:13:36 +0100 Subject: [PATCH 62/84] Add tests for processing statuses using bearcap URIs (#28904) --- spec/lib/activitypub/activity/create_spec.rb | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 3e3a4978c8..e4966cffa3 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -893,6 +893,49 @@ RSpec.describe ActivityPub::Activity::Create do end end + context 'when object URI uses bearcaps' do + subject { described_class.new(json, sender) } + + let(:token) { 'foo' } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join, + type: 'Create', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: Addressable::URI.new(scheme: 'bear', query_values: { t: token, u: object_json[:id] }).to_s, + }.with_indifferent_access + end + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + before do + stub_request(:get, object_json[:id]) + .with(headers: { Authorization: "Bearer #{token}" }) + .to_return(body: Oj.dump(object_json), headers: { 'Content-Type': 'application/activity+json' }) + + subject.perform + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status).to have_attributes( + visibility: 'public', + text: 'Lorem ipsum' + ) + end + end + context 'with an encrypted message' do subject { described_class.new(json, sender, delivery: true, delivered_to_account_id: recipient.id) } From d158f7e6228e9c3f9d93bff86eccd71bdb542558 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:33:56 +0100 Subject: [PATCH 63/84] chore(deps): update dependency rspec-rails to v6.1.1 (#28905) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d573debe4b..a31d0a929c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -360,7 +360,7 @@ GEM rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) idn-ruby (0.1.5) - io-console (0.7.1) + io-console (0.7.2) irb (1.11.1) rdoc reline (>= 0.4.2) @@ -445,7 +445,7 @@ GEM mime-types-data (3.2023.1205) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.21.1) + minitest (5.21.2) msgpack (1.7.2) multi_json (1.15.0) multipart-post (2.3.0) @@ -636,7 +636,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.1.0) + rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) From 1a565e4bea45828bfbe16c2c581ec5bc676c5223 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:36:20 +0000 Subject: [PATCH 64/84] fix(deps): update dependency axios to v1.6.6 (#28895) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 53d2caaed9..29bcb3575b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4673,13 +4673,13 @@ __metadata: linkType: hard "axios@npm:^1.4.0": - version: 1.6.5 - resolution: "axios@npm:1.6.5" + version: 1.6.6 + resolution: "axios@npm:1.6.6" dependencies: follow-redirects: "npm:^1.15.4" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: aeb9acf87590d8aa67946072ced38e01ca71f5dfe043782c0ccea667e5dd5c45830c08afac9be3d7c894f09684b8ab2a458f497d197b73621233bcf202d9d468 + checksum: 974f54cfade94fd4c0191309122a112c8d233089cecb0070cd8e0904e9bd9c364ac3a6fd0f981c978508077249788950427c565f54b7b2110e5c3426006ff343 languageName: node linkType: hard From 7c9c6c7f80d57ea0fd504b59debe6439d28cb1b5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 07:37:07 -0500 Subject: [PATCH 65/84] Fix remaining `Rails/WhereExists` cop violations, regenerate todo (#28892) --- .rubocop_todo.yml | 17 +---------------- .../activitypub/inboxes_controller.rb | 2 +- .../admin/email_domain_blocks_controller.rb | 2 +- app/policies/status_policy.rb | 2 +- app/serializers/rest/announcement_serializer.rb | 2 +- app/workers/move_worker.rb | 2 +- .../process_collection_service_spec.rb | 2 +- 7 files changed, 7 insertions(+), 22 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 302c66a16a..77f7e70734 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.59.0. +# using RuboCop version 1.60.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -70,21 +70,6 @@ Rails/UniqueValidationWithoutIndex: - 'app/models/identity.rb' - 'app/models/webauthn_credential.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: exists, where -Rails/WhereExists: - Exclude: - - 'app/controllers/activitypub/inboxes_controller.rb' - - 'app/controllers/admin/email_domain_blocks_controller.rb' - - 'app/policies/status_policy.rb' - - 'app/serializers/rest/announcement_serializer.rb' - - 'app/workers/move_worker.rb' - - 'spec/models/account_spec.rb' - - 'spec/services/activitypub/process_collection_service_spec.rb' - - 'spec/services/purge_domain_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 5ee85474e7..ba85e0a722 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -24,7 +24,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController def unknown_affected_account? json = Oj.load(body, mode: :strict) - json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? + json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.exists?(uri: json['actor']) rescue Oj::ParseError false end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index ff754bc0b4..faa0a061a6 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -38,7 +38,7 @@ module Admin log_action :create, @email_domain_block (@email_domain_block.other_domains || []).uniq.each do |domain| - next if EmailDomainBlock.where(domain: domain).exists? + next if EmailDomainBlock.exists?(domain: domain) other_email_domain_block = EmailDomainBlock.create!(domain: domain, allow_with_approval: @email_domain_block.allow_with_approval, parent: @email_domain_block) log_action :create, other_email_domain_block diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 322d3aec5c..540e266427 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -57,7 +57,7 @@ class StatusPolicy < ApplicationPolicy if record.mentions.loaded? record.mentions.any? { |mention| mention.account_id == current_account.id } else - record.mentions.where(account: current_account).exists? + record.mentions.exists?(account: current_account) end end diff --git a/app/serializers/rest/announcement_serializer.rb b/app/serializers/rest/announcement_serializer.rb index 23b2fa514b..8cee271272 100644 --- a/app/serializers/rest/announcement_serializer.rb +++ b/app/serializers/rest/announcement_serializer.rb @@ -23,7 +23,7 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer end def read - object.announcement_mutes.where(account: current_user.account).exists? + object.announcement_mutes.exists?(account: current_user.account) end def content diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 73ae268bee..a18f38556b 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -123,7 +123,7 @@ class MoveWorker end def add_account_note_if_needed!(account, id) - unless AccountNote.where(account: account, target_account: @target_account).exists? + unless AccountNote.exists?(account: account, target_account: @target_account) text = I18n.with_locale(account.user&.locale.presence || I18n.default_locale) do I18n.t(id, acct: @source_account.acct) end diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb index f4a2b8fec6..63502c546e 100644 --- a/spec/services/activitypub/process_collection_service_spec.rb +++ b/spec/services/activitypub/process_collection_service_spec.rb @@ -265,7 +265,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do anything ) - expect(Status.where(uri: 'https://example.com/users/bob/fake-status').exists?).to be false + expect(Status.exists?(uri: 'https://example.com/users/bob/fake-status')).to be false end end end From a69506a434a605fd8ad67f4eb938a328753bbe06 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:37:23 +0100 Subject: [PATCH 66/84] fix(deps): update dependency dotenv to v16.4.1 (#28889) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 29bcb3575b..78261b3b84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6901,9 +6901,9 @@ __metadata: linkType: hard "dotenv@npm:^16.0.3": - version: 16.4.0 - resolution: "dotenv@npm:16.4.0" - checksum: 70c3b422cefaffdba300aecd9157668590c3b5e66efb3742b7dec207f85023e5997364f04030fc0393fae52bf3a874979632d289ab4fafc1386ff2c68f2f2e8d + version: 16.4.1 + resolution: "dotenv@npm:16.4.1" + checksum: ef3d95f48f38146df0881a4b58447ae437d2da3f6d645074b84de4e64ef64ba75fc357c5ed66b3c2b813b5369fdeb6a4777d6ade2d50e54eed6aa06dddc98bc4 languageName: node linkType: hard From 6b6586f5d099a9089c8bf9dc7d7406a86c0a48eb Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:00:34 -0500 Subject: [PATCH 67/84] Add `CustomFilterKeyword#to_regex` method (#28893) --- app/models/custom_filter.rb | 11 +------ app/models/custom_filter_keyword.rb | 16 +++++++++++ spec/models/custom_filter_keyword_spec.rb | 35 +++++++++++++++++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 spec/models/custom_filter_keyword_spec.rb diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index 371267fc28..c8120c2395 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -68,16 +68,7 @@ class CustomFilter < ApplicationRecord scope = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()')) scope.to_a.group_by(&:custom_filter).each do |filter, keywords| - keywords.map! do |keyword| - if keyword.whole_word - sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : '' - eb = /[[:word:]]\z/.match?(keyword.keyword) ? '\b' : '' - - /(?mix:#{sb}#{Regexp.escape(keyword.keyword)}#{eb})/ - else - /#{Regexp.escape(keyword.keyword)}/i - end - end + keywords.map!(&:to_regex) filters_hash[filter.id] = { keywords: Regexp.union(keywords), filter: filter } end.to_h diff --git a/app/models/custom_filter_keyword.rb b/app/models/custom_filter_keyword.rb index 3158b3b79a..979d0b822e 100644 --- a/app/models/custom_filter_keyword.rb +++ b/app/models/custom_filter_keyword.rb @@ -23,8 +23,24 @@ class CustomFilterKeyword < ApplicationRecord before_destroy :prepare_cache_invalidation! after_commit :invalidate_cache! + def to_regex + if whole_word? + /(?mix:#{to_regex_sb}#{Regexp.escape(keyword)}#{to_regex_eb})/ + else + /#{Regexp.escape(keyword)}/i + end + end + private + def to_regex_sb + /\A[[:word:]]/.match?(keyword) ? '\b' : '' + end + + def to_regex_eb + /[[:word:]]\z/.match?(keyword) ? '\b' : '' + end + def prepare_cache_invalidation! custom_filter.prepare_cache_invalidation! end diff --git a/spec/models/custom_filter_keyword_spec.rb b/spec/models/custom_filter_keyword_spec.rb new file mode 100644 index 0000000000..4e3ab060a0 --- /dev/null +++ b/spec/models/custom_filter_keyword_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe CustomFilterKeyword do + describe '#to_regex' do + context 'when whole_word is true' do + it 'builds a regex with boundaries and the keyword' do + keyword = described_class.new(whole_word: true, keyword: 'test') + + expect(keyword.to_regex).to eq(/(?mix:\b#{Regexp.escape(keyword.keyword)}\b)/) + end + + it 'builds a regex with starting boundary and the keyword when end with non-word' do + keyword = described_class.new(whole_word: true, keyword: 'test#') + + expect(keyword.to_regex).to eq(/(?mix:\btest\#)/) + end + + it 'builds a regex with end boundary and the keyword when start with non-word' do + keyword = described_class.new(whole_word: true, keyword: '#test') + + expect(keyword.to_regex).to eq(/(?mix:\#test\b)/) + end + end + + context 'when whole_word is false' do + it 'builds a regex with the keyword' do + keyword = described_class.new(whole_word: false, keyword: 'test') + + expect(keyword.to_regex).to eq(/test/i) + end + end + end +end From 59d2ea0d82493fdd948c91c6c715ea461b60619b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:00:44 +0100 Subject: [PATCH 68/84] New Crowdin Translations (automated) (#28899) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/af.json | 1 + app/javascript/mastodon/locales/ca.json | 2 +- app/javascript/mastodon/locales/oc.json | 13 ++++++++ app/javascript/mastodon/locales/zh-TW.json | 12 +++---- config/locales/br.yml | 3 ++ config/locales/ca.yml | 3 ++ config/locales/da.yml | 3 ++ config/locales/de.yml | 3 ++ config/locales/devise.fr-CA.yml | 4 +++ config/locales/devise.fr.yml | 4 +++ config/locales/devise.sv.yml | 1 + config/locales/devise.zh-TW.yml | 14 ++++---- config/locales/es-AR.yml | 3 ++ config/locales/es-MX.yml | 3 ++ config/locales/es.yml | 3 ++ config/locales/eu.yml | 3 ++ config/locales/fi.yml | 7 ++-- config/locales/fo.yml | 3 ++ config/locales/fr-CA.yml | 10 ++++++ config/locales/fr.yml | 10 ++++++ config/locales/gl.yml | 3 ++ config/locales/he.yml | 5 ++- config/locales/hu.yml | 2 ++ config/locales/is.yml | 3 ++ config/locales/it.yml | 3 ++ config/locales/ja.yml | 6 ++++ config/locales/ko.yml | 7 ++++ config/locales/lad.yml | 3 ++ config/locales/lt.yml | 3 ++ config/locales/nl.yml | 6 +++- config/locales/nn.yml | 3 ++ config/locales/no.yml | 3 ++ config/locales/pl.yml | 3 ++ config/locales/pt-BR.yml | 3 ++ config/locales/pt-PT.yml | 3 ++ config/locales/ru.yml | 3 ++ config/locales/sk.yml | 3 ++ config/locales/sq.yml | 3 ++ config/locales/sr-Latn.yml | 3 ++ config/locales/sr.yml | 3 ++ config/locales/sv.yml | 3 ++ config/locales/tr.yml | 3 ++ config/locales/uk.yml | 3 ++ config/locales/vi.yml | 3 ++ config/locales/zh-CN.yml | 3 ++ config/locales/zh-HK.yml | 3 ++ config/locales/zh-TW.yml | 39 ++++++++++++---------- 47 files changed, 197 insertions(+), 36 deletions(-) diff --git a/app/javascript/mastodon/locales/af.json b/app/javascript/mastodon/locales/af.json index 6c37cdf5ca..d1873d6dce 100644 --- a/app/javascript/mastodon/locales/af.json +++ b/app/javascript/mastodon/locales/af.json @@ -3,6 +3,7 @@ "about.contact": "Kontak:", "about.disclaimer": "Mastodon is gratis oopbronsagteware en ’n handelsmerk van Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Rede nie beskikbaar nie", + "about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.", "about.domain_blocks.silenced.title": "Beperk", "about.domain_blocks.suspended.title": "Opgeskort", "about.not_available": "Hierdie inligting is nie op hierdie bediener beskikbaar gestel nie.", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 7d1049a30f..c763a32ba8 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -521,7 +521,7 @@ "poll.total_people": "{count, plural, one {# persona} other {# persones}}", "poll.total_votes": "{count, plural, one {# vot} other {# vots}}", "poll.vote": "Vota", - "poll.voted": "Vas votar per aquesta resposta", + "poll.voted": "Vau votar aquesta resposta", "poll.votes": "{votes, plural, one {# vot} other {# vots}}", "poll_button.add_poll": "Afegeix una enquesta", "poll_button.remove_poll": "Elimina l'enquesta", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 833bfe6ace..1ecfbcaf06 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -18,6 +18,7 @@ "account.blocked": "Blocat", "account.browse_more_on_origin_server": "Navigar sul perfil original", "account.cancel_follow_request": "Retirar la demanda d’abonament", + "account.copy": "Copiar lo ligam del perfil", "account.direct": "Mencionar @{name} en privat", "account.disable_notifications": "Quitar de m’avisar quand @{name} publica quicòm", "account.domain_blocked": "Domeni amagat", @@ -28,6 +29,7 @@ "account.featured_tags.last_status_never": "Cap de publicacion", "account.featured_tags.title": "Etiquetas en avant de {name}", "account.follow": "Sègre", + "account.follow_back": "Sègre en retorn", "account.followers": "Seguidors", "account.followers.empty": "Degun sèc pas aqueste utilizaire pel moment.", "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}", @@ -48,6 +50,7 @@ "account.mute_notifications_short": "Amudir las notificacions", "account.mute_short": "Amudir", "account.muted": "Mes en silenci", + "account.mutual": "Mutual", "account.no_bio": "Cap de descripcion pas fornida.", "account.open_original_page": "Dobrir la pagina d’origina", "account.posts": "Tuts", @@ -172,6 +175,7 @@ "conversation.mark_as_read": "Marcar coma legida", "conversation.open": "Veire la conversacion", "conversation.with": "Amb {names}", + "copy_icon_button.copied": "Copiat al quichapapièr", "copypaste.copied": "Copiat", "copypaste.copy_to_clipboard": "Copiar al quichapapièr", "directory.federated": "Del fediverse conegut", @@ -294,6 +298,8 @@ "keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.down": "far davalar dins la lista", "keyboard_shortcuts.enter": "dobrir los estatuts", + "keyboard_shortcuts.favourite": "Marcar coma favorit", + "keyboard_shortcuts.favourites": "Dobrir la lista dels favorits", "keyboard_shortcuts.federated": "dobrir lo flux public global", "keyboard_shortcuts.heading": "Acorchis clavièr", "keyboard_shortcuts.home": "dobrir lo flux public local", @@ -339,6 +345,7 @@ "lists.search": "Cercar demest lo mond que seguètz", "lists.subheading": "Vòstras listas", "load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}", + "loading_indicator.label": "Cargament…", "media_gallery.toggle_visible": "Modificar la visibilitat", "mute_modal.duration": "Durada", "mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?", @@ -371,6 +378,7 @@ "not_signed_in_indicator.not_signed_in": "Devètz vos connectar per accedir a aquesta ressorsa.", "notification.admin.report": "{name} senhalèt {target}", "notification.admin.sign_up": "{name} se marquèt", + "notification.favourite": "{name} a mes vòstre estatut en favorit", "notification.follow": "{name} vos sèc", "notification.follow_request": "{name} a demandat a vos sègre", "notification.mention": "{name} vos a mencionat", @@ -423,6 +431,8 @@ "onboarding.compose.template": "Adiu #Mastodon !", "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", "onboarding.follows.title": "Popular on Mastodon", + "onboarding.profile.display_name": "Nom d’afichatge", + "onboarding.profile.note": "Biografia", "onboarding.share.title": "Partejar vòstre perfil", "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.start.skip": "Want to skip right ahead?", @@ -504,6 +514,7 @@ "report_notification.categories.spam": "Messatge indesirable", "report_notification.categories.violation": "Violacion de las règlas", "report_notification.open": "Dobrir lo senhalament", + "search.no_recent_searches": "Cap de recèrcas recentas", "search.placeholder": "Recercar", "search.search_or_paste": "Recercar o picar una URL", "search_popout.language_code": "Còdi ISO de lenga", @@ -536,6 +547,7 @@ "status.copy": "Copiar lo ligam de l’estatut", "status.delete": "Escafar", "status.detailed_status": "Vista detalhada de la convèrsa", + "status.direct": "Mencionar @{name} en privat", "status.direct_indicator": "Mencion privada", "status.edit": "Modificar", "status.edited": "Modificat {date}", @@ -626,6 +638,7 @@ "upload_modal.preview_label": "Apercebut ({ratio})", "upload_progress.label": "Mandadís…", "upload_progress.processing": "Tractament…", + "username.taken": "Aqueste nom d’utilizaire es pres. Ensajatz-ne un autre", "video.close": "Tampar la vidèo", "video.download": "Telecargar lo fichièr", "video.exit_fullscreen": "Sortir plen ecran", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 9983936953..cc8b583120 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -48,7 +48,7 @@ "account.locked_info": "此帳號的隱私狀態設定為鎖定。該擁有者會手動審核能跟隨此帳號的人。", "account.media": "媒體", "account.mention": "提及 @{name}", - "account.moved_to": "{name} 現在的新帳號為:", + "account.moved_to": "{name} 目前的新帳號為:", "account.mute": "靜音 @{name}", "account.mute_notifications_short": "靜音推播通知", "account.mute_short": "靜音", @@ -59,7 +59,7 @@ "account.posts": "嘟文", "account.posts_with_replies": "嘟文與回覆", "account.report": "檢舉 @{name}", - "account.requested": "正在等待核准。按一下以取消跟隨請求", + "account.requested": "正在等候審核。按一下以取消跟隨請求", "account.requested_follow": "{name} 要求跟隨您", "account.share": "分享 @{name} 的個人檔案", "account.show_reblogs": "顯示來自 @{name} 的嘟文", @@ -84,7 +84,7 @@ "admin.impact_report.title": "影響總結", "alert.rate_limited.message": "請於 {retry_time, time, medium} 後重試。", "alert.rate_limited.title": "已限速", - "alert.unexpected.message": "發生了非預期的錯誤。", + "alert.unexpected.message": "發生非預期的錯誤。", "alert.unexpected.title": "哎呀!", "announcement.announcement": "公告", "attachments_list.unprocessed": "(未經處理)", @@ -241,7 +241,7 @@ "empty_column.followed_tags": "您還沒有跟隨任何主題標籤。當您跟隨主題標籤時,它們將於此顯示。", "empty_column.hashtag": "這個主題標籤下什麼也沒有。", "empty_column.home": "您的首頁時間軸是空的!跟隨更多人來將它填滿吧!", - "empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出了新的嘟文時,它們將顯示於此。", + "empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出新的嘟文時,它們將顯示於此。", "empty_column.lists": "您還沒有建立任何列表。當您建立列表時,它將於此顯示。", "empty_column.mutes": "您尚未靜音任何使用者。", "empty_column.notifications": "您還沒有收到任何通知,當您與別人開始互動時,它將於此顯示。", @@ -303,8 +303,8 @@ "hashtag.counter_by_accounts": "{count, plural, one {{counter} 名} other {{counter} 名}}參與者", "hashtag.counter_by_uses": "{count, plural, one {{counter} 則} other {{counter} 則}}嘟文", "hashtag.counter_by_uses_today": "本日有 {count, plural, one {{counter} 則} other {{counter} 則}}嘟文", - "hashtag.follow": "追蹤主題標籤", - "hashtag.unfollow": "取消追蹤主題標籤", + "hashtag.follow": "跟隨主題標籤", + "hashtag.unfollow": "取消跟隨主題標籤", "hashtags.and_other": "…及其他 {count, plural, other {# 個}}", "home.actions.go_to_explore": "看看發生什麼新鮮事", "home.actions.go_to_suggestions": "尋找一些人來跟隨", diff --git a/config/locales/br.yml b/config/locales/br.yml index 7af72457d0..d20609a8ce 100644 --- a/config/locales/br.yml +++ b/config/locales/br.yml @@ -443,6 +443,9 @@ br: preferences: other: All posting_defaults: Arventennoù embann dre ziouer + redirects: + prompt: M'ho peus fiziañs el liamm-mañ, klikit warnañ evit kenderc'hel. + title: O kuitaat %{instance} emaoc'h. relationships: dormant: O kousket followers: Heulier·ezed·ien diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 38ef976b83..58f6e26374 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1546,6 +1546,9 @@ ca: errors: limit_reached: Límit de diferents reaccions assolit unrecognized_emoji: no és un emoji reconegut + redirects: + prompt: Si confieu en aquest enllaç, feu-hi clic per a continuar. + title: Esteu sortint de %{instance}. relationships: activity: Activitat del compte confirm_follow_selected_followers: Segur que vols seguir els seguidors seleccionats? diff --git a/config/locales/da.yml b/config/locales/da.yml index d92d001905..57899d5f71 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1546,6 +1546,9 @@ da: errors: limit_reached: Grænse for forskellige reaktioner nået unrecognized_emoji: er ikke en genkendt emoji + redirects: + prompt: Er der tillid til dette link, så klik på det for at fortsætte. + title: Nu forlades %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Sikker på, at de valgte følgere skal følges? diff --git a/config/locales/de.yml b/config/locales/de.yml index 9568f698d1..b77f415190 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1546,6 +1546,9 @@ de: errors: limit_reached: Limit für verschiedene Reaktionen erreicht unrecognized_emoji: ist ein unbekanntes Emoji + redirects: + prompt: Wenn du diesem Link vertraust, dann klicke ihn an, um fortzufahren. + title: Du verlässt %{instance}. relationships: activity: Kontoaktivität confirm_follow_selected_followers: Möchtest du den ausgewählten Followern folgen? diff --git a/config/locales/devise.fr-CA.yml b/config/locales/devise.fr-CA.yml index 34104e0ac5..7f13f67828 100644 --- a/config/locales/devise.fr-CA.yml +++ b/config/locales/devise.fr-CA.yml @@ -73,9 +73,13 @@ fr-CA: subject: 'Mastodon: Clé de sécurité supprimée' title: Une de vos clés de sécurité a été supprimée webauthn_disabled: + explanation: L'authentification avec les clés de sécurité a été désactivée pour votre compte. + extra: La connexion est maintenant possible en utilisant uniquement le jeton généré par l'application TOTP associée. subject: 'Mastodon: Authentification avec clés de sécurité désactivée' title: Clés de sécurité désactivées webauthn_enabled: + explanation: L'authentification par clé de sécurité a été activée pour votre compte. + extra: Votre clé de sécurité peut maintenant être utilisée pour vous connecter. subject: 'Mastodon: Authentification de la clé de sécurité activée' title: Clés de sécurité activées omniauth_callbacks: diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index 1fc6663bfe..8a5b8384e0 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -73,9 +73,13 @@ fr: subject: 'Mastodon: Clé de sécurité supprimée' title: Une de vos clés de sécurité a été supprimée webauthn_disabled: + explanation: L'authentification avec les clés de sécurité a été désactivée pour votre compte. + extra: La connexion est maintenant possible en utilisant uniquement le jeton généré par l'application TOTP associée. subject: 'Mastodon: Authentification avec clés de sécurité désactivée' title: Clés de sécurité désactivées webauthn_enabled: + explanation: L'authentification par clé de sécurité a été activée pour votre compte. + extra: Votre clé de sécurité peut maintenant être utilisée pour vous connecter. subject: 'Mastodon: Authentification de la clé de sécurité activée' title: Clés de sécurité activées omniauth_callbacks: diff --git a/config/locales/devise.sv.yml b/config/locales/devise.sv.yml index b089f21427..6544f426bd 100644 --- a/config/locales/devise.sv.yml +++ b/config/locales/devise.sv.yml @@ -77,6 +77,7 @@ sv: subject: 'Mastodon: Autentisering med säkerhetsnycklar är inaktiverat' title: Säkerhetsnycklar inaktiverade webauthn_enabled: + extra: Din säkerhetsnyckel kan nu användas för inloggning. subject: 'Mastodon: Autentisering med säkerhetsnyckel är aktiverat' title: Säkerhetsnycklar aktiverade omniauth_callbacks: diff --git a/config/locales/devise.zh-TW.yml b/config/locales/devise.zh-TW.yml index 762c8eba84..06438971a7 100644 --- a/config/locales/devise.zh-TW.yml +++ b/config/locales/devise.zh-TW.yml @@ -47,14 +47,14 @@ zh-TW: subject: Mastodon:重設密碼指引 title: 重設密碼 two_factor_disabled: - explanation: 現在僅可使用電子郵件地址與密碼登入。 + explanation: 目前僅可使用電子郵件地址與密碼登入。 subject: Mastodon:已停用兩階段驗證 - subtitle: 您帳號的兩步驟驗證已停用。 + subtitle: 您帳號之兩階段驗證已停用。 title: 已停用兩階段驗證 two_factor_enabled: - explanation: 登入時需要配對的 TOTP 應用程式產生的權杖。 + explanation: 登入時需要配對的 TOTP 應用程式產生之 token。 subject: Mastodon:已啟用兩階段驗證 - subtitle: 您的帳號已啟用兩步驟驗證。 + subtitle: 您的帳號之兩階段驗證已啟用。 title: 已啟用兩階段驗證 two_factor_recovery_codes_changed: explanation: 之前的備用驗證碼已經失效,且已產生新的。 @@ -74,12 +74,12 @@ zh-TW: title: 您的一支安全密鑰已經被移除 webauthn_disabled: explanation: 您的帳號已停用安全金鑰身份驗證。 - extra: 現在僅可使用配對的 TOTP 應用程式產生的權杖登入。 + extra: 現在僅可使用配對的 TOTP 應用程式產生之 token 登入。 subject: Mastodon:安全密鑰認證方式已停用 title: 已停用安全密鑰 webauthn_enabled: - explanation: 您的帳號已啟用安全金鑰驗證。 - extra: 您的安全金鑰現在可用於登入。 + explanation: 您的帳號已啟用安全金鑰身分驗證。 + extra: 您的安全金鑰現在已可用於登入。 subject: Mastodon:已啟用安全密鑰認證 title: 已啟用安全密鑰 omniauth_callbacks: diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index cc55d3d3ff..d1dbdbf0b8 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -1546,6 +1546,9 @@ es-AR: errors: limit_reached: Se alcanzó el límite de reacciones diferentes unrecognized_emoji: no es un emoji conocido + redirects: + prompt: Si confiás en este enlace, dale clic o un toque para continuar. + title: Estás dejando %{instance}. relationships: activity: Actividad de la cuenta confirm_follow_selected_followers: "¿Estás seguro que querés seguir a los seguidores seleccionados?" diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index b84fb7cf96..4d228e98d4 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1546,6 +1546,9 @@ es-MX: errors: limit_reached: Límite de reacciones diferentes alcanzado unrecognized_emoji: no es un emoji conocido + redirects: + prompt: Si confías en este enlace, púlsalo para continuar. + title: Vas a salir de %{instance}. relationships: activity: Actividad de la cuenta confirm_follow_selected_followers: "¿Estás seguro de que quieres seguir a las cuentas seleccionadas?" diff --git a/config/locales/es.yml b/config/locales/es.yml index 95816d6bcb..08fc0988e4 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1546,6 +1546,9 @@ es: errors: limit_reached: Límite de reacciones diferentes alcanzado unrecognized_emoji: no es un emoji conocido + redirects: + prompt: Si confías en este enlace, púlsalo para continuar. + title: Vas a salir de %{instance}. relationships: activity: Actividad de la cuenta confirm_follow_selected_followers: "¿Estás seguro de que quieres seguir a las cuentas seleccionadas?" diff --git a/config/locales/eu.yml b/config/locales/eu.yml index bd6ea8c832..44688577a9 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1550,6 +1550,9 @@ eu: errors: limit_reached: Erreakzio desberdinen muga gaindituta unrecognized_emoji: ez da emoji ezaguna + redirects: + prompt: Esteka honetan fidatzen bazara, egin klik jarraitzeko. + title: "%{instance} instantziatik zoaz." relationships: activity: Kontuaren aktibitatea confirm_follow_selected_followers: Ziur hautatutako jarraitzaileei jarraitu nahi dituzula? diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 8e61c7b2a0..856532f8f1 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1546,6 +1546,9 @@ fi: errors: limit_reached: Erilaisten reaktioiden raja saavutettu unrecognized_emoji: ei ole tunnistettu emoji + redirects: + prompt: Jos luotat tähän linkkiin, jatka napsauttamalla. + title: Olet poistumassa palvelimelta %{instance}. relationships: activity: Tilin aktiivisuus confirm_follow_selected_followers: Haluatko varmasti seurata valittuja seuraajia? @@ -1791,8 +1794,8 @@ fi: subject: Arkisto on valmiina ladattavaksi title: Arkiston tallennus failed_2fa: - details: 'Tässä on tiedot kirjautumisyrityksestä:' - explanation: Joku on yrittänyt kirjautua tilillesi, mutta antanut virheellisen kaksivaiheisen todennuksen. + details: 'Tässä on tietoja kirjautumisyrityksestä:' + explanation: Joku on yrittänyt kirjautua tilillesi mutta on antanut virheellisen toisen vaiheen todennustekijän. further_actions_html: Jos se et ollut sinä, suosittelemme, että %{action} välittömästi, sillä se on saattanut vaarantua. subject: Kaksivaiheisen todennuksen virhe title: Epäonnistunut kaksivaiheinen todennus diff --git a/config/locales/fo.yml b/config/locales/fo.yml index 8e34265313..10b1e76f5f 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -1546,6 +1546,9 @@ fo: errors: limit_reached: Mark fyri ymisk aftursvar rokkið unrecognized_emoji: er ikki eitt kenslutekn, sum kennist aftur + redirects: + prompt: Um tú lítir á hetta leinkið, so kanst tú klikkja á tað fyri at halda fram. + title: Tú fer burtur úr %{instance}. relationships: activity: Kontuvirksemi confirm_follow_selected_followers: Vil tú veruliga fylgja valdu fylgjarunum? diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index dbdff5f52c..3676d0b7b5 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -1546,6 +1546,9 @@ fr-CA: errors: limit_reached: Limite de réactions différentes atteinte unrecognized_emoji: n’est pas un émoji reconnu + redirects: + prompt: Si vous faites confiance à ce lien, cliquez pour continuer. + title: Vous quittez %{instance}. relationships: activity: Activité du compte confirm_follow_selected_followers: Voulez-vous vraiment suivre les abonné⋅e⋅s sélectionné⋅e⋅s ? @@ -1790,6 +1793,12 @@ fr-CA: extra: Elle est maintenant prête à être téléchargée ! subject: Votre archive est prête à être téléchargée title: Récupération de l’archive + failed_2fa: + details: 'Voici les détails de la tentative de connexion :' + explanation: Quelqu'un a essayé de se connecter à votre compte mais a fourni un second facteur d'authentification invalide. + further_actions_html: Si ce n'était pas vous, nous vous recommandons %{action} immédiatement car il pourrait être compromis. + subject: Échec de l'authentification à double facteur + title: Échec de l'authentification à double facteur suspicious_sign_in: change_password: changer votre mot de passe details: 'Voici les détails de la connexion :' @@ -1843,6 +1852,7 @@ fr-CA: go_to_sso_account_settings: Accédez aux paramètres du compte de votre fournisseur d'identité invalid_otp_token: Le code d’authentification à deux facteurs est invalide otp_lost_help_html: Si vous perdez accès aux deux, vous pouvez contacter %{email} + rate_limited: Trop de tentatives d'authentification, réessayez plus tard. seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles. signed_in_as: 'Connecté·e en tant que :' verification: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index fe1a219a31..a3aaf7a26e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1546,6 +1546,9 @@ fr: errors: limit_reached: Limite de réactions différentes atteinte unrecognized_emoji: n’est pas un émoji reconnu + redirects: + prompt: Si vous faites confiance à ce lien, cliquez pour continuer. + title: Vous quittez %{instance}. relationships: activity: Activité du compte confirm_follow_selected_followers: Voulez-vous vraiment suivre les abonné⋅e⋅s sélectionné⋅e⋅s ? @@ -1790,6 +1793,12 @@ fr: extra: Elle est maintenant prête à être téléchargée ! subject: Votre archive est prête à être téléchargée title: Récupération de l’archive + failed_2fa: + details: 'Voici les détails de la tentative de connexion :' + explanation: Quelqu'un a essayé de se connecter à votre compte mais a fourni un second facteur d'authentification invalide. + further_actions_html: Si ce n'était pas vous, nous vous recommandons %{action} immédiatement car il pourrait être compromis. + subject: Échec de l'authentification à double facteur + title: Échec de l'authentification à double facteur suspicious_sign_in: change_password: changer votre mot de passe details: 'Voici les détails de la connexion :' @@ -1843,6 +1852,7 @@ fr: go_to_sso_account_settings: Accédez aux paramètres du compte de votre fournisseur d'identité invalid_otp_token: Le code d’authentification à deux facteurs est invalide otp_lost_help_html: Si vous perdez accès aux deux, vous pouvez contacter %{email} + rate_limited: Trop de tentatives d'authentification, réessayez plus tard. seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles. signed_in_as: 'Connecté·e en tant que :' verification: diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 087ed2ec76..7b3fd1a6eb 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -1546,6 +1546,9 @@ gl: errors: limit_reached: Acadouse o límite das diferentes reaccións unrecognized_emoji: non é unha emoticona recoñecida + redirects: + prompt: Se confías nesta ligazón, preme nela para continuar. + title: Vas saír de %{instance}. relationships: activity: Actividade da conta confirm_follow_selected_followers: Tes a certeza de querer seguir as seguidoras seleccionadas? diff --git a/config/locales/he.yml b/config/locales/he.yml index 1f5fd096ac..05b52213a7 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1598,6 +1598,9 @@ he: errors: limit_reached: גבול מספר התגובות השונות הושג unrecognized_emoji: הוא לא אמוג'י מוכר + redirects: + prompt: יש ללחוץ על הקישור, אם לדעתך ניתן לסמוך עליו. + title: יציאה מתוך %{instance}. relationships: activity: רמת פעילות confirm_follow_selected_followers: האם את/ה בטוח/ה שברצונך לעקוב אחרי החשבונות שסומנו? @@ -1856,7 +1859,7 @@ he: title: הוצאת ארכיון failed_2fa: details: 'הנה פרטי נסיון ההתחברות:' - explanation: פולני אלמוני ניסה להתחבר לחשבונך אך האימות המשני נכשל. + explanation: פלוני אלמוני ניסה להתחבר לחשבונך אך האימות המשני נכשל. further_actions_html: אם הנסיון לא היה שלך, אנו ממליצים על %{action} באופן מיידי כדי שהחשבון לא יפול קורבן. subject: נכשל אימות בגורם שני title: אימות בגורם שני נכשל diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 2870435ea7..8cbbb64c97 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1546,6 +1546,8 @@ hu: errors: limit_reached: A különböző reakciók száma elérte a határértéket unrecognized_emoji: nem ismert emodzsi + redirects: + prompt: Ha megbízunk ebben a hivatkozásban, kattintsunk rá a folytatáshoz. relationships: activity: Fiók aktivitás confirm_follow_selected_followers: Biztos, hogy követni akarod a kiválasztott követőket? diff --git a/config/locales/is.yml b/config/locales/is.yml index 191383f56c..d374c60755 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -1550,6 +1550,9 @@ is: errors: limit_reached: Hámarki mismunandi viðbragða náð unrecognized_emoji: er ekki þekkt tjáningartákn + redirects: + prompt: Ef þú treystir þessum tengli, geturðu smellt á hann til að halda áfram. + title: Þú ert að yfirgefa %{instance}. relationships: activity: Virkni aðgangs confirm_follow_selected_followers: Ertu viss um að þú viljir fylgjast með völdum fylgjendum? diff --git a/config/locales/it.yml b/config/locales/it.yml index 89ff071f36..31de2252d1 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1548,6 +1548,9 @@ it: errors: limit_reached: Raggiunto il limite di reazioni diverse unrecognized_emoji: non è un emoji riconosciuto + redirects: + prompt: Se ti fidi di questo collegamento, fai clic su di esso per continuare. + title: Stai lasciando %{instance}. relationships: activity: Attività dell'account confirm_follow_selected_followers: Sei sicuro di voler seguire i follower selezionati? diff --git a/config/locales/ja.yml b/config/locales/ja.yml index c966cbe36f..2051e30aee 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1758,6 +1758,12 @@ ja: extra: ダウンロードの準備ができました! subject: アーカイブの準備ができました title: アーカイブの取り出し + failed_2fa: + details: '試行されたログインの詳細は以下のとおりです:' + explanation: アカウントへのログインが試行されましたが、二要素認証で不正な回答が送信されました。 + further_actions_html: このログインに心当たりがない場合は、ただちに%{action}してください。 + subject: 二要素認証に失敗しました + title: 二要素認証に失敗した記録があります suspicious_sign_in: change_password: パスワードを変更 details: 'ログインの詳細は以下のとおりです:' diff --git a/config/locales/ko.yml b/config/locales/ko.yml index b3c786e265..9f4f1343c7 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1522,6 +1522,9 @@ ko: errors: limit_reached: 리액션 갯수 제한에 도달했습니다 unrecognized_emoji: 인식 되지 않은 에모지입니다 + redirects: + prompt: 이 링크를 믿을 수 있다면, 클릭해서 계속하세요. + title: "%{instance}를 떠나려고 합니다." relationships: activity: 계정 활동 confirm_follow_selected_followers: 정말로 선택된 팔로워들을 팔로우하시겠습니까? @@ -1762,6 +1765,10 @@ ko: title: 아카이브 테이크아웃 failed_2fa: details: '로그인 시도에 대한 상세 정보입니다:' + explanation: 누군가가 내 계정에 로그인을 시도했지만 2차인증에 올바른 값을 입력하지 못했습니다. + further_actions_html: 만약 당신이 한 게 아니었다면 유출의 가능성이 있으니 가능한 빨리 %{action} 하시기 바랍니다. + subject: 2차 인증 실패 + title: 2차 인증에 실패했습니다 suspicious_sign_in: change_password: 암호 변경 details: '로그인에 대한 상세 정보입니다:' diff --git a/config/locales/lad.yml b/config/locales/lad.yml index be5d2d21bd..02308cf2f0 100644 --- a/config/locales/lad.yml +++ b/config/locales/lad.yml @@ -1516,6 +1516,9 @@ lad: errors: limit_reached: Limito de reaksyones desferentes alkansado unrecognized_emoji: no es un emoji konesido + redirects: + prompt: Si konfiyas en este atadijo, klikalo para kontinuar. + title: Estas salyendo de %{instance}. relationships: activity: Aktivita del kuento confirm_follow_selected_followers: Estas siguro ke keres segir a los suivantes eskojidos? diff --git a/config/locales/lt.yml b/config/locales/lt.yml index ba8b53fdc9..1d159bf45a 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -478,6 +478,9 @@ lt: other: Kita privacy: hint_html: "Tikrink, kaip nori, kad tavo profilis ir įrašai būtų randami. Įjungus įvairias Mastodon funkcijas, jos gali padėti pasiekti platesnę auditoriją. Akimirką peržiūrėk šiuos nustatymus, kad įsitikintum, jog jie atitinka tavo naudojimo būdą." + redirects: + prompt: Jei pasitiki šia nuoroda, spustelėk ją, kad tęstum. + title: Palieki %{instance} remote_follow: missing_resource: Jūsų paskyros nukreipimo URL nerasta scheduled_statuses: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 2d27f9165d..a3657890da 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1546,6 +1546,9 @@ nl: errors: limit_reached: Limiet van verschillende emoji-reacties bereikt unrecognized_emoji: is geen bestaande emoji-reactie + redirects: + prompt: Als je deze link vertrouwt, klik er dan op om door te gaan. + title: Je verlaat %{instance}. relationships: activity: Accountactiviteit confirm_follow_selected_followers: Weet je zeker dat je de geselecteerde volgers wilt volgen? @@ -1792,7 +1795,8 @@ nl: title: Archief ophalen failed_2fa: details: 'Hier zijn details van de aanmeldpoging:' - explanation: Iemand heeft geprobeerd om in te loggen op uw account maar heeft een ongeldige tweede verificatiefactor opgegeven. + explanation: Iemand heeft geprobeerd om in te loggen op jouw account maar heeft een ongeldige tweede verificatiefactor opgegeven. + further_actions_html: Als jij dit niet was, raden we je aan om onmiddellijk %{action} aangezien het in gevaar kan zijn. subject: Tweede factor authenticatiefout title: Tweestapsverificatie mislukt suspicious_sign_in: diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 95eed49785..ffa5198a3a 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1546,6 +1546,9 @@ nn: errors: limit_reached: Grensen for forskjellige reaksjoner nådd unrecognized_emoji: er ikke en gjenkjent emoji + redirects: + prompt: Hvis du stoler på denne lenken, så trykk på den for å fortsette. + title: Du forlater %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Er du sikker på at du ynskjer å fylgja dei valde fylgjarane? diff --git a/config/locales/no.yml b/config/locales/no.yml index 7ece8564fc..d26b20379e 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -1546,6 +1546,9 @@ errors: limit_reached: Grensen for ulike reaksjoner nådd unrecognized_emoji: er ikke en gjenkjent emoji + redirects: + prompt: Hvis du stoler på denne lenken, så trykk på den for å fortsette. + title: Du forlater %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Er du sikker på at du vil følge valgte følgere? diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 6718f1994b..5bc78a6adf 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1598,6 +1598,9 @@ pl: errors: limit_reached: Przekroczono limit różnych reakcji unrecognized_emoji: nie jest znanym emoji + redirects: + prompt: Kliknij ten link jeżeli mu ufasz. + title: Opuszczasz %{instance}. relationships: activity: Aktywność konta confirm_follow_selected_followers: Czy na pewno chcesz obserwować wybranych obserwujących? diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index c1a47c0161..79396d627f 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1546,6 +1546,9 @@ pt-BR: errors: limit_reached: Limite de reações diferentes atingido unrecognized_emoji: não é um emoji reconhecido + redirects: + prompt: Se você confia neste link, clique nele para continuar. + title: Você está saindo de %{instance}. relationships: activity: Atividade da conta confirm_follow_selected_followers: Tem certeza que deseja seguir os seguidores selecionados? diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index 268531718d..8a20bc68a1 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -1546,6 +1546,9 @@ pt-PT: errors: limit_reached: Alcançado limite de reações diferentes unrecognized_emoji: não é um emoji reconhecido + redirects: + prompt: Se confia nesta hiperligação, clique nela para continuar. + title: Está a deixar %{instance}. relationships: activity: Atividade da conta confirm_follow_selected_followers: Tem a certeza que deseja seguir os seguidores selecionados? diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 24edbdc75e..04e49e0427 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1598,6 +1598,9 @@ ru: errors: limit_reached: Достигнут лимит разных реакций unrecognized_emoji: не является распознанным эмодзи + redirects: + prompt: Если вы доверяете этой ссылке, нажмите на нее, чтобы продолжить. + title: Вы покидаете %{instance}. relationships: activity: Активность учётной записи confirm_follow_selected_followers: Вы уверены, что хотите подписаться на выбранных подписчиков? diff --git a/config/locales/sk.yml b/config/locales/sk.yml index e83ae348f6..20df763463 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -1101,6 +1101,9 @@ sk: errors: limit_reached: Maximálny počet rôznorodých reakcií bol dosiahnutý unrecognized_emoji: je neznámy smajlík + redirects: + prompt: Ak tomuto odkazu veríš, klikni naňho pre pokračovanie. + title: Opúšťaš %{instance}. relationships: activity: Aktivita účtu confirm_follow_selected_followers: Si si istý/á, že chceš nasledovať vybraných sledujúcich? diff --git a/config/locales/sq.yml b/config/locales/sq.yml index d6e6925c70..3dd4731209 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -1542,6 +1542,9 @@ sq: errors: limit_reached: U mbërrit në kufirin e reagimeve të ndryshme unrecognized_emoji: s’është emotikon i pranuar + redirects: + prompt: Nëse e besoni këtë lidhje, klikoni që të vazhdohet. + title: Po e braktisni %{instance}. relationships: activity: Veprimtari llogarie confirm_follow_selected_followers: Jeni i sigurt se doni të ndiqet ndjekësit e përzgjedhur? diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 9cb555c943..b55b6e0d19 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -1572,6 +1572,9 @@ sr-Latn: errors: limit_reached: Dostignuto je ograničenje različitih reakcija unrecognized_emoji: nije prepoznat emodži + redirects: + prompt: Ako verujete ovoj vezi, kliknite na nju za nastavak. + title: Napuštate %{instance}. relationships: activity: Aktivnost naloga confirm_follow_selected_followers: Da li ste sigurni da želite da pratite izabrane pratioce? diff --git a/config/locales/sr.yml b/config/locales/sr.yml index e1c2e992ed..8de7c90e73 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -1572,6 +1572,9 @@ sr: errors: limit_reached: Достигнуто је ограничење различитих реакција unrecognized_emoji: није препознат емоџи + redirects: + prompt: Ако верујете овој вези, кликните на њу за наставак. + title: Напуштате %{instance}. relationships: activity: Активност налога confirm_follow_selected_followers: Да ли сте сигурни да желите да пратите изабране пратиоце? diff --git a/config/locales/sv.yml b/config/locales/sv.yml index c9000d50fc..deac7cc638 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -1545,6 +1545,9 @@ sv: errors: limit_reached: Gränsen för unika reaktioner uppnådd unrecognized_emoji: är inte en igenkänd emoji + redirects: + prompt: Om du litar på denna länk, klicka på den för att fortsätta. + title: Du lämnar %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Är du säker på att du vill följa valda följare? diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 2b5b5ad45b..b3a52715b7 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1546,6 +1546,9 @@ tr: errors: limit_reached: Farklı reaksiyonların sınırına ulaşıldı unrecognized_emoji: tanınan bir emoji değil + redirects: + prompt: Eğer bu bağlantıya güveniyorsanız, tıklayıp devam edebilirsiniz. + title: "%{instance} sunucusundan ayrılıyorsunuz." relationships: activity: Hesap etkinliği confirm_follow_selected_followers: Seçili takipçileri takip etmek istediğinizden emin misiniz? diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 40a858d72a..531bdb3d59 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1598,6 +1598,9 @@ uk: errors: limit_reached: Досягнуто обмеження різних реакцій unrecognized_emoji: не є розпізнаним емоджі + redirects: + prompt: Якщо ви довіряєте цьому посиланню, натисніть, щоб продовжити. + title: Ви покидаєте %{instance}. relationships: activity: Діяльність облікового запису confirm_follow_selected_followers: Ви справді бажаєте підписатися на обраних підписників? diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 1ece72e154..045a000e38 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1520,6 +1520,9 @@ vi: errors: limit_reached: Bạn không nên thao tác liên tục unrecognized_emoji: không phải là emoji + redirects: + prompt: Nếu bạn tin tưởng, hãy nhấn tiếp tục. + title: Bạn đang thoát khỏi %{instance}. relationships: activity: Tương tác confirm_follow_selected_followers: Bạn có chắc muốn theo dõi những người đã chọn? diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 272787ce25..d1255bfefe 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1520,6 +1520,9 @@ zh-CN: errors: limit_reached: 互动种类的限制 unrecognized_emoji: 不是一个可识别的表情 + redirects: + prompt: 如果您信任此链接,请单击以继续跳转。 + title: 您正在离开 %{instance} 。 relationships: activity: 账号活动 confirm_follow_selected_followers: 您确定想要关注所选的关注者吗? diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 0c39aa8c0b..b010a75c04 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -1520,6 +1520,9 @@ zh-HK: errors: limit_reached: 已達到可以給予反應極限 unrecognized_emoji: 不能識別這個emoji + redirects: + prompt: 如果你信任此連結,點擊它繼續。 + title: 你即將離開 %{instance}。 relationships: activity: 帳戶活動 confirm_follow_selected_followers: 你確定要追蹤選取的追蹤者嗎? diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 8726ea72a4..72e63e47d3 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -57,7 +57,7 @@ zh-TW: destroyed_msg: 即將刪除 %{username} 的資料 disable: 停用 disable_sign_in_token_auth: 停用電子郵件 token 驗證 - disable_two_factor_authentication: 停用兩階段認證 + disable_two_factor_authentication: 停用兩階段驗證 disabled: 已停用 display_name: 暱稱 domain: 站點 @@ -195,7 +195,7 @@ zh-TW: destroy_status: 刪除狀態 destroy_unavailable_domain: 刪除無法存取的網域 destroy_user_role: 移除角色 - disable_2fa_user: 停用兩階段認證 + disable_2fa_user: 停用兩階段驗證 disable_custom_emoji: 停用自訂顏文字 disable_sign_in_token_auth_user: 停用使用者電子郵件 token 驗證 disable_user: 停用帳號 @@ -254,7 +254,7 @@ zh-TW: destroy_status_html: "%{name} 已刪除 %{target} 的嘟文" destroy_unavailable_domain_html: "%{name} 已恢復對網域 %{target} 的發送" destroy_user_role_html: "%{name} 已刪除 %{target} 角色" - disable_2fa_user_html: "%{name} 已停用使用者 %{target} 的兩階段認證 (2FA) " + disable_2fa_user_html: "%{name} 已停用使用者 %{target} 的兩階段驗證 (2FA) " disable_custom_emoji_html: "%{name} 已停用自訂表情符號 %{target}" disable_sign_in_token_auth_user_html: "%{name} 已停用 %{target} 之使用者電子郵件 token 驗證" disable_user_html: "%{name} 將使用者 %{target} 設定為禁止登入" @@ -418,7 +418,7 @@ zh-TW: view: 顯示已封鎖網域 email_domain_blocks: add_new: 加入新項目 - allow_registrations_with_approval: 經允許後可註冊 + allow_registrations_with_approval: 經審核後可註冊 attempts_over_week: other: 上週共有 %{count} 次註冊嘗試 created_msg: 已成功將電子郵件網域加入黑名單 @@ -505,7 +505,7 @@ zh-TW: delivery_available: 可傳送 delivery_error_days: 遞送失敗天數 delivery_error_hint: 若 %{count} 日皆無法遞送 ,則會自動標記無法遞送。 - destroyed_msg: 來自 %{domain} 的資料現在正在佇列中等待刪除。 + destroyed_msg: 來自 %{domain} 的資料目前正在佇列中等待刪除。 empty: 找不到網域 known_accounts: other: "%{count} 個已知帳號" @@ -759,7 +759,7 @@ zh-TW: title: 註冊 registrations_mode: modes: - approved: 註冊需要核准 + approved: 註冊需要審核 none: 沒有人可註冊 open: 任何人皆能註冊 security: @@ -870,7 +870,7 @@ zh-TW: links: allow: 允許連結 allow_provider: 允許發行者 - description_html: 這些連結是正在被您伺服器上看到該嘟文之帳號大量分享。這些連結可以幫助您的使用者探索現在世界上正在發生的事情。除非您核准該發行者,連結將不被公開展示。您也可以核准或駁回個別連結。 + description_html: 這些連結是正在被您伺服器上看到該嘟文之帳號大量分享。這些連結可以幫助您的使用者探索目前世界上正在發生的事情。除非您核准該發行者,連結將不被公開展示。您也可以核准或駁回個別連結。 disallow: 不允許連結 disallow_provider: 不允許發行者 no_link_selected: 因未選取任何連結,所以什麼事都沒發生 @@ -1062,7 +1062,7 @@ zh-TW: cas: CAS saml: SAML register: 註冊 - registration_closed: "%{instance} 現在不開放新成員" + registration_closed: "%{instance} 目前不開放新成員" resend_confirmation: 重新傳送確認連結 reset_password: 重設密碼 rules: @@ -1522,6 +1522,9 @@ zh-TW: errors: limit_reached: 達到可回應之上限 unrecognized_emoji: 並非一個可識別的 emoji + redirects: + prompt: 若您信任此連結,請點擊以繼續。 + title: 您將要離開 %{instance} 。 relationships: activity: 帳號動態 confirm_follow_selected_followers: 您確定要跟隨選取的跟隨者嗎? @@ -1627,7 +1630,7 @@ zh-TW: relationships: 跟隨中與跟隨者 statuses_cleanup: 自動嘟文刪除 strikes: 管理警告 - two_factor_authentication: 兩階段認證 + two_factor_authentication: 兩階段驗證 webauthn_authentication: 安全金鑰 statuses: attached: @@ -1733,11 +1736,11 @@ zh-TW: disable: 停用兩階段驗證 disabled_success: 已成功啟用兩階段驗證 edit: 編輯 - enabled: 兩階段認證已啟用 - enabled_success: 已成功啟用兩階段認證 + enabled: 兩階段驗證已啟用 + enabled_success: 兩階段驗證已成功啟用 generate_recovery_codes: 產生備用驗證碼 lost_recovery_codes: 讓您能於遺失手機時,使用備用驗證碼登入。若您已遺失備用驗證碼,可於此產生一批新的,舊有的備用驗證碼將會失效。 - methods: 兩步驟方式 + methods: 兩階段驗證 otp: 驗證應用程式 recovery_codes: 備份備用驗證碼 recovery_codes_regenerated: 成功產生新的備用驗證碼 @@ -1757,15 +1760,15 @@ zh-TW: title: 申訴被駁回 backup_ready: explanation: 您要求完整備份您的 Mastodon 帳號。 - extra: 準備好下載了! + extra: 準備好可供下載了! subject: 您的備份檔已可供下載 title: 檔案匯出 failed_2fa: details: 以下是該登入嘗試之詳細資訊: - explanation: 有人嘗試登入您的帳號,但提供了無效的第二個驗證因子。 + explanation: 有人嘗試登入您的帳號,但提供了無效的兩階段驗證。 further_actions_html: 若這並非您所為,我們建議您立刻 %{action},因為其可能已被入侵。 - subject: 第二因子驗證失敗 - title: 第二因子身份驗證失敗 + subject: 兩階段驗證失敗 + title: 兩階段驗證失敗 suspicious_sign_in: change_password: 變更密碼 details: 以下是該登入之詳細資訊: @@ -1817,9 +1820,9 @@ zh-TW: users: follow_limit_reached: 您無法跟隨多於 %{limit} 個人 go_to_sso_account_settings: 前往您的身分提供商 (identity provider) 之帳號設定 - invalid_otp_token: 兩階段認證碼不正確 + invalid_otp_token: 兩階段驗證碼不正確 otp_lost_help_html: 如果您無法存取這兩者,您可以透過 %{email} 與我們聯繫 - rate_limited: 身份驗證嘗試太多次,請稍後再試。 + rate_limited: 過多次身份驗證嘗試,請稍後再試。 seamless_external_login: 由於您是由外部系統登入,所以不能設定密碼與電子郵件。 signed_in_as: 目前登入的帳號: verification: From ca7053f19c425616b90774cd2f2553f0842cd314 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:10:39 -0500 Subject: [PATCH 69/84] Consolidate db test prep steps to rake task (#28886) --- .../workflows/test-migrations-one-step.yml | 19 ++-------------- .../workflows/test-migrations-two-step.yml | 22 +++---------------- lib/tasks/tests.rake | 18 +++++++++++++++ 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index 5dca8e376d..1ff5cc06b9 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -78,23 +78,8 @@ jobs: - name: Create database run: './bin/rails db:create' - - name: Run migrations up to v2.0.0 - run: './bin/rails db:migrate VERSION=20171010025614' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2' - - - name: Run migrations up to v2.4.0 - run: './bin/rails db:migrate VERSION=20180514140000' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4' - - - name: Run migrations up to v2.4.3 - run: './bin/rails db:migrate VERSION=20180707154237' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4_3' + - name: Run historical migrations with data population + run: './bin/rails tests:migrations:prepare_database' - name: Run all remaining migrations run: './bin/rails db:migrate' diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 59485d285d..6698847315 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -45,6 +45,7 @@ jobs: --health-retries 5 ports: - 5432:5432 + redis: image: redis:7-alpine options: >- @@ -77,28 +78,11 @@ jobs: - name: Create database run: './bin/rails db:create' - - name: Run migrations up to v2.0.0 - run: './bin/rails db:migrate VERSION=20171010025614' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2' - - - name: Run pre-deployment migrations up to v2.4.0 - run: './bin/rails db:migrate VERSION=20180514140000' + - name: Run historical migrations with data population + run: './bin/rails tests:migrations:prepare_database' env: SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4' - - - name: Run migrations up to v2.4.3 - run: './bin/rails db:migrate VERSION=20180707154237' - env: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4_3' - - name: Run all remaining pre-deployment migrations run: './bin/rails db:migrate' env: diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index 45f055e218..885be79f41 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -2,6 +2,22 @@ namespace :tests do namespace :migrations do + desc 'Prepares all migrations and test data for consistency checks' + task prepare_database: :environment do + { + '2' => 2017_10_10_025614, + '2_4' => 2018_05_14_140000, + '2_4_3' => 2018_07_07_154237, + }.each do |release, version| + ActiveRecord::Tasks::DatabaseTasks + .migration_connection + .migration_context + .migrate(version) + Rake::Task["tests:migrations:populate_v#{release}"] + .invoke + end + end + desc 'Check that database state is consistent with a successful migration from populated data' task check_database: :environment do unless Account.find_by(username: 'admin', domain: nil)&.hide_collections? == false @@ -88,6 +104,8 @@ namespace :tests do puts 'Locale for fr-QC users not updated to fr-CA as expected' exit(1) end + + puts 'No errors found. Database state is consistent with a successful migration process.' end desc 'Populate the database with test data for 2.4.3' From c8f59d2ca4d693f328d8f68a21749ab4efc8c820 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:28:49 -0500 Subject: [PATCH 70/84] Fix `Style/TernaryParentheses` cop (#28387) --- .rubocop_todo.yml | 7 ------- config/environments/development.rb | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 77f7e70734..fd9dc18ac7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -279,13 +279,6 @@ Style/StringLiterals: - 'config/initializers/webauthn.rb' - 'config/routes.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex -Style/TernaryParentheses: - Exclude: - - 'config/environments/development.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, no_comma diff --git a/config/environments/development.rb b/config/environments/development.rb index 3c13ada380..a855f5a16b 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -85,7 +85,7 @@ Rails.application.configure do # If using a Heroku, Vagrant or generic remote development environment, # use letter_opener_web, accessible at /letter_opener. # Otherwise, use letter_opener, which launches a browser window to view sent mail. - config.action_mailer.delivery_method = (ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV']) ? :letter_opener_web : :letter_opener + config.action_mailer.delivery_method = ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV'] ? :letter_opener_web : :letter_opener # We provide a default secret for the development environment here. # This value should not be used in production environments! From 2866106ec1fb1ac5b30f432730bd20c6459f6600 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:37:25 -0500 Subject: [PATCH 71/84] Reduce factory creation in `spec/models/account_statuses_cleanup_policy` (#28361) --- .../account_statuses_cleanup_policy_spec.rb | 165 +++++++----------- 1 file changed, 59 insertions(+), 106 deletions(-) diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb index da2a774b2d..a08fd723a4 100644 --- a/spec/models/account_statuses_cleanup_policy_spec.rb +++ b/spec/models/account_statuses_cleanup_policy_spec.rb @@ -296,16 +296,11 @@ RSpec.describe AccountStatusesCleanupPolicy do let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } - it 'returns statuses including max_id' do - expect(subject).to include(old_status.id) - end - - it 'returns statuses including older than max_id' do - expect(subject).to include(very_old_status.id) - end - - it 'does not return statuses newer than max_id' do - expect(subject).to_not include(slightly_less_old_status.id) + it 'returns statuses included the max_id and older than the max_id but not newer than max_id' do + expect(subject) + .to include(old_status.id) + .and include(very_old_status.id) + .and not_include(slightly_less_old_status.id) end end @@ -315,16 +310,11 @@ RSpec.describe AccountStatusesCleanupPolicy do let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } - it 'returns statuses including min_id' do - expect(subject).to include(old_status.id) - end - - it 'returns statuses including newer than max_id' do - expect(subject).to include(slightly_less_old_status.id) - end - - it 'does not return statuses older than min_id' do - expect(subject).to_not include(very_old_status.id) + it 'returns statuses including min_id and newer than min_id, but not older than min_id' do + expect(subject) + .to include(old_status.id) + .and include(slightly_less_old_status.id) + .and not_include(very_old_status.id) end end @@ -339,12 +329,10 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.min_status_age = 2.years.seconds end - it 'does not return unrelated old status' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns only oldest status for deletion' do - expect(subject.pluck(:id)).to eq [very_old_status.id] + it 'does not return unrelated old status and does return oldest status' do + expect(subject.pluck(:id)) + .to not_include(unrelated_status.id) + .and eq [very_old_status.id] end end @@ -358,12 +346,10 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old direct message for deletion' do - expect(subject.pluck(:id)).to_not include(direct_message.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status except does not return the old direct message for deletion' do + expect(subject.pluck(:id)) + .to not_include(direct_message.id) + .and include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -377,12 +363,10 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.keep_self_bookmark = true end - it 'does not return the old self-bookmarked message for deletion' do - expect(subject.pluck(:id)).to_not include(self_bookmarked.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old self-bookmarked message for deletion' do + expect(subject.pluck(:id)) + .to not_include(self_bookmarked.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -396,12 +380,10 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old self-bookmarked message for deletion' do - expect(subject.pluck(:id)).to_not include(self_faved.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old self-faved message for deletion' do + expect(subject.pluck(:id)) + .to not_include(self_faved.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -415,12 +397,10 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old message with media for deletion' do - expect(subject.pluck(:id)).to_not include(status_with_media.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old message with media for deletion' do + expect(subject.pluck(:id)) + .to not_include(status_with_media.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -434,12 +414,10 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old poll message for deletion' do - expect(subject.pluck(:id)).to_not include(status_with_poll.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old poll message for deletion' do + expect(subject.pluck(:id)) + .to not_include(status_with_poll.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -453,12 +431,10 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old pinned message for deletion' do - expect(subject.pluck(:id)).to_not include(pinned_status.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old pinned message for deletion' do + expect(subject.pluck(:id)) + .to not_include(pinned_status.id) + .and include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -472,16 +448,11 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the recent toot' do - expect(subject.pluck(:id)).to_not include(recent_status.id) - end - - it 'does not return the unrelated toot' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the recent or unrelated statuses' do + expect(subject.pluck(:id)) + .to not_include(recent_status.id) + .and not_include(unrelated_status.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -495,12 +466,10 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.keep_self_bookmark = true end - it 'does not return unrelated old status' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns only normal statuses for deletion' do - expect(subject.pluck(:id)).to contain_exactly(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns normal statuses and does not return unrelated old status' do + expect(subject.pluck(:id)) + .to not_include(unrelated_status.id) + .and contain_exactly(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -509,20 +478,12 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.min_reblogs = 5 end - it 'does not return the recent toot' do - expect(subject.pluck(:id)).to_not include(recent_status.id) - end - - it 'does not return the toot reblogged 5 times' do - expect(subject.pluck(:id)).to_not include(reblogged_secondary.id) - end - - it 'does not return the unrelated toot' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns old statuses not reblogged as much' do - expect(subject.pluck(:id)).to include(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id) + it 'returns old not-reblogged statuses but does not return the recent, 5-times reblogged, or unrelated statuses' do + expect(subject.pluck(:id)) + .to not_include(recent_status.id) + .and not_include(reblogged_secondary.id) + .and not_include(unrelated_status.id) + .and include(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id) end end @@ -531,20 +492,12 @@ RSpec.describe AccountStatusesCleanupPolicy do account_statuses_cleanup_policy.min_favs = 5 end - it 'does not return the recent toot' do - expect(subject.pluck(:id)).to_not include(recent_status.id) - end - - it 'does not return the toot faved 5 times' do - expect(subject.pluck(:id)).to_not include(faved_secondary.id) - end - - it 'does not return the unrelated toot' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns old statuses not faved as much' do - expect(subject.pluck(:id)).to include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns old not-faved statuses but does not return the recent, 5-times faved, or unrelated statuses' do + expect(subject.pluck(:id)) + .to not_include(recent_status.id) + .and not_include(faved_secondary.id) + .and not_include(unrelated_status.id) + .and include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id) end end end From 274a48a9f4cd5bb36ad6933d736c04d04db75af0 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:49:33 -0500 Subject: [PATCH 72/84] Extract helper methods for db connection and table existence check in `CLI::Maintenance` task (#28281) --- lib/mastodon/cli/maintenance.rb | 150 +++++++++++++++++--------------- 1 file changed, 81 insertions(+), 69 deletions(-) diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index 73012812fd..c644729b5e 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -72,6 +72,10 @@ module Mastodon::CLI local? ? username : "#{username}@#{domain}" end + def db_table_exists?(table) + ActiveRecord::Base.connection.table_exists?(table) + end + # This is a duplicate of the Account::Merging concern because we need it # to be independent from code version. def merge_with!(other_account) @@ -88,12 +92,12 @@ module Mastodon::CLI AccountModerationNote, AccountPin, AccountStat, ListAccount, PollVote, Mention ] - owned_classes << AccountDeletionRequest if ActiveRecord::Base.connection.table_exists?(:account_deletion_requests) - owned_classes << AccountNote if ActiveRecord::Base.connection.table_exists?(:account_notes) - owned_classes << FollowRecommendationSuppression if ActiveRecord::Base.connection.table_exists?(:follow_recommendation_suppressions) - owned_classes << AccountIdentityProof if ActiveRecord::Base.connection.table_exists?(:account_identity_proofs) - owned_classes << Appeal if ActiveRecord::Base.connection.table_exists?(:appeals) - owned_classes << BulkImport if ActiveRecord::Base.connection.table_exists?(:bulk_imports) + owned_classes << AccountDeletionRequest if db_table_exists?(:account_deletion_requests) + owned_classes << AccountNote if db_table_exists?(:account_notes) + owned_classes << FollowRecommendationSuppression if db_table_exists?(:follow_recommendation_suppressions) + owned_classes << AccountIdentityProof if db_table_exists?(:account_identity_proofs) + owned_classes << Appeal if db_table_exists?(:appeals) + owned_classes << BulkImport if db_table_exists?(:bulk_imports) owned_classes.each do |klass| klass.where(account_id: other_account.id).find_each do |record| @@ -104,7 +108,7 @@ module Mastodon::CLI end target_classes = [Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin] - target_classes << AccountNote if ActiveRecord::Base.connection.table_exists?(:account_notes) + target_classes << AccountNote if db_table_exists?(:account_notes) target_classes.each do |klass| klass.where(target_account_id: other_account.id).find_each do |record| @@ -114,13 +118,13 @@ module Mastodon::CLI end end - if ActiveRecord::Base.connection.table_exists?(:canonical_email_blocks) + if db_table_exists?(:canonical_email_blocks) CanonicalEmailBlock.where(reference_account_id: other_account.id).find_each do |record| record.update_attribute(:reference_account_id, id) end end - if ActiveRecord::Base.connection.table_exists?(:appeals) + if db_table_exists?(:appeals) Appeal.where(account_warning_id: other_account.id).find_each do |record| record.update_attribute(:account_warning_id, id) end @@ -234,16 +238,16 @@ module Mastodon::CLI say 'Restoring index_accounts_on_username_and_domain_lower…' if migrator_version < 2020_06_20_164023 - ActiveRecord::Base.connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true + database_connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true else - ActiveRecord::Base.connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true + database_connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true end say 'Reindexing textual indexes on accounts…' - ActiveRecord::Base.connection.execute('REINDEX INDEX search_index;') - ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_uri;') - ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_url;') - ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_domain_and_id;') if migrator_version >= 2023_05_24_190515 + database_connection.execute('REINDEX INDEX search_index;') + database_connection.execute('REINDEX INDEX index_accounts_on_uri;') + database_connection.execute('REINDEX INDEX index_accounts_on_url;') + database_connection.execute('REINDEX INDEX index_accounts_on_domain_and_id;') if migrator_version >= 2023_05_24_190515 end def deduplicate_users! @@ -260,21 +264,21 @@ module Mastodon::CLI deduplicate_users_process_password_token say 'Restoring users indexes…' - ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true - ActiveRecord::Base.connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true - ActiveRecord::Base.connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if migrator_version < 2022_01_18_183010 + database_connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true + database_connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true + database_connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if migrator_version < 2022_01_18_183010 if migrator_version < 2022_03_10_060641 - ActiveRecord::Base.connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true + database_connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true else - ActiveRecord::Base.connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true, where: 'reset_password_token IS NOT NULL', opclass: :text_pattern_ops + database_connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true, where: 'reset_password_token IS NOT NULL', opclass: :text_pattern_ops end - ActiveRecord::Base.connection.execute('REINDEX INDEX index_users_on_unconfirmed_email;') if migrator_version >= 2023_07_02_151753 + database_connection.execute('REINDEX INDEX index_users_on_unconfirmed_email;') if migrator_version >= 2023_07_02_151753 end def deduplicate_users_process_email - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row| users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).includes(:account).to_a ref_user = users.shift say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow @@ -288,7 +292,7 @@ module Mastodon::CLI end def deduplicate_users_process_confirmation_token - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row| users = User.where(id: row['ids'].split(',')).order(created_at: :desc).includes(:account).to_a.drop(1) say "Unsetting confirmation token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow @@ -300,7 +304,7 @@ module Mastodon::CLI def deduplicate_users_process_remember_token if migrator_version < 2022_01_18_183010 - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row| users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).to_a.drop(1) say "Unsetting remember token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow @@ -312,7 +316,7 @@ module Mastodon::CLI end def deduplicate_users_process_password_token - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row| users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).includes(:account).to_a.drop(1) say "Unsetting password reset token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow @@ -326,47 +330,47 @@ module Mastodon::CLI remove_index_if_exists!(:account_domain_blocks, 'index_account_domain_blocks_on_account_id_and_domain') say 'Removing duplicate account domain blocks…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_domain_blocks GROUP BY account_id, domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_domain_blocks GROUP BY account_id, domain HAVING count(*) > 1").each do |row| AccountDomainBlock.where(id: row['ids'].split(',').drop(1)).delete_all end say 'Restoring account domain blocks indexes…' - ActiveRecord::Base.connection.add_index :account_domain_blocks, %w(account_id domain), name: 'index_account_domain_blocks_on_account_id_and_domain', unique: true + database_connection.add_index :account_domain_blocks, %w(account_id domain), name: 'index_account_domain_blocks_on_account_id_and_domain', unique: true end def deduplicate_account_identity_proofs! - return unless ActiveRecord::Base.connection.table_exists?(:account_identity_proofs) + return unless db_table_exists?(:account_identity_proofs) remove_index_if_exists!(:account_identity_proofs, 'index_account_proofs_on_account_and_provider_and_username') say 'Removing duplicate account identity proofs…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row| AccountIdentityProof.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring account identity proofs indexes…' - ActiveRecord::Base.connection.add_index :account_identity_proofs, %w(account_id provider provider_username), name: 'index_account_proofs_on_account_and_provider_and_username', unique: true + database_connection.add_index :account_identity_proofs, %w(account_id provider provider_username), name: 'index_account_proofs_on_account_and_provider_and_username', unique: true end def deduplicate_announcement_reactions! - return unless ActiveRecord::Base.connection.table_exists?(:announcement_reactions) + return unless db_table_exists?(:announcement_reactions) remove_index_if_exists!(:announcement_reactions, 'index_announcement_reactions_on_account_id_and_announcement_id') say 'Removing duplicate announcement reactions…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row| AnnouncementReaction.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring announcement_reactions indexes…' - ActiveRecord::Base.connection.add_index :announcement_reactions, %w(account_id announcement_id name), name: 'index_announcement_reactions_on_account_id_and_announcement_id', unique: true + database_connection.add_index :announcement_reactions, %w(account_id announcement_id name), name: 'index_announcement_reactions_on_account_id_and_announcement_id', unique: true end def deduplicate_conversations! remove_index_if_exists!(:conversations, 'index_conversations_on_uri') say 'Deduplicating conversations…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| conversations = Conversation.where(id: row['ids'].split(',')).order(id: :desc).to_a ref_conversation = conversations.shift @@ -379,9 +383,9 @@ module Mastodon::CLI say 'Restoring conversations indexes…' if migrator_version < 2022_03_07_083603 - ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true + database_connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true else - ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops + database_connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops end end @@ -389,7 +393,7 @@ module Mastodon::CLI remove_index_if_exists!(:custom_emojis, 'index_custom_emojis_on_shortcode_and_domain') say 'Deduplicating custom_emojis…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row| emojis = CustomEmoji.where(id: row['ids'].split(',')).order(id: :desc).to_a ref_emoji = emojis.shift @@ -401,14 +405,14 @@ module Mastodon::CLI end say 'Restoring custom_emojis indexes…' - ActiveRecord::Base.connection.add_index :custom_emojis, %w(shortcode domain), name: 'index_custom_emojis_on_shortcode_and_domain', unique: true + database_connection.add_index :custom_emojis, %w(shortcode domain), name: 'index_custom_emojis_on_shortcode_and_domain', unique: true end def deduplicate_custom_emoji_categories! remove_index_if_exists!(:custom_emoji_categories, 'index_custom_emoji_categories_on_name') say 'Deduplicating custom_emoji_categories…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row| categories = CustomEmojiCategory.where(id: row['ids'].split(',')).order(id: :desc).to_a ref_category = categories.shift @@ -420,26 +424,26 @@ module Mastodon::CLI end say 'Restoring custom_emoji_categories indexes…' - ActiveRecord::Base.connection.add_index :custom_emoji_categories, ['name'], name: 'index_custom_emoji_categories_on_name', unique: true + database_connection.add_index :custom_emoji_categories, ['name'], name: 'index_custom_emoji_categories_on_name', unique: true end def deduplicate_domain_allows! remove_index_if_exists!(:domain_allows, 'index_domain_allows_on_domain') say 'Deduplicating domain_allows…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row| DomainAllow.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring domain_allows indexes…' - ActiveRecord::Base.connection.add_index :domain_allows, ['domain'], name: 'index_domain_allows_on_domain', unique: true + database_connection.add_index :domain_allows, ['domain'], name: 'index_domain_allows_on_domain', unique: true end def deduplicate_domain_blocks! remove_index_if_exists!(:domain_blocks, 'index_domain_blocks_on_domain') say 'Deduplicating domain_blocks…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| domain_blocks = DomainBlock.where(id: row['ids'].split(',')).by_severity.reverse.to_a reject_media = domain_blocks.any?(&:reject_media?) @@ -456,49 +460,49 @@ module Mastodon::CLI end say 'Restoring domain_blocks indexes…' - ActiveRecord::Base.connection.add_index :domain_blocks, ['domain'], name: 'index_domain_blocks_on_domain', unique: true + database_connection.add_index :domain_blocks, ['domain'], name: 'index_domain_blocks_on_domain', unique: true end def deduplicate_unavailable_domains! - return unless ActiveRecord::Base.connection.table_exists?(:unavailable_domains) + return unless db_table_exists?(:unavailable_domains) remove_index_if_exists!(:unavailable_domains, 'index_unavailable_domains_on_domain') say 'Deduplicating unavailable_domains…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row| UnavailableDomain.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring unavailable_domains indexes…' - ActiveRecord::Base.connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true + database_connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true end def deduplicate_email_domain_blocks! remove_index_if_exists!(:email_domain_blocks, 'index_email_domain_blocks_on_domain') say 'Deduplicating email_domain_blocks…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| domain_blocks = EmailDomainBlock.where(id: row['ids'].split(',')).order(EmailDomainBlock.arel_table[:parent_id].asc.nulls_first).to_a domain_blocks.drop(1).each(&:destroy) end say 'Restoring email_domain_blocks indexes…' - ActiveRecord::Base.connection.add_index :email_domain_blocks, ['domain'], name: 'index_email_domain_blocks_on_domain', unique: true + database_connection.add_index :email_domain_blocks, ['domain'], name: 'index_email_domain_blocks_on_domain', unique: true end def deduplicate_media_attachments! remove_index_if_exists!(:media_attachments, 'index_media_attachments_on_shortcode') say 'Deduplicating media_attachments…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM media_attachments WHERE shortcode IS NOT NULL GROUP BY shortcode HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM media_attachments WHERE shortcode IS NOT NULL GROUP BY shortcode HAVING count(*) > 1").each do |row| MediaAttachment.where(id: row['ids'].split(',').drop(1)).update_all(shortcode: nil) end say 'Restoring media_attachments indexes…' if migrator_version < 2022_03_10_060626 - ActiveRecord::Base.connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true + database_connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true else - ActiveRecord::Base.connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true, where: 'shortcode IS NOT NULL', opclass: :text_pattern_ops + database_connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true, where: 'shortcode IS NOT NULL', opclass: :text_pattern_ops end end @@ -506,19 +510,19 @@ module Mastodon::CLI remove_index_if_exists!(:preview_cards, 'index_preview_cards_on_url') say 'Deduplicating preview_cards…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row| PreviewCard.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring preview_cards indexes…' - ActiveRecord::Base.connection.add_index :preview_cards, ['url'], name: 'index_preview_cards_on_url', unique: true + database_connection.add_index :preview_cards, ['url'], name: 'index_preview_cards_on_url', unique: true end def deduplicate_statuses! remove_index_if_exists!(:statuses, 'index_statuses_on_uri') say 'Deduplicating statuses…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| statuses = Status.where(id: row['ids'].split(',')).order(id: :asc).to_a ref_status = statuses.shift statuses.each do |status| @@ -529,9 +533,9 @@ module Mastodon::CLI say 'Restoring statuses indexes…' if migrator_version < 2022_03_10_060706 - ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true + database_connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true else - ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops + database_connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops end end @@ -540,7 +544,7 @@ module Mastodon::CLI remove_index_if_exists!(:tags, 'index_tags_on_name_lower_btree') say 'Deduplicating tags…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row| tags = Tag.where(id: row['ids'].split(',')).order(Arel.sql('(usable::int + trendable::int + listable::int) desc')).to_a ref_tag = tags.shift tags.each do |tag| @@ -551,38 +555,38 @@ module Mastodon::CLI say 'Restoring tags indexes…' if migrator_version < 2021_04_21_121431 - ActiveRecord::Base.connection.add_index :tags, 'lower((name)::text)', name: 'index_tags_on_name_lower', unique: true + database_connection.add_index :tags, 'lower((name)::text)', name: 'index_tags_on_name_lower', unique: true else - ActiveRecord::Base.connection.execute 'CREATE UNIQUE INDEX index_tags_on_name_lower_btree ON tags (lower(name) text_pattern_ops)' + database_connection.execute 'CREATE UNIQUE INDEX index_tags_on_name_lower_btree ON tags (lower(name) text_pattern_ops)' end end def deduplicate_webauthn_credentials! - return unless ActiveRecord::Base.connection.table_exists?(:webauthn_credentials) + return unless db_table_exists?(:webauthn_credentials) remove_index_if_exists!(:webauthn_credentials, 'index_webauthn_credentials_on_external_id') say 'Deduplicating webauthn_credentials…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row| WebauthnCredential.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring webauthn_credentials indexes…' - ActiveRecord::Base.connection.add_index :webauthn_credentials, ['external_id'], name: 'index_webauthn_credentials_on_external_id', unique: true + database_connection.add_index :webauthn_credentials, ['external_id'], name: 'index_webauthn_credentials_on_external_id', unique: true end def deduplicate_webhooks! - return unless ActiveRecord::Base.connection.table_exists?(:webhooks) + return unless db_table_exists?(:webhooks) remove_index_if_exists!(:webhooks, 'index_webhooks_on_url') say 'Deduplicating webhooks…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row| Webhook.where(id: row['ids'].split(',')).order(id: :desc).drop(1).each(&:destroy) end say 'Restoring webhooks indexes…' - ActiveRecord::Base.connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true + database_connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true end def deduplicate_software_updates! @@ -672,7 +676,7 @@ module Mastodon::CLI def merge_statuses!(main_status, duplicate_status) owned_classes = [Favourite, Mention, Poll] - owned_classes << Bookmark if ActiveRecord::Base.connection.table_exists?(:bookmarks) + owned_classes << Bookmark if db_table_exists?(:bookmarks) owned_classes.each do |klass| klass.where(status_id: duplicate_status.id).find_each do |record| record.update_attribute(:status_id, main_status.id) @@ -715,13 +719,21 @@ module Mastodon::CLI end def find_duplicate_accounts - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM accounts GROUP BY lower(username), COALESCE(lower(domain), '') HAVING count(*) > 1") + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM accounts GROUP BY lower(username), COALESCE(lower(domain), '') HAVING count(*) > 1") end def remove_index_if_exists!(table, name) - ActiveRecord::Base.connection.remove_index(table, name: name) if ActiveRecord::Base.connection.index_name_exists?(table, name) + database_connection.remove_index(table, name: name) if database_connection.index_name_exists?(table, name) rescue ArgumentError, ActiveRecord::StatementInvalid nil end + + def database_connection + ActiveRecord::Base.connection + end + + def db_table_exists?(table) + database_connection.table_exists?(table) + end end end From 3205a654caf903002c2db872f802a3332201678b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Jan 2024 15:34:26 +0100 Subject: [PATCH 73/84] Refactor conversations components in web UI (#28833) Co-authored-by: Claire --- .../components/conversation.jsx | 288 ++++++++++-------- .../components/conversations_list.jsx | 117 ++++--- .../containers/conversation_container.js | 80 ----- .../conversations_list_container.js | 16 - .../features/direct_timeline/index.jsx | 134 ++++---- 5 files changed, 274 insertions(+), 361 deletions(-) delete mode 100644 app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js delete mode 100644 app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index 274cfa69f5..3af89f9974 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -1,17 +1,24 @@ import PropTypes from 'prop-types'; +import { useCallback } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { Link, withRouter } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; +import { createSelector } from '@reduxjs/toolkit'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useDispatch, useSelector } from 'react-redux'; + import { HotKeys } from 'react-hotkeys'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; +import { replyCompose } from 'mastodon/actions/compose'; +import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations'; +import { openModal } from 'mastodon/actions/modal'; +import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'mastodon/actions/statuses'; import AttachmentList from 'mastodon/components/attachment_list'; import AvatarComposite from 'mastodon/components/avatar_composite'; import { IconButton } from 'mastodon/components/icon_button'; @@ -19,7 +26,7 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import StatusContent from 'mastodon/components/status_content'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import { autoPlayGif } from 'mastodon/initial_state'; -import { WithRouterPropTypes } from 'mastodon/utils/react_router'; +import { makeGetStatus } from 'mastodon/selectors'; const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, @@ -29,25 +36,31 @@ const messages = defineMessages({ delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, }); -class Conversation extends ImmutablePureComponent { +const getAccounts = createSelector( + (state) => state.get('accounts'), + (_, accountIds) => accountIds, + (accounts, accountIds) => + accountIds.map(id => accounts.get(id)) +); - static propTypes = { - conversationId: PropTypes.string.isRequired, - accounts: ImmutablePropTypes.list.isRequired, - lastStatus: ImmutablePropTypes.map, - unread:PropTypes.bool.isRequired, - scrollKey: PropTypes.string, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - markRead: PropTypes.func.isRequired, - delete: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - ...WithRouterPropTypes, - }; +const getStatus = makeGetStatus(); - handleMouseEnter = ({ currentTarget }) => { +export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) => { + const id = conversation.get('id'); + const unread = conversation.get('unread'); + const lastStatusId = conversation.get('last_status'); + const accountIds = conversation.get('accounts'); + const intl = useIntl(); + const dispatch = useDispatch(); + const history = useHistory(); + const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId })); + const accounts = useSelector(state => getAccounts(state, accountIds)); + + const handleMouseEnter = useCallback(({ currentTarget }) => { if (autoPlayGif) { return; } @@ -58,9 +71,9 @@ class Conversation extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-original'); } - }; + }, []); - handleMouseLeave = ({ currentTarget }) => { + const handleMouseLeave = useCallback(({ currentTarget }) => { if (autoPlayGif) { return; } @@ -71,136 +84,161 @@ class Conversation extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-static'); } - }; - - handleClick = () => { - if (!this.props.history) { - return; - } - - const { lastStatus, unread, markRead } = this.props; + }, []); + const handleClick = useCallback(() => { if (unread) { - markRead(); + dispatch(markConversationRead(id)); } - this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); - }; + history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); + }, [dispatch, history, unread, id, lastStatus]); - handleMarkAsRead = () => { - this.props.markRead(); - }; + const handleMarkAsRead = useCallback(() => { + dispatch(markConversationRead(id)); + }, [dispatch, id]); - handleReply = () => { - this.props.reply(this.props.lastStatus, this.props.history); - }; + const handleReply = useCallback(() => { + dispatch((_, getState) => { + let state = getState(); - handleDelete = () => { - this.props.delete(); - }; + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(lastStatus, history)), + }, + })); + } else { + dispatch(replyCompose(lastStatus, history)); + } + }); + }, [dispatch, lastStatus, history, intl]); - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.conversationId); - }; + const handleDelete = useCallback(() => { + dispatch(deleteConversation(id)); + }, [dispatch, id]); - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.conversationId); - }; + const handleHotkeyMoveUp = useCallback(() => { + onMoveUp(id); + }, [id, onMoveUp]); - handleConversationMute = () => { - this.props.onMute(this.props.lastStatus); - }; + const handleHotkeyMoveDown = useCallback(() => { + onMoveDown(id); + }, [id, onMoveDown]); - handleShowMore = () => { - this.props.onToggleHidden(this.props.lastStatus); - }; - - render () { - const { accounts, lastStatus, unread, scrollKey, intl } = this.props; - - if (lastStatus === null) { - return null; + const handleConversationMute = useCallback(() => { + if (lastStatus.get('muted')) { + dispatch(unmuteStatus(lastStatus.get('id'))); + } else { + dispatch(muteStatus(lastStatus.get('id'))); } + }, [dispatch, lastStatus]); - const menu = [ - { text: intl.formatMessage(messages.open), action: this.handleClick }, - null, - ]; - - menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute }); - - if (unread) { - menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead }); - menu.push(null); + const handleShowMore = useCallback(() => { + if (lastStatus.get('hidden')) { + dispatch(revealStatus(lastStatus.get('id'))); + } else { + dispatch(hideStatus(lastStatus.get('id'))); } + }, [dispatch, lastStatus]); - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); + if (!lastStatus) { + return null; + } - const names = accounts.map(a => ).reduce((prev, cur) => [prev, ', ', cur]); + const menu = [ + { text: intl.formatMessage(messages.open), action: handleClick }, + null, + { text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: handleConversationMute }, + ]; - const handlers = { - reply: this.handleReply, - open: this.handleClick, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleHidden: this.handleShowMore, - }; + if (unread) { + menu.push({ text: intl.formatMessage(messages.markAsRead), action: handleMarkAsRead }); + menu.push(null); + } - return ( - -
-
- -
+ menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete }); -
-
-
- {unread && } -
+ const names = accounts.map(a => ( + + + + + + )).reduce((prev, cur) => [prev, ', ', cur]); -
- {names} }} /> -
+ const handlers = { + reply: handleReply, + open: handleClick, + moveUp: handleHotkeyMoveUp, + moveDown: handleHotkeyMoveDown, + toggleHidden: handleShowMore, + }; + + return ( + +
+
+ +
+ +
+
+
+ {unread && }
- + {names} }} /> +
+
+ + + + {lastStatus.get('media_attachments').size > 0 && ( + + )} - {lastStatus.get('media_attachments').size > 0 && ( - + + +
+ - )} - -
- - -
- -
-
- ); - } +
+ + ); +}; -} - -export default withRouter(injectIntl(Conversation)); +Conversation.propTypes = { + conversation: ImmutablePropTypes.map.isRequired, + scrollKey: PropTypes.string, + onMoveUp: PropTypes.func, + onMoveDown: PropTypes.func, +}; diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversations_list.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.jsx index 8c12ea9e5f..c9fc098a52 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversations_list.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.jsx @@ -1,77 +1,72 @@ import PropTypes from 'prop-types'; +import { useRef, useMemo, useCallback } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useSelector, useDispatch } from 'react-redux'; import { debounce } from 'lodash'; -import ScrollableList from '../../../components/scrollable_list'; -import ConversationContainer from '../containers/conversation_container'; +import { expandConversations } from 'mastodon/actions/conversations'; +import ScrollableList from 'mastodon/components/scrollable_list'; -export default class ConversationsList extends ImmutablePureComponent { +import { Conversation } from './conversation'; - static propTypes = { - conversations: ImmutablePropTypes.list.isRequired, - scrollKey: PropTypes.string.isRequired, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - onLoadMore: PropTypes.func, - }; +const focusChild = (node, index, alignTop) => { + const element = node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id); - - handleMoveUp = id => { - const elementIndex = this.getCurrentIndex(id) - 1; - this._selectChild(elementIndex, true); - }; - - handleMoveDown = id => { - const elementIndex = this.getCurrentIndex(id) + 1; - this._selectChild(elementIndex, false); - }; - - _selectChild (index, align_top) { - const container = this.node.node; - const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - if (align_top && container.scrollTop > element.offsetTop) { - element.scrollIntoView(true); - } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { - element.scrollIntoView(false); - } - element.focus(); + if (element) { + if (alignTop && node.scrollTop > element.offsetTop) { + element.scrollIntoView(true); + } else if (!alignTop && node.scrollTop + node.clientHeight < element.offsetTop + element.offsetHeight) { + element.scrollIntoView(false); } + + element.focus(); } +}; - setRef = c => { - this.node = c; - }; +export const ConversationsList = ({ scrollKey, ...other }) => { + const listRef = useRef(); + const conversations = useSelector(state => state.getIn(['conversations', 'items'])); + const isLoading = useSelector(state => state.getIn(['conversations', 'isLoading'], true)); + const hasMore = useSelector(state => state.getIn(['conversations', 'hasMore'], false)); + const dispatch = useDispatch(); + const lastStatusId = conversations.last()?.get('last_status'); - handleLoadOlder = debounce(() => { - const last = this.props.conversations.last(); + const handleMoveUp = useCallback(id => { + const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1; + focusChild(listRef.current.node, elementIndex, true); + }, [listRef, conversations]); - if (last && last.get('last_status')) { - this.props.onLoadMore(last.get('last_status')); + const handleMoveDown = useCallback(id => { + const elementIndex = conversations.findIndex(x => x.get('id') === id) + 1; + focusChild(listRef.current.node, elementIndex, false); + }, [listRef, conversations]); + + const debouncedLoadMore = useMemo(() => debounce(id => { + dispatch(expandConversations({ maxId: id })); + }, 300, { leading: true }), [dispatch]); + + const handleLoadMore = useCallback(() => { + if (lastStatusId) { + debouncedLoadMore(lastStatusId); } - }, 300, { leading: true }); + }, [debouncedLoadMore, lastStatusId]); - render () { - const { conversations, isLoading, onLoadMore, ...other } = this.props; + return ( + + {conversations.map(item => ( + + ))} + + ); +}; - return ( - - {conversations.map(item => ( - - ))} - - ); - } - -} +ConversationsList.propTypes = { + scrollKey: PropTypes.string.isRequired, +}; diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js deleted file mode 100644 index 456fc7d7cc..0000000000 --- a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js +++ /dev/null @@ -1,80 +0,0 @@ -import { defineMessages, injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { replyCompose } from 'mastodon/actions/compose'; -import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations'; -import { openModal } from 'mastodon/actions/modal'; -import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'mastodon/actions/statuses'; -import { makeGetStatus } from 'mastodon/selectors'; - -import Conversation from '../components/conversation'; - -const messages = defineMessages({ - replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, - replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, -}); - -const mapStateToProps = () => { - const getStatus = makeGetStatus(); - - return (state, { conversationId }) => { - const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); - const lastStatusId = conversation.get('last_status', null); - - return { - accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), - unread: conversation.get('unread'), - lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }), - }; - }; -}; - -const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({ - - markRead () { - dispatch(markConversationRead(conversationId)); - }, - - reply (status, router) { - dispatch((_, getState) => { - let state = getState(); - - if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, router)), - }, - })); - } else { - dispatch(replyCompose(status, router)); - } - }); - }, - - delete () { - dispatch(deleteConversation(conversationId)); - }, - - onMute (status) { - if (status.get('muted')) { - dispatch(unmuteStatus(status.get('id'))); - } else { - dispatch(muteStatus(status.get('id'))); - } - }, - - onToggleHidden (status) { - if (status.get('hidden')) { - dispatch(revealStatus(status.get('id'))); - } else { - dispatch(hideStatus(status.get('id'))); - } - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation)); diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js deleted file mode 100644 index 1dcd3ec1bd..0000000000 --- a/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; - -import { expandConversations } from '../../../actions/conversations'; -import ConversationsList from '../components/conversations_list'; - -const mapStateToProps = state => ({ - conversations: state.getIn(['conversations', 'items']), - isLoading: state.getIn(['conversations', 'isLoading'], true), - hasMore: state.getIn(['conversations', 'hasMore'], false), -}); - -const mapDispatchToProps = dispatch => ({ - onLoadMore: maxId => dispatch(expandConversations({ maxId })), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList); diff --git a/app/javascript/mastodon/features/direct_timeline/index.jsx b/app/javascript/mastodon/features/direct_timeline/index.jsx index af29d7a5b8..7aee83ec10 100644 --- a/app/javascript/mastodon/features/direct_timeline/index.jsx +++ b/app/javascript/mastodon/features/direct_timeline/index.jsx @@ -1,11 +1,11 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useRef, useCallback, useEffect } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Helmet } from 'react-helmet'; -import { connect } from 'react-redux'; +import { useDispatch } from 'react-redux'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; @@ -14,103 +14,79 @@ import { connectDirectStream } from 'mastodon/actions/streaming'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import ConversationsListContainer from './containers/conversations_list_container'; +import { ConversationsList } from './components/conversations_list'; const messages = defineMessages({ title: { id: 'column.direct', defaultMessage: 'Private mentions' }, }); -class DirectTimeline extends PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columnId: PropTypes.string, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; +const DirectTimeline = ({ columnId, multiColumn }) => { + const columnRef = useRef(); + const intl = useIntl(); + const dispatch = useDispatch(); + const pinned = !!columnId; + const handlePin = useCallback(() => { if (columnId) { dispatch(removeColumn(columnId)); } else { dispatch(addColumn('DIRECT', {})); } - }; + }, [dispatch, columnId]); - handleMove = (dir) => { - const { columnId, dispatch } = this.props; + const handleMove = useCallback((dir) => { dispatch(moveColumn(columnId, dir)); - }; + }, [dispatch, columnId]); - handleHeaderClick = () => { - this.column.scrollTop(); - }; - - componentDidMount () { - const { dispatch } = this.props; + const handleHeaderClick = useCallback(() => { + columnRef.current.scrollTop(); + }, [columnRef]); + useEffect(() => { dispatch(mountConversations()); dispatch(expandConversations()); - this.disconnect = dispatch(connectDirectStream()); - } - componentWillUnmount () { - this.props.dispatch(unmountConversations()); + const disconnect = dispatch(connectDirectStream()); - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } + return () => { + dispatch(unmountConversations()); + disconnect(); + }; + }, [dispatch]); - setRef = c => { - this.column = c; - }; + return ( + + - handleLoadMore = maxId => { - this.props.dispatch(expandConversations({ maxId })); - }; + } + bindToDocument={!multiColumn} + prepend={
} + alwaysPrepend + /> - render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; - const pinned = !!columnId; + + {intl.formatMessage(messages.title)} + + +
+ ); +}; - return ( - - +DirectTimeline.propTypes = { + columnId: PropTypes.string, + multiColumn: PropTypes.bool, +}; -
} - alwaysPrepend - emptyMessage={} - /> - - - {intl.formatMessage(messages.title)} - - - - ); - } - -} - -export default connect()(injectIntl(DirectTimeline)); +export default DirectTimeline; From 17ea22671de0705ec805bb157754d5ae5f24f9e3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 10:13:41 -0500 Subject: [PATCH 74/84] Fix `Style/GuardClause` cop in app/controllers (#28420) --- .rubocop_todo.yml | 4 ---- .../admin/confirmations_controller.rb | 14 +++++++------ .../auth/confirmations_controller.rb | 12 ++++++----- app/controllers/auth/passwords_controller.rb | 10 ++++------ .../webauthn_credentials_controller.rb | 20 ++++++++----------- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fd9dc18ac7..c8165c1edf 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -121,10 +121,6 @@ Style/GlobalStdStream: # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: Exclude: - - 'app/controllers/admin/confirmations_controller.rb' - - 'app/controllers/auth/confirmations_controller.rb' - - 'app/controllers/auth/passwords_controller.rb' - - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - 'app/lib/activitypub/activity/block.rb' - 'app/lib/request.rb' - 'app/lib/request_pool.rb' diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 7ccf5c9012..702550eecc 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -3,7 +3,7 @@ module Admin class ConfirmationsController < BaseController before_action :set_user - before_action :check_confirmation, only: [:resend] + before_action :redirect_confirmed_user, only: [:resend], if: :user_confirmed? def create authorize @user, :confirm? @@ -25,11 +25,13 @@ module Admin private - def check_confirmation - if @user.confirmed? - flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') - redirect_to admin_accounts_path - end + def redirect_confirmed_user + flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') + redirect_to admin_accounts_path + end + + def user_confirmed? + @user.confirmed? end end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index d9cd630905..7ca7be5f8e 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -7,7 +7,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController before_action :set_body_classes before_action :set_confirmation_user!, only: [:show, :confirm_captcha] - before_action :require_unconfirmed! + before_action :redirect_confirmed_user, if: :signed_in_confirmed_user? before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha] before_action :require_captcha_if_needed!, only: [:show] @@ -65,10 +65,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController @confirmation_user.nil? || @confirmation_user.confirmed? end - def require_unconfirmed! - if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? - redirect_to(current_user.approved? ? root_path : edit_user_registration_path) - end + def redirect_confirmed_user + redirect_to(current_user.approved? ? root_path : edit_user_registration_path) + end + + def signed_in_confirmed_user? + user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? end def set_body_classes diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index a752194d5b..de001f062b 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -2,7 +2,7 @@ class Auth::PasswordsController < Devise::PasswordsController skip_before_action :check_self_destruct! - before_action :check_validity_of_reset_password_token, only: :edit + before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid? before_action :set_body_classes layout 'auth' @@ -19,11 +19,9 @@ class Auth::PasswordsController < Devise::PasswordsController private - def check_validity_of_reset_password_token - unless reset_password_token_is_valid? - flash[:error] = I18n.t('auth.invalid_reset_password_token') - redirect_to new_password_path(resource_name) - end + def redirect_invalid_reset_token + flash[:error] = I18n.t('auth.invalid_reset_password_token') + redirect_to new_password_path(resource_name) end def set_body_classes diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index c86ede4f3a..9714d54f95 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -6,8 +6,8 @@ module Settings skip_before_action :check_self_destruct! skip_before_action :require_functional! - before_action :require_otp_enabled - before_action :require_webauthn_enabled, only: [:index, :destroy] + before_action :redirect_invalid_otp, unless: -> { current_user.otp_enabled? } + before_action :redirect_invalid_webauthn, only: [:index, :destroy], unless: -> { current_user.webauthn_enabled? } def index; end def new; end @@ -85,18 +85,14 @@ module Settings private - def require_otp_enabled - unless current_user.otp_enabled? - flash[:error] = t('webauthn_credentials.otp_required') - redirect_to settings_two_factor_authentication_methods_path - end + def redirect_invalid_otp + flash[:error] = t('webauthn_credentials.otp_required') + redirect_to settings_two_factor_authentication_methods_path end - def require_webauthn_enabled - unless current_user.webauthn_enabled? - flash[:error] = t('webauthn_credentials.not_enabled') - redirect_to settings_two_factor_authentication_methods_path - end + def redirect_invalid_webauthn + flash[:error] = t('webauthn_credentials.not_enabled') + redirect_to settings_two_factor_authentication_methods_path end end end From 0b38946c874f4e02295a303514aaf8895cf8a918 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 10:18:15 -0500 Subject: [PATCH 75/84] Update paperclip and climate_control gems (#28379) --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 2d4e504ac7..4951304e39 100644 --- a/Gemfile +++ b/Gemfile @@ -123,7 +123,7 @@ group :test do gem 'database_cleaner-active_record' # Used to mock environment variables - gem 'climate_control', '~> 0.2' + gem 'climate_control' # Generating fake data for specs gem 'faker', '~> 3.2' diff --git a/Gemfile.lock b/Gemfile.lock index a31d0a929c..57b2580722 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -185,7 +185,7 @@ GEM elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl chunky_png (1.4.0) - climate_control (0.2.0) + climate_control (1.2.0) cocoon (1.2.15) color_diff (0.1) concurrent-ruby (1.2.3) @@ -746,8 +746,8 @@ GEM temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terrapin (0.6.0) - climate_control (>= 0.0.3, < 1.0) + terrapin (1.0.1) + climate_control test-prof (1.3.1) thor (1.3.0) tilt (2.3.0) @@ -836,7 +836,7 @@ DEPENDENCIES capybara (~> 3.39) charlock_holmes (~> 0.7.7) chewy (~> 7.3) - climate_control (~> 0.2) + climate_control cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby From 4cdf62e576488e8c41f79c2a04a1630df9685592 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 10:26:51 -0500 Subject: [PATCH 76/84] Extract `rebuild_index` method in maintenance CLI (#28911) --- lib/mastodon/cli/maintenance.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index c644729b5e..a64206065d 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -244,10 +244,10 @@ module Mastodon::CLI end say 'Reindexing textual indexes on accounts…' - database_connection.execute('REINDEX INDEX search_index;') - database_connection.execute('REINDEX INDEX index_accounts_on_uri;') - database_connection.execute('REINDEX INDEX index_accounts_on_url;') - database_connection.execute('REINDEX INDEX index_accounts_on_domain_and_id;') if migrator_version >= 2023_05_24_190515 + rebuild_index(:search_index) + rebuild_index(:index_accounts_on_uri) + rebuild_index(:index_accounts_on_url) + rebuild_index(:index_accounts_on_domain_and_id) if migrator_version >= 2023_05_24_190515 end def deduplicate_users! @@ -274,7 +274,7 @@ module Mastodon::CLI database_connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true, where: 'reset_password_token IS NOT NULL', opclass: :text_pattern_ops end - database_connection.execute('REINDEX INDEX index_users_on_unconfirmed_email;') if migrator_version >= 2023_07_02_151753 + rebuild_index(:index_users_on_unconfirmed_email) if migrator_version >= 2023_07_02_151753 end def deduplicate_users_process_email @@ -735,5 +735,9 @@ module Mastodon::CLI def db_table_exists?(table) database_connection.table_exists?(table) end + + def rebuild_index(name) + database_connection.execute("REINDEX INDEX #{name}") + end end end From 42ab855b2339c5cea3229c856ab539f883736b12 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 10:28:27 -0500 Subject: [PATCH 77/84] Add specs for `Instance` model scopes and add `with_domain_follows` scope (#28767) --- .../admin/export_domain_blocks_controller.rb | 6 +- app/models/instance.rb | 14 +++ spec/models/instance_spec.rb | 104 ++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 spec/models/instance_spec.rb diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index ffc4478172..9caafd9684 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -49,7 +49,7 @@ module Admin next end - @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) + @warning_domains = instances_from_imported_blocks.pluck(:domain) rescue ActionController::ParameterMissing flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') set_dummy_import! @@ -58,6 +58,10 @@ module Admin private + def instances_from_imported_blocks + Instance.with_domain_follows(@domain_blocks.map(&:domain)) + end + def export_filename 'domain_blocks.csv' end diff --git a/app/models/instance.rb b/app/models/instance.rb index 2dec75d6fe..0fd31c8097 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -25,11 +25,25 @@ class Instance < ApplicationRecord scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :domain_starts_with, ->(value) { where(arel_table[:domain].matches("#{sanitize_sql_like(value)}%", false, true)) } scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") } + scope :with_domain_follows, ->(domains) { where(domain: domains).where(domain_account_follows) } def self.refresh Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false) end + def self.domain_account_follows + Arel.sql( + <<~SQL.squish + EXISTS ( + SELECT 1 + FROM follows + JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id + WHERE accounts.domain = instances.domain + ) + SQL + ) + end + def readonly? true end diff --git a/spec/models/instance_spec.rb b/spec/models/instance_spec.rb new file mode 100644 index 0000000000..3e811d3325 --- /dev/null +++ b/spec/models/instance_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Instance do + describe 'Scopes' do + before { described_class.refresh } + + describe '#searchable' do + let(:expected_domain) { 'host.example' } + let(:blocked_domain) { 'other.example' } + + before do + Fabricate :account, domain: expected_domain + Fabricate :account, domain: blocked_domain + Fabricate :domain_block, domain: blocked_domain + end + + it 'returns records not domain blocked' do + results = described_class.searchable.pluck(:domain) + + expect(results) + .to include(expected_domain) + .and not_include(blocked_domain) + end + end + + describe '#matches_domain' do + let(:host_domain) { 'host.example.com' } + let(:host_under_domain) { 'host_under.example.com' } + let(:other_domain) { 'other.example' } + + before do + Fabricate :account, domain: host_domain + Fabricate :account, domain: host_under_domain + Fabricate :account, domain: other_domain + end + + it 'returns matching records' do + expect(described_class.matches_domain('host.exa').pluck(:domain)) + .to include(host_domain) + .and not_include(other_domain) + + expect(described_class.matches_domain('ple.com').pluck(:domain)) + .to include(host_domain) + .and not_include(other_domain) + + expect(described_class.matches_domain('example').pluck(:domain)) + .to include(host_domain) + .and include(other_domain) + + expect(described_class.matches_domain('host_').pluck(:domain)) # Preserve SQL wildcards + .to include(host_domain) + .and include(host_under_domain) + .and not_include(other_domain) + end + end + + describe '#by_domain_and_subdomains' do + let(:exact_match_domain) { 'example.com' } + let(:subdomain_domain) { 'foo.example.com' } + let(:partial_domain) { 'grexample.com' } + + before do + Fabricate(:account, domain: exact_match_domain) + Fabricate(:account, domain: subdomain_domain) + Fabricate(:account, domain: partial_domain) + end + + it 'returns matching instances' do + results = described_class.by_domain_and_subdomains('example.com').pluck(:domain) + + expect(results) + .to include(exact_match_domain) + .and include(subdomain_domain) + .and not_include(partial_domain) + end + end + + describe '#with_domain_follows' do + let(:example_domain) { 'example.host' } + let(:other_domain) { 'other.host' } + let(:none_domain) { 'none.host' } + + before do + example_account = Fabricate(:account, domain: example_domain) + other_account = Fabricate(:account, domain: other_domain) + Fabricate(:account, domain: none_domain) + + Fabricate :follow, account: example_account + Fabricate :follow, target_account: other_account + end + + it 'returns instances with domain accounts that have follows' do + results = described_class.with_domain_follows(['example.host', 'other.host', 'none.host']).pluck(:domain) + + expect(results) + .to include(example_domain) + .and include(other_domain) + .and not_include(none_domain) + end + end + end +end From ae9c67593062c2e0950d47394b4aaa0301fe8ae4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:45:04 +0100 Subject: [PATCH 78/84] New Crowdin Translations (automated) (#2564) * New Crowdin translations * Fix bogus translation files --------- Co-authored-by: GitHub Actions Co-authored-by: Claire --- .../flavours/glitch/locales/ar.json | 6 + .../flavours/glitch/locales/de.json | 1 + .../flavours/glitch/locales/es-AR.json | 1 + .../flavours/glitch/locales/es-MX.json | 1 + .../flavours/glitch/locales/es.json | 1 + .../flavours/glitch/locales/fr-CA.json | 159 ++++++++++++++++++ .../flavours/glitch/locales/fr.json | 1 + .../flavours/glitch/locales/ko.json | 2 + .../flavours/glitch/locales/vi.json | 5 +- .../flavours/glitch/locales/zh-CN.json | 5 +- .../flavours/glitch/locales/zh-TW.json | 1 + config/locales-glitch/fr-CA.yml | 1 + config/locales-glitch/simple_form.fr-CA.yml | 1 + 13 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 app/javascript/flavours/glitch/locales/fr-CA.json create mode 100644 config/locales-glitch/fr-CA.yml create mode 100644 config/locales-glitch/simple_form.fr-CA.yml diff --git a/app/javascript/flavours/glitch/locales/ar.json b/app/javascript/flavours/glitch/locales/ar.json index 20ca870f7c..a259146328 100644 --- a/app/javascript/flavours/glitch/locales/ar.json +++ b/app/javascript/flavours/glitch/locales/ar.json @@ -32,11 +32,13 @@ "compose_form.spoiler": "إخفاء النص خلف تحذير", "confirmation_modal.do_not_ask_again": "لا تطلب التأكيد مرة أخرى", "confirmations.deprecated_settings.confirm": "استخدام تفضيلات ماستدون", + "confirmations.deprecated_settings.message": "تم استبدال بعض من الجهاز الخاص بالماستدون {preferences} الذي تستخدمه {app_settings} الخاص بجهاز ماستدون سيتم تجاوزه:", "confirmations.missing_media_description.confirm": "أرسل على أيّة حال", "confirmations.missing_media_description.edit": "تعديل الوسائط", "confirmations.unfilter.author": "المؤلف", "confirmations.unfilter.confirm": "عرض", "confirmations.unfilter.edit_filter": "تعديل عامل التصفية", + "confirmations.unfilter.filters": "مطابقة {count, plural, zero {}one {فلتر} two {فلاتر} few {فلاتر} many {فلاتر} other {فلاتر}}", "content-type.change": "نوع المحتوى", "direct.group_by_conversations": "تجميع حسب المحادثة", "endorsed_accounts_editor.endorsed_accounts": "الحسابات المميزة", @@ -61,6 +63,10 @@ "notification_purge.start": "أدخل وضع تنظيف الإشعارات", "notifications.marked_clear": "مسح الإشعارات المحددة", "notifications.marked_clear_confirmation": "هل أنت متأكد من أنك تريد مسح جميع الإشعارات المحددة نهائياً؟", + "settings.always_show_spoilers_field": "تمكين دائما حقل تحذير المحتوى", + "settings.auto_collapse_height": "الارتفاع (بالبكسل) لاعتبار التبويق طويل", + "settings.auto_collapse_reblogs": "دفع", + "settings.auto_collapse_replies": "ردود {{count}}", "settings.close": "إغلاق", "settings.content_warnings": "Content warnings", "settings.preferences": "Preferences" diff --git a/app/javascript/flavours/glitch/locales/de.json b/app/javascript/flavours/glitch/locales/de.json index a83c4df787..96650a5efa 100644 --- a/app/javascript/flavours/glitch/locales/de.json +++ b/app/javascript/flavours/glitch/locales/de.json @@ -2,6 +2,7 @@ "about.fork_disclaimer": "Glitch-soc ist freie, quelloffene Software geforkt von Mastodon.", "account.disclaimer_full": "Die folgenden Informationen könnten das Profil des Nutzers unvollständig wiedergeben.", "account.follows": "Folgt", + "account.follows_you": "Folgt dir", "account.joined": "Beigetreten am {date}", "account.suspended_disclaimer_full": "Dieser Nutzer wurde durch einen Moderator gesperrt.", "account.view_full_profile": "Vollständiges Profil anzeigen", diff --git a/app/javascript/flavours/glitch/locales/es-AR.json b/app/javascript/flavours/glitch/locales/es-AR.json index 860c2c0beb..48ae0f0683 100644 --- a/app/javascript/flavours/glitch/locales/es-AR.json +++ b/app/javascript/flavours/glitch/locales/es-AR.json @@ -2,6 +2,7 @@ "about.fork_disclaimer": "Glitch-soc es software gratuito, de código abierto, bifurcado de Mastodon.", "account.disclaimer_full": "La información aquí presentada puede reflejar de manera incompleta el perfil del usuario.", "account.follows": "Sigue", + "account.follows_you": "Te sigue", "account.joined": "Unido el {date}", "account.suspended_disclaimer_full": "Este usuario ha sido suspendido por un moderador.", "account.view_full_profile": "Ver perfil completo", diff --git a/app/javascript/flavours/glitch/locales/es-MX.json b/app/javascript/flavours/glitch/locales/es-MX.json index 00b87e9c12..bb0a5ab74f 100644 --- a/app/javascript/flavours/glitch/locales/es-MX.json +++ b/app/javascript/flavours/glitch/locales/es-MX.json @@ -2,6 +2,7 @@ "about.fork_disclaimer": "Glitch-soc es software gratuito, de código abierto, bifurcado de Mastodon.", "account.disclaimer_full": "La información aquí presentada puede reflejar de manera incompleta el perfil del usuario.", "account.follows": "Seguir", + "account.follows_you": "Te sigue", "account.joined": "Unido {date}", "account.suspended_disclaimer_full": "Este usuario ha sido suspendido por un moderador.", "account.view_full_profile": "Ver perfil completo", diff --git a/app/javascript/flavours/glitch/locales/es.json b/app/javascript/flavours/glitch/locales/es.json index b7f266aa3e..35f0fa8deb 100644 --- a/app/javascript/flavours/glitch/locales/es.json +++ b/app/javascript/flavours/glitch/locales/es.json @@ -2,6 +2,7 @@ "about.fork_disclaimer": "Glitch-soc es software gratuito, de código abierto, bifurcado de Mastodon.", "account.disclaimer_full": "La información que figura a continuación puede reflejar el perfil de la cuenta de forma incompleta.", "account.follows": "Sigue", + "account.follows_you": "Te sigue", "account.joined": "Se unió el {date}", "account.suspended_disclaimer_full": "Este usuario ha sido suspendido por un moderador.", "account.view_full_profile": "Ver perfil completo", diff --git a/app/javascript/flavours/glitch/locales/fr-CA.json b/app/javascript/flavours/glitch/locales/fr-CA.json new file mode 100644 index 0000000000..6015f4097d --- /dev/null +++ b/app/javascript/flavours/glitch/locales/fr-CA.json @@ -0,0 +1,159 @@ +{ + "about.fork_disclaimer": "Glitch-soc est un logiciel gratuit et open source, fork de Mastodon.", + "account.disclaimer_full": "Les informations ci-dessous peuvent être incomplètes.", + "account.follows": "Abonnements", + "account.follows_you": "Vous suit", + "account.joined": "Ici depuis {date}", + "account.suspended_disclaimer_full": "Cet utilisateur a été suspendu par un modérateur.", + "account.view_full_profile": "Voir le profil complet", + "advanced_options.icon_title": "Options avancées", + "advanced_options.local-only.long": "Ne pas envoyer aux autres instances", + "advanced_options.local-only.short": "Uniquement en local", + "advanced_options.local-only.tooltip": "Ce post est uniquement local", + "advanced_options.threaded_mode.long": "Ouvre automatiquement une réponse lors de la publication", + "advanced_options.threaded_mode.short": "Mode thread", + "advanced_options.threaded_mode.tooltip": "Mode thread activé", + "boost_modal.missing_description": "Ce post contient des médias sans description", + "column.favourited_by": "Ajouté en favori par", + "column.heading": "Divers", + "column.reblogged_by": "Partagé par", + "column.subheading": "Autres options", + "column_header.profile": "Profil", + "column_subheading.lists": "Listes", + "column_subheading.navigation": "Navigation", + "community.column_settings.allow_local_only": "Afficher seulement les posts locaux", + "compose.attach": "Joindre…", + "compose.attach.doodle": "Dessiner quelque chose", + "compose.attach.upload": "Téléverser un fichier", + "compose.content-type.html": "HTML", + "compose.content-type.markdown": "Markdown", + "compose.content-type.plain": "Text brut", + "compose_form.poll.multiple_choices": "Choix multiples", + "compose_form.poll.single_choice": "Choix unique", + "compose_form.spoiler": "Cacher le texte derrière un avertissement", + "confirmation_modal.do_not_ask_again": "Ne plus demander confirmation", + "confirmations.deprecated_settings.confirm": "Utiliser les préférences de Mastodon", + "confirmations.deprecated_settings.message": "Certaines {app_settings} de glitch-soc que vous utilisez ont été remplacées par les {preferences} de Mastodon et seront remplacées :", + "confirmations.missing_media_description.confirm": "Envoyer quand même", + "confirmations.missing_media_description.edit": "Modifier le média", + "confirmations.missing_media_description.message": "Au moins un média joint manque d'une description. Pensez à décrire tous les médias attachés pour les malvoyant·e·s avant de publier votre post.", + "confirmations.unfilter.author": "Auteur", + "confirmations.unfilter.confirm": "Afficher", + "confirmations.unfilter.edit_filter": "Modifier le filtre", + "confirmations.unfilter.filters": "Correspondance avec {count, plural, one {un filtre} other {plusieurs filtres}}", + "content-type.change": "Type de contenu", + "direct.group_by_conversations": "Grouper par conversation", + "endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant", + "favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois", + "firehose.column_settings.allow_local_only": "Afficher les messages locaux dans \"Tous\"", + "home.column_settings.advanced": "Avancé", + "home.column_settings.filter_regex": "Filtrer par expression régulière", + "home.column_settings.show_direct": "Afficher les MPs", + "home.settings": "Paramètres de la colonne", + "keyboard_shortcuts.bookmark": "ajouter aux marque-pages", + "keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité", + "keyboard_shortcuts.toggle_collapse": "Plier/déplier les posts", + "media_gallery.sensitive": "Sensible", + "moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.", + "navigation_bar.app_settings": "Paramètres de l'application", + "navigation_bar.featured_users": "Utilisateurs mis en avant", + "navigation_bar.keyboard_shortcuts": "Raccourcis clavier", + "navigation_bar.misc": "Autres", + "notification.markForDeletion": "Ajouter aux éléments à supprimer", + "notification_purge.btn_all": "Sélectionner\ntout", + "notification_purge.btn_apply": "Effacer\nla sélection", + "notification_purge.btn_invert": "Inverser\nla sélection", + "notification_purge.btn_none": "Annuler\nla sélection", + "notification_purge.start": "Activer le mode de nettoyage des notifications", + "notifications.marked_clear": "Effacer les notifications sélectionnées", + "notifications.marked_clear_confirmation": "Voulez-vous vraiment effacer de manière permanente toutes les notifications sélectionnées ?", + "settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu", + "settings.auto_collapse": "Repliage automatique", + "settings.auto_collapse_all": "Tout", + "settings.auto_collapse_height": "Hauteur (en pixels) pour qu'un pouet soit considéré comme long", + "settings.auto_collapse_lengthy": "Posts longs", + "settings.auto_collapse_media": "Posts avec média", + "settings.auto_collapse_notifications": "Notifications", + "settings.auto_collapse_reblogs": "Boosts", + "settings.auto_collapse_replies": "Réponses", + "settings.close": "Fermer", + "settings.collapsed_statuses": "Posts repliés", + "settings.compose_box_opts": "Zone de rédaction", + "settings.confirm_before_clearing_draft": "Afficher une fenêtre de confirmation avant d'écraser le message en cours de rédaction", + "settings.confirm_boost_missing_media_description": "Afficher une fenêtre de confirmation avant de partager des posts manquant de description des médias", + "settings.confirm_missing_media_description": "Afficher une fenêtre de confirmation avant de publier des posts manquant de description de média", + "settings.content_warnings": "Content warnings", + "settings.content_warnings.regexp": "Expression rationnelle", + "settings.content_warnings_filter": "Avertissement de contenu à ne pas automatiquement déplier :", + "settings.content_warnings_media_outside": "Afficher les médias en dehors des avertissements de contenu", + "settings.content_warnings_media_outside_hint": "Reproduit le comportement par défaut de Mastodon, les médias attachés ne sont plus affectés par le bouton d'affichage d'un post avec avertissement", + "settings.content_warnings_shared_state": "Affiche/cache le contenu de toutes les copies à la fois", + "settings.content_warnings_shared_state_hint": "Reproduit le comportement par défaut de Mastodon, le bouton d'avertissement de contenu affecte toutes les copies d'un post à la fois. Cela empêchera le repliement automatique de n'importe quelle copie d'un post avec un avertissement déplié", + "settings.content_warnings_unfold_opts": "Options de dépliement automatique", + "settings.deprecated_setting": "Cette option est maintenant définie par les {settings_page_link} de Mastodon", + "settings.enable_collapsed": "Activer le repliement des posts", + "settings.enable_collapsed_hint": "Les posts repliés ont une partie de leur contenu caché pour libérer de l'espace sur l'écran. C'est une option différente de l'avertissement de contenu", + "settings.enable_content_warnings_auto_unfold": "Déplier automatiquement les avertissements de contenu", + "settings.general": "Général", + "settings.hicolor_privacy_icons": "Indicateurs de confidentialité en couleurs", + "settings.hicolor_privacy_icons.hint": "Affiche les indicateurs de confidentialité dans des couleurs facilement distinguables", + "settings.image_backgrounds": "Images en arrière-plan", + "settings.image_backgrounds_media": "Prévisualiser les médias d'un post replié", + "settings.image_backgrounds_media_hint": "Si le post a un média attaché, utiliser le premier comme arrière-plan du post", + "settings.image_backgrounds_users": "Donner aux posts repliés une image en arrière-plan", + "settings.inline_preview_cards": "Cartes d'aperçu pour les liens externes", + "settings.layout_opts": "Mise en page", + "settings.media": "Média", + "settings.media_fullwidth": "Utiliser toute la largeur pour les aperçus", + "settings.media_letterbox": "Afficher les médias en Letterbox", + "settings.media_letterbox_hint": "Réduit le média et utilise une letterbox pour afficher l'image entière plutôt que de l'étirer et de la rogner", + "settings.media_reveal_behind_cw": "Toujours afficher les médias sensibles avec avertissement", + "settings.notifications.favicon_badge": "Badge de notifications non lues dans la favicon", + "settings.notifications.favicon_badge.hint": "Ajoute un badge dans la favicon pour alerter d'une notification non lue", + "settings.notifications.tab_badge": "Badge de notifications non lues", + "settings.notifications.tab_badge.hint": "Affiche un badge de notifications non lues dans les icônes des colonnes quand la colonne n'est pas ouverte", + "settings.notifications_opts": "Options des notifications", + "settings.pop_in_left": "Gauche", + "settings.pop_in_player": "Activer le lecteur pop-in", + "settings.pop_in_position": "Position du lecteur pop-in :", + "settings.pop_in_right": "Droite", + "settings.preferences": "Preferences", + "settings.prepend_cw_re": "Préfixer les avertissements avec \"re: \" lors d'une réponse", + "settings.preselect_on_reply": "Présélectionner les noms d’utilisateur·rices lors de la réponse", + "settings.preselect_on_reply_hint": "Présélectionner les noms d'utilisateurs après le premier lors d'une réponse à une conversation à plusieurs participants", + "settings.rewrite_mentions": "Réécrire les mentions dans les posts affichés", + "settings.rewrite_mentions_acct": "Réécrire avec le nom d'utilisateur·rice et le domaine (lorsque le compte est distant)", + "settings.rewrite_mentions_no": "Ne pas réécrire les mentions", + "settings.rewrite_mentions_username": "Réécrire avec le nom d’utilisateur·rice", + "settings.shared_settings_link": "préférences de l'utilisateur", + "settings.show_action_bar": "Afficher les boutons d'action dans les posts repliés", + "settings.show_content_type_choice": "Afficher le choix du type de contenu lors de la création des posts", + "settings.show_reply_counter": "Afficher une estimation du nombre de réponses", + "settings.side_arm": "Bouton secondaire de publication :", + "settings.side_arm.none": "Aucun", + "settings.side_arm_reply_mode": "Quand vous répondez à un post, le bouton secondaire de publication devrait :", + "settings.side_arm_reply_mode.copy": "Copier la confidentialité du post auquel vous répondez", + "settings.side_arm_reply_mode.keep": "Garder la confidentialité établie", + "settings.side_arm_reply_mode.restrict": "Restreindre la confidentialité de la réponse à celle du post auquel vous répondez", + "settings.status_icons": "Icônes des posts", + "settings.status_icons_language": "Indicateur de langue", + "settings.status_icons_local_only": "Indicateur de post local", + "settings.status_icons_media": "Indicateur de médias et sondage", + "settings.status_icons_reply": "Indicateur de réponses", + "settings.status_icons_visibility": "Indicateur de la confidentialité du post", + "settings.swipe_to_change_columns": "Glissement latéral pour changer de colonne (mobile uniquement)", + "settings.tag_misleading_links": "Étiqueter les liens trompeurs", + "settings.tag_misleading_links.hint": "Ajouter une indication visuelle avec l'hôte cible du lien à chaque lien ne le mentionnant pas explicitement", + "settings.wide_view": "Vue élargie (mode ordinateur uniquement)", + "settings.wide_view_hint": "Étire les colonnes pour mieux remplir l'espace disponible.", + "status.collapse": "Replier", + "status.has_audio": "Contient des fichiers audio attachés", + "status.has_pictures": "Contient des images attachées", + "status.has_preview_card": "Contient une carte de prévisualisation attachée", + "status.has_video": "Contient des vidéos attachées", + "status.in_reply_to": "Ce post est une réponse", + "status.is_poll": "Ce post est un sondage", + "status.local_only": "Visible uniquement depuis votre instance", + "status.sensitive_toggle": "Cliquer pour voir", + "status.uncollapse": "Déplier" +} diff --git a/app/javascript/flavours/glitch/locales/fr.json b/app/javascript/flavours/glitch/locales/fr.json index a9d0108ce4..6015f4097d 100644 --- a/app/javascript/flavours/glitch/locales/fr.json +++ b/app/javascript/flavours/glitch/locales/fr.json @@ -2,6 +2,7 @@ "about.fork_disclaimer": "Glitch-soc est un logiciel gratuit et open source, fork de Mastodon.", "account.disclaimer_full": "Les informations ci-dessous peuvent être incomplètes.", "account.follows": "Abonnements", + "account.follows_you": "Vous suit", "account.joined": "Ici depuis {date}", "account.suspended_disclaimer_full": "Cet utilisateur a été suspendu par un modérateur.", "account.view_full_profile": "Voir le profil complet", diff --git a/app/javascript/flavours/glitch/locales/ko.json b/app/javascript/flavours/glitch/locales/ko.json index 96f0b7aa97..49fcb0b46a 100644 --- a/app/javascript/flavours/glitch/locales/ko.json +++ b/app/javascript/flavours/glitch/locales/ko.json @@ -2,6 +2,7 @@ "about.fork_disclaimer": "글리치는 마스토돈에서 포크한 자유 오픈소스 소프트웨어입니다.", "account.disclaimer_full": "아래에 있는 정보들은 사용자의 프로필을 완벽하게 나타내지 못하고 있을 수도 있습니다.", "account.follows": "팔로우", + "account.follows_you": "날 팔로우합니다", "account.joined": "{date}에 가입함", "account.suspended_disclaimer_full": "이 사용자는 중재자에 의해 정지되었습니다.", "account.view_full_profile": "전체 프로필 보기", @@ -44,6 +45,7 @@ "direct.group_by_conversations": "대화별로 묶기", "endorsed_accounts_editor.endorsed_accounts": "추천하는 계정들", "favourite_modal.combo": "다음엔 {combo}를 눌러 건너뛸 수 있습니다", + "firehose.column_settings.allow_local_only": "\"모두\" 탭에서 로컬 전용 글 보여주기", "home.column_settings.advanced": "고급", "home.column_settings.filter_regex": "정규표현식으로 필터", "home.column_settings.show_direct": "DM 보여주기", diff --git a/app/javascript/flavours/glitch/locales/vi.json b/app/javascript/flavours/glitch/locales/vi.json index d360fed722..0967ef424b 100644 --- a/app/javascript/flavours/glitch/locales/vi.json +++ b/app/javascript/flavours/glitch/locales/vi.json @@ -1,4 +1 @@ -{ - "settings.content_warnings": "Content warnings", - "settings.preferences": "Preferences" -} +{} diff --git a/app/javascript/flavours/glitch/locales/zh-CN.json b/app/javascript/flavours/glitch/locales/zh-CN.json index 5a620c9346..742624108c 100644 --- a/app/javascript/flavours/glitch/locales/zh-CN.json +++ b/app/javascript/flavours/glitch/locales/zh-CN.json @@ -1,7 +1,8 @@ { - "about.fork_disclaimer": "Glitch-soc是从Mastodon派生的自由开源软件。", - "account.disclaimer_full": "以下信息可能无法完整代表你的个人资料。", + "about.fork_disclaimer": "Glitch-soc是从Mastodon生成的免费开源软件。", + "account.disclaimer_full": "下面的信息可能不完全反映用户的个人资料。", "account.follows": "正在关注", + "account.follows_you": "关注了你", "account.joined": "加入于 {date}", "account.suspended_disclaimer_full": "该用户已被管理员封禁。", "account.view_full_profile": "查看完整资料", diff --git a/app/javascript/flavours/glitch/locales/zh-TW.json b/app/javascript/flavours/glitch/locales/zh-TW.json index fcd4f4a3a6..414bd44f78 100644 --- a/app/javascript/flavours/glitch/locales/zh-TW.json +++ b/app/javascript/flavours/glitch/locales/zh-TW.json @@ -2,6 +2,7 @@ "about.fork_disclaimer": "Glitch-soc 是從 Mastodon 分支出來的自由開源軟體。", "account.disclaimer_full": "下面的資訊可能不完全反映使用者的個人資料。", "account.follows": "跟隨", + "account.follows_you": "跟隨了您", "account.joined": "加入於 {date}", "account.suspended_disclaimer_full": "使用者已被管理者停權。", "account.view_full_profile": "查看完整個人資料", diff --git a/config/locales-glitch/fr-CA.yml b/config/locales-glitch/fr-CA.yml new file mode 100644 index 0000000000..2fbf0ffd71 --- /dev/null +++ b/config/locales-glitch/fr-CA.yml @@ -0,0 +1 @@ +--- {} diff --git a/config/locales-glitch/simple_form.fr-CA.yml b/config/locales-glitch/simple_form.fr-CA.yml new file mode 100644 index 0000000000..2fbf0ffd71 --- /dev/null +++ b/config/locales-glitch/simple_form.fr-CA.yml @@ -0,0 +1 @@ +--- {} From e5f50478b517444d46776c5f5f81042c80fdf9b7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 24 Jan 2024 11:49:19 +0100 Subject: [PATCH 79/84] [Glitch] Add confirmation when redirecting logged-out requests to permalink Port SCSS changes from b19ae521b7d28a76e8e1d8da8157e051e9d8de6c to glitch-soc Co-authored-by: Claire Signed-off-by: Claire --- .../flavours/glitch/styles/containers.scss | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index 4d3d4c546c..6d72e43924 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -107,3 +107,59 @@ margin-inline-start: 10px; } } + +.redirect { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + font-size: 14px; + line-height: 18px; + + &__logo { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 30px; + + img { + height: 48px; + } + } + + &__message { + text-align: center; + + h1 { + font-size: 17px; + line-height: 22px; + font-weight: 700; + margin-bottom: 30px; + } + + p { + margin-bottom: 30px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: $highlight-text-color; + font-weight: 500; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } + + &__link { + margin-top: 15px; + } +} From 54ece5040d50c155c4ebe4f732caf3b29546ad7f Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Jan 2024 13:37:43 +0100 Subject: [PATCH 80/84] [Glitch] Use active variants for boost icons and increase icon size Port 5a838ceaa9a003bc2e2fdee727d4aa87cd53de4f to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/components/status_action_bar.jsx | 4 +++- .../flavours/glitch/features/status/components/action_bar.jsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index 5727f36c86..c10122d234 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -17,8 +17,10 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -303,7 +305,7 @@ class StatusActionBar extends ImmutablePureComponent { if (status.get('reblogged')) { reblogTitle = intl.formatMessage(messages.cancel_reblog_private); - reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon; + reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon; } else if (publicStatus) { reblogTitle = intl.formatMessage(messages.reblog); reblogIconComponent = RepeatIcon; diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx index 9edd47f5b0..1d8707ccbf 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -16,8 +16,10 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -235,7 +237,7 @@ class ActionBar extends PureComponent { if (status.get('reblogged')) { reblogTitle = intl.formatMessage(messages.cancel_reblog_private); - reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon; + reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon; } else if (publicStatus) { reblogTitle = intl.formatMessage(messages.reblog); reblogIconComponent = RepeatIcon; From dd7a66949aca9e2d8b793664f2f19d3681e9178f Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 26 Jan 2024 21:04:02 +0100 Subject: [PATCH 81/84] Fix CSS loading in redirect controller --- app/controllers/redirect/base_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/redirect/base_controller.rb b/app/controllers/redirect/base_controller.rb index 90894ec1ed..ce55cfc53a 100644 --- a/app/controllers/redirect/base_controller.rb +++ b/app/controllers/redirect/base_controller.rb @@ -3,6 +3,7 @@ class Redirect::BaseController < ApplicationController vary_by 'Accept-Language' + before_action :set_pack before_action :set_resource before_action :set_app_body_class @@ -21,4 +22,8 @@ class Redirect::BaseController < ApplicationController def set_resource raise NotImplementedError end + + def set_pack + use_pack 'public' + end end From 80308d384a9f914c6500961f3e0fa5b4444fd30d Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 28 Jan 2024 14:53:08 +0100 Subject: [PATCH 82/84] [Glitch] Refactor conversations components in web UI (#2589) Port 3205a654caf903002c2db872f802a3332201678b to glitch-soc Signed-off-by: Claire Co-authored-by: Eugen Rochko --- .../components/conversation.jsx | 305 ++++++++++-------- .../components/conversations_list.jsx | 117 ++++--- .../containers/conversation_container.js | 81 ----- .../conversations_list_container.js | 16 - .../glitch/features/direct_timeline/index.jsx | 174 ++++------ 5 files changed, 288 insertions(+), 405 deletions(-) delete mode 100644 app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js delete mode 100644 app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx index 00fbc8d464..6ccd1497a7 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx @@ -1,17 +1,24 @@ import PropTypes from 'prop-types'; +import { useCallback, useState } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { withRouter } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; +import { createSelector } from '@reduxjs/toolkit'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useDispatch, useSelector } from 'react-redux'; + import { HotKeys } from 'react-hotkeys'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; +import { replyCompose } from 'flavours/glitch/actions/compose'; +import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'flavours/glitch/actions/statuses'; import AttachmentList from 'flavours/glitch/components/attachment_list'; import AvatarComposite from 'flavours/glitch/components/avatar_composite'; import { IconButton } from 'flavours/glitch/components/icon_button'; @@ -20,7 +27,7 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp import StatusContent from 'flavours/glitch/components/status_content'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { autoPlayGif } from 'flavours/glitch/initial_state'; -import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; +import { makeGetStatus } from 'flavours/glitch/selectors'; const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, @@ -30,45 +37,48 @@ const messages = defineMessages({ delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, }); -class Conversation extends ImmutablePureComponent { +const getAccounts = createSelector( + (state) => state.get('accounts'), + (_, accountIds) => accountIds, + (accounts, accountIds) => + accountIds.map(id => accounts.get(id)) +); - static propTypes = { - conversationId: PropTypes.string.isRequired, - accounts: ImmutablePropTypes.list.isRequired, - lastStatus: ImmutablePropTypes.map, - unread:PropTypes.bool.isRequired, - scrollKey: PropTypes.string, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - markRead: PropTypes.func.isRequired, - delete: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - ...WithRouterPropTypes, - }; +const getStatus = makeGetStatus(); - state = { - isExpanded: undefined, - }; +export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) => { + const id = conversation.get('id'); + const unread = conversation.get('unread'); + const lastStatusId = conversation.get('last_status'); + const accountIds = conversation.get('accounts'); + const intl = useIntl(); + const dispatch = useDispatch(); + const history = useHistory(); + const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId })); + const accounts = useSelector(state => getAccounts(state, accountIds)); - parseClick = (e, destination) => { - const { history, lastStatus, unread, markRead } = this.props; - if (!history) return; + // glitch-soc additions + const sharedCWState = useSelector(state => state.getIn(['state', 'content_warnings', 'shared_state'])); + const [expanded, setExpanded] = useState(undefined); + const parseClick = useCallback((e, destination) => { if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) { if (destination === undefined) { if (unread) { - markRead(); + dispatch(markConversationRead(id)); } destination = `/statuses/${lastStatus.get('id')}`; } history.push(destination); e.preventDefault(); } - }; + }, [dispatch, history, unread, id, lastStatus]); - handleMouseEnter = ({ currentTarget }) => { + const handleMouseEnter = useCallback(({ currentTarget }) => { if (autoPlayGif) { return; } @@ -79,9 +89,9 @@ class Conversation extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-original'); } - }; + }, []); - handleMouseLeave = ({ currentTarget }) => { + const handleMouseLeave = useCallback(({ currentTarget }) => { if (autoPlayGif) { return; } @@ -92,145 +102,160 @@ class Conversation extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-static'); } - }; - - handleClick = () => { - if (!this.props.history) { - return; - } - - const { lastStatus, unread, markRead } = this.props; + }, []); + const handleClick = useCallback(() => { if (unread) { - markRead(); + dispatch(markConversationRead(id)); } - this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); - }; + history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); + }, [dispatch, history, unread, id, lastStatus]); - handleMarkAsRead = () => { - this.props.markRead(); - }; + const handleMarkAsRead = useCallback(() => { + dispatch(markConversationRead(id)); + }, [dispatch, id]); - handleReply = () => { - this.props.reply(this.props.lastStatus, this.props.history); - }; + const handleReply = useCallback(() => { + dispatch((_, getState) => { + let state = getState(); - handleDelete = () => { - this.props.delete(); - }; + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(lastStatus, history)), + }, + })); + } else { + dispatch(replyCompose(lastStatus, history)); + } + }); + }, [dispatch, lastStatus, history, intl]); - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.conversationId); - }; + const handleDelete = useCallback(() => { + dispatch(deleteConversation(id)); + }, [dispatch, id]); - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.conversationId); - }; + const handleHotkeyMoveUp = useCallback(() => { + onMoveUp(id); + }, [id, onMoveUp]); - handleConversationMute = () => { - this.props.onMute(this.props.lastStatus); - }; + const handleHotkeyMoveDown = useCallback(() => { + onMoveDown(id); + }, [id, onMoveDown]); - handleShowMore = () => { - this.props.onToggleHidden(this.props.lastStatus); - - if (this.props.lastStatus.get('spoiler_text')) { - this.setExpansion(!this.state.isExpanded); + const handleConversationMute = useCallback(() => { + if (lastStatus.get('muted')) { + dispatch(unmuteStatus(lastStatus.get('id'))); + } else { + dispatch(muteStatus(lastStatus.get('id'))); } - }; + }, [dispatch, lastStatus]); - setExpansion = value => { - this.setState({ isExpanded: value }); - }; - - render () { - const { accounts, lastStatus, unread, scrollKey, intl } = this.props; - - if (lastStatus === null) { - return null; + const handleShowMore = useCallback(() => { + if (lastStatus.get('hidden')) { + dispatch(revealStatus(lastStatus.get('id'))); + } else { + dispatch(hideStatus(lastStatus.get('id'))); } - const isExpanded = this.props.settings.getIn(['content_warnings', 'shared_state']) ? !lastStatus.get('hidden') : this.state.isExpanded; - - const menu = [ - { text: intl.formatMessage(messages.open), action: this.handleClick }, - null, - ]; - - menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute }); - - if (unread) { - menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead }); - menu.push(null); + if (lastStatus.get('spoiler_text')) { + setExpanded(!expanded); } + }, [dispatch, lastStatus, expanded]); - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); + const menu = [ + { text: intl.formatMessage(messages.open), action: handleClick }, + null, + { text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: handleConversationMute }, + ]; - const names = accounts.map(a => ).reduce((prev, cur) => [prev, ', ', cur]); + if (unread) { + menu.push({ text: intl.formatMessage(messages.markAsRead), action: handleMarkAsRead }); + menu.push(null); + } - const handlers = { - reply: this.handleReply, - open: this.handleClick, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleHidden: this.handleShowMore, - }; + menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete }); - let media = null; - if (lastStatus.get('media_attachments').size > 0) { - media = ; - } + const names = accounts.map(a => ( + + + + + + )).reduce((prev, cur) => [prev, ', ', cur]); - return ( - -
-
- -
+ const handlers = { + reply: handleReply, + open: handleClick, + moveUp: handleHotkeyMoveUp, + moveDown: handleHotkeyMoveDown, + toggleHidden: handleShowMore, + }; -
-
-
- {unread && } -
+ let media = null; + if (lastStatus.get('media_attachments').size > 0) { + media = ; + } -
- {names} }} /> -
+ return ( + +
+
+ +
+ +
+
+
+ {unread && }
- +
+ {names} }} /> +
+
-
- + -
- -
+
+ + +
+
- - ); - } +
+ + ); +}; -} - -export default withRouter(injectIntl(Conversation)); +Conversation.propTypes = { + conversation: ImmutablePropTypes.map.isRequired, + scrollKey: PropTypes.string, + onMoveUp: PropTypes.func, + onMoveDown: PropTypes.func, +}; diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx index 8c12ea9e5f..b1a8fd09b6 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx @@ -1,77 +1,72 @@ import PropTypes from 'prop-types'; +import { useRef, useMemo, useCallback } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useSelector, useDispatch } from 'react-redux'; import { debounce } from 'lodash'; -import ScrollableList from '../../../components/scrollable_list'; -import ConversationContainer from '../containers/conversation_container'; +import { expandConversations } from 'flavours/glitch/actions/conversations'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; -export default class ConversationsList extends ImmutablePureComponent { +import { Conversation } from './conversation'; - static propTypes = { - conversations: ImmutablePropTypes.list.isRequired, - scrollKey: PropTypes.string.isRequired, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - onLoadMore: PropTypes.func, - }; +const focusChild = (node, index, alignTop) => { + const element = node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id); - - handleMoveUp = id => { - const elementIndex = this.getCurrentIndex(id) - 1; - this._selectChild(elementIndex, true); - }; - - handleMoveDown = id => { - const elementIndex = this.getCurrentIndex(id) + 1; - this._selectChild(elementIndex, false); - }; - - _selectChild (index, align_top) { - const container = this.node.node; - const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - if (align_top && container.scrollTop > element.offsetTop) { - element.scrollIntoView(true); - } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { - element.scrollIntoView(false); - } - element.focus(); + if (element) { + if (alignTop && node.scrollTop > element.offsetTop) { + element.scrollIntoView(true); + } else if (!alignTop && node.scrollTop + node.clientHeight < element.offsetTop + element.offsetHeight) { + element.scrollIntoView(false); } + + element.focus(); } +}; - setRef = c => { - this.node = c; - }; +export const ConversationsList = ({ scrollKey, ...other }) => { + const listRef = useRef(); + const conversations = useSelector(state => state.getIn(['conversations', 'items'])); + const isLoading = useSelector(state => state.getIn(['conversations', 'isLoading'], true)); + const hasMore = useSelector(state => state.getIn(['conversations', 'hasMore'], false)); + const dispatch = useDispatch(); + const lastStatusId = conversations.last()?.get('last_status'); - handleLoadOlder = debounce(() => { - const last = this.props.conversations.last(); + const handleMoveUp = useCallback(id => { + const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1; + focusChild(listRef.current.node, elementIndex, true); + }, [listRef, conversations]); - if (last && last.get('last_status')) { - this.props.onLoadMore(last.get('last_status')); + const handleMoveDown = useCallback(id => { + const elementIndex = conversations.findIndex(x => x.get('id') === id) + 1; + focusChild(listRef.current.node, elementIndex, false); + }, [listRef, conversations]); + + const debouncedLoadMore = useMemo(() => debounce(id => { + dispatch(expandConversations({ maxId: id })); + }, 300, { leading: true }), [dispatch]); + + const handleLoadMore = useCallback(() => { + if (lastStatusId) { + debouncedLoadMore(lastStatusId); } - }, 300, { leading: true }); + }, [debouncedLoadMore, lastStatusId]); - render () { - const { conversations, isLoading, onLoadMore, ...other } = this.props; + return ( + + {conversations.map(item => ( + + ))} + + ); +}; - return ( - - {conversations.map(item => ( - - ))} - - ); - } - -} +ConversationsList.propTypes = { + scrollKey: PropTypes.string.isRequired, +}; diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js deleted file mode 100644 index 207d3ebb65..0000000000 --- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js +++ /dev/null @@ -1,81 +0,0 @@ -import { defineMessages, injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { replyCompose } from 'flavours/glitch/actions/compose'; -import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations'; -import { openModal } from 'flavours/glitch/actions/modal'; -import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'flavours/glitch/actions/statuses'; -import { makeGetStatus } from 'flavours/glitch/selectors'; - -import Conversation from '../components/conversation'; - -const messages = defineMessages({ - replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, - replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, -}); - -const mapStateToProps = () => { - const getStatus = makeGetStatus(); - - return (state, { conversationId }) => { - const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); - const lastStatusId = conversation.get('last_status', null); - - return { - accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), - unread: conversation.get('unread'), - lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }), - settings: state.get('local_settings'), - }; - }; -}; - -const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({ - - markRead () { - dispatch(markConversationRead(conversationId)); - }, - - reply (status, router) { - dispatch((_, getState) => { - let state = getState(); - - if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, router)), - }, - })); - } else { - dispatch(replyCompose(status, router)); - } - }); - }, - - delete () { - dispatch(deleteConversation(conversationId)); - }, - - onMute (status) { - if (status.get('muted')) { - dispatch(unmuteStatus(status.get('id'))); - } else { - dispatch(muteStatus(status.get('id'))); - } - }, - - onToggleHidden (status) { - if (status.get('hidden')) { - dispatch(revealStatus(status.get('id'))); - } else { - dispatch(hideStatus(status.get('id'))); - } - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation)); diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js deleted file mode 100644 index 1dcd3ec1bd..0000000000 --- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; - -import { expandConversations } from '../../../actions/conversations'; -import ConversationsList from '../components/conversations_list'; - -const mapStateToProps = state => ({ - conversations: state.getIn(['conversations', 'items']), - isLoading: state.getIn(['conversations', 'isLoading'], true), - hasMore: state.getIn(['conversations', 'hasMore'], false), -}); - -const mapDispatchToProps = dispatch => ({ - onLoadMore: maxId => dispatch(expandConversations({ maxId })), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList); diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.jsx b/app/javascript/flavours/glitch/features/direct_timeline/index.jsx index 9de5751ffb..25f0dd9997 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/index.jsx @@ -1,12 +1,11 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useRef, useCallback, useEffect } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Helmet } from 'react-helmet'; -import { connect } from 'react-redux'; - +import { useDispatch, useSelector } from 'react-redux'; import MailIcon from '@/material-icons/400-24px/mail.svg?react'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; @@ -17,51 +16,44 @@ import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { ConversationsList } from './components/conversations_list'; import ColumnSettingsContainer from './containers/column_settings_container'; -import ConversationsListContainer from './containers/conversations_list_container'; const messages = defineMessages({ title: { id: 'column.direct', defaultMessage: 'Private mentions' }, }); -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0, - conversationsMode: state.getIn(['settings', 'direct', 'conversations']), -}); +const DirectTimeline = ({ columnId, multiColumn }) => { + const columnRef = useRef(); + const intl = useIntl(); + const dispatch = useDispatch(); + const pinned = !!columnId; -class DirectTimeline extends PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columnId: PropTypes.string, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - conversationsMode: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; + // glitch-soc additions + const hasUnread = useSelector(state => state.getIn(['timelines', 'direct', 'unread']) > 0); + const conversationsMode = useSelector(state => state.getIn(['settings', 'direct', 'conversations'])); + const handlePin = useCallback(() => { if (columnId) { dispatch(removeColumn(columnId)); } else { dispatch(addColumn('DIRECT', {})); } - }; + }, [dispatch, columnId]); - handleMove = (dir) => { - const { columnId, dispatch } = this.props; + const handleMove = useCallback((dir) => { dispatch(moveColumn(columnId, dir)); - }; + }, [dispatch, columnId]); - handleHeaderClick = () => { - this.column.scrollTop(); - }; + const handleHeaderClick = useCallback(() => { + columnRef.current.scrollTop(); + }, [columnRef]); - componentDidMount () { - const { dispatch, conversationsMode } = this.props; + const handleLoadMoreTimeline = useCallback(maxId => { + dispatch(expandDirectTimeline({ maxId })); + }, [dispatch]); + useEffect(() => { dispatch(mountConversations()); if (conversationsMode) { @@ -70,99 +62,67 @@ class DirectTimeline extends PureComponent { dispatch(expandDirectTimeline()); } - this.disconnect = dispatch(connectDirectStream()); - } + const disconnect = dispatch(connectDirectStream()); - componentDidUpdate(prevProps) { - const { dispatch, conversationsMode } = this.props; + return () => { + dispatch(unmountConversations()); + disconnect(); + }; + }, [dispatch, conversationsMode]); - if (prevProps.conversationsMode && !conversationsMode) { - dispatch(expandDirectTimeline()); - } else if (!prevProps.conversationsMode && conversationsMode) { - dispatch(expandConversations()); - } - } + return ( + + + + - componentWillUnmount () { - this.props.dispatch(unmountConversations()); - - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - }; - - handleLoadMoreTimeline = maxId => { - this.props.dispatch(expandDirectTimeline({ maxId })); - }; - - handleLoadMoreConversations = maxId => { - this.props.dispatch(expandConversations({ maxId })); - }; - - render () { - const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props; - const pinned = !!columnId; - - let contents; - if (conversationsMode) { - contents = ( - } bindToDocument={!multiColumn} - onLoadMore={this.handleLoadMore} prepend={
} alwaysPrepend - emptyMessage={} /> - ); - } else { - contents = ( + ) : (
} + onLoadMore={handleLoadMoreTimeline} + prepend={ +
+ +
+ } alwaysPrepend emptyMessage={} /> - ); - } + )} - return ( - - - - + + {intl.formatMessage(messages.title)} + + + + ); +}; - {contents} +DirectTimeline.propTypes = { + columnId: PropTypes.string, + multiColumn: PropTypes.bool, +}; - - {intl.formatMessage(messages.title)} - - - - ); - } - -} - -export default connect(mapStateToProps)(injectIntl(DirectTimeline)); +export default DirectTimeline; From 3ede233146426a95d12349e2e7dd74915a83d99e Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 30 Jan 2024 22:42:22 +0100 Subject: [PATCH 83/84] Fix crash in private mention conversations in glitch-soc flavor (#2595) --- .../features/direct_timeline/components/conversation.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx index 6ccd1497a7..458a547d02 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx @@ -167,6 +167,10 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) } }, [dispatch, lastStatus, expanded]); + if (!lastStatus) { + return null; + } + const menu = [ { text: intl.formatMessage(messages.open), action: handleClick }, null, From 8b87673f5edeaced6b5e4dad29557ff53b11aa85 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 30 Jan 2024 23:21:35 +0100 Subject: [PATCH 84/84] Remove obsolete locale file (#2596) --- .../flavours/glitch/locales/fr-QC.json | 158 ------------------ 1 file changed, 158 deletions(-) delete mode 100644 app/javascript/flavours/glitch/locales/fr-QC.json diff --git a/app/javascript/flavours/glitch/locales/fr-QC.json b/app/javascript/flavours/glitch/locales/fr-QC.json deleted file mode 100644 index a9d0108ce4..0000000000 --- a/app/javascript/flavours/glitch/locales/fr-QC.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "about.fork_disclaimer": "Glitch-soc est un logiciel gratuit et open source, fork de Mastodon.", - "account.disclaimer_full": "Les informations ci-dessous peuvent être incomplètes.", - "account.follows": "Abonnements", - "account.joined": "Ici depuis {date}", - "account.suspended_disclaimer_full": "Cet utilisateur a été suspendu par un modérateur.", - "account.view_full_profile": "Voir le profil complet", - "advanced_options.icon_title": "Options avancées", - "advanced_options.local-only.long": "Ne pas envoyer aux autres instances", - "advanced_options.local-only.short": "Uniquement en local", - "advanced_options.local-only.tooltip": "Ce post est uniquement local", - "advanced_options.threaded_mode.long": "Ouvre automatiquement une réponse lors de la publication", - "advanced_options.threaded_mode.short": "Mode thread", - "advanced_options.threaded_mode.tooltip": "Mode thread activé", - "boost_modal.missing_description": "Ce post contient des médias sans description", - "column.favourited_by": "Ajouté en favori par", - "column.heading": "Divers", - "column.reblogged_by": "Partagé par", - "column.subheading": "Autres options", - "column_header.profile": "Profil", - "column_subheading.lists": "Listes", - "column_subheading.navigation": "Navigation", - "community.column_settings.allow_local_only": "Afficher seulement les posts locaux", - "compose.attach": "Joindre…", - "compose.attach.doodle": "Dessiner quelque chose", - "compose.attach.upload": "Téléverser un fichier", - "compose.content-type.html": "HTML", - "compose.content-type.markdown": "Markdown", - "compose.content-type.plain": "Text brut", - "compose_form.poll.multiple_choices": "Choix multiples", - "compose_form.poll.single_choice": "Choix unique", - "compose_form.spoiler": "Cacher le texte derrière un avertissement", - "confirmation_modal.do_not_ask_again": "Ne plus demander confirmation", - "confirmations.deprecated_settings.confirm": "Utiliser les préférences de Mastodon", - "confirmations.deprecated_settings.message": "Certaines {app_settings} de glitch-soc que vous utilisez ont été remplacées par les {preferences} de Mastodon et seront remplacées :", - "confirmations.missing_media_description.confirm": "Envoyer quand même", - "confirmations.missing_media_description.edit": "Modifier le média", - "confirmations.missing_media_description.message": "Au moins un média joint manque d'une description. Pensez à décrire tous les médias attachés pour les malvoyant·e·s avant de publier votre post.", - "confirmations.unfilter.author": "Auteur", - "confirmations.unfilter.confirm": "Afficher", - "confirmations.unfilter.edit_filter": "Modifier le filtre", - "confirmations.unfilter.filters": "Correspondance avec {count, plural, one {un filtre} other {plusieurs filtres}}", - "content-type.change": "Type de contenu", - "direct.group_by_conversations": "Grouper par conversation", - "endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant", - "favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois", - "firehose.column_settings.allow_local_only": "Afficher les messages locaux dans \"Tous\"", - "home.column_settings.advanced": "Avancé", - "home.column_settings.filter_regex": "Filtrer par expression régulière", - "home.column_settings.show_direct": "Afficher les MPs", - "home.settings": "Paramètres de la colonne", - "keyboard_shortcuts.bookmark": "ajouter aux marque-pages", - "keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité", - "keyboard_shortcuts.toggle_collapse": "Plier/déplier les posts", - "media_gallery.sensitive": "Sensible", - "moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.", - "navigation_bar.app_settings": "Paramètres de l'application", - "navigation_bar.featured_users": "Utilisateurs mis en avant", - "navigation_bar.keyboard_shortcuts": "Raccourcis clavier", - "navigation_bar.misc": "Autres", - "notification.markForDeletion": "Ajouter aux éléments à supprimer", - "notification_purge.btn_all": "Sélectionner\ntout", - "notification_purge.btn_apply": "Effacer\nla sélection", - "notification_purge.btn_invert": "Inverser\nla sélection", - "notification_purge.btn_none": "Annuler\nla sélection", - "notification_purge.start": "Activer le mode de nettoyage des notifications", - "notifications.marked_clear": "Effacer les notifications sélectionnées", - "notifications.marked_clear_confirmation": "Voulez-vous vraiment effacer de manière permanente toutes les notifications sélectionnées ?", - "settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu", - "settings.auto_collapse": "Repliage automatique", - "settings.auto_collapse_all": "Tout", - "settings.auto_collapse_height": "Hauteur (en pixels) pour qu'un pouet soit considéré comme long", - "settings.auto_collapse_lengthy": "Posts longs", - "settings.auto_collapse_media": "Posts avec média", - "settings.auto_collapse_notifications": "Notifications", - "settings.auto_collapse_reblogs": "Boosts", - "settings.auto_collapse_replies": "Réponses", - "settings.close": "Fermer", - "settings.collapsed_statuses": "Posts repliés", - "settings.compose_box_opts": "Zone de rédaction", - "settings.confirm_before_clearing_draft": "Afficher une fenêtre de confirmation avant d'écraser le message en cours de rédaction", - "settings.confirm_boost_missing_media_description": "Afficher une fenêtre de confirmation avant de partager des posts manquant de description des médias", - "settings.confirm_missing_media_description": "Afficher une fenêtre de confirmation avant de publier des posts manquant de description de média", - "settings.content_warnings": "Content warnings", - "settings.content_warnings.regexp": "Expression rationnelle", - "settings.content_warnings_filter": "Avertissement de contenu à ne pas automatiquement déplier :", - "settings.content_warnings_media_outside": "Afficher les médias en dehors des avertissements de contenu", - "settings.content_warnings_media_outside_hint": "Reproduit le comportement par défaut de Mastodon, les médias attachés ne sont plus affectés par le bouton d'affichage d'un post avec avertissement", - "settings.content_warnings_shared_state": "Affiche/cache le contenu de toutes les copies à la fois", - "settings.content_warnings_shared_state_hint": "Reproduit le comportement par défaut de Mastodon, le bouton d'avertissement de contenu affecte toutes les copies d'un post à la fois. Cela empêchera le repliement automatique de n'importe quelle copie d'un post avec un avertissement déplié", - "settings.content_warnings_unfold_opts": "Options de dépliement automatique", - "settings.deprecated_setting": "Cette option est maintenant définie par les {settings_page_link} de Mastodon", - "settings.enable_collapsed": "Activer le repliement des posts", - "settings.enable_collapsed_hint": "Les posts repliés ont une partie de leur contenu caché pour libérer de l'espace sur l'écran. C'est une option différente de l'avertissement de contenu", - "settings.enable_content_warnings_auto_unfold": "Déplier automatiquement les avertissements de contenu", - "settings.general": "Général", - "settings.hicolor_privacy_icons": "Indicateurs de confidentialité en couleurs", - "settings.hicolor_privacy_icons.hint": "Affiche les indicateurs de confidentialité dans des couleurs facilement distinguables", - "settings.image_backgrounds": "Images en arrière-plan", - "settings.image_backgrounds_media": "Prévisualiser les médias d'un post replié", - "settings.image_backgrounds_media_hint": "Si le post a un média attaché, utiliser le premier comme arrière-plan du post", - "settings.image_backgrounds_users": "Donner aux posts repliés une image en arrière-plan", - "settings.inline_preview_cards": "Cartes d'aperçu pour les liens externes", - "settings.layout_opts": "Mise en page", - "settings.media": "Média", - "settings.media_fullwidth": "Utiliser toute la largeur pour les aperçus", - "settings.media_letterbox": "Afficher les médias en Letterbox", - "settings.media_letterbox_hint": "Réduit le média et utilise une letterbox pour afficher l'image entière plutôt que de l'étirer et de la rogner", - "settings.media_reveal_behind_cw": "Toujours afficher les médias sensibles avec avertissement", - "settings.notifications.favicon_badge": "Badge de notifications non lues dans la favicon", - "settings.notifications.favicon_badge.hint": "Ajoute un badge dans la favicon pour alerter d'une notification non lue", - "settings.notifications.tab_badge": "Badge de notifications non lues", - "settings.notifications.tab_badge.hint": "Affiche un badge de notifications non lues dans les icônes des colonnes quand la colonne n'est pas ouverte", - "settings.notifications_opts": "Options des notifications", - "settings.pop_in_left": "Gauche", - "settings.pop_in_player": "Activer le lecteur pop-in", - "settings.pop_in_position": "Position du lecteur pop-in :", - "settings.pop_in_right": "Droite", - "settings.preferences": "Preferences", - "settings.prepend_cw_re": "Préfixer les avertissements avec \"re: \" lors d'une réponse", - "settings.preselect_on_reply": "Présélectionner les noms d’utilisateur·rices lors de la réponse", - "settings.preselect_on_reply_hint": "Présélectionner les noms d'utilisateurs après le premier lors d'une réponse à une conversation à plusieurs participants", - "settings.rewrite_mentions": "Réécrire les mentions dans les posts affichés", - "settings.rewrite_mentions_acct": "Réécrire avec le nom d'utilisateur·rice et le domaine (lorsque le compte est distant)", - "settings.rewrite_mentions_no": "Ne pas réécrire les mentions", - "settings.rewrite_mentions_username": "Réécrire avec le nom d’utilisateur·rice", - "settings.shared_settings_link": "préférences de l'utilisateur", - "settings.show_action_bar": "Afficher les boutons d'action dans les posts repliés", - "settings.show_content_type_choice": "Afficher le choix du type de contenu lors de la création des posts", - "settings.show_reply_counter": "Afficher une estimation du nombre de réponses", - "settings.side_arm": "Bouton secondaire de publication :", - "settings.side_arm.none": "Aucun", - "settings.side_arm_reply_mode": "Quand vous répondez à un post, le bouton secondaire de publication devrait :", - "settings.side_arm_reply_mode.copy": "Copier la confidentialité du post auquel vous répondez", - "settings.side_arm_reply_mode.keep": "Garder la confidentialité établie", - "settings.side_arm_reply_mode.restrict": "Restreindre la confidentialité de la réponse à celle du post auquel vous répondez", - "settings.status_icons": "Icônes des posts", - "settings.status_icons_language": "Indicateur de langue", - "settings.status_icons_local_only": "Indicateur de post local", - "settings.status_icons_media": "Indicateur de médias et sondage", - "settings.status_icons_reply": "Indicateur de réponses", - "settings.status_icons_visibility": "Indicateur de la confidentialité du post", - "settings.swipe_to_change_columns": "Glissement latéral pour changer de colonne (mobile uniquement)", - "settings.tag_misleading_links": "Étiqueter les liens trompeurs", - "settings.tag_misleading_links.hint": "Ajouter une indication visuelle avec l'hôte cible du lien à chaque lien ne le mentionnant pas explicitement", - "settings.wide_view": "Vue élargie (mode ordinateur uniquement)", - "settings.wide_view_hint": "Étire les colonnes pour mieux remplir l'espace disponible.", - "status.collapse": "Replier", - "status.has_audio": "Contient des fichiers audio attachés", - "status.has_pictures": "Contient des images attachées", - "status.has_preview_card": "Contient une carte de prévisualisation attachée", - "status.has_video": "Contient des vidéos attachées", - "status.in_reply_to": "Ce post est une réponse", - "status.is_poll": "Ce post est un sondage", - "status.local_only": "Visible uniquement depuis votre instance", - "status.sensitive_toggle": "Cliquer pour voir", - "status.uncollapse": "Déplier" -}