Merge branch 'develop' into feat-1714

This commit is contained in:
かっこかり
2024-07-31 21:00:34 +09:00
committed by GitHub
165 changed files with 6316 additions and 4265 deletions

View File

@@ -69,6 +69,7 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' },
},
required: [],
} as const;
@@ -87,7 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
query.andWhere('announcement.isActive = true');
if (ps.status === 'archived') {
query.andWhere('announcement.isActive = false');
} else if (ps.status === 'active') {
query.andWhere('announcement.isActive = true');
}
if (ps.userId) {
query.andWhere('announcement.userId = :userId', { userId: ps.userId });
} else {

View File

@@ -128,6 +128,16 @@ export const meta = {
nullable: false,
},
},
mediaSilencedHosts: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'string',
optional: false,
nullable: false,
},
},
pinnedUsers: {
type: 'array',
optional: false, nullable: false,
@@ -552,6 +562,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
preservedUsernames: instance.preservedUsernames,

View File

@@ -150,6 +150,13 @@ export const paramDef = {
type: 'string',
},
},
mediaSilencedHosts: {
type: 'array',
nullable: true,
items: {
type: 'string',
},
},
summalyProxy: {
type: 'string', nullable: true,
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
@@ -203,6 +210,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
if (Array.isArray(ps.mediaSilencedHosts)) {
let lastValue = '';
set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => {
const lv = lastValue;
lastValue = h;
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor;
}

View File

@@ -143,6 +143,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
];
}
const [
followings,
] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(me.id),
]);
const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
untilId,
sinceId,
@@ -153,6 +159,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
noteFilter: note => {
if (note.reply && note.reply.visibility === 'followers') {
if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
}
return true;
},
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
untilId,
sinceId,

View File

@@ -114,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludePureRenotes: !ps.withRenotes,
noteFilter: note => {
if (note.reply && note.reply.visibility === 'followers') {
if (!Object.hasOwn(followings, note.reply.userId)) return false;
if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
}
return true;

View File

@@ -57,88 +57,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
ps.query = ps.query.trim();
const isUsername = ps.query.startsWith('@');
const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
let users: MiUser[] = [];
if (isUsername) {
const usernameQuery = this.usersRepository.createQueryBuilder('user')
.where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' })
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
usernameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
usernameQuery.andWhere('user.host IS NOT NULL');
}
users = await usernameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
} else {
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
// Also search username if it qualifies as username
if (this.userEntityService.validateLocalUsername(ps.query)) {
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
if (isUsername) {
qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
} else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
}
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
}
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });

View File

@@ -60,7 +60,7 @@ class HomeTimelineChannel extends Channel {
const reply = note.reply;
if (this.following[note.userId]?.withReplies) {
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
} else {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
@@ -73,7 +73,7 @@ class HomeTimelineChannel extends Channel {
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
}
}

View File

@@ -76,14 +76,22 @@ class HybridTimelineChannel extends Channel {
const reply = note.reply;
if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) {
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
} else {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
}
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
// 純粋なリノート(引用リノートでないリノート)の場合
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
}
}
if (this.user && note.renoteId && !note.text) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {