Compare commits

..

6 Commits

Author SHA1 Message Date
syuilo f08d6e46df Update ReactionService.ts 2026-01-29 17:42:40 +09:00
syuilo d53b2f532a Create 1769664628306-sp-reactions.js 2026-01-29 14:31:16 +09:00
syuilo 40f6acd720 wip 2026-01-29 14:23:09 +09:00
syuilo 9dc4762fe0 wip 2026-01-29 13:50:37 +09:00
syuilo f3d26722bb wip 2026-01-29 11:33:05 +09:00
syuilo 5eb873ff91 wip 2026-01-29 11:21:12 +09:00
29 changed files with 397 additions and 25 deletions

View File

@ -31,7 +31,6 @@
- JSONによるClient Information Discoveryを行うには、レスポンスの`Content-Type`ヘッダーが`application/json`である必要があります - JSONによるClient Information Discoveryを行うには、レスポンスの`Content-Type`ヘッダーが`application/json`である必要があります
- 従来の実装12 February 2022版・HTML Microformat形式も引き続きサポートされます - 従来の実装12 February 2022版・HTML Microformat形式も引き続きサポートされます
- Enhance: メモリ使用量を削減 - Enhance: メモリ使用量を削減
- Fix: `/admin/get-user-ips` エンドポイントのアクセス権限を管理者のみに修正
## 2025.12.2 ## 2025.12.2

View File

@ -1778,6 +1778,12 @@ _serverSettings:
entrancePageStyle: "エントランスページのスタイル" entrancePageStyle: "エントランスページのスタイル"
showTimelineForVisitor: "タイムラインを表示する" showTimelineForVisitor: "タイムラインを表示する"
showActivitiesForVisitor: "アクティビティを表示する" showActivitiesForVisitor: "アクティビティを表示する"
features: "機能"
_spReactions:
enable: "スペシャルリアクションを有効にする"
description1: "通常のリアクションより目立つ「スペシャルリアクション」をノートに送れる機能です。"
description2: "有効にする場合、ロールポリシーで、毎月送ることのできる最大数を設定してください。"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "全て公開" all: "全て公開"
@ -2890,6 +2896,7 @@ _notification:
renote: "リノート" renote: "リノート"
quote: "引用" quote: "引用"
reaction: "リアクション" reaction: "リアクション"
spReaction: "スペシャルリアクション"
pollEnded: "アンケートが終了" pollEnded: "アンケートが終了"
scheduledNotePosted: "予約投稿が成功した" scheduledNotePosted: "予約投稿が成功した"
scheduledNotePostFailed: "予約投稿が失敗した" scheduledNotePostFailed: "予約投稿が失敗した"

View File

@ -59,7 +59,7 @@
"ignore-walk": "8.0.0", "ignore-walk": "8.0.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"postcss": "8.5.6", "postcss": "8.5.6",
"tar": "7.5.7", "tar": "7.5.6",
"terser": "5.46.0" "terser": "5.46.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SpReactions1769664628306 {
name = 'SpReactions1769664628306'
/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "note_sp_reaction" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "reaction" character varying(260) NOT NULL, "noteUserId" character varying(32) NOT NULL, CONSTRAINT "PK_11fd5ecdd9bb91517edfcf890d9" PRIMARY KEY ("id")); COMMENT ON COLUMN "note_sp_reaction"."noteUserId" IS '[Denormalized]'`);
await queryRunner.query(`CREATE INDEX "IDX_3463a48b09fa41e1826ebd9f58" ON "note_sp_reaction" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_bfe4caa46cc0526bc2932d6dbe" ON "note_sp_reaction" ("noteId") `);
await queryRunner.query(`CREATE INDEX "IDX_8be6eb3f4edc9940a3f8142669" ON "note_sp_reaction" ("noteUserId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b5f210b20bd987fe8584c85d33" ON "note_sp_reaction" ("userId", "noteId") `);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableSpReaction" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "note_sp_reaction" ADD CONSTRAINT "FK_3463a48b09fa41e1826ebd9f585" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "note_sp_reaction" ADD CONSTRAINT "FK_bfe4caa46cc0526bc2932d6dbed" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note_sp_reaction" DROP CONSTRAINT "FK_bfe4caa46cc0526bc2932d6dbed"`);
await queryRunner.query(`ALTER TABLE "note_sp_reaction" DROP CONSTRAINT "FK_3463a48b09fa41e1826ebd9f585"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableSpReaction"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b5f210b20bd987fe8584c85d33"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8be6eb3f4edc9940a3f8142669"`);
await queryRunner.query(`DROP INDEX "public"."IDX_bfe4caa46cc0526bc2932d6dbe"`);
await queryRunner.query(`DROP INDEX "public"."IDX_3463a48b09fa41e1826ebd9f58"`);
await queryRunner.query(`DROP TABLE "note_sp_reaction"`);
}
}

View File

@ -4,8 +4,9 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js'; import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta, MiNoteSpReaction, NoteSpReactionsRepository } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
@ -73,6 +74,9 @@ export class ReactionService {
@Inject(DI.meta) @Inject(DI.meta)
private meta: MiMeta, private meta: MiMeta,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -82,6 +86,9 @@ export class ReactionService {
@Inject(DI.noteReactionsRepository) @Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository, private noteReactionsRepository: NoteReactionsRepository,
@Inject(DI.noteSpReactionsRepository)
private noteSpReactionsRepository: NoteSpReactionsRepository,
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
@ -337,6 +344,118 @@ export class ReactionService {
//#endregion //#endregion
} }
@bindThis
public async createSp(user: { id: MiUser['id']; isBot: MiUser['isBot'] }, note: MiNote, reaction: string) {
if (!this.meta.enableSpReaction) {
throw new IdentifiableError('52c432ea-b166-491c-a73b-5dd703221b20');
}
if (note.userId === user.id) {
throw new IdentifiableError('afa694bf-6661-4d72-b8f7-bfb86a7545a1');
}
if (note.userHost !== null) {
throw new IdentifiableError('b59abda2-0d81-49e3-8148-80cf35ac4402');
}
if (note.reactionAcceptance === 'likeOnly') {
throw new IdentifiableError('1b168811-a1aa-470a-9b61-7fcf807cf9c1');
}
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
throw new IdentifiableError('3ce0e3bc-7d48-4e87-a902-578c6ffd369e', 'Note not accessible for you.');
}
// monthly limit
const policies = await this.roleService.getUserPolicies(user.id);
if (policies.spReactionsMonthlyLimit === 0) {
throw new IdentifiableError('e371be02-9478-4133-90ef-8401ee38e474');
}
const month = new Date().getUTCMonth() + 1;
const monthlySpReactionsCountMapKey = `monthlySpReactionsCountMap:${user.id}:${month}`;
const count = await this.redisClient.get(monthlySpReactionsCountMapKey);
if (count != null && parseInt(count, 10) >= policies.spReactionsMonthlyLimit) {
throw new IdentifiableError('82e1a10c-52a8-4ccb-8ff7-3678bff68444');
}
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
if (blocked) {
throw new IdentifiableError('388ee683-8720-4aea-9ac8-b8c92d260815');
}
const custom = reaction.match(isCustomEmojiRegexp);
if (custom) {
const name = custom[1];
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
if (emoji == null) {
throw new IdentifiableError('47c098e2-d0b6-4197-8d00-5a68bbb156be');
}
// センシティブ
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
throw new IdentifiableError('7fc2efbd-2652-4a60-975b-6eb65f60c7b3');
}
if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 && !(await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) {
// リアクションとして使う権限がない
throw new IdentifiableError('63288e20-4251-4c62-a9d5-9da4e0bdd41e');
}
reaction = `:${name}:`;
} else {
reaction = this.normalize(reaction);
}
const record: MiNoteSpReaction = {
id: this.idService.gen(),
noteId: note.id,
userId: user.id,
reaction,
noteUserId: note.userId,
};
try {
await this.noteSpReactionsRepository.insert(record);
} catch (e) {
if (isDuplicateKeyValueError(e)) {
throw new IdentifiableError('c9e8b0d0-d532-4453-8cc1-5cf8e95ba764');
} else {
throw e;
}
}
// increment monthly reactions count
const redisPipeline = this.redisClient.pipeline();
redisPipeline.incr(monthlySpReactionsCountMapKey);
redisPipeline.expireat(monthlySpReactionsCountMapKey,
(Date.now() / 1000) + (60 * 60 * 24 * 40), // TTLは最低でも一か月存続しさえすれば厳密でなくていい
'NX',
);
redisPipeline.exec();
// 3日以内に投稿されたートの場合ハイライト用ランキング更新
if (
(Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3
) {
if (note.channelId != null) {
if (note.replyId == null) {
this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1);
}
} else {
if (note.visibility === 'public' && note.replyId == null) {
this.featuredService.updateGlobalNotesRanking(note.id, 1);
this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1);
}
}
}
this.notificationService.createNotification(note.userId, 'spReaction', {
noteId: note.id,
reaction: reaction,
}, user.id);
}
/** /**
* - * -
* - `@.` `decodeReaction()` * - `@.` `decodeReaction()`

View File

@ -71,6 +71,7 @@ export type RolePolicies = {
noteDraftLimit: number; noteDraftLimit: number;
scheduledNoteLimit: number; scheduledNoteLimit: number;
watermarkAvailable: boolean; watermarkAvailable: boolean;
spReactionsMonthlyLimit: number;
}; };
export const DEFAULT_POLICIES: RolePolicies = { export const DEFAULT_POLICIES: RolePolicies = {
@ -118,6 +119,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
noteDraftLimit: 10, noteDraftLimit: 10,
scheduledNoteLimit: 1, scheduledNoteLimit: 1,
watermarkAvailable: true, watermarkAvailable: true,
spReactionsMonthlyLimit: 0,
}; };
@Injectable() @Injectable()

View File

@ -29,6 +29,7 @@ const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([
'renote:grouped', 'renote:grouped',
'quote', 'quote',
'reaction', 'reaction',
'spReaction',
'reaction:grouped', 'reaction:grouped',
'pollEnded', 'pollEnded',
'scheduledNotePosted', 'scheduledNotePosted',

View File

@ -24,6 +24,7 @@ export const DI = {
noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteFavoritesRepository: Symbol('noteFavoritesRepository'),
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
noteReactionsRepository: Symbol('noteReactionsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'),
noteSpReactionsRepository: Symbol('noteSpReactionsRepository'),
pollsRepository: Symbol('pollsRepository'), pollsRepository: Symbol('pollsRepository'),
pollVotesRepository: Symbol('pollVotesRepository'), pollVotesRepository: Symbol('pollVotesRepository'),
userProfilesRepository: Symbol('userProfilesRepository'), userProfilesRepository: Symbol('userProfilesRepository'),

View File

@ -722,6 +722,11 @@ export class MiMeta {
}) })
public showRoleBadgesOfRemoteUsers: boolean; public showRoleBadgesOfRemoteUsers: boolean;
@Column('boolean', {
default: false,
})
public enableSpReaction: boolean;
@Column('jsonb', { @Column('jsonb', {
default: { }, default: { },
}) })

View File

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiNote } from './Note.js';
@Entity('note_sp_reaction')
@Index(['userId', 'noteId'], { unique: true })
export class MiNoteSpReaction {
@PrimaryColumn(id())
public id: string;
@Index()
@Column(id())
public userId: MiUser['id'];
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user?: MiUser | null;
@Index()
@Column(id())
public noteId: MiNote['id'];
@ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
public note?: MiNote | null;
@Column('varchar', {
length: 260,
})
public reaction: string;
//#region Denormalized fields
@Index()
@Column({
...id(),
comment: '[Denormalized]',
})
public noteUserId: MiUser['id'];
//#endregion
}

View File

@ -55,6 +55,13 @@ export type MiNotification = {
notifierId: MiUser['id']; notifierId: MiUser['id'];
noteId: MiNote['id']; noteId: MiNote['id'];
reaction: string; reaction: string;
} | {
type: 'spReaction';
id: string;
createdAt: string;
notifierId: MiUser['id'];
noteId: MiNote['id'];
reaction: string;
} | { } | {
type: 'pollEnded'; type: 'pollEnded';
id: string; id: string;

View File

@ -42,6 +42,7 @@ import {
MiNote, MiNote,
MiNoteFavorite, MiNoteFavorite,
MiNoteReaction, MiNoteReaction,
MiNoteSpReaction,
MiNoteThreadMuting, MiNoteThreadMuting,
MiNoteDraft, MiNoteDraft,
MiPage, MiPage,
@ -142,6 +143,12 @@ const $noteReactionsRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $noteSpReactionsRepository: Provider = {
provide: DI.noteSpReactionsRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteSpReaction).extend(miRepository as MiRepository<MiNoteSpReaction>),
inject: [DI.db],
};
const $noteDraftsRepository: Provider = { const $noteDraftsRepository: Provider = {
provide: DI.noteDraftsRepository, provide: DI.noteDraftsRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteDraft).extend(miRepository as MiRepository<MiNoteDraft>), useFactory: (db: DataSource) => db.getRepository(MiNoteDraft).extend(miRepository as MiRepository<MiNoteDraft>),
@ -556,6 +563,7 @@ const $reversiGamesRepository: Provider = {
$noteFavoritesRepository, $noteFavoritesRepository,
$noteThreadMutingsRepository, $noteThreadMutingsRepository,
$noteReactionsRepository, $noteReactionsRepository,
$noteSpReactionsRepository,
$noteDraftsRepository, $noteDraftsRepository,
$pollsRepository, $pollsRepository,
$pollVotesRepository, $pollVotesRepository,
@ -634,6 +642,7 @@ const $reversiGamesRepository: Provider = {
$noteFavoritesRepository, $noteFavoritesRepository,
$noteThreadMutingsRepository, $noteThreadMutingsRepository,
$noteReactionsRepository, $noteReactionsRepository,
$noteSpReactionsRepository,
$noteDraftsRepository, $noteDraftsRepository,
$pollsRepository, $pollsRepository,
$pollVotesRepository, $pollVotesRepository,

View File

@ -23,7 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiChannel } from '@/models/Channel.js'; import { MiChannel } from '@/models/Channel.js';
import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
import { MiChannelMuting } from "@/models/ChannelMuting.js"; import { MiChannelMuting } from '@/models/ChannelMuting.js';
import { MiChatApproval } from '@/models/ChatApproval.js'; import { MiChatApproval } from '@/models/ChatApproval.js';
import { MiChatMessage } from '@/models/ChatMessage.js'; import { MiChatMessage } from '@/models/ChatMessage.js';
import { MiChatRoom } from '@/models/ChatRoom.js'; import { MiChatRoom } from '@/models/ChatRoom.js';
@ -50,6 +50,7 @@ import { MiNote } from '@/models/Note.js';
import { MiNoteDraft } from '@/models/NoteDraft.js'; import { MiNoteDraft } from '@/models/NoteDraft.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js'; import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteSpReaction } from '@/models/NoteSpReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
import { MiPage } from '@/models/Page.js'; import { MiPage } from '@/models/Page.js';
import { MiPageLike } from '@/models/PageLike.js'; import { MiPageLike } from '@/models/PageLike.js';
@ -131,6 +132,7 @@ export {
MiNoteDraft, MiNoteDraft,
MiNoteFavorite, MiNoteFavorite,
MiNoteReaction, MiNoteReaction,
MiNoteSpReaction,
MiNoteThreadMuting, MiNoteThreadMuting,
MiPage, MiPage,
MiPageLike, MiPageLike,
@ -211,6 +213,7 @@ export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
export type NoteDraftsRepository = Repository<MiNoteDraft> & MiRepository<MiNoteDraft>; export type NoteDraftsRepository = Repository<MiNoteDraft> & MiRepository<MiNoteDraft>;
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>; export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>; export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
export type NoteSpReactionsRepository = Repository<MiNoteSpReaction> & MiRepository<MiNoteSpReaction>;
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>; export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>; export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>; export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;

View File

@ -182,6 +182,35 @@ export const packedNotificationSchema = {
optional: false, nullable: false, optional: false, nullable: false,
}, },
}, },
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['spReaction'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
reaction: {
type: 'string',
optional: false, nullable: false,
},
},
}, { }, {
type: 'object', type: 'object',
properties: { properties: {

View File

@ -608,6 +608,7 @@ export const packedMeDetailedOnlySchema = {
renote: { optional: true, ...notificationRecieveConfig }, renote: { optional: true, ...notificationRecieveConfig },
quote: { optional: true, ...notificationRecieveConfig }, quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig },
spReaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig },
scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig }, scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },

View File

@ -44,6 +44,7 @@ import { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js'; import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteSpReaction } from '@/models/NoteSpReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
import { MiNoteDraft } from '@/models/NoteDraft.js'; import { MiNoteDraft } from '@/models/NoteDraft.js';
import { MiPage } from '@/models/Page.js'; import { MiPage } from '@/models/Page.js';
@ -204,6 +205,7 @@ export const entities = [
MiNote, MiNote,
MiNoteFavorite, MiNoteFavorite,
MiNoteReaction, MiNoteReaction,
MiNoteSpReaction,
MiNoteThreadMuting, MiNoteThreadMuting,
MiNoteDraft, MiNoteDraft,
MiPage, MiPage,

View File

@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
kind: 'read:admin:user-ips', kind: 'read:admin:user-ips',
res: { res: {
type: 'array', type: 'array',

View File

@ -596,6 +596,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
enableSpReaction: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
}, },
} as const; } as const;
@ -752,6 +756,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes, remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes,
remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes, remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes,
showRoleBadgesOfRemoteUsers: instance.showRoleBadgesOfRemoteUsers, showRoleBadgesOfRemoteUsers: instance.showRoleBadgesOfRemoteUsers,
enableSpReaction: instance.enableSpReaction,
}; };
}); });
} }

View File

@ -218,6 +218,7 @@ export const paramDef = {
remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' }, remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' },
remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' }, remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' },
showRoleBadgesOfRemoteUsers: { type: 'boolean' }, showRoleBadgesOfRemoteUsers: { type: 'boolean' },
enableSpReaction: { type: 'boolean' },
}, },
required: [], required: [],
} as const; } as const;
@ -762,6 +763,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.showRoleBadgesOfRemoteUsers = ps.showRoleBadgesOfRemoteUsers; set.showRoleBadgesOfRemoteUsers = ps.showRoleBadgesOfRemoteUsers;
} }
if (ps.enableSpReaction !== undefined) {
set.enableSpReaction = ps.enableSpReaction;
}
const before = await this.metaService.fetch(true); const before = await this.metaService.fetch(true);
await this.metaService.update(set); await this.metaService.update(set);

View File

@ -208,6 +208,7 @@ export const paramDef = {
renote: notificationRecieveConfig, renote: notificationRecieveConfig,
quote: notificationRecieveConfig, quote: notificationRecieveConfig,
reaction: notificationRecieveConfig, reaction: notificationRecieveConfig,
spReaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig, pollEnded: notificationRecieveConfig,
scheduledNotePosted: notificationRecieveConfig, scheduledNotePosted: notificationRecieveConfig,
scheduledNotePostFailed: notificationRecieveConfig, scheduledNotePostFailed: notificationRecieveConfig,

View File

@ -11,6 +11,7 @@
* renote - 稿Renoteされた * renote - 稿Renoteされた
* quote - 稿Renoteされた * quote - 稿Renoteされた
* reaction - 稿 * reaction - 稿
* spReaction -
* pollEnded - * pollEnded -
* scheduledNotePosted - 稿 * scheduledNotePosted - 稿
* scheduledNotePostFailed - 稿 * scheduledNotePostFailed - 稿
@ -33,6 +34,7 @@ export const notificationTypes = [
'renote', 'renote',
'quote', 'quote',
'reaction', 'reaction',
'spReaction',
'pollEnded', 'pollEnded',
'scheduledNotePosted', 'scheduledNotePosted',
'scheduledNotePostFailed', 'scheduledNotePostFailed',

View File

@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i> <i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'spReaction'" class="ti ti-octahedron-plus"></i>
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i> <i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i>
<i v-else-if="notification.type === 'scheduledNotePostFailed'" class="ti ti-alert-triangle"></i> <i v-else-if="notification.type === 'scheduledNotePostFailed'" class="ti ti-alert-triangle"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
@ -74,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span> <span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span> <span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'spReaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> <span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span> <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
@ -82,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header> </header>
<div> <div>
<MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped' || notification.type === 'spReaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i> <i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i> <i class="ti ti-quote" :class="$style.quote"></i>

View File

@ -96,6 +96,28 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder> </MkFolder>
</SearchMarker> </SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['features']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-puzzle"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.features }}</SearchLabel></template>
<template v-if="featuresForm.modified.value" #footer>
<MkFormFooter :form="featuresForm"/>
</template>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="featuresForm.state.enableSpReaction">
<template #label><SearchLabel>{{ i18n.ts._serverSettings._spReactions.enable }}</SearchLabel><span v-if="featuresForm.modifiedStates.enableSpReaction" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<SearchText>{{ i18n.ts._serverSettings._spReactions.description1 }}</SearchText>
<div>{{ i18n.ts._serverSettings._spReactions.description2 }}</div>
</template>
</MkSwitch>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['pinned', 'users']"> <SearchMarker v-slot="slotProps" :keywords="['pinned', 'users']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget"> <MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-user-star"></i></SearchIcon></template> <template #icon><SearchIcon><i class="ti ti-user-star"></i></SearchIcon></template>
@ -426,6 +448,15 @@ const infoForm = useForm({
fetchInstance(true); fetchInstance(true);
}); });
const featuresForm = useForm({
enableSpReaction: meta.enableSpReaction,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableSpReaction: state.enableSpReaction,
});
fetchInstance(true);
});
const pinnedUsersForm = useForm({ const pinnedUsersForm = useForm({
pinnedUsers: meta.pinnedUsers.join('\n'), pinnedUsers: meta.pinnedUsers.join('\n'),
}, async (state) => { }, async (state) => {

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.body"> <div :class="$style.body">
<div :class="$style.top"> <div :class="$style.top">
<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu"> <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="view-transition-name: navbar-serverIcon;"/> <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/>
</button> </button>
<button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode"> <button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode">
<i v-if="store.r.realtimeMode.value" class="ti ti-bolt ti-fw"></i> <i v-if="store.r.realtimeMode.value" class="ti ti-bolt ti-fw"></i>
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div :class="$style.middle"> <div :class="$style.middle">
<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact> <MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw" style="view-transition-name: navbar-homeIcon;"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> <i :class="$style.itemIcon" class="ti ti-home ti-fw" style="viewTransitionName: navbar-homeIcon;"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
</MkA> </MkA>
<template v-for="item in prefer.r.menu.value"> <template v-for="item in prefer.r.menu.value">
<div v-if="item === '-'" :class="$style.divider"></div> <div v-if="item === '-'" :class="$style.divider"></div>
@ -43,14 +43,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<div :class="$style.divider"></div> <div :class="$style.divider"></div>
<MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin"> <MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw" style="view-transition-name: navbar-controlPanel;"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw" style="viewTransitionName: navbar-controlPanel;"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
</MkA> </MkA>
<button class="_button" :class="$style.item" @click="more"> <button class="_button" :class="$style.item" @click="more">
<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw" style="view-transition-name: navbar-more;"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw" style="viewTransitionName: navbar-more;"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span> <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
</button> </button>
<MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings"> <MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings">
<i :class="$style.itemIcon" class="ti ti-settings ti-fw" style="view-transition-name: navbar-settings;"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> <i :class="$style.itemIcon" class="ti ti-settings ti-fw" style="viewTransitionName: navbar-settings;"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
</MkA> </MkA>
</div> </div>
<div :class="$style.bottom"> <div :class="$style.bottom">
@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span> <i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
</button> </button>
<button v-if="$i != null" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu"> <button v-if="$i != null" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
<MkAvatar :user="$i" :class="$style.avatar" style="view-transition-name: navbar-avatar;"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/> <MkAvatar :user="$i" :class="$style.avatar" style="viewTransitionName: navbar-avatar;"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
</button> </button>
</div> </div>
</div> </div>

View File

@ -6972,6 +6972,24 @@ export interface Locale extends ILocale {
* *
*/ */
"showActivitiesForVisitor": string; "showActivitiesForVisitor": string;
/**
*
*/
"features": string;
"_spReactions": {
/**
*
*/
"enable": string;
/**
*
*/
"description1": string;
/**
*
*/
"description2": string;
};
"_userGeneratedContentsVisibilityForVisitor": { "_userGeneratedContentsVisibilityForVisitor": {
/** /**
* *
@ -10929,6 +10947,10 @@ export interface Locale extends ILocale {
* *
*/ */
"reaction": string; "reaction": string;
/**
*
*/
"spReaction": string;
/** /**
* *
*/ */

View File

@ -3251,7 +3251,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json']; type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "scheduledNotePosted", "scheduledNotePostFailed", "receiveFollowRequest", "followRequestAccepted", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"]; export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "spReaction", "pollEnded", "scheduledNotePosted", "scheduledNotePostFailed", "receiveFollowRequest", "followRequestAccepted", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"];
// @public (undocumented) // @public (undocumented)
export function nyaize(text: string): string; export function nyaize(text: string): string;
@ -3466,7 +3466,7 @@ type RoleLite = components['schemas']['RoleLite'];
type RolePolicies = components['schemas']['RolePolicies']; type RolePolicies = components['schemas']['RolePolicies'];
// @public (undocumented) // @public (undocumented)
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable"]; export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable", "spReactionsMonthlyLimit"];
// @public (undocumented) // @public (undocumented)
type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json']; type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json'];

View File

@ -4186,6 +4186,15 @@ export type components = {
/** Format: misskey:id */ /** Format: misskey:id */
userListId: string; userListId: string;
}; };
spReaction?: {
/** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
} | {
/** @enum {string} */
type: 'list';
/** Format: misskey:id */
userListId: string;
};
pollEnded?: { pollEnded?: {
/** @enum {string} */ /** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
@ -4622,6 +4631,18 @@ export type components = {
userId: string; userId: string;
note: components['schemas']['Note']; note: components['schemas']['Note'];
reaction: string; reaction: string;
} | {
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
/** @enum {string} */
type: 'spReaction';
user: components['schemas']['UserLite'];
/** Format: id */
userId: string;
note: components['schemas']['Note'];
reaction: string;
} | { } | {
/** Format: id */ /** Format: id */
id: string; id: string;
@ -9520,6 +9541,7 @@ export interface operations {
remoteNotesCleaningExpiryDaysForEachNotes: number; remoteNotesCleaningExpiryDaysForEachNotes: number;
remoteNotesCleaningMaxProcessingDurationInMinutes: number; remoteNotesCleaningMaxProcessingDurationInMinutes: number;
showRoleBadgesOfRemoteUsers: boolean; showRoleBadgesOfRemoteUsers: boolean;
enableSpReaction: boolean;
}; };
}; };
}; };
@ -12846,6 +12868,7 @@ export interface operations {
remoteNotesCleaningExpiryDaysForEachNotes?: number; remoteNotesCleaningExpiryDaysForEachNotes?: number;
remoteNotesCleaningMaxProcessingDurationInMinutes?: number; remoteNotesCleaningMaxProcessingDurationInMinutes?: number;
showRoleBadgesOfRemoteUsers?: boolean; showRoleBadgesOfRemoteUsers?: boolean;
enableSpReaction?: boolean;
}; };
}; };
}; };
@ -26250,8 +26273,8 @@ export interface operations {
untilDate?: number; untilDate?: number;
/** @default true */ /** @default true */
markAsRead?: boolean; markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'spReaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'spReaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
}; };
}; };
}; };
@ -26335,8 +26358,8 @@ export interface operations {
untilDate?: number; untilDate?: number;
/** @default true */ /** @default true */
markAsRead?: boolean; markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'spReaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'spReaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
}; };
}; };
}; };
@ -27601,6 +27624,15 @@ export interface operations {
/** Format: misskey:id */ /** Format: misskey:id */
userListId: string; userListId: string;
}; };
spReaction?: {
/** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
} | {
/** @enum {string} */
type: 'list';
/** Format: misskey:id */
userListId: string;
};
pollEnded?: { pollEnded?: {
/** @enum {string} */ /** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';

View File

@ -24,6 +24,7 @@ export const notificationTypes = [
'renote', 'renote',
'quote', 'quote',
'reaction', 'reaction',
'spReaction',
'pollEnded', 'pollEnded',
'scheduledNotePosted', 'scheduledNotePosted',
'scheduledNotePostFailed', 'scheduledNotePostFailed',
@ -229,6 +230,7 @@ export const rolePolicies = [
'noteDraftLimit', 'noteDraftLimit',
'scheduledNoteLimit', 'scheduledNoteLimit',
'watermarkAvailable', 'watermarkAvailable',
'spReactionsMonthlyLimit',
] as const; ] as const;
export const queueTypes = [ export const queueTypes = [

View File

@ -32,8 +32,8 @@ importers:
specifier: 8.5.6 specifier: 8.5.6
version: 8.5.6 version: 8.5.6
tar: tar:
specifier: 7.5.7 specifier: 7.5.6
version: 7.5.7 version: 7.5.6
terser: terser:
specifier: 5.46.0 specifier: 5.46.0
version: 5.46.0 version: 5.46.0
@ -10178,8 +10178,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
tar@7.5.7: tar@7.5.6:
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==}
engines: {node: '>=18'} engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
@ -20126,7 +20126,7 @@ snapshots:
nopt: 9.0.0 nopt: 9.0.0
proc-log: 6.1.0 proc-log: 6.1.0
semver: 7.7.3 semver: 7.7.3
tar: 7.5.7 tar: 7.5.6
tinyglobby: 0.2.15 tinyglobby: 0.2.15
which: 6.0.0 which: 6.0.0
transitivePeerDependencies: transitivePeerDependencies:
@ -21992,7 +21992,7 @@ snapshots:
yallist: 4.0.0 yallist: 4.0.0
optional: true optional: true
tar@7.5.7: tar@7.5.6:
dependencies: dependencies:
'@isaacs/fs-minipass': 4.0.1 '@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0 chownr: 3.0.0