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: 'なんなんなんなんなん' + }; + + 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: 'なんなんなんなんなん' + }; + + 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 = {