feat: set license information for custom emojis (#9719)

Closes: #9711 (please check this issue first)

I cherry-picked two commits ([1](8ae9d2eaa8), [2](ed51209172)) from [Misskey](https://github.com/misskey-dev/misskey) and made a few changes.
「ライセンス」should be written as "License" in the following screenshots, but it has not yet been translated.

It would be nice if we could include multiple lines of text, but I just ported what's been implemented so far in Misskey not to mess things up.

This is my first pull request (aside from typo correction). Feel free to point out any issues!

![](https://cdn.discordapp.com/attachments/823878222897741868/1086372711841935440/2023-03-18_042011.png)
![](https://cdn.discordapp.com/attachments/823878222897741868/1086373178214981853/01.png)
![](https://cdn.discordapp.com/attachments/823878222897741868/1086373336709341246/2023-03-18_042629.png)

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: naskya <m@naskya.net>
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9719
Co-authored-by: naskya <naskya@noreply.codeberg.org>
Co-committed-by: naskya <naskya@noreply.codeberg.org>
This commit is contained in:
naskya 2023-03-19 07:22:28 +00:00 committed by Kainoa Kanter
parent c5d9f3bf59
commit 1c0d4546f7
20 changed files with 203 additions and 6 deletions

View File

@ -935,6 +935,7 @@ moveFromLabel: "Account you're moving from:"
moveFromDescription: "This will set an alias of your old account so that you can move from that account to this current one. Do this BEFORE moving from your older account. Please enter the tag of the account formatted like @person@instance.com"
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}? Once you do this, you won't be able to reverse it, and you won't be able to use your account normally again.\nAlso, please ensure that you've set this current account as the account you're moving from."
defaultReaction: "Default emoji reaction for outgoing and incoming posts"
license: "License"
_sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."

View File

@ -935,6 +935,7 @@ moveFromLabel: "引っ越し元のアカウント:"
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用することはできません。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
defaultReaction: "リモートとローカルの投稿に対するデフォルトの絵文字リアクション"
license: "ライセンス"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"

View File

@ -0,0 +1,11 @@
export class addPropsForCustomEmoji1678945242650 {
name = 'addPropsForCustomEmoji1678945242650'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "emoji" ADD "license" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "license"`);
}
}

View File

@ -55,4 +55,9 @@ export class Emoji {
array: true, length: 128, default: '{}',
})
public aliases: string[];
@Column('varchar', {
length: 1024, nullable: true,
})
public license: string | null;
}

View File

@ -15,6 +15,7 @@ export const EmojiRepository = db.getRepository(Emoji).extend({
host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のため
url: emoji.publicUrl || emoji.originalUrl,
license: emoji.license,
};
},

View File

@ -40,5 +40,10 @@ export const packedEmojiSchema = {
optional: false,
nullable: false,
},
license: {
type: "string",
optional: false,
nullable: true,
},
},
} as const;

View File

@ -75,6 +75,7 @@ export async function importCustomEmojis(
originalUrl: driveFile.url,
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
type: driveFile.webpublicType ?? driveFile.type,
license: emojiInfo.license,
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
}

View File

@ -29,6 +29,7 @@ import * as ep___admin_emoji_list from "./endpoints/admin/emoji/list.js";
import * as ep___admin_emoji_removeAliasesBulk from "./endpoints/admin/emoji/remove-aliases-bulk.js";
import * as ep___admin_emoji_setAliasesBulk from "./endpoints/admin/emoji/set-aliases-bulk.js";
import * as ep___admin_emoji_setCategoryBulk from "./endpoints/admin/emoji/set-category-bulk.js";
import * as ep___admin_emoji_setLicenseBulk from "./endpoints/admin/emoji/set-license-bulk.js";
import * as ep___admin_emoji_update from "./endpoints/admin/emoji/update.js";
import * as ep___admin_federation_deleteAllFiles from "./endpoints/admin/federation/delete-all-files.js";
import * as ep___admin_federation_refreshRemoteInstanceMetadata from "./endpoints/admin/federation/refresh-remote-instance-metadata.js";
@ -131,6 +132,7 @@ import * as ep___drive_folders_show from "./endpoints/drive/folders/show.js";
import * as ep___drive_folders_update from "./endpoints/drive/folders/update.js";
import * as ep___drive_stream from "./endpoints/drive/stream.js";
import * as ep___emailAddress_available from "./endpoints/email-address/available.js";
import * as ep___emoji from "./endpoints/emoji.js";
import * as ep___endpoint from "./endpoints/endpoint.js";
import * as ep___endpoints from "./endpoints/endpoints.js";
import * as ep___exportCustomEmojis from "./endpoints/export-custom-emojis.js";
@ -363,6 +365,7 @@ const eps = [
["admin/emoji/remove-aliases-bulk", ep___admin_emoji_removeAliasesBulk],
["admin/emoji/set-aliases-bulk", ep___admin_emoji_setAliasesBulk],
["admin/emoji/set-category-bulk", ep___admin_emoji_setCategoryBulk],
["admin/emoji/set-license-bulk", ep___admin_emoji_setLicenseBulk],
["admin/emoji/update", ep___admin_emoji_update],
["admin/federation/delete-all-files", ep___admin_federation_deleteAllFiles],
[
@ -471,6 +474,7 @@ const eps = [
["drive/folders/update", ep___drive_folders_update],
["drive/stream", ep___drive_stream],
["email-address/available", ep___emailAddress_available],
["emoji", ep___emoji],
["endpoint", ep___endpoint],
["endpoints", ep___endpoints],
["export-custom-emojis", ep___exportCustomEmojis],

View File

@ -49,6 +49,7 @@ export default define(meta, paramDef, async (ps, me) => {
originalUrl: file.url,
publicUrl: file.webpublicUrl ?? file.url,
type: file.webpublicType ?? file.type,
license: null,
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
await db.queryResultCache!.remove(["meta_emojis"]);

View File

@ -73,6 +73,7 @@ export default define(meta, paramDef, async (ps, me) => {
originalUrl: driveFile.url,
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
type: driveFile.webpublicType ?? driveFile.type,
license: emoji.license,
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
await db.queryResultCache!.remove(["meta_emojis"]);

View File

@ -55,6 +55,11 @@ export const meta = {
optional: false,
nullable: false,
},
license: {
type: "string",
optional: false,
nullable: true,
},
},
},
},

View File

@ -55,6 +55,11 @@ export const meta = {
optional: false,
nullable: false,
},
license: {
type: "string",
optional: false,
nullable: true,
},
},
},
},

View File

@ -0,0 +1,45 @@
import define from "../../../define.js";
import { Emojis } from "@/models/index.js";
import { In } from "typeorm";
import { ApiError } from "../../../error.js";
import { db } from "@/db/postgre.js";
export const meta = {
tags: ["admin"],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: "object",
properties: {
ids: {
type: "array",
items: {
type: "string",
format: "misskey:id",
},
},
license: {
type: "string",
nullable: true,
description: "Use `null` to reset the license.",
},
},
required: ["ids"],
} as const;
export default define(meta, paramDef, async (ps) => {
await Emojis.update(
{
id: In(ps.ids),
},
{
updatedAt: new Date(),
license: ps.license,
},
);
await db.queryResultCache!.remove(["meta_emojis"]);
});

View File

@ -34,6 +34,10 @@ export const paramDef = {
type: "string",
},
},
license: {
type: "string",
nullable: true,
},
},
required: ["id", "name", "aliases"],
} as const;
@ -48,6 +52,7 @@ export default define(meta, paramDef, async (ps) => {
name: ps.name,
category: ps.category,
aliases: ps.aliases,
license: ps.license,
});
await db.queryResultCache!.remove(["meta_emojis"]);

View File

@ -0,0 +1,38 @@
import { IsNull } from "typeorm";
import { Emojis } from "@/models/index.js";
import define from "../define.js";
export const meta = {
tags: ["meta"],
requireCredential: false,
allowGet: true,
cacheSec: 3600,
res: {
type: "object",
optional: false, nullable: false,
ref: "Emoji",
},
} as const;
export const paramDef = {
type: "object",
properties: {
name: {
type: "string",
},
},
required: ["name"],
} as const;
export default define(meta, paramDef, async (ps, me) => {
const emoji = await Emojis.findOneOrFail({
where: {
name: ps.name,
host: IsNull(),
},
});
return Emojis.pack(emoji);
});

View File

@ -14,9 +14,11 @@
</div>
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div>
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
<MkInput v-if="input && input.type !== 'paragraph'" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ph-password ph-bold ph-lg"></i></template>
</MkInput>
<MkTextarea v-if="input && input.type === 'paragraph'" v-model="inputValue" autofocus :type="paragraph" :placeholder="input.placeholder || undefined">
</MkTextarea>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
@ -49,6 +51,7 @@ import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkSelect from '@/components/form/select.vue';
import { i18n } from '@/i18n';

View File

@ -359,6 +359,40 @@ export function inputText(props: {
});
}
export function inputParagraph(props: {
title?: string | null;
text?: string | null;
placeholder?: string | null;
default?: string | null;
}): Promise<
| { canceled: true; result: undefined }
| {
canceled: false;
result: string;
}
> {
return new Promise((resolve, reject) => {
popup(
defineAsyncComponent(() => import("@/components/MkDialog.vue")),
{
title: props.title,
text: props.text,
input: {
type: "paragraph",
placeholder: props.placeholder,
default: props.default,
},
},
{
done: (result) => {
resolve(result ? result : { canceled: true });
},
},
"closed",
);
});
}
export function inputNumber(props: {
title?: string | null;
text?: string | null;

View File

@ -22,6 +22,9 @@
<template #label>{{ i18n.ts.tags }}</template>
<template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
</MkInput>
<MkTextarea v-model="license" class="_formBlock">
<template #label>{{ i18n.ts.license }}</template>
</MkTextarea>
<MkButton danger @click="del()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
@ -33,6 +36,7 @@ import { } from 'vue';
import XModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import * as os from '@/os';
import { unique } from '@/scripts/array';
import { i18n } from '@/i18n';
@ -47,6 +51,7 @@ let name: string = $ref(props.emoji.name);
let category: string = $ref(props.emoji.category);
let aliases: string = $ref(props.emoji.aliases.join(' '));
let categories: string[] = $ref(emojiCategories);
let license: string = $ref(props.emoji.license ?? '');
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
@ -63,6 +68,7 @@ async function update() {
name,
category,
aliases: aliases.split(' '),
license: license === '' ? null : license,
});
emit('done', {
@ -71,6 +77,7 @@ async function update() {
name,
category,
aliases: aliases.split(' '),
license: license === '' ? null : license,
},
});

View File

@ -18,6 +18,7 @@
<MkButton inline @click="addTagBulk">Add tag</MkButton>
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
<MkButton inline @click="setTagBulk">Set tag</MkButton>
<MkButton inline @click="setLicenseBulk">Set license</MkButton>
<MkButton inline danger @click="delBulk">Delete</MkButton>
</div>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
@ -258,6 +259,18 @@ const setTagBulk = async () => {
emojisPaginationComponent.value.reload();
};
const setLicenseBulk = async () => {
const { canceled, result } = await os.inputParagraph({
title: 'License',
});
if (canceled) return;
await os.apiWithDialog('admin/emoji/set-license-bulk', {
ids: selectedEmojis.value,
license: result,
});
emojisPaginationComponent.value.reload();
};
const delBulk = async () => {
const { canceled } = await os.confirm({
type: 'warning',

View File

@ -3,7 +3,7 @@
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.aliases.join(' ') }}</div>
<div class="info">{{ emoji.aliases.join(" ") }}</div>
</div>
</button>
</template>
@ -20,15 +20,26 @@ const props = defineProps<{
function menu(ev) {
os.popupMenu([{
type: 'label',
text: ':' + props.emoji.name + ':',
type: "label",
text: ":" + props.emoji.name + ":",
}, {
text: i18n.ts.copy,
icon: 'ph-clipboard-text ph-bold ph-lg',
icon: "ph-clipboard-text ph-bold ph-lg",
action: () => {
copyToClipboard(`:${props.emoji.name}:`);
os.success();
}
},
}, {
text: i18n.ts.license,
icon: "ph-info ph-bold ph-lg",
action: () => {
os.apiGet("emoji", { name: props.emoji.name }).then(res => {
os.alert({
type: "info",
text: `${res.license || i18n.ts.notSet}`,
});
});
},
}], ev.currentTarget ?? ev.target);
}
</script>