feat: カスタム絵文字ごとにそれをリアクションとして使えるロールを設定できるように
This commit is contained in:
@@ -7,7 +7,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import type { EmojisRepository, Role } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@@ -15,6 +15,8 @@ import type { Config } from '@/config.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
import type { Serialized } from '@/server/api/stream/types.js';
|
||||
|
||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService {
|
||||
private cache: MemoryKVCache<Emoji | null>;
|
||||
@@ -63,6 +65,9 @@ export class CustomEmojiService {
|
||||
aliases: string[];
|
||||
host: string | null;
|
||||
license: string | null;
|
||||
isSensitive: boolean;
|
||||
localOnly: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][];
|
||||
}): Promise<Emoji> {
|
||||
const emoji = await this.emojisRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
@@ -75,6 +80,9 @@ export class CustomEmojiService {
|
||||
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
|
||||
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
||||
license: data.license,
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
if (data.host == null) {
|
||||
@@ -90,10 +98,14 @@ export class CustomEmojiService {
|
||||
|
||||
@bindThis
|
||||
public async update(id: Emoji['id'], data: {
|
||||
driveFile?: DriveFile;
|
||||
name?: string;
|
||||
category?: string | null;
|
||||
aliases?: string[];
|
||||
license?: string | null;
|
||||
isSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][];
|
||||
}): Promise<void> {
|
||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||
@@ -105,6 +117,12 @@ export class CustomEmojiService {
|
||||
category: data.category,
|
||||
aliases: data.aliases,
|
||||
license: data.license,
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
|
||||
publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined,
|
||||
type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
@@ -259,7 +277,7 @@ export class CustomEmojiService {
|
||||
|
||||
@bindThis
|
||||
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||
const match = emojiName.match(parseEmojiStrRegexp);
|
||||
if (!match) return { name: null, host: null };
|
||||
|
||||
const name = match[1];
|
||||
|
@@ -83,7 +83,7 @@ export class MfmService {
|
||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||
text += txt;
|
||||
// メンション
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||
const part = txt.split('@');
|
||||
|
||||
if (part.length === 2 && href) {
|
||||
|
@@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
|
||||
@@ -75,6 +76,7 @@ export class ReactionService {
|
||||
private utilityService: UtilityService,
|
||||
private metaService: MetaService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private roleService: RoleService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
@@ -88,7 +90,7 @@ export class ReactionService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) {
|
||||
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, _reaction?: string | null) {
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
@@ -102,10 +104,36 @@ export class ReactionService {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
let reaction = _reaction ?? FALLBACK;
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '❤️';
|
||||
} else {
|
||||
reaction = await this.toDbReaction(reaction, user.host);
|
||||
} else if (_reaction) {
|
||||
const custom = reaction.match(isCustomEmojiRegexp);
|
||||
if (custom) {
|
||||
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
||||
|
||||
const name = custom[1];
|
||||
const emoji = reacterHost == null
|
||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||
: await this.emojisRepository.findOneBy({
|
||||
host: reacterHost,
|
||||
name,
|
||||
});
|
||||
|
||||
if (emoji) {
|
||||
if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) {
|
||||
reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||
} else {
|
||||
// リアクションとして使う権限がない
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
} else {
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
} else {
|
||||
reaction = this.normalize(reaction ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
const record: NoteReaction = {
|
||||
@@ -291,11 +319,9 @@ export class ReactionService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
|
||||
public normalize(reaction: string | null): string {
|
||||
if (reaction == null) return FALLBACK;
|
||||
|
||||
reacterHost = this.utilityService.toPunyNullable(reacterHost);
|
||||
|
||||
// 文字列タイプのリアクションを絵文字に変換
|
||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||
|
||||
@@ -309,19 +335,6 @@ export class ReactionService {
|
||||
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
|
||||
}
|
||||
|
||||
const custom = reaction.match(isCustomEmojiRegexp);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = reacterHost == null
|
||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||
: await this.emojisRepository.findOneBy({
|
||||
host: reacterHost,
|
||||
name,
|
||||
});
|
||||
|
||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||
}
|
||||
|
||||
return FALLBACK;
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,9 @@ type IWebFinger = {
|
||||
subject: string;
|
||||
};
|
||||
|
||||
const urlRegex = /^https?:\/\//;
|
||||
const mRegex = /^([^@]+)@(.*)/;
|
||||
|
||||
@Injectable()
|
||||
export class WebfingerService {
|
||||
constructor(
|
||||
@@ -35,12 +38,12 @@ export class WebfingerService {
|
||||
|
||||
@bindThis
|
||||
private genUrl(query: string): string {
|
||||
if (query.match(/^https?:\/\//)) {
|
||||
if (query.match(urlRegex)) {
|
||||
const u = new URL(query);
|
||||
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
|
||||
}
|
||||
|
||||
const m = query.match(/^([^@]+)@(.*)/);
|
||||
const m = query.match(mRegex);
|
||||
if (m) {
|
||||
const hostname = m[2];
|
||||
const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
|
||||
|
@@ -26,6 +26,7 @@ export class EmojiEntityService {
|
||||
category: emoji.category,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
isSensitive: emoji.isSensitive,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +52,9 @@ export class EmojiEntityService {
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
license: emoji.license,
|
||||
isSensitive: emoji.isSensitive,
|
||||
localOnly: emoji.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user