diff --git a/src/models/note.ts b/src/models/note.ts
index 8ca65bb42..9945ee42a 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -377,15 +377,40 @@ export const pack = async (
}
//#endregion
- if (_note.user.isCat && _note.text) {
- _note.text = (_note.text
- // ja-JP
- .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
- // ko-KR
- .replace(/[나-낳]/g, (match: string) => String.fromCharCode(
- match.codePointAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
- ))
- );
+ const nyamap: { [x: string]: string } = {
+ //#region nyanize: ja-JP
+ 'な': 'にゃ',
+ 'ナ': 'ニャ',
+ 'ナ': 'ニャ'
+ //#endregion
+ };
+
+ //#region nyanize: ko-KR
+ const diffKoKr = '냐'.charCodeAt(0) - '나'.charCodeAt(0);
+ for (let i = '나'.charCodeAt(0); i <= '낳'.charCodeAt(0); i++)
+ nyamap[String.fromCharCode(i)] = String.fromCharCode(i + diffKoKr);
+ //#endregion
+
+ const raw: string = _note.text;
+ const stack = [!!_note.user.isCat];
+ for (let i = 0; i < raw.length; i++) {
+ const head = raw[i];
+
+ if (head === '<') {
+ const [, tag, state] = raw.slice(i).match(/^<((\/?!?)nya>)/i) || [, , , ];
+
+ if (typeof state === 'string') {
+ if (state[0] === '/')
+ stack.shift();
+ else
+ stack.unshift(!state);
+
+ i += tag.length;
+ continue;
+ }
+ }
+
+ _note.text += stack[0] && nyamap[head] || head;
}
if (!opts.skipHide) {
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index d3c8699b2..c6c72269e 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -147,6 +147,17 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
if (data.text) {
data.text = data.text.trim();
+
+ const stack: string[] = [];
+ for (const tag of [...data.text.match(/<\/?!?nya>/ig)]
+ .map(x => x.toLocaleLowerCase()))
+ if (tag.includes('/')) {
+ if (stack.pop() !== tag.replace('/', ''))
+ return rej('Invalid nyanize syntax');
+ } else
+ stack.push(tag);
+ if (stack.length)
+ return rej('Invalid nyanize syntax');
}
let tags = data.apHashtags;
@@ -155,10 +166,11 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
// Parse MFM if needed
if (!tags || !emojis || !mentionedUsers) {
- const tokens = data.text ? parse(data.text) : [];
+ const text = data.text && data.text.replace(/^<\/?!?nya>/ig, '');
+ const tokens = text ? parse(text) : [];
const cwTokens = data.cw ? parse(data.cw) : [];
const choiceTokens = data.poll && data.poll.choices
- ? concat((data.poll.choices as IChoice[]).map(choice => parse(choice.text)))
+ ? concat((data.poll.choices as IChoice[]).map(choice => parse(choice.text.replace(/^<\/?!?nya>/ig, ''))))
: [];
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
@@ -303,7 +315,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
// If it is renote
if (data.renote) {
- const type = data.text ? 'quote' : 'renote';
+ const type = data.text && data.text.replace(/^<\/?!?nya>/ig, '') ? 'quote' : 'renote';
// Notify
if (isLocalUser(data.renote._user)) {
@@ -339,7 +351,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
async function renderActivity(data: Option, note: INote) {
if (data.localOnly) return null;
- const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
+ const content = data.renote && !data.text && !data.text.replace(/^<\/?!?nya>/ig, '') && !data.poll && (!data.files || !data.files.length)
? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note)
: renderCreate(await renderNote(note, false), note);
@@ -484,15 +496,17 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
}
function index(note: INote) {
- if (note.text == null || config.elasticsearch == null) return;
+ if (!note.text || !config.elasticsearch) return;
+
+ const text = note.text.replace(/^<\/?!?nya>/ig, '');
+
+ if (!text) return;
es.index({
index: 'misskey',
type: 'note',
id: note._id.toString(),
- body: {
- text: note.text
- }
+ body: { text }
});
}
diff --git a/test/api.ts b/test/api.ts
index 72a0efbbd..7f26b95bf 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -369,6 +369,52 @@ describe('API', () => {
expect(res).have.status(400);
}));
+ it('can make nyanize enable', async(async () => {
+ const me = await signup();
+
+ const post = {
+ text: 'なんなんなん!nya>なんなん'
+ };
+
+ const res = await request('/notes/create', post, me);
+
+ expect(res).have.status(200);
+ expect(res.body).be.a('object');
+ expect(res.body).have.property('createdNote');
+ expect(res.body.createdNote).have.property('text').eql('なんにゃんなんにゃんなん');
+ }));
+
+ it('can make nyanize disable', async(async () => {
+ const me = await signup();
+
+ await request('/i/update', {
+ isCat: true
+ }, me);
+
+ const post = {
+ text: 'なんなんなんなん!nya>なん'
+ };
+
+ const res = await request('/notes/create', post, me);
+
+ expect(res).have.status(200);
+ expect(res.body).be.a('object');
+ expect(res.body).have.property('createdNote');
+ expect(res.body.createdNote).have.property('text').eql('にゃんなんにゃんなんにゃん');
+ }));
+
+ it('throw error when invalid syntax', async(async () => {
+ const me = await signup();
+
+ const post = {
+ text: 'なんなん'
+ };
+
+ const res = await request('/notes/create', post, me);
+
+ expect(res).have.status(400);
+ }));
+
it('存在しないリプライ先で怒られる', async(async () => {
const me = await signup();
const post = {