Commit 6846ccf6 authored by Angelina Filippova's avatar Angelina Filippova

Add forms for configuring server-settings

parent 031ae4ce
......@@ -141,7 +141,8 @@ module.exports = {
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
'args': 'none',
'ignoreRestSiblings': true
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
......
export const initialSettings = [
{
group: 'pleroma',
key: ':instance',
value: [
{ 'tuple': [':name', 'Pleroma'] },
{ 'tuple': [':email', 'example@example.com'] },
{ 'tuple': [':notify_email', 'noreply@example.com'] },
{ 'tuple': [':description', 'A Pleroma instance, an alternative fediverse server'] },
{ 'tuple': [':limit', 5000] },
{ 'tuple': [':remote_limit', 100000] },
{ 'tuple': [':upload_limit', 16 * 1048576] },
{ 'tuple': [':avatar_upload_limit', 2 * 1048576] },
{ 'tuple': [':background_upload_limit', 4 * 1048576] },
{ 'tuple': [':banner_upload_limit', 4 * 1048576] },
{ 'tuple': [':poll_limits', [
{ 'tuple': [':max_options', 20] },
{ 'tuple': [':max_option_chars', 200] },
{ 'tuple': [':min_expiration', 0] },
{ 'tuple': [':max_expiration', 365 * 86400] }
]] },
{ 'tuple': [':registrations_open', true] },
{ 'tuple': [':invites_enabled', false] },
{ 'tuple': [':account_activation_required', false] },
{ 'tuple': [':federating', true] },
{ 'tuple': [':federation_reachability_timeout_days', 7] },
{ 'tuple':
[':federation_publisher_modules', ['Pleroma.Web.ActivityPub.Publisher', 'Pleroma.Web.Websub', 'Pleroma.Web.Salmon']] },
{ 'tuple': [':allow_relay', true] },
{ 'tuple': [':rewrite_policy', 'Pleroma.Web.ActivityPub.MRF.NoOpPolicy'] },
{ 'tuple': [':public', true] },
{ 'tuple': [':managed_config', true] },
{ 'tuple': [':static_dir', 'instance/static/'] },
{ 'tuple': [':allowed_post_formats', ['text/plain', 'text/html', 'text/markdown', 'text/bbcode']] },
{ 'tuple': [':mrf_transparency', true] },
{ 'tuple': [':extended_nickname_format', false] },
{ 'tuple': [':max_pinned_statuses', 1] },
{ 'tuple': [':no_attachment_links', false] },
{ 'tuple': [':max_report_comment_size', 1000] },
{ 'tuple': [':safe_dm_mentions', false] },
{ 'tuple': [':healthcheck', false] },
{ 'tuple': [':remote_post_retention_days', 90] },
{ 'tuple': [':skip_thread_containment', true] },
{ 'tuple': [':limit_to_local_content', ':unauthenticated'] },
{ 'tuple': [':dynamic_configuration', true] }
]
},
{
group: 'mime',
key: ':types',
value: {
'application/activity+json': ['activity+json'],
'application/jrd+json': ['jrd+json'],
'application/ld+json': ['activity+json'],
'application/xml': ['xml'],
'application/xrd+xml': ['xrd+xml']
}
},
{
group: 'cors_plug',
key: ':max_age',
value: 86400
},
{
group: 'cors_plug',
key: ':methods',
value: ['POST', 'PUT', 'DELETE', 'GET', 'PATCH', 'OPTIONS']
},
{
group: 'cors_plug',
key: ':expose',
value: [
'Link',
'X-RateLimit-Reset',
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
'X-Request-Id',
'Idempotency-Key'
]
},
{
group: 'cors_plug',
key: ':credentials',
value: true
},
{
group: 'cors_plug',
key: ':headers',
value: ['Authorization', 'Content-Type', 'Idempotency-Key']
},
{
group: 'tesla',
key: ':adapter',
value: 'Tesla.Adapter.Hackney'
},
{
group: 'pleroma',
key: ':markup',
value: [
{ 'tuple': [':allow_inline_images', true] },
{ 'tuple': [':allow_headings', false] },
{ 'tuple': [':allow_tables', false] },
{ 'tuple': [':allow_fonts', false] },
{ 'tuple': [':scrub_policy', [
'Pleroma.HTML.Transform.MediaProxy',
'Pleroma.HTML.Scrubber.Default'
]] }
]
}
]
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function fetchSettings(authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/config`,
method: 'get',
headers: authHeaders(token)
})
}
export async function updateSettings(configs, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/config`,
method: 'post',
headers: authHeaders(token),
data: { configs }
})
}
export async function uploadMedia(file, authHost, token) {
const formData = new FormData()
formData.append('file', file)
return await request({
baseURL: baseName(authHost),
url: `/api/v1/media`,
method: 'post',
headers: authHeaders(token),
data: formData
})
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 490.2 490.2" style="enable-background:new 0 0 490.2 490.2;" xml:space="preserve">
<g>
<g>
<g>
<path d="M469.1,173.1h-37.5c-1-3.1-3.1-6.3-4.2-9.4l26.1-26.1c8.3-8.3,8.3-20.9,0-29.2l-71.9-71.9c-8.3-8.3-20.9-8.3-29.2,0
l-26.1,26.1c-3.1-2.1-6.3-3.1-9.4-4.2V20.9C316.9,9.4,307.5,0,296,0H193.9C182.4,0,173,9.4,173,20.9v37.5c-3.1,1-6.3,3.1-9.4,4.2
l-26.1-26.1c-8.3-8.3-20.9-8.3-29.2,0l-71.9,71.9c-4.2,4.2-6.3,9.4-6.3,14.6s2.1,10.4,6.3,14.6l26.1,26.1
c-2.1,3.1-3.1,6.3-4.2,9.4H20.9C9.4,173.1,0,182.5,0,194v102.2c0,11.5,9.4,20.9,20.9,20.9h37.5c1,3.1,3.1,6.3,4.2,9.4l-26.1,26.1
c-4.2,4.2-6.3,9.4-6.3,14.6s2.1,10.4,6.3,14.6l71.9,71.9c8.3,8.3,20.9,8.3,29.2,0l26.1-26.1c3.1,2.1,6.3,3.1,9.4,4.2v37.5
c0,11.5,9.4,20.9,20.9,20.9h102.2c11.5,0,20.9-9.4,20.9-20.9v-37.5c3.1-1,6.3-3.1,9.4-4.2l26.1,26.1c8.3,8.3,20.9,8.3,29.2,0
l71.9-71.9c8.3-8.3,8.3-20.9,0-29.2l-26.1-26.1c2.1-3.1,3.1-6.3,4.2-9.4h37.5c11.5,0,20.9-9.4,20.9-20.9V193.9
C490,182.4,480.6,173.1,469.1,173.1z M448.3,275.2H417c-9.4,0-16.7,6.3-19.8,14.6c-3.1,10.4-7.3,20.9-12.5,30.2
c-5.2,8.3-3.1,18.8,3.1,25l21.9,21.9L367,409.7l-21.9-21.9c-7.3-6.3-16.7-7.3-25-3.1c-9.4,5.2-19.8,9.4-30.2,12.5
c-8.3,2.1-14.6,10.4-14.6,19.8v31.3h-60.5l0,0V417c0-9.4-6.3-16.7-14.6-19.8c-10.4-3.1-20.9-7.3-30.2-12.5
c-8.3-5.2-18.8-3.1-25,3.1l-22,21.9L80.3,367l21.9-21.9c6.3-7.3,7.3-16.7,3.1-25c-5.2-9.4-9.4-19.8-12.5-30.2
c-2.1-8.3-10.4-14.6-19.8-14.6H41.7v-60.5H73c9.4,0,16.7-6.3,19.8-14.6c3.1-10.4,7.3-20.9,12.5-30.2c5.2-8.3,3.1-18.8-3.1-25
l-21.9-22L123,80.3l21.9,21.9c7.3,6.3,16.7,7.3,25,3.1c9.4-5.2,19.8-9.4,30.2-12.5c8.3-2.1,14.6-10.4,14.6-19.8V41.7h60.5V73
c0,9.4,6.3,16.7,14.6,19.8c10.4,3.1,20.9,7.3,30.2,12.5c8.3,5.2,18.8,3.1,25-3.1l22-21.9l42.7,42.7l-21.9,21.9
c-6.3,7.3-7.3,16.7-3.1,25c5.2,9.4,9.4,19.8,12.5,30.2c2.1,8.3,10.4,14.6,19.8,14.6h31.3L448.3,275.2L448.3,275.2z"/>
<path d="M245,131.4c-62.6,0-113.6,51.1-113.6,113.6s51,113.6,113.6,113.6s113.6-51,113.6-113.6S307.6,131.4,245,131.4z
M245,316.9c-39.6,0-71.9-32.3-71.9-71.9s32.3-71.9,71.9-71.9s71.9,32.3,71.9,71.9S284.6,316.9,245,316.9z"/>
</g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
......@@ -65,7 +65,8 @@ export default {
i18n: 'I18n',
externalLink: 'External Link',
users: 'Users',
reports: 'Reports'
reports: 'Reports',
settings: 'Settings'
},
navbar: {
logOut: 'Log Out',
......@@ -273,5 +274,30 @@ export default {
open: 'Open',
closed: 'Closed',
resolved: 'Resolved'
},
settings: {
settings: 'Settings',
instance: 'Instance',
upload: 'Upload',
mailer: 'Mailer',
logger: 'Logger',
activityPub: 'ActivityPub',
auth: 'Authentication',
autoLinker: 'Auto Linker',
captcha: 'Captcha',
frontend: 'Frontend',
http: 'HTTP',
mrf: 'MRF',
mediaProxy: 'Media Proxy',
metadata: 'Metadata',
gopher: 'Gopher',
endpoint: 'Endpoint',
jobQueue: 'Job queue',
webPush: 'Web push encryption',
esshd: 'BBS / SSH access',
rateLimiters: 'Rate limiters',
database: 'Database',
other: 'Other',
success: 'Settings changed successfully!'
}
}
......@@ -76,6 +76,18 @@ export const asyncRouterMap = [
}
]
},
{
path: '/settings',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/settings/index'),
name: 'Settings',
meta: { title: 'settings', icon: 'settings', noCache: true }
}
]
},
{
path: '/users/:id',
component: Layout,
......
......@@ -16,6 +16,82 @@ const getters = {
addRouters: state => state.permission.addRouters,
errorLogs: state => state.errorLog.logs,
users: state => state.users.fetchedUsers,
authHost: state => state.user.authHost
authHost: state => state.user.authHost,
activityPubConfig: state => state.settings.settings['activitypub'],
adminTokenConfig: state => state.settings.settings['admin_token'],
assetsConfig: state => state.settings.settings['assets'],
authConfig: state => state.settings.settings['auth'],
autoLinkerConfig: state => state.settings.settings['auto_linker'],
captchaConfig: state => state.settings.settings['Pleroma.Captcha'],
chatConfig: state => state.settings.settings['chat'],
consoleConfig: state => state.settings.settings['console'],
corsPlugCredentials: state => state.settings.settings['credentials'],
corsPlugExposeConfig: state => state.settings.settings['expose'],
corsPlugHeaders: state => state.settings.settings['headers'],
corsPlugMaxAge: state => state.settings.settings['max_age'],
corsPlugMethods: state => state.settings.settings['methods'],
databaseConfig: state => state.settings.settings['database'],
ectoReposConfig: state => state.settings.settings['ecto_repos'],
emojiConfig: state => state.settings.settings['emoji'],
enabledConfig: state => state.settings.settings['enabled'],
endpointConfig: state => state.settings.settings['Pleroma.Web.Endpoint'],
exsysloggerConfig: state => state.settings.settings['ex_syslogger'],
facebookConfig: state => state.settings.settings['Ueberauth.Strategy.Facebook.OAuth'],
fetchInitialPostsConfig: state => state.settings.settings['fetch_initial_posts'],
formatEncodersConfig: state => state.settings.settings['format_encoders'],
frontendConfig: state => state.settings.settings['frontend_configurations'],
googleConfig: state => state.settings.settings['Ueberauth.Strategy.Google.OAuth'],
gopherConfig: state => state.settings.settings['gopher'],
hackneyPoolsConfig: state => state.settings.settings['hackney_pools'],
handlerConfig: state => state.settings.settings['handler'],
httpConfig: state => state.settings.settings['http'],
httpSecurityConfig: state => state.settings.settings['http_security'],
instanceConfig: state => state.settings.settings['instance'],
kocaptchaConfig: state => state.settings.settings['Pleroma.Captcha.Kocaptcha'],
levelConfig: state => state.settings.settings['level'],
ldapConfig: state => state.settings.settings['ldap'],
loggerBackendsConfig: state => state.settings.settings['backends'],
mailerConfig: state => state.settings.settings['Pleroma.Emails.Mailer'],
markupConfig: state => state.settings.settings['markup'],
mediaProxyConfig: state => state.settings.settings['media_proxy'],
metaConfig: state => state.settings.settings['meta'],
metadataConfig: state => state.settings.settings['Pleroma.Web.Metadata'],
microsoftConfig: state => state.settings.settings['Ueberauth.Strategy.Microsoft.OAuth'],
mimeTypesConfig: state => state.settings.settings['types'],
mrfHellthreadConfig: state => state.settings.settings['mrf_hellthread'],
mrfKeywordConfig: state => state.settings.settings['mrf_keyword'],
mrfMentionConfig: state => state.settings.settings['mrf_mention'],
mrfNormalizeMarkupConfig: state => state.settings.settings['mrf_normalize_markup'],
mrfRejectnonpublicConfig: state => state.settings.settings['mrf_rejectnonpublic'],
mrfSimpleConfig: state => state.settings.settings['mrf_simple'],
mrfSubchainConfig: state => state.settings.settings['mrf_subchain'],
mrfUserAllowlistConfig: state => state.settings.settings['mrf_user_allowlist'],
oauth2Config: state => state.settings.settings['oauth2'],
passwordAuthenticatorConfig: state => state.settings.settings['password_authenticator'],
pleromaAuthenticatorConfig: state => state.settings.settings['Pleroma.Web.Auth.Authenticator'],
pleromaRepoConfig: state => state.settings.settings['Pleroma.Repo'],
pleromaUserConfig: state => state.settings.settings['Pleroma.User'],
portConfig: state => state.settings.settings['port'],
privDirConfig: state => state.settings.settings['priv_dir'],
queuesConfig: state => state.settings.settings['queues'],
rateLimitersConfig: state => state.settings.settings['rate_limit'],
retryQueueConfig: state => state.settings.settings['Pleroma.Web.Federator.RetryQueue'],
richMediaConfig: state => state.settings.settings['rich_media'],
suggestionsConfig: state => state.settings.settings['suggestions'],
scheduledActivityConfig: state => state.settings.settings['Pleroma.ScheduledActivity'],
teslaAdapterConfig: state => state.settings.settings['adapter'],
twitterConfig: state => state.settings.settings['Ueberauth.Strategy.Twitter.OAuth'],
ueberauthConfig: state => state.settings.settings['Ueberauth'],
uploadAnonymizeFilenameConfig: state => state.settings.settings['Pleroma.Upload.Filter.AnonymizeFilename'],
uploadConfig: state => state.settings.settings['Pleroma.Upload'],
uploadFilterMogrifyConfig: state => state.settings.settings['Pleroma.Upload.Filter.Mogrify'],
uploadersLocalConfig: state => state.settings.settings['Pleroma.Uploaders.Local'],
uploadMDIIConfig: state => state.settings.settings['Pleroma.Uploaders.MDII'],
uploadS3Config: state => state.settings.settings['Pleroma.Uploaders.S3'],
uriSchemesConfig: state => state.settings.settings['uri_schemes'],
userConfig: state => state.settings.settings['user'],
vapidDetailsConfig: state => state.settings.settings['vapid_details'],
webhookUrlConfig: state => state.settings.settings['webhook_url']
}
export default getters
......@@ -4,6 +4,7 @@ import app from './modules/app'
import errorLog from './modules/errorLog'
import permission from './modules/permission'
import reports from './modules/reports'
import settings from './modules/settings'
import tagsView from './modules/tagsView'
import user from './modules/user'
import userProfile from './modules/userProfile'
......@@ -18,6 +19,7 @@ const store = new Vuex.Store({
errorLog,
permission,
reports,
settings,
tagsView,
user,
userProfile,
......
const nonAtomsTuples = ['replace', 'match_actor', ':replace', ':match_actor']
const groups = {
'cors_plug': [
'credentials',
'expose',
'headers',
'max_age',
'methods'
],
'esshd': [
'enabled',
'handler',
'password_authenticator',
'port',
'priv_dir'
],
'logger': ['backends', 'console', 'ex_syslogger'],
'mime': ['types'],
'phoenix': ['format_encoders'],
'pleroma': [
'Pleroma.Captcha',
'Pleroma.Captcha.Kocaptcha',
'Pleroma.Emails.Mailer',
'Pleroma.Repo',
'Pleroma.ScheduledActivity',
'Pleroma.Upload',
'Pleroma.Upload.Filter.AnonymizeFilename',
'Pleroma.Upload.Filter.Mogrify',
'Pleroma.Uploaders.Local',
'Pleroma.Uploaders.MDII',
'Pleroma.Uploaders.S3',
'Pleroma.User',
'Pleroma.Web.Auth.Authenticator',
'Pleroma.Web.Endpoint',
'Pleroma.Web.Federator.RetryQueue',
'Pleroma.Web.Metadata',
'activitypub',
'admin_token',
'assets',
'auth',
'auto_linker',
'chat',
'database',
'ecto_repos',
'emoji',
'env',
'fetch_initial_posts',
'frontend_configurations',
'gopher',
'hackney_pools',
'http',
'http_security',
'instance',
'ldap',
'markup',
'media_proxy',
'mrf_hellthread',
'mrf_keyword',
'mrf_mention',
'mrf_normalize_markup',
'mrf_rejectnonpublic',
'mrf_simple',
'mrf_subchain',
'mrf_user_allowlist',
'oauth2',
'rate_limit',
'rich_media',
'suggestions',
'uri_schemes',
'user'
],
'pleroma_job_queue': ['queues'],
'quack': ['level', 'meta', 'webhook_url'],
'tesla': ['adapter'],
'ueberauth': [
'Ueberauth',
'Ueberauth.Strategy.Facebook.OAuth',
'Ueberauth.Strategy.Google.OAuth',
'Ueberauth.Strategy.Microsoft.OAuth',
'Ueberauth.Strategy.Twitter.OAuth'
],
'web_push_encryption': ['vapid_details']
}
export const filterIgnored = (settings, ignored) => {
if (settings.enabled.value === true) {
return settings
}
return ignored.reduce((acc, name) => {
const { [name]: ignored, ...newAcc } = acc
return newAcc
}, settings)
}
// REFACTOR
export const parseTuples = (tuples, key) => {
return tuples.reduce((accum, item) => {
if (key === 'rate_limit') {
accum[item.tuple[0].substr(1)] = item.tuple[1]
} else if (Array.isArray(item.tuple[1]) &&
(typeof item.tuple[1][0] === 'object' && !Array.isArray(item.tuple[1][0])) && item.tuple[1][0]['tuple']) {
nonAtomsTuples.includes(item.tuple[0])
? accum[item.tuple[0].substr(1)] = parseNonAtomTuples(item.tuple[1])
: accum[item.tuple[0].substr(1)] = parseTuples(item.tuple[1])
} else if (item.tuple[1] && typeof item.tuple[1] === 'object' && 'tuple' in item.tuple[1]) {
accum[item.tuple[0].substr(1)] = item.tuple[1]['tuple'].join('.')
} else {
key === 'mrf_user_allowlist'
? accum[item.tuple[0]] = item.tuple[1]
: accum[item.tuple[0].substr(1)] = item.tuple[1]
}
return accum
}, {})
}
const parseNonAtomTuples = (tuples) => {
return tuples.reduce((acc, item) => {
acc[item.tuple[0]] = item.tuple[1]
return acc
}, {})
}
export const valueHasTuples = (key, value) => {
const valueIsArrayOfNonObjects = Array.isArray(value) && value.length > 0 && typeof value[0] !== 'object'
return key === 'meta' ||
key === 'types' ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
valueIsArrayOfNonObjects
}
// REFACTOR
export const wrapConfig = settings => {
return Object.keys(settings).map(config => {
const group = getGroup(config)
const key = config.startsWith('Pleroma') || config.startsWith('Ueberauth') ? config : `:${config}`
const value = (settings[config]['value'] !== undefined)
? settings[config]['value']
: Object.keys(settings[config]).reduce((acc, settingName) => {
const data = settings[config][settingName]
if (data === '') {
return acc
} else if (key === ':rate_limit') {
return [...acc, { 'tuple': [`:${settingName}`, data] }]
} else if (settingName === 'ip') {
const ip = data.split('.')
return [...acc, { 'tuple': [`:${settingName}`, { 'tuple': ip }] }]
} else if (!Array.isArray(data) && typeof data === 'object') {
return nonAtomsTuples.includes(settingName)
? [...acc, { 'tuple': [`:${settingName}`, wrapNonAtomsTuples(data)] }]
: [...acc, { 'tuple': [`:${settingName}`, wrapNestedTuples(data)] }]
}
return key === ':mrf_user_allowlist'
? [...acc, { 'tuple': [`${settingName}`, settings[config][settingName]] }]
: [...acc, { 'tuple': [`:${settingName}`, settings[config][settingName]] }]
}, [])
return { group, key, value }
})
}
const wrapNestedTuples = setting => {
return Object.keys(setting).reduce((acc, settingName) => {
const data = setting[settingName]
if (data === '') {
return acc
} else if (settingName === 'ip') {
const ip = data.split('.')
return [...acc, { 'tuple': [`:${settingName}`, { 'tuple': ip }] }]
} else if (!Array.isArray(data) && typeof data === 'object') {
return [...acc, { 'tuple': [`:${settingName}`, wrapNestedTuples(data)] }]
}
return [...acc, { 'tuple': [`:${settingName}`, setting[settingName]] }]
}, [])
}
const wrapNonAtomsTuples = setting => {
return Object.keys(setting).reduce((acc, settingName) => {
return [...acc, { 'tuple': [`${settingName}`, setting[settingName]] }]
}, [])
}
const getGroup = key => {
return Object.keys(groups).find(i => groups[i].includes(key))
}
import { fetchSettings, updateSettings, uploadMedia } from '@/api/settings'
import { initialSettings } from '@/api/initialDataForConfig'
import { filterIgnored, parseTuples, valueHasTuples, wrapConfig } from './normalizers'
const settings = {
state: {
settings: {
'activitypub': {},
'adapter': {},
'admin_token': {},
'assets': { mascots: {}},
'auth': {},
'auto_linker': { opts: {}},
'backends': {},
'chat': {},
'console': { colors: {}},
'credentials': {},
'database': {},
'ecto_repos': {},
'emoji': { groups: {}},
'enabled': {},
'ex_syslogger': {},
'expose': {},
'fetch_initial_posts': {},
'format_encoders': {},
'frontend_configurations': { pleroma_fe: {}, masto_fe: {}},
'gopher': {},
'hackney_pools': { federation: {}, media: {}, upload: {}},
'handler': {},
'headers': {},
'http': { adapter: {}},
'http_security': {},
'instance': { poll_limits: {}},
'level': {},
'ldap': {},
'markup': {},
'max_age': {},
'media_proxy': { proxy_opts: {}},
'meta': {},
'methods': {},
'mrf_hellthread': {},
'mrf_keyword': { replace: {}},
'mrf_mention': {},
'mrf_normalize_markup': {},
'mrf_rejectnonpublic': {},
'mrf_simple': {},
'mrf_subchain': { match_actor: {}},
'mrf_user_allowlist': {},
'oauth2': {},
'password_authenticator': {},
'Pleroma.Captcha': {},