Compare commits
12 Commits
7022b16bce
...
77498f84d8
| Author | SHA1 | Date |
|---|---|---|
|
|
77498f84d8 | |
|
|
0575207463 | |
|
|
5ee93dc4a2 | |
|
|
10ae0b329a | |
|
|
7a3dd400d8 | |
|
|
e840544dd2 | |
|
|
0e58f515fd | |
|
|
c3714c02ba | |
|
|
9de11da170 | |
|
|
e12943c15b | |
|
|
000abcd2f0 | |
|
|
e00fdc2d59 |
|
|
@ -22,8 +22,12 @@
|
|||
|
||||
### General
|
||||
- ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
|
||||
- ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
|
||||
- ソフトワードミュートとハードワードミュートは統合されました
|
||||
|
||||
### Client
|
||||
- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正
|
||||
|
||||
### Server
|
||||
- タイムライン取得時のパフォーマンスを改善
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
export class UserListMembership1696323464251 {
|
||||
name = 'UserListMembership1696323464251'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_list_joining" RENAME TO "user_list_membership"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_list_membership" RENAME TO "user_list_joining"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export class Hibernation1696331570827 {
|
||||
name = 'Hibernation1696331570827'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`);
|
||||
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
|
@ -42,8 +42,8 @@ export class AccountMoveService {
|
|||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
|
@ -215,40 +215,40 @@ export class AccountMoveService {
|
|||
@bindThis
|
||||
public async updateLists(src: ThinUser, dst: MiUser): Promise<void> {
|
||||
// Return if there is no list to be updated.
|
||||
const oldJoinings = await this.userListJoiningsRepository.find({
|
||||
const oldMemberships = await this.userListMembershipsRepository.find({
|
||||
where: {
|
||||
userId: src.id,
|
||||
},
|
||||
});
|
||||
if (oldJoinings.length === 0) return;
|
||||
if (oldMemberships.length === 0) return;
|
||||
|
||||
const existingUserListIds = await this.userListJoiningsRepository.find({
|
||||
const existingUserListIds = await this.userListMembershipsRepository.find({
|
||||
where: {
|
||||
userId: dst.id,
|
||||
},
|
||||
}).then(joinings => joinings.map(joining => joining.userListId));
|
||||
}).then(memberships => memberships.map(membership => membership.userListId));
|
||||
|
||||
const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
|
||||
const newMemberships: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
|
||||
|
||||
// 重複しないようにIDを生成
|
||||
const genId = (): string => {
|
||||
let id: string;
|
||||
do {
|
||||
id = this.idService.genId();
|
||||
} while (newJoinings.has(id));
|
||||
} while (newMemberships.has(id));
|
||||
return id;
|
||||
};
|
||||
for (const joining of oldJoinings) {
|
||||
if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list
|
||||
newJoinings.set(genId(), {
|
||||
for (const membership of oldMemberships) {
|
||||
if (existingUserListIds.includes(membership.userListId)) continue; // skip if dst exists in this user's list
|
||||
newMemberships.set(genId(), {
|
||||
createdAt: new Date(),
|
||||
userId: dst.id,
|
||||
userListId: joining.userListId,
|
||||
userListId: membership.userListId,
|
||||
});
|
||||
}
|
||||
|
||||
const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
||||
await this.userListJoiningsRepository.insert(arrayToInsert);
|
||||
const arrayToInsert = Array.from(newMemberships.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
||||
await this.userListMembershipsRepository.insert(arrayToInsert);
|
||||
|
||||
// Have the proxy account follow the new account in the same way as UserListService.push
|
||||
if (this.userEntityService.isRemoteUser(dst)) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|||
import * as Acct from '@/misc/acct.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
|
||||
import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
|
|
@ -33,8 +33,8 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
@Inject(DI.antennasRepository)
|
||||
private antennasRepository: AntennasRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
|
|
@ -108,7 +108,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
if (antenna.src === 'home') {
|
||||
// TODO
|
||||
} else if (antenna.src === 'list') {
|
||||
const listUsers = (await this.userListJoiningsRepository.findBy({
|
||||
const listUsers = (await this.userListMembershipsRepository.findBy({
|
||||
userListId: antenna.userListId!,
|
||||
})).map(x => x.userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import { SignupService } from './SignupService.js';
|
|||
import { WebAuthnService } from './WebAuthnService.js';
|
||||
import { UserBlockingService } from './UserBlockingService.js';
|
||||
import { CacheService } from './CacheService.js';
|
||||
import { UserService } from './UserService.js';
|
||||
import { UserFollowingService } from './UserFollowingService.js';
|
||||
import { UserKeypairService } from './UserKeypairService.js';
|
||||
import { UserListService } from './UserListService.js';
|
||||
|
|
@ -173,6 +174,7 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup
|
|||
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
|
||||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
||||
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
||||
const $UserService: Provider = { provide: 'UserService', useExisting: UserService };
|
||||
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||
|
|
@ -303,6 +305,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
WebAuthnService,
|
||||
UserBlockingService,
|
||||
CacheService,
|
||||
UserService,
|
||||
UserFollowingService,
|
||||
UserKeypairService,
|
||||
UserListService,
|
||||
|
|
@ -426,6 +429,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$WebAuthnService,
|
||||
$UserBlockingService,
|
||||
$CacheService,
|
||||
$UserService,
|
||||
$UserFollowingService,
|
||||
$UserKeypairService,
|
||||
$UserListService,
|
||||
|
|
@ -550,6 +554,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
WebAuthnService,
|
||||
UserBlockingService,
|
||||
CacheService,
|
||||
UserService,
|
||||
UserFollowingService,
|
||||
UserKeypairService,
|
||||
UserListService,
|
||||
|
|
@ -672,6 +677,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$WebAuthnService,
|
||||
$UserBlockingService,
|
||||
$CacheService,
|
||||
$UserService,
|
||||
$UserFollowingService,
|
||||
$UserKeypairService,
|
||||
$UserListService,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { setImmediate } from 'node:timers/promises';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { In, DataSource, IsNull } from 'typeorm';
|
||||
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import RE2 from 're2';
|
||||
|
|
@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
|||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListJoiningsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiApp } from '@/models/App.js';
|
||||
import { concat } from '@/misc/prelude/array.js';
|
||||
|
|
@ -173,8 +173,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
|
@ -829,22 +829,21 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: 休眠ユーザーを弾く
|
||||
// TODO: チャンネルフォロー
|
||||
// TODO: キャッシュ?
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerHost: IsNull(),
|
||||
isFollowerHibernated: false,
|
||||
},
|
||||
select: ['followerId', 'withReplies'],
|
||||
});
|
||||
|
||||
const userLists = await this.userListJoiningsRepository.find({
|
||||
const userListMemberships = await this.userListMembershipsRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: ['userListId'],
|
||||
select: ['userListId', 'withReplies'],
|
||||
});
|
||||
|
||||
// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする
|
||||
|
|
@ -875,21 +874,21 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
// userLists = userLists.filter(x => followings.some(f => f.followerId === x.userListUserId));
|
||||
//}
|
||||
|
||||
for (const userList of userLists) {
|
||||
for (const userListMembership of userListMemberships) {
|
||||
// 自分自身以外への返信
|
||||
if (note.replyId && note.replyUserId !== note.userId) {
|
||||
if (!userList.withReplies) continue;
|
||||
if (!userListMembership.withReplies) continue;
|
||||
}
|
||||
|
||||
redisPipeline.xadd(
|
||||
`userListTimeline:${userList.userListId}`,
|
||||
`userListTimeline:${userListMembership.userListId}`,
|
||||
'MAXLEN', '~', '200',
|
||||
'*',
|
||||
'note', note.id);
|
||||
|
||||
if (note.fileIds.length > 0) {
|
||||
redisPipeline.xadd(
|
||||
`userListTimelineWithFiles:${userList.userListId}`,
|
||||
`userListTimelineWithFiles:${userListMembership.userListId}`,
|
||||
'MAXLEN', '~', '100',
|
||||
'*',
|
||||
'note', note.id);
|
||||
|
|
@ -917,20 +916,20 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
if (note.replyId && note.replyUserId !== note.userId) {
|
||||
redisPipeline.xadd(
|
||||
`userTimelineWithReplies:${user.id}`,
|
||||
'MAXLEN', '~', '200',
|
||||
'MAXLEN', '~', '1000',
|
||||
'*',
|
||||
'note', note.id);
|
||||
} else {
|
||||
redisPipeline.xadd(
|
||||
`userTimeline:${user.id}`,
|
||||
'MAXLEN', '~', '200',
|
||||
'MAXLEN', '~', '1000',
|
||||
'*',
|
||||
'note', note.id);
|
||||
|
||||
if (note.fileIds.length > 0) {
|
||||
redisPipeline.xadd(
|
||||
`userTimelineWithFiles:${user.id}`,
|
||||
'MAXLEN', '~', '100',
|
||||
'MAXLEN', '~', '500',
|
||||
'*',
|
||||
'note', note.id);
|
||||
}
|
||||
|
|
@ -952,11 +951,55 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.random() < 0.1) {
|
||||
process.nextTick(() => {
|
||||
this.checkHibernation(followings);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async checkHibernation(followings: MiFollowing[]) {
|
||||
if (followings.length === 0) return;
|
||||
|
||||
const shuffle = (array: MiFollowing[]) => {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
};
|
||||
|
||||
// ランダムに最大1000件サンプリング
|
||||
const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000));
|
||||
|
||||
const hibernatedUsers = await this.usersRepository.find({
|
||||
where: {
|
||||
id: In(samples.map(x => x.followerId)),
|
||||
lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
|
||||
},
|
||||
select: ['id'],
|
||||
});
|
||||
|
||||
if (hibernatedUsers.length > 0) {
|
||||
this.usersRepository.update({
|
||||
id: In(hibernatedUsers.map(x => x.id)),
|
||||
}, {
|
||||
isHibernated: true,
|
||||
});
|
||||
|
||||
this.followingsRepository.update({
|
||||
followerId: In(hibernatedUsers.map(x => x.id)),
|
||||
}, {
|
||||
isFollowerHibernated: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type { MiBlocking } from '@/models/Blocking.js';
|
|||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
|
||||
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
|
|
@ -38,8 +38,8 @@ export class UserBlockingService implements OnModuleInit {
|
|||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
|
|
@ -149,7 +149,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||
});
|
||||
|
||||
for (const userList of userLists) {
|
||||
await this.userListJoiningsRepository.delete({
|
||||
await this.userListMembershipsRepository.delete({
|
||||
userListId: userList.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { UserListJoiningsRepository } from '@/models/_.js';
|
||||
import type { UserListMembershipsRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiUserList } from '@/models/UserList.js';
|
||||
import type { MiUserListJoining } from '@/models/UserListJoining.js';
|
||||
import type { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
|
@ -33,8 +33,8 @@ export class UserListService implements OnApplicationShutdown {
|
|||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
|
|
@ -46,7 +46,7 @@ export class UserListService implements OnApplicationShutdown {
|
|||
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
|
||||
fetcher: (key) => this.userListMembershipsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
|
@ -85,19 +85,19 @@ export class UserListService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
|
||||
const currentCount = await this.userListJoiningsRepository.countBy({
|
||||
const currentCount = await this.userListMembershipsRepository.countBy({
|
||||
userListId: list.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||
throw new UserListService.TooManyUsersError();
|
||||
}
|
||||
|
||||
await this.userListJoiningsRepository.insert({
|
||||
await this.userListMembershipsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
userId: target.id,
|
||||
userListId: list.id,
|
||||
} as MiUserListJoining);
|
||||
} as MiUserListMembership);
|
||||
|
||||
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
|
||||
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
||||
|
|
@ -113,7 +113,7 @@ export class UserListService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public async removeMember(target: MiUser, list: MiUserList) {
|
||||
await this.userListJoiningsRepository.delete({
|
||||
await this.userListMembershipsRepository.delete({
|
||||
userId: target.id,
|
||||
userListId: list.id,
|
||||
});
|
||||
|
|
@ -122,6 +122,24 @@ export class UserListService implements OnApplicationShutdown {
|
|||
this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateMembership(target: MiUser, list: MiUserList, options: { withReplies?: boolean }) {
|
||||
const membership = await this.userListMembershipsRepository.findOneBy({
|
||||
userId: target.id,
|
||||
userListId: list.id,
|
||||
});
|
||||
|
||||
if (membership == null) {
|
||||
throw new Error('User is not a member of the list');
|
||||
}
|
||||
|
||||
await this.userListMembershipsRepository.update({
|
||||
id: membership.id,
|
||||
}, {
|
||||
withReplies: options.withReplies,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateLastActiveDate(user: MiUser): Promise<void> {
|
||||
if (user.isHibernated) {
|
||||
const result = await this.usersRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
lastActiveDate: new Date(),
|
||||
})
|
||||
.where('id = :id', { id: user.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
const wokeUp = result.isHibernated;
|
||||
if (wokeUp) {
|
||||
this.usersRepository.update(user.id, {
|
||||
isHibernated: false,
|
||||
});
|
||||
this.followingsRepository.update({
|
||||
followerId: user.id,
|
||||
}, {
|
||||
isFollowerHibernated: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,12 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUserList } from '@/models/UserList.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserListEntityService {
|
||||
|
|
@ -17,8 +18,10 @@ export class UserListEntityService {
|
|||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +31,7 @@ export class UserListEntityService {
|
|||
): Promise<Packed<'UserList'>> {
|
||||
const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await this.userListJoiningsRepository.findBy({
|
||||
const users = await this.userListMembershipsRepository.findBy({
|
||||
userListId: userList.id,
|
||||
});
|
||||
|
||||
|
|
@ -40,5 +43,18 @@ export class UserListEntityService {
|
|||
isPublic: userList.isPublic,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMembershipsMany(
|
||||
memberships: MiUserListMembership[],
|
||||
) {
|
||||
return Promise.all(memberships.map(async x => ({
|
||||
id: x.id,
|
||||
createdAt: x.createdAt.toISOString(),
|
||||
userId: x.userId,
|
||||
user: await this.userEntityService.pack(x.userId),
|
||||
withReplies: x.withReplies,
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const DI = {
|
|||
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
||||
userListsRepository: Symbol('userListsRepository'),
|
||||
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
|
||||
userListJoiningsRepository: Symbol('userListJoiningsRepository'),
|
||||
userListMembershipsRepository: Symbol('userListMembershipsRepository'),
|
||||
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
||||
userIpsRepository: Symbol('userIpsRepository'),
|
||||
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { MiUser } from './User.js';
|
|||
|
||||
@Entity('following')
|
||||
@Index(['followerId', 'followeeId'], { unique: true })
|
||||
@Index(['followeeId', 'followerHost'])
|
||||
@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
|
||||
export class MiFollowing {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
|
@ -46,6 +46,11 @@ export class MiFollowing {
|
|||
@JoinColumn()
|
||||
public follower: MiUser | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isFollowerHibernated: boolean;
|
||||
|
||||
// タイムラインにその人のリプライまで含めるかどうか
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
|
|
@ -117,9 +117,9 @@ const $userListFavoritesRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userListJoiningsRepository: Provider = {
|
||||
provide: DI.userListJoiningsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiUserListJoining),
|
||||
const $userListMembershipsRepository: Provider = {
|
||||
provide: DI.userListMembershipsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
|
|
@ -415,7 +415,7 @@ const $userMemosRepository: Provider = {
|
|||
$userPublickeysRepository,
|
||||
$userListsRepository,
|
||||
$userListFavoritesRepository,
|
||||
$userListJoiningsRepository,
|
||||
$userListMembershipsRepository,
|
||||
$userNotePiningsRepository,
|
||||
$userIpsRepository,
|
||||
$usedUsernamesRepository,
|
||||
|
|
@ -481,7 +481,7 @@ const $userMemosRepository: Provider = {
|
|||
$userPublickeysRepository,
|
||||
$userListsRepository,
|
||||
$userListFavoritesRepository,
|
||||
$userListJoiningsRepository,
|
||||
$userListMembershipsRepository,
|
||||
$userNotePiningsRepository,
|
||||
$userIpsRepository,
|
||||
$usedUsernamesRepository,
|
||||
|
|
|
|||
|
|
@ -187,6 +187,11 @@ export class MiUser {
|
|||
})
|
||||
public isExplorable: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isHibernated: boolean;
|
||||
|
||||
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ import { id } from './util/id.js';
|
|||
import { MiUser } from './User.js';
|
||||
import { MiUserList } from './UserList.js';
|
||||
|
||||
@Entity('user_list_joining')
|
||||
@Entity('user_list_membership')
|
||||
@Index(['userId', 'userListId'], { unique: true })
|
||||
export class MiUserListJoining {
|
||||
export class MiUserListMembership {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the UserListJoining.',
|
||||
comment: 'The created date of the UserListMembership.',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ import { MiUser } from '@/models/User.js';
|
|||
import { MiUserIp } from '@/models/UserIp.js';
|
||||
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||
import { MiUserList } from '@/models/UserList.js';
|
||||
import { MiUserListJoining } from '@/models/UserListJoining.js';
|
||||
import { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
||||
import { MiUserPending } from '@/models/UserPending.js';
|
||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
|
|
@ -120,7 +120,7 @@ export {
|
|||
MiUserKeypair,
|
||||
MiUserList,
|
||||
MiUserListFavorite,
|
||||
MiUserListJoining,
|
||||
MiUserListMembership,
|
||||
MiUserNotePining,
|
||||
MiUserPending,
|
||||
MiUserProfile,
|
||||
|
|
@ -186,7 +186,7 @@ export type UserIpsRepository = Repository<MiUserIp>;
|
|||
export type UserKeypairsRepository = Repository<MiUserKeypair>;
|
||||
export type UserListsRepository = Repository<MiUserList>;
|
||||
export type UserListFavoritesRepository = Repository<MiUserListFavorite>;
|
||||
export type UserListJoiningsRepository = Repository<MiUserListJoining>;
|
||||
export type UserListMembershipsRepository = Repository<MiUserListMembership>;
|
||||
export type UserNotePiningsRepository = Repository<MiUserNotePining>;
|
||||
export type UserPendingsRepository = Repository<MiUserPending>;
|
||||
export type UserProfilesRepository = Repository<MiUserProfile>;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ import { MiUserIp } from '@/models/UserIp.js';
|
|||
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||
import { MiUserList } from '@/models/UserList.js';
|
||||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
||||
import { MiUserListJoining } from '@/models/UserListJoining.js';
|
||||
import { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
||||
import { MiUserPending } from '@/models/UserPending.js';
|
||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
|
|
@ -137,7 +137,7 @@ export const entities = [
|
|||
MiUserPublickey,
|
||||
MiUserList,
|
||||
MiUserListFavorite,
|
||||
MiUserListJoining,
|
||||
MiUserListMembership,
|
||||
MiUserNotePining,
|
||||
MiUserSecurityKey,
|
||||
MiUsedUsername,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { format as DateFormat } from 'date-fns';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, MiUser } from '@/models/_.js';
|
||||
import type { AntennasRepository, UsersRepository, UserListMembershipsRepository, MiUser } from '@/models/_.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
|
@ -29,8 +29,8 @@ export class ExportAntennasProcessorService {
|
|||
@Inject(DI.antennasRepository)
|
||||
private antennsRepository: AntennasRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private driveService: DriveService,
|
||||
private utilityService: UtilityService,
|
||||
|
|
@ -65,9 +65,9 @@ export class ExportAntennasProcessorService {
|
|||
for (const [index, antenna] of antennas.entries()) {
|
||||
let users: MiUser[] | undefined;
|
||||
if (antenna.userListId !== null) {
|
||||
const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId });
|
||||
const memberships = await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId });
|
||||
users = await this.usersRepository.findBy({
|
||||
id: In(joinings.map(j => j.userId)),
|
||||
id: In(memberships.map(j => j.userId)),
|
||||
});
|
||||
}
|
||||
write(JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { In } from 'typeorm';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { UserListMembershipsRepository, UserListsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
|
|
@ -29,8 +29,8 @@ export class ExportUserListsProcessorService {
|
|||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private driveService: DriveService,
|
||||
|
|
@ -61,9 +61,9 @@ export class ExportUserListsProcessorService {
|
|||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
for (const list of lists) {
|
||||
const joinings = await this.userListJoiningsRepository.findBy({ userListId: list.id });
|
||||
const memberships = await this.userListMembershipsRepository.findBy({ userListId: list.id });
|
||||
const users = await this.usersRepository.findBy({
|
||||
id: In(joinings.map(j => j.userId)),
|
||||
id: In(memberships.map(j => j.userId)),
|
||||
});
|
||||
|
||||
for (const u of users) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { UsersRepository, DriveFilesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
|
|
@ -33,8 +33,8 @@ export class ImportUserListsProcessorService {
|
|||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
|
|
@ -99,7 +99,7 @@ export class ImportUserListsProcessorService {
|
|||
target = await this.remoteUserResolveService.resolveUser(username, host);
|
||||
}
|
||||
|
||||
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
||||
if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
||||
|
||||
this.userListService.addMember(target, list!, user);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -335,7 +335,9 @@ import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
|||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
|
||||
import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js';
|
||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||
import * as ep___users_flashs from './endpoints/users/flashs.js';
|
||||
|
|
@ -683,7 +685,9 @@ const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass:
|
|||
const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
|
||||
const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
|
||||
const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
|
||||
const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default };
|
||||
const $users_lists_createFromPublic: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_createFromPublic.default };
|
||||
const $users_lists_updateMembership: Provider = { provide: 'ep:users/lists/update-membership', useClass: ep___users_lists_updateMembership.default };
|
||||
const $users_lists_getMemberships: Provider = { provide: 'ep:users/lists/get-memberships', useClass: ep___users_lists_getMemberships.default };
|
||||
const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
|
||||
const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
|
||||
const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default };
|
||||
|
|
@ -1035,7 +1039,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$users_lists_update,
|
||||
$users_lists_favorite,
|
||||
$users_lists_unfavorite,
|
||||
$users_lists_create_from_public,
|
||||
$users_lists_createFromPublic,
|
||||
$users_lists_updateMembership,
|
||||
$users_lists_getMemberships,
|
||||
$users_notes,
|
||||
$users_pages,
|
||||
$users_flashs,
|
||||
|
|
@ -1378,7 +1384,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$users_lists_update,
|
||||
$users_lists_favorite,
|
||||
$users_lists_unfavorite,
|
||||
$users_lists_create_from_public,
|
||||
$users_lists_createFromPublic,
|
||||
$users_lists_updateMembership,
|
||||
$users_lists_getMemberships,
|
||||
$users_notes,
|
||||
$users_pages,
|
||||
$users_flashs,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { NotificationService } from '@/core/NotificationService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { UserService } from '@/core/UserService.js';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/Connection.js';
|
||||
import { ChannelsService } from './stream/ChannelsService.js';
|
||||
|
|
@ -37,6 +38,7 @@ export class StreamingApiServerService {
|
|||
private authenticateService: AuthenticateService,
|
||||
private channelsService: ChannelsService,
|
||||
private notificationService: NotificationService,
|
||||
private usersService: UserService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -130,14 +132,10 @@ export class StreamingApiServerService {
|
|||
this.#connections.set(connection, Date.now());
|
||||
|
||||
const userUpdateIntervalId = user ? setInterval(() => {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
});
|
||||
this.usersService.updateLastActiveDate(user);
|
||||
}, 1000 * 60 * 5) : null;
|
||||
if (user) {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
});
|
||||
this.usersService.updateLastActiveDate(user);
|
||||
}
|
||||
|
||||
connection.once('close', () => {
|
||||
|
|
|
|||
|
|
@ -334,8 +334,10 @@ import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
|||
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||
import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
|
||||
import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js';
|
||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||
import * as ep___users_flashs from './endpoints/users/flashs.js';
|
||||
|
|
@ -681,7 +683,9 @@ const eps = [
|
|||
['users/lists/favorite', ep___users_lists_favorite],
|
||||
['users/lists/unfavorite', ep___users_lists_unfavorite],
|
||||
['users/lists/update', ep___users_lists_update],
|
||||
['users/lists/create-from-public', ep___users_lists_create_from_public],
|
||||
['users/lists/create-from-public', ep___users_lists_createFromPublic],
|
||||
['users/lists/update-membership', ep___users_lists_updateMembership],
|
||||
['users/lists/get-memberships', ep___users_lists_getMemberships],
|
||||
['users/notes', ep___users_notes],
|
||||
['users/pages', ep___users_pages],
|
||||
['users/flashs', ep___users_flashs],
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { NotesRepository, UserListsRepository, UserListJoiningsRepository, MiNote } from '@/models/_.js';
|
||||
import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUserList } from '@/models/UserList.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
|
|
@ -76,8 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
|
@ -110,7 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
name: ps.name,
|
||||
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const users = (await this.userListJoiningsRepository.findBy({
|
||||
const users = (await this.userListMembershipsRepository.findBy({
|
||||
userListId: ps.listId,
|
||||
})).map(x => x.userId);
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
}
|
||||
|
||||
const exist = await this.userListJoiningsRepository.exist({
|
||||
const exist = await this.userListMembershipsRepository.exist({
|
||||
where: {
|
||||
userListId: userList.id,
|
||||
userId: currentUser.id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository, UserListFavoritesRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'account'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
forPublic: { type: 'boolean', default: false },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['listId'],
|
||||
} as const;
|
||||
|
||||
@Injectable() // eslint-disable-next-line import/no-default-export
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private userListEntityService: UserListEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? {
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
} : {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.userListMembershipsRepository.createQueryBuilder('membership'), ps.sinceId, ps.untilId)
|
||||
.andWhere('membership.userListId = :userListId', { userListId: userList.id })
|
||||
.innerJoinAndSelect('membership.user', 'user');
|
||||
|
||||
const memberships = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return this.userListEntityService.packMembershipsMany(memberships);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
|
|
@ -76,8 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
|
@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
}
|
||||
|
||||
const exist = await this.userListJoiningsRepository.exist({
|
||||
const exist = await this.userListMembershipsRepository.exist({
|
||||
where: {
|
||||
userListId: userList.id,
|
||||
userId: user.id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'users'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02',
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '588e7f72-c744-4a61-b180-d354e912bda2',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
withReplies: { type: 'boolean' },
|
||||
},
|
||||
required: ['listId', 'userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
private userListService: UserListService,
|
||||
private getterService: GetterService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
const user = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
|
||||
await this.userListService.updateMembership(user, userList, {
|
||||
withReplies: ps.withReplies,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
|
@ -18,12 +18,12 @@ class UserListChannel extends Channel {
|
|||
public static shouldShare = false;
|
||||
public static requireCredential = false;
|
||||
private listId: string;
|
||||
public listUsers: MiUser['id'][] = [];
|
||||
public membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||
private listUsersClock: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private userListsRepository: UserListsRepository,
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
||||
id: string,
|
||||
|
|
@ -58,19 +58,25 @@ class UserListChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async updateListUsers() {
|
||||
const users = await this.userListJoiningsRepository.find({
|
||||
const memberships = await this.userListMembershipsRepository.find({
|
||||
where: {
|
||||
userListId: this.listId,
|
||||
},
|
||||
select: ['userId'],
|
||||
});
|
||||
|
||||
this.listUsers = users.map(x => x.userId);
|
||||
const membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||
for (const membership of memberships) {
|
||||
membershipsMap[membership.userId] = {
|
||||
withReplies: membership.withReplies,
|
||||
};
|
||||
}
|
||||
this.membershipsMap = membershipsMap;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
if (!this.listUsers.includes(note.userId)) return;
|
||||
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
|
||||
|
||||
if (['followers', 'specified'].includes(note.visibility)) {
|
||||
note = await this.noteEntityService.pack(note.id, this.user, {
|
||||
|
|
@ -95,6 +101,13 @@ class UserListChannel extends Channel {
|
|||
}
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.membershipsMap[note.userId]?.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
|
@ -124,8 +137,8 @@ export class UserListChannelService {
|
|||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
) {
|
||||
|
|
@ -135,7 +148,7 @@ export class UserListChannelService {
|
|||
public create(id: string, connection: Channel['connection']): UserListChannel {
|
||||
return new UserListChannel(
|
||||
this.userListsRepository,
|
||||
this.userListJoiningsRepository,
|
||||
this.userListMembershipsRepository,
|
||||
this.noteEntityService,
|
||||
id,
|
||||
connection,
|
||||
|
|
|
|||
|
|
@ -600,6 +600,53 @@ describe('Timelines', () => {
|
|||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, null);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||
|
||||
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||
|
||||
await sleep(100); // redisに追加されるのを待つ
|
||||
|
||||
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||
const bobNote1 = await post(bob, { text: 'hi' });
|
||||
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
|
||||
|
||||
await sleep(100); // redisに追加されるのを待つ
|
||||
|
||||
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
|
||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||
|
||||
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||
await api('/users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice);
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||
|
||||
await sleep(100); // redisに追加されるのを待つ
|
||||
|
||||
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ describe('AnnouncementService', () => {
|
|||
})
|
||||
.compile();
|
||||
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
announcementService = app.get<AnnouncementService>(AnnouncementService);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ describe('DriveService', () => {
|
|||
imports: [GlobalModule, CoreModule],
|
||||
providers: [DriveService],
|
||||
}).compile();
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
driveService = app.get<DriveService>(DriveService);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ describe('FetchInstanceMetadataService', () => {
|
|||
}})
|
||||
.compile();
|
||||
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ describe('FileInfoService', () => {
|
|||
})
|
||||
.compile();
|
||||
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
fileInfoService = app.get<FileInfoService>(FileInfoService);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ describe('MetaService', () => {
|
|||
],
|
||||
}).compile();
|
||||
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
metaService = app.get<MetaService>(MetaService, { strict: false });
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ describe('MfmService', () => {
|
|||
const app = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule],
|
||||
}).compile();
|
||||
await app.init();
|
||||
mfmService = app.get<MfmService>(MfmService);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ describe('ReactionService', () => {
|
|||
const app = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule],
|
||||
}).compile();
|
||||
await app.init();
|
||||
reactionService = app.get<ReactionService>(ReactionService);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ describe('RelayService', () => {
|
|||
})
|
||||
.compile();
|
||||
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
relayService = app.get<RelayService>(RelayService);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ describe('RoleService', () => {
|
|||
})
|
||||
.compile();
|
||||
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
roleService = app.get<RoleService>(RoleService);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ describe('S3Service', () => {
|
|||
imports: [GlobalModule, CoreModule],
|
||||
providers: [S3Service],
|
||||
}).compile();
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
s3Service = app.get<S3Service>(S3Service);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ const patrons = [
|
|||
'kino3277',
|
||||
'美少女JKぐーちゃん',
|
||||
'てば',
|
||||
'たっくん',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
|||
|
|
@ -29,16 +29,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div class="_gaps_s">
|
||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
<div v-for="user in users" :key="user.id" :class="$style.userItem">
|
||||
<MkA :class="$style.userItemBody" :to="`${userPage(user)}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-if="fetching" class="loading"/>
|
||||
|
||||
<MkPagination ref="paginationEl" :pagination="membershipsPagination">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.id">
|
||||
<div :class="$style.userItem">
|
||||
<MkA :class="$style.userItemBody" :to="`${userPage(item.user)}`">
|
||||
<MkUserCardMini :user="item.user"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ti ti-dots"></i></button>
|
||||
<button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
|
@ -59,9 +65,11 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { userListsCache } from '@/cache';
|
||||
import { userListsCache } from '@/cache.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
|
||||
const {
|
||||
enableInfiniteScroll,
|
||||
} = defaultStore.reactiveState;
|
||||
|
|
@ -70,40 +78,25 @@ const props = defineProps<{
|
|||
listId: string;
|
||||
}>();
|
||||
|
||||
const FETCH_USERS_LIMIT = 20;
|
||||
|
||||
const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||
let list = $ref<Misskey.entities.UserList | null>(null);
|
||||
let users = $ref<Misskey.entities.UserLite[]>([]);
|
||||
let queueUserIds = $ref<string[]>([]);
|
||||
let fetching = $ref(true);
|
||||
const isPublic = ref(false);
|
||||
const name = ref('');
|
||||
const membershipsPagination = {
|
||||
endpoint: 'users/lists/get-memberships' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
listId: props.listId,
|
||||
})),
|
||||
};
|
||||
|
||||
function fetchList() {
|
||||
fetching = true;
|
||||
os.api('users/lists/show', {
|
||||
listId: props.listId,
|
||||
}).then(_list => {
|
||||
list = _list;
|
||||
name.value = list.name;
|
||||
isPublic.value = list.isPublic;
|
||||
queueUserIds = list.userIds;
|
||||
|
||||
return fetchMoreUsers();
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMoreUsers() {
|
||||
if (!list) return;
|
||||
if (fetching && users.length !== 0) return; // fetchingがtrueならやめるが、usersが空なら続行
|
||||
fetching = true;
|
||||
os.api('users/show', {
|
||||
userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT),
|
||||
}).then(_users => {
|
||||
users = users.concat(_users);
|
||||
queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT);
|
||||
}).finally(() => {
|
||||
fetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -114,12 +107,12 @@ function addUser() {
|
|||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
users.push(user);
|
||||
paginationEl.value.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function removeUser(user, ev) {
|
||||
async function removeUser(item, ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.remove,
|
||||
icon: 'ti ti-x',
|
||||
|
|
@ -128,9 +121,28 @@ async function removeUser(user, ev) {
|
|||
if (!list) return;
|
||||
os.api('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
userId: item.userId,
|
||||
}).then(() => {
|
||||
users = users.filter(x => x.id !== user.id);
|
||||
paginationEl.value.removeItem(item.id);
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function showMembershipMenu(item, ev) {
|
||||
os.popupMenu([{
|
||||
text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
|
||||
icon: item.withReplies ? 'ti ti-messages-off' : 'ti ti-messages',
|
||||
action: async () => {
|
||||
os.api('users/lists/update-membership', {
|
||||
listId: list.id,
|
||||
userId: item.userId,
|
||||
withReplies: !item.withReplies,
|
||||
}).then(() => {
|
||||
paginationEl.value.updateItem(item.id, (old) => ({
|
||||
...old,
|
||||
withReplies: !item.withReplies,
|
||||
}));
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
|
@ -202,6 +214,12 @@ definePageMetadata(computed(() => list ? {
|
|||
align-self: center;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkTab v-model="tab">
|
||||
<option value="renoteMute">{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</option>
|
||||
<option value="mute">{{ i18n.ts.mutedUsers }}</option>
|
||||
<option value="block">{{ i18n.ts.blockedUsers }}</option>
|
||||
</MkTab>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-repeat-off"></i></template>
|
||||
<template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template>
|
||||
|
||||
<div v-if="tab === 'renoteMute'">
|
||||
<MkPagination :pagination="renoteMutingPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
|
|
@ -37,9 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-eye-off"></i></template>
|
||||
<template #label>{{ i18n.ts.mutedUsers }}</template>
|
||||
|
||||
<div v-else-if="tab === 'mute'">
|
||||
<MkPagination :pagination="mutingPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
|
|
@ -67,9 +67,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-ban"></i></template>
|
||||
<template #label>{{ i18n.ts.blockedUsers }}</template>
|
||||
|
||||
<div v-else-if="tab === 'block'">
|
||||
<MkPagination :pagination="blockingPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
|
|
@ -97,24 +100,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
let tab = $ref('renoteMute');
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const renoteMutingPagination = {
|
||||
endpoint: 'renote-mute/list' as const,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ export function useTooltip(
|
|||
|
||||
let changeShowingState: (() => void) | null;
|
||||
|
||||
let autoHidingTimer;
|
||||
|
||||
const open = () => {
|
||||
close();
|
||||
if (!isHovering) return;
|
||||
|
|
@ -33,6 +35,16 @@ export function useTooltip(
|
|||
changeShowingState = () => {
|
||||
showing.value = false;
|
||||
};
|
||||
|
||||
autoHidingTimer = window.setInterval(() => {
|
||||
if (!document.body.contains(elRef.value)) {
|
||||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
window.clearTimeout(timeoutId);
|
||||
close();
|
||||
window.clearInterval(autoHidingTimer);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
|
|
@ -53,6 +65,7 @@ export function useTooltip(
|
|||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
window.clearTimeout(timeoutId);
|
||||
window.clearInterval(autoHidingTimer);
|
||||
close();
|
||||
};
|
||||
|
||||
|
|
@ -67,6 +80,7 @@ export function useTooltip(
|
|||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
window.clearTimeout(timeoutId);
|
||||
window.clearInterval(autoHidingTimer);
|
||||
close();
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue