Skip to content

Commit

Permalink
fix: iroiro
Browse files Browse the repository at this point in the history
  • Loading branch information
rassi0429 committed Oct 14, 2024
1 parent 38fd881 commit 2eca776
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 15 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.10.0-resonite-love.0",
"version": "2024.10.0-resonite-love.1",
"codename": "nasubi",
"repository": {
"type": "git",
Expand Down
8 changes: 7 additions & 1 deletion packages/backend/src/core/FanoutTimelineService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';

export type FanoutTimelineName =
// home timeline
// home timeline
| `homeTimeline:${string}`
| `homeTimelineWithFiles:${string}` // only notes with files are included
// local timeline
Expand Down Expand Up @@ -42,6 +42,7 @@ export type FanoutTimelineName =
| 'vmimiRelayTimeline' // replies are not included
| 'vmimiRelayTimelineWithFiles' // only non-reply notes with files are included
| 'vmimiRelayTimelineWithReplies' // only replies are included
| `vmimiRelayTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id.

@Injectable()
export class FanoutTimelineService {
Expand All @@ -53,6 +54,11 @@ export class FanoutTimelineService {
) {
}

@bindThis
public remove(tl: FanoutTimelineName, id: string, pipeline: Redis.ChainableCommander) {
pipeline.lrem('list:' + tl, 0, id);
}

@bindThis
public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) {
// リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、
Expand Down
44 changes: 44 additions & 0 deletions packages/backend/src/core/FeaturedService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,50 @@ export class FeaturedService {
return Array.from(ranking.keys());
}

// TODO: find better place?
@bindThis
public shouldBeIncludedInGlobalOrUserFeatured(note: MiNote): boolean {
if (note.visibility !== 'public') return false; // non-public note
if (note.userHost != null) return false; // remote
if (note.replyId != null) return false; // reply
// Channels are checked outside

// In nirila misskey, it was very common to notes with `:ohayo_nirila_misskey:` or `:oyasumi_nirila_misskey:`
// Will get many reaction`:ohayo_nirila_misskey:` or `:oyasumi_nirila_misskey:` so exclude them
// if they don't have any images.
if (note.fileIds.length === 0) {
for (const exclusion of ["おはよう", "おやすみ", ":ohayo_nirila_misskey:", ":oyasumi_nirila_misskey:", ":kon_nirila_misskey:"]) {
if (note.text?.includes(exclusion)) return false;
if (note.cw?.includes(exclusion)) return false;
}
}

return true;
}

@bindThis
private removeNoteFromRankingOf(name: string, windowRange: number, element: string, redisPipeline: Redis.ChainableCommander) {
// removing from current & previous window is enough
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;

redisPipeline.zrem(`${name}:${currentWindow}`, element);
redisPipeline.zrem(`${name}:${previousWindow}`, element);
}

@bindThis
public async removeNote(note: MiNote): Promise<void> {
const redisPipeline = this.redisClient.pipeline();
this.removeNoteFromRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, note.id, redisPipeline);
this.removeNoteFromRankingOf(`featuredPerUserNotesRanking:${note.userId}`, PER_USER_NOTES_RANKING_WINDOW, note.id, redisPipeline);

if (note.channelId) {
this.removeNoteFromRankingOf(`featuredInChannelNotesRanking:${note.channelId}`, GLOBAL_NOTES_RANKING_WINDOW, note.id, redisPipeline);
}

await redisPipeline.exec();
}

@bindThis
private async removeFromRanking(name: string, windowRange: number, element: string): Promise<void> {
const currentWindow = this.getCurrentWindow(windowRange);
Expand Down
57 changes: 47 additions & 10 deletions packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';

type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';

Expand Down Expand Up @@ -147,6 +149,7 @@ type Option = {

@Injectable()
export class NoteCreateService implements OnApplicationShutdown {
private logger: Logger;
#shutdownController = new AbortController();
private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;

Expand Down Expand Up @@ -219,8 +222,10 @@ export class NoteCreateService implements OnApplicationShutdown {
private instanceChart: InstanceChart,
private utilityService: UtilityService,
private userBlockingService: UserBlockingService,
private loggerService: LoggerService,
) {
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
this.logger = this.loggerService.getLogger('note:create');
}

@bindThis
Expand Down Expand Up @@ -368,6 +373,34 @@ export class NoteCreateService implements OnApplicationShutdown {
// if the host is media-silenced, custom emojis are not allowed
if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];

const willCauseNotification = mentionedUsers.some(u => u.host === null)
|| (data.visibility === 'specified' && data.visibleUsers?.some(u => u.host === null))
|| data.reply?.userHost === null || (this.isRenote(data) && this.isQuote(data) && data.renote?.userHost === null) || false;

const isAllowedToCreateNotification = () => {
const targetUserIds: string[] = [
...mentionedUsers.filter(x => x.host == null).map(x => x.id),
...(data.visibility === 'specified' && data.visibleUsers != null ? data.visibleUsers.filter(x => x.host == null).map(x => x.id) : []),
...(data.reply != null && data.reply.userHost == null ? [data.reply.userId] : []),
...(this.isRenote(data) && this.isQuote(data) && data.renote.userHost === null ? [data.renote.userId] : []),
];
const allowedIds = new Set(this.meta.nirilaAllowedUnfamiliarRemoteUserIds);
for (const targetUserId of targetUserIds) {
if (!allowedIds.has(targetUserId)) {
return false;
}
}
return true;
};

if (this.meta.nirilaBlockMentionsFromUnfamiliarRemoteUsers && user.host !== null && willCauseNotification && !isAllowedToCreateNotification()) {
const userEntity = await this.usersRepository.findOneBy({ id: user.id });
if ((userEntity?.followersCount ?? 0) === 0) {
this.logger.error('Request rejected because user has no local followers', { user: user.id, note: data });
throw new IdentifiableError('e11b3a16-f543-4885-8eb1-66cad131dbfd', 'Notes including mentions, replies, or renotes from remote users are not allowed until user has at least one local follower.');
}
}

tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);

if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
Expand Down Expand Up @@ -605,7 +638,8 @@ export class NoteCreateService implements OnApplicationShutdown {
this.roleService.addNoteToRoleTimeline(noteObj);

this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
const userNoteEvent = `note@${user.username}` as const;
webhooks = webhooks.filter(x => (x.userId === user.id && x.on.includes('note')) || x.on.includes(userNoteEvent));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'note', {
note: noteObj,
Expand Down Expand Up @@ -734,7 +768,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private isQuote(note: Option & { renote: MiNote }): note is Option & { renote: MiNote } & (
{ text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
) {
) {
// NOTE: SYNC WITH misc/is-quote.ts
return note.text != null ||
note.reply != null ||
Expand All @@ -752,14 +786,14 @@ export class NoteCreateService implements OnApplicationShutdown {
.where('id = :id', { id: renote.id })
.execute();

// 30%の確率、3日以内に投稿されたノートの場合ハイライト用ランキング更新
if (Math.random() < 0.3 && (Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) {
// ~~30%の確率、~~3日以内に投稿されたノートの場合ハイライト用ランキング更新
if ((Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) {
if (renote.channelId != null) {
if (renote.replyId == null) {
this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5);
}
} else {
if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) {
if (this.featuredService.shouldBeIncludedInGlobalOrUserFeatured(renote)) {
this.featuredService.updateGlobalNotesRanking(renote.id, 5);
this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5);
}
Expand Down Expand Up @@ -954,8 +988,11 @@ export class NoteCreateService implements OnApplicationShutdown {
this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r);
}
}
if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost)) {
this.fanoutTimelineService.push('vmimiRelayTimelineWithReplies', note.id, meta.vmimiRelayTimelineCacheMax, r);
if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost) && !note.localOnly) {
this.fanoutTimelineService.push('vmimiRelayTimelineWithReplies', note.id, this.meta.vmimiRelayTimelineCacheMax, r);
if (note.replyUserHost == null) {
this.fanoutTimelineService.push(`vmimiRelayTimelineWithReplyTo:${note.replyUserId}`, note.id, this.meta.vmimiRelayTimelineCacheMax / 10, r);
}
}
} else {
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
Expand All @@ -969,10 +1006,10 @@ export class NoteCreateService implements OnApplicationShutdown {
this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
}
}
if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost)) {
this.fanoutTimelineService.push('vmimiRelayTimeline', note.id, meta.vmimiRelayTimelineCacheMax, r);
if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost) && !note.localOnly) {
this.fanoutTimelineService.push('vmimiRelayTimeline', note.id, this.meta.vmimiRelayTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.fanoutTimelineService.push('vmimiRelayTimelineWithFiles', note.id, meta.vmimiRelayTimelineCacheMax / 2, r);
this.fanoutTimelineService.push('vmimiRelayTimelineWithFiles', note.id, this.meta.vmimiRelayTimelineCacheMax / 2, r);
}
}
}
Expand Down
14 changes: 13 additions & 1 deletion packages/backend/src/models/Meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ export class MiMeta {

@Column('varchar', {
length: 1024,
default: 'https://github.com/misskey-dev/misskey',
default: 'https://github.com/anatawa12/misskey/tree/vmimi-relay-timeline-releases?tab=readme-ov-file#vmimi-relay-timeline',
nullable: true,
})
public repositoryUrl: string | null;
Expand Down Expand Up @@ -648,4 +648,16 @@ export class MiMeta {
default: '{}',
})
public federationHosts: string[];

@Column('boolean', {
default: false,
})
public nirilaBlockMentionsFromUnfamiliarRemoteUsers: boolean;

@Column('varchar', {
length: 32,
array: true,
default: '{}',
})
public nirilaAllowedUnfamiliarRemoteUserIds: string[];
}
4 changes: 2 additions & 2 deletions packages/backend/src/models/Webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';

export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
export type WebhookEventTypes = typeof webhookEventTypes[number];
export type WebhookEventTypes = typeof webhookEventTypes[number] | `note@${string}`;

@Entity('webhook')
export class MiWebhook {
Expand Down Expand Up @@ -38,7 +38,7 @@ export class MiWebhook {
@Column('varchar', {
length: 128, array: true, default: '{}',
})
public on: (typeof webhookEventTypes)[number][];
public on: WebhookEventTypes[];

@Column('varchar', {
length: 1024,
Expand Down

0 comments on commit 2eca776

Please sign in to comment.