Merge branch 'develop' into merge/2024-02-03

# Conflicts:
#	locales/index.d.ts
#	packages/backend/src/core/entities/UserEntityService.ts
#	packages/frontend/src/_dev_boot_.ts
#	packages/misskey-js/src/autogen/types.ts
#	sharkey-locales/en-US.yml
This commit is contained in:
Hazelnoot 2025-02-07 11:54:29 -05:00
commit f36029f795
24 changed files with 512 additions and 35 deletions

View file

@ -0,0 +1,11 @@
export class AddUserProfileDefaultCw1738446745738 {
name = 'AddUserProfileDefaultCw1738446745738'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "default_cw" text`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "default_cw"`);
}
}

View file

@ -0,0 +1,13 @@
export class AddUserProfileDefaultCwPriority1738468079662 {
name = 'AddUserProfileDefaultCwPriority1738468079662'
async up(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_default_cw_priority_enum" AS ENUM ('default', 'parent', 'defaultParent', 'parentDefault')`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "default_cw_priority" "public"."user_profile_default_cw_priority_enum" NOT NULL DEFAULT 'parent'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "default_cw_priority"`);
await queryRunner.query(`DROP TYPE "public"."user_profile_default_cw_priority_enum"`);
}
}

View file

@ -263,6 +263,67 @@ export class MfmService {
break;
}
case 'rp': break;
case 'rt': {
appendChildren(node.childNodes);
break;
}
case 'ruby': {
if (node.childNodes) {
/*
we get:
```
<ruby>
some text <rp>(</rp> <rt>annotation</rt> <rp>)</rp>
more text <rt>more annotation<rt>
</ruby>
```
and we want to produce:
```
$[ruby $[group some text] annotation]
$[ruby $[group more text] more annotation]
```
that `group` is a hack, because when the `ruby` render
sees just text inside the `$[ruby]`, it splits on
whitespace, considers the first "word" to be the main
content, and the rest the annotation
with that `group`, we force it to consider the whole
group as the main content
(note that the `rp` are to be ignored, they only exist
for browsers who don't understand ruby)
*/
let nonRtNodes = [];
// scan children, ignore `rp`, split on `rt`
for (const child of node.childNodes) {
if (treeAdapter.isTextNode(child)) {
nonRtNodes.push(child);
continue;
}
if (!treeAdapter.isElementNode(child)) {
continue;
}
if (child.nodeName === 'rp') {
continue;
}
if (child.nodeName === 'rt') {
text += '$[ruby $[group ';
appendChildren(nonRtNodes);
text += '] ';
analyze(child);
text += '] ';
nonRtNodes = [];
continue;
}
nonRtNodes.push(child);
}
}
break;
}
default: // includes inline elements
{
appendChildren(node.childNodes);
@ -381,6 +442,14 @@ export class MfmService {
}
}
// hack for ruby, should never be needed because we should
// never send this out to other instances
case 'group': {
const el = doc.createElement('span');
appendChildren(node.children, el);
return el;
}
default: {
return fnDefault(node);
}
@ -559,11 +628,65 @@ export class MfmService {
},
async fn(node) {
const el = doc.createElement('span');
el.textContent = '*';
await appendChildren(node.children, el);
el.textContent += '*';
return el;
switch (node.props.name) {
case 'group': { // hack for ruby
const el = doc.createElement('span');
await appendChildren(node.children, el);
return el;
}
case 'ruby': {
if (node.children.length === 1) {
const child = node.children[0];
const text = child.type === 'text' ? child.props.text : '';
const rubyEl = doc.createElement('ruby');
const rtEl = doc.createElement('rt');
const rpStartEl = doc.createElement('rp');
rpStartEl.appendChild(doc.createTextNode('('));
const rpEndEl = doc.createElement('rp');
rpEndEl.appendChild(doc.createTextNode(')'));
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
rubyEl.appendChild(rpStartEl);
rubyEl.appendChild(rtEl);
rubyEl.appendChild(rpEndEl);
return rubyEl;
} else {
const rt = node.children.at(-1);
if (!rt) {
const el = doc.createElement('span');
await appendChildren(node.children, el);
return el;
}
const text = rt.type === 'text' ? rt.props.text : '';
const rubyEl = doc.createElement('ruby');
const rtEl = doc.createElement('rt');
const rpStartEl = doc.createElement('rp');
rpStartEl.appendChild(doc.createTextNode('('));
const rpEndEl = doc.createElement('rp');
rpEndEl.appendChild(doc.createTextNode(')'));
await appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
rtEl.appendChild(doc.createTextNode(text.trim()));
rubyEl.appendChild(rpStartEl);
rubyEl.appendChild(rtEl);
rubyEl.appendChild(rpEndEl);
return rubyEl;
}
}
default: {
const el = doc.createElement('span');
el.textContent = '*';
await appendChildren(node.children, el);
el.textContent += '*';
return el;
}
}
},
blockCode(node) {

View file

@ -55,6 +55,8 @@ import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const Ajv = _Ajv.default;
const ajv = new Ajv();
@ -669,6 +671,8 @@ export class UserEntityService implements OnModuleInit {
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
policies: this.roleService.getUserPolicies(user.id),
defaultCW: profile!.defaultCW,
defaultCWPriority: profile!.defaultCWPriority,
} : {}),
...(opts.includeSecrets ? {

View file

@ -4,7 +4,7 @@
*/
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes, noteVisibilities, defaultCWPriorities } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiPage } from './Page.js';
@ -36,10 +36,10 @@ export class MiUserProfile {
})
public birthday: string | null;
@Column("varchar", {
@Column('varchar', {
length: 128,
nullable: true,
comment: "The ListenBrainz username of the User.",
comment: 'The ListenBrainz username of the User.',
})
public listenbrainz: string | null;
@ -290,6 +290,19 @@ export class MiUserProfile {
unlockedAt: number;
}[];
@Column('text', {
name: 'default_cw',
nullable: true,
})
public defaultCW: string | null;
@Column('enum', {
name: 'default_cw_priority',
enum: defaultCWPriorities,
default: 'parent',
})
public defaultCWPriority: typeof defaultCWPriorities[number];
//#region Denormalized fields
@Index()
@Column('varchar', {

View file

@ -752,6 +752,15 @@ export const packedMeDetailedOnlySchema = {
},
},
//#endregion
defaultCW: {
type: 'string',
nullable: true, optional: false,
},
defaultCWPriority: {
type: 'string',
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
nullable: false, optional: false,
},
},
} as const;

View file

@ -13,10 +13,11 @@ export const meta = {
requireCredential: false,
// 2 calls per second
// Up to 10 calls, then 4 / second.
// This allows for reliable automation.
limit: {
duration: 1000,
max: 2,
max: 10,
dripRate: 250,
},
} as const;

View file

@ -31,10 +31,12 @@ export const meta = {
},
},
// 3 calls per second
// up to 20 calls, then 1 per second.
// This handles bursty traffic when all tabs reload as a group
limit: {
duration: 1000,
max: 3,
max: 20,
dripSize: 1,
dripRate: 1000,
},
} as const;

View file

@ -133,6 +133,12 @@ export const meta = {
id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
httpStatusCode: 422,
},
maxCwLength: {
message: 'You tried setting a default content warning which is too long.',
code: 'MAX_CW_LENGTH',
id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
},
},
res: {
@ -243,6 +249,12 @@ export const paramDef = {
uniqueItems: true,
items: { type: 'string' },
},
defaultCW: { type: 'string', nullable: true },
defaultCWPriority: {
type: 'string',
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
nullable: false,
},
},
} as const;
@ -494,6 +506,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null;
}
let defaultCW = ps.defaultCW;
if (defaultCW !== undefined) {
if (defaultCW === '') defaultCW = null;
if (defaultCW && defaultCW.length > this.config.maxCwLength) {
throw new ApiError(meta.errors.maxCwLength);
}
profileUpdates.defaultCW = defaultCW;
}
if (ps.defaultCWPriority !== undefined) {
profileUpdates.defaultCWPriority = ps.defaultCWPriority;
}
//#region emojis/tags
let emojis = [] as string[];

View file

@ -57,10 +57,10 @@ export const meta = {
},
},
// 5 calls per 2 seconds
// up to 50 calls @ 4 per second
limit: {
duration: 1000 * 2,
max: 5,
max: 50,
dripRate: 250,
},
} as const;

View file

@ -48,7 +48,7 @@
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
// Fallback
if (lang == null) lang = 'en-US';

View file

@ -39,7 +39,7 @@
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
// Fallback
if (lang == null) lang = 'en-US';

View file

@ -58,6 +58,8 @@ export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
export const followingVisibilities = ['public', 'followers', 'private'] as const;
export const followersVisibilities = ['public', 'followers', 'private'] as const;
export const defaultCWPriorities = ['default', 'parent', 'defaultParent', 'parentDefault'] as const;
/**
*
*

View file

@ -45,6 +45,50 @@ describe('MfmService', () => {
const output = '<p><pre><code>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('ruby', () => {
const input = '$[ruby some text ignore me]';
const output = '<p><ruby>some<rp>(</rp><rt>text</rt><rp>)</rp></ruby></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('ruby2', () => {
const input = '$[ruby *some text* ignore me]';
const output = '<p><ruby><i>some text</i><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('ruby 3', () => {
const input = '$[ruby $[group *some* text] ignore me]';
const output = '<p><ruby><span><i>some</i> text</span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
});
describe('toMastoApiHtml', () => {
test('br', async () => {
const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
});
test('br alt', async () => {
const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
});
test('escape', async () => {
const input = '```\n<p>Hello, world!</p>\n```';
const output = '<p><pre><code>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre></p>';
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
});
test('ruby', async () => {
const input = '$[ruby $[group *some* text] ignore me]';
const output = '<p><ruby><span><span>*some*</span><span> text</span></span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
});
});
describe('fromHtml', () => {
@ -133,5 +177,12 @@ describe('MfmService', () => {
test('hashtag', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <a href="https://example.com/tags/a">#a</a> d</p>', ['#a']), 'a #a d');
});
test('ruby', () => {
assert.deepStrictEqual(
mfmService.fromHtml('<ruby> <i>some</i> text <rp>(</rp><rt>ignore me</rt><rp>)</rp> and <rt>more</rt></ruby>'),
'$[ruby $[group <i>some</i> text ] ignore me] $[ruby $[group and ] more]'
);
});
});
});