[backend] Rework note hard mutes

It's been shown that the current approach doesn't scale. This implementation should scale perfectly fine.
This commit is contained in:
Laura Hausmann 2023-11-27 17:56:03 +01:00
parent 2d475cb632
commit ef3463e8dc
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
42 changed files with 148 additions and 253 deletions

View file

@ -203,6 +203,11 @@ reservedUsernames: [
# prewarm: false
# dbFallback: false
# Duration hard muted notes are stored in redis for.
# Increasing this trades higher memory consumption for lower cpu usage on repeated requests within the specified ttl.
#wordMuteCache:
# ttl: 24h
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Congrats, you've reached the end of the config file needed for most deployments!
# Enjoy your Iceshrimp server!

View file

@ -203,6 +203,11 @@ reservedUsernames: [
# prewarm: false
# dbFallback: false
# Duration hard muted notes are stored in redis for.
# Increasing this trades higher memory consumption for lower cpu usage on repeated requests within the specified ttl.
#wordMuteCache:
# ttl: 24h
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Congrats, you've reached the end of the config file needed for most deployments!
# Enjoy your Iceshrimp server!

View file

@ -61,7 +61,13 @@ export default function load() {
...config.htmlCache,
}
if (config.htmlCache.ttlSeconds == null) throw new Error('Failed to parse config.ttl');
if (config.htmlCache.ttlSeconds == null) throw new Error('Failed to parse config.htmlCache.ttl');
config.wordMuteCache = {
ttlSeconds: parseDuration(config.wordMuteCache?.ttl ?? '24h', 's')!,
}
if (config.wordMuteCache.ttlSeconds == null) throw new Error('Failed to parse config.wordMuteCache.ttl');
config.searchEngine = config.searchEngine ?? 'https://duckduckgo.com/?q=';

View file

@ -48,6 +48,11 @@ export type Source = {
dbFallback?: boolean;
}
wordMuteCache?: {
ttl?: string;
ttlSeconds?: number;
}
searchEngine?: string;
proxy?: string;

View file

@ -61,7 +61,6 @@ import { Antenna } from "@/models/entities/antenna.js";
import { PromoNote } from "@/models/entities/promo-note.js";
import { PromoRead } from "@/models/entities/promo-read.js";
import { Relay } from "@/models/entities/relay.js";
import { MutedNote } from "@/models/entities/muted-note.js";
import { Channel } from "@/models/entities/channel.js";
import { ChannelFollowing } from "@/models/entities/channel-following.js";
import { ChannelNotePining } from "@/models/entities/channel-note-pining.js";
@ -168,7 +167,6 @@ export const entities = [
PromoNote,
PromoRead,
Relay,
MutedNote,
Channel,
ChannelFollowing,
ChannelNotePining,

View file

@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ReworkHardMutes1701108527387 implements MigrationInterface {
name = 'ReworkHardMutes1701108527387'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1"`);
await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_70ab9786313d78e4201d81cdb89"`);
await queryRunner.query(`DROP INDEX "public"."IDX_a8c6bfd637d3f1d67a27c48e27"`);
await queryRunner.query(`DROP INDEX "public"."IDX_636e977ff90b23676fb5624b25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d8e07aa18c2d64e86201601aec"`);
await queryRunner.query(`DROP INDEX "public"."IDX_70ab9786313d78e4201d81cdb8"`);
await queryRunner.query(`DROP TABLE "muted_note"`);
await queryRunner.query(`DROP TYPE "public"."muted_note_reason_enum"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."muted_note_reason_enum" AS ENUM('word', 'manual', 'spam', 'other')`);
await queryRunner.query(`CREATE TABLE "muted_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "reason" "public"."muted_note_reason_enum" NOT NULL, CONSTRAINT "PK_897e2eff1c0b9b64e55ca1418a4" PRIMARY KEY ("id")); COMMENT ON COLUMN "muted_note"."noteId" IS 'The note ID.'; COMMENT ON COLUMN "muted_note"."userId" IS 'The user ID.'; COMMENT ON COLUMN "muted_note"."reason" IS 'The reason of the MutedNote.'`);
await queryRunner.query(`CREATE INDEX "IDX_70ab9786313d78e4201d81cdb8" ON "muted_note" ("noteId") `);
await queryRunner.query(`CREATE INDEX "IDX_d8e07aa18c2d64e86201601aec" ON "muted_note" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_636e977ff90b23676fb5624b25" ON "muted_note" ("reason") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a8c6bfd637d3f1d67a27c48e27" ON "muted_note" ("noteId", "userId") `);
await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_70ab9786313d78e4201d81cdb89" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
}

View file

@ -1,12 +1,16 @@
import RE2 from "re2";
import type { Note } from "@/models/entities/note.js";
import type { User } from "@/models/entities/user.js";
import { Packed } from "@/misc/schema.js";
type NoteLike = {
userId: Note["userId"];
text: Note["text"];
files?: Note["files"];
cw?: Note["cw"];
reply?: Note["reply"];
renote?: Note["renote"];
isFiltered?: Packed<"Note">["isFiltered"];
};
type UserLike = {
@ -14,7 +18,7 @@ type UserLike = {
};
function checkWordMute(
note: NoteLike,
note: NoteLike | null | undefined,
mutedWords: Array<string | string[]>,
): boolean {
if (note == null) return false;
@ -63,17 +67,13 @@ export async function getWordHardMute(
mutedWords: Array<string | string[]>,
): Promise<boolean> {
// 自分自身
if (me && note.userId === me.id) {
return false;
}
if (me && note.userId === me.id) return false;
if (mutedWords.length <= 0) return false;
if (note.isFiltered) return true;
if (mutedWords.length > 0) {
return (
checkWordMute(note, mutedWords) ||
checkWordMute(note.reply, mutedWords) ||
checkWordMute(note.renote, mutedWords)
);
}
return false;
return (
checkWordMute(note, mutedWords) ||
checkWordMute(note.reply, mutedWords) ||
checkWordMute(note.renote, mutedWords)
);
}

View file

@ -0,0 +1,16 @@
import { User } from "@/models/entities/user.js";
import { Note } from "@/models/entities/note.js";
import { UserProfile } from "@/models/entities/user-profile.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { Cache } from "@/misc/cache.js";
import { unique } from "@/prelude/array.js";
import config from "@/config/index.js";
const filteredNoteCache = new Cache<boolean>("filteredNote", config.wordMuteCache?.ttlSeconds ?? 60 * 60 * 24);
export function isFiltered(note: Note, user: { id: User["id"] } | null | undefined, profile: { mutedWords: UserProfile["mutedWords"] } | null): boolean | Promise<boolean> {
if (!user || !profile) return false;
if (profile.mutedWords.length < 1) return false;
return filteredNoteCache.fetch(`${note.id}:${(note.updatedAt ?? note.createdAt).getTime()}:${user.id}`,
() => getWordHardMute(note, user, unique(profile.mutedWords)));
}

View file

@ -520,7 +520,7 @@ export class Meta {
public donationLink: string | null;
@Column("varchar", {
length: 64,
length: 128,
nullable: true,
})
public autofollowedAccount: string | null;

View file

@ -1,55 +0,0 @@
import {
Entity,
Index,
JoinColumn,
Column,
ManyToOne,
PrimaryColumn,
} from "typeorm";
import { Note } from "./note.js";
import { User } from "./user.js";
import { id } from "../id.js";
import { mutedNoteReasons } from "../../types.js";
@Entity()
@Index(["noteId", "userId"], { unique: true })
export class MutedNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
comment: "The note ID.",
})
public noteId: Note["id"];
@ManyToOne((type) => Note, {
onDelete: "CASCADE",
})
@JoinColumn()
public note: Note | null;
@Index()
@Column({
...id(),
comment: "The user ID.",
})
public userId: User["id"];
@ManyToOne((type) => User, {
onDelete: "CASCADE",
})
@JoinColumn()
public user: User | null;
/**
*
*/
@Index()
@Column("enum", {
enum: mutedNoteReasons,
comment: "The reason of the MutedNote.",
})
public reason: typeof mutedNoteReasons[number];
}

View file

@ -259,7 +259,7 @@ export class Note {
nullable: true,
comment: "The updated date of the Note.",
})
public updatedAt: Date;
public updatedAt: Date | null;
//#endregion
constructor(data: Partial<Note>) {

View file

@ -56,7 +56,6 @@ import { PromoRead } from "./entities/promo-read.js";
import { EmojiRepository } from "./repositories/emoji.js";
import { RelayRepository } from "./repositories/relay.js";
import { ChannelRepository } from "./repositories/channel.js";
import { MutedNote } from "./entities/muted-note.js";
import { ChannelFollowing } from "./entities/channel-following.js";
import { ChannelNotePining } from "./entities/channel-note-pining.js";
import { RegistryItem } from "./entities/registry-item.js";
@ -129,7 +128,6 @@ export const Antennas = AntennaRepository;
export const PromoNotes = db.getRepository(PromoNote);
export const PromoReads = db.getRepository(PromoRead);
export const Relays = RelayRepository;
export const MutedNotes = db.getRepository(MutedNote);
export const Channels = ChannelRepository;
export const ChannelFollowings = db.getRepository(ChannelFollowing);
export const ChannelNotePinings = db.getRepository(ChannelNotePining);

View file

@ -10,7 +10,7 @@ import {
Followings,
Polls,
Channels,
Notes,
Notes, UserProfiles,
} from "../index.js";
import type { Packed } from "@/misc/schema.js";
import { nyaize } from "@/misc/nyaize.js";
@ -29,6 +29,11 @@ import {
import { db } from "@/db/postgre.js";
import { IdentifiableError } from "@/misc/identifiable-error.js";
import { PackedUserCache } from "@/models/repositories/user.js";
import { isFiltered } from "@/misc/is-filtered.js";
import { UserProfile } from "@/models/entities/user-profile.js";
import { Cache } from "@/misc/cache.js";
const mutedWordsCache = new Cache<UserProfile["mutedWords"]>("mutedWords", 60 * 5);
export async function populatePoll(note: Note, meId: User["id"] | null) {
const poll = await Polls.findOneByOrFail({ noteId: note.id });
@ -175,6 +180,7 @@ export const NoteRepository = db.getRepository(Note).extend({
};
},
userCache: PackedUserCache = Users.getFreshPackedUserCache(),
profile?: { mutedWords: UserProfile["mutedWords"] } | null,
): Promise<Packed<"Note">> {
const opts = Object.assign(
{
@ -187,6 +193,11 @@ export const NoteRepository = db.getRepository(Note).extend({
const note =
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
const host = note.userHost;
const meProfile = profile !== undefined
? profile
: meId !== null
? { mutedWords: await mutedWordsCache.fetch(meId, async () => UserProfiles.findOneBy({ userId: meId }).then(p => p?.mutedWords ?? [])) }
: null;
if (!(await this.isVisibleForMe(note, meId))) {
throw new IdentifiableError(
@ -257,7 +268,8 @@ export const NoteRepository = db.getRepository(Note).extend({
...(meId
? {
myReaction: populateMyReaction(note, meId, options?._hint_),
isRenoted: populateIsRenoted(note, meId, options?._hint_)
isRenoted: populateIsRenoted(note, meId, options?._hint_),
isFiltered: isFiltered(note, me, await meProfile),
}
: {}),
@ -326,6 +338,7 @@ export const NoteRepository = db.getRepository(Note).extend({
options?: {
detail?: boolean;
},
profile?: { mutedWords: UserProfile["mutedWords"] } | null,
) {
if (notes.length === 0) return [];
@ -361,6 +374,10 @@ export const NoteRepository = db.getRepository(Note).extend({
!!myRenotes.find(p => p.renoteId == target),
);
}
profile = profile !== undefined
? profile
: { mutedWords: await mutedWordsCache.fetch(meId, async () => UserProfiles.findOneBy({ userId: meId }).then(p => p?.mutedWords ?? [])) };
}
await prefetchEmojis(aggregateNoteEmojis(notes));
@ -373,7 +390,7 @@ export const NoteRepository = db.getRepository(Note).extend({
myReactions: myReactionsMap,
myRenotes: myRenotesMap
},
}),
}, undefined, profile),
),
);

View file

@ -199,6 +199,11 @@ export const packedNoteSchema = {
type: "boolean",
optional: true,
nullable: true,
}
},
isFiltered: {
type: "boolean",
optional: true,
nullable: true,
},
},
} as const;

View file

@ -1,16 +0,0 @@
import type { User } from "@/models/entities/user.js";
import { MutedNotes } from "@/models/index.js";
import type { SelectQueryBuilder } from "typeorm";
export function generateMutedNoteQuery(
q: SelectQueryBuilder<any>,
me: { id: User["id"] },
) {
const mutedQuery = MutedNotes.createQueryBuilder("muted")
.select("muted.noteId")
.where("muted.userId = :userId", { userId: me.id });
q.andWhere(`note.id NOT IN (${mutedQuery.getQuery()})`);
q.setParameters(mutedQuery.getParameters());
}

View file

@ -186,7 +186,6 @@ import * as ep___i_exportUserLists from "./endpoints/i/export-user-lists.js";
import * as ep___i_favorites from "./endpoints/i/favorites.js";
import * as ep___i_gallery_likes from "./endpoints/i/gallery/likes.js";
import * as ep___i_gallery_posts from "./endpoints/i/gallery/posts.js";
import * as ep___i_getWordMutedNotesCount from "./endpoints/i/get-word-muted-notes-count.js";
import * as ep___i_importBlocking from "./endpoints/i/import-blocking.js";
import * as ep___i_importFollowing from "./endpoints/i/import-following.js";
import * as ep___i_importMuting from "./endpoints/i/import-muting.js";
@ -535,7 +534,6 @@ const eps = [
["i/favorites", ep___i_favorites],
["i/gallery/likes", ep___i_gallery_likes],
["i/gallery/posts", ep___i_gallery_posts],
["i/get-word-muted-notes-count", ep___i_getWordMutedNotesCount],
["i/import-blocking", ep___i_importBlocking],
["i/import-following", ep___i_importFollowing],
["i/import-muting", ep___i_importMuting],

View file

@ -1,38 +0,0 @@
import define from "../../define.js";
import { MutedNotes } from "@/models/index.js";
export const meta = {
tags: ["account"],
requireCredential: true,
kind: "read:account",
res: {
type: "object",
optional: false,
nullable: false,
properties: {
count: {
type: "number",
optional: false,
nullable: false,
},
},
},
} as const;
export const paramDef = {
type: "object",
properties: {},
required: [],
} as const;
export default define(meta, paramDef, async (ps, user) => {
return {
count: await MutedNotes.countBy({
userId: user.id,
reason: "word",
}),
};
});

View file

@ -6,7 +6,6 @@ import { ApiError } from "../../error.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
@ -89,7 +88,6 @@ export default define(meta, paramDef, async (ps, user) => {
generateRepliesQuery(query, ps.withReplies, user);
if (user) {
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);
}

View file

@ -8,7 +8,6 @@ import { makePaginationQuery } from "../../common/make-pagination-query.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js";
import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
@ -108,7 +107,6 @@ export default define(meta, paramDef, async (ps, user) => {
generateRepliesQuery(query, ps.withReplies, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);

View file

@ -6,9 +6,7 @@ import define from "../../define.js";
import { ApiError } from "../../error.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js";
import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
@ -99,7 +97,6 @@ export default define(meta, paramDef, async (ps, user) => {
generateChannelQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
if (user) generateMutedUserRenotesQueryForNotes(query, user);

View file

@ -6,9 +6,7 @@ import define from "../../define.js";
import { ApiError } from "../../error.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js";
import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
@ -99,7 +97,6 @@ export default define(meta, paramDef, async (ps, user) => {
generateChannelQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
if (user) generateMutedUserRenotesQueryForNotes(query, user);

View file

@ -6,7 +6,6 @@ import { makePaginationQuery } from "../../common/make-pagination-query.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js";
import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
@ -86,7 +85,6 @@ export default define(meta, paramDef, async (ps, user) => {
generateRepliesQuery(query, ps.withReplies, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);

View file

@ -172,7 +172,7 @@ export class NoteConverter {
reactions: populated.then(populated => Promise.resolve(reaction).then(reaction => this.encodeReactions(note.reactions, reaction?.reaction, populated))),
bookmarked: isBookmarked,
quote: reblog.then(reblog => isQuote(note) ? reblog : null),
edited_at: note.updatedAt?.toISOString()
edited_at: note.updatedAt?.toISOString() ?? null
});
}
@ -336,7 +336,7 @@ export class NoteConverter {
.then(p => p ?? escapeMFM(text))
: null;
HtmlNoteCacheEntries.upsert({ noteId: note.id, updatedAt: note.updatedAt, content: await content }, ["noteId"]);
HtmlNoteCacheEntries.upsert({ noteId: note.id, updatedAt: note.updatedAt ?? note.createdAt, content: await content }, ["noteId"]);
await this.noteContentHtmlCache.set(identifier, await content);
return { content } as HtmlNoteCacheEntry;
});
@ -367,6 +367,6 @@ export class NoteConverter {
this.noteContentHtmlCache.set(identifier, await content);
if (config.htmlCache?.dbFallback)
HtmlNoteCacheEntries.upsert({ noteId: note.id, updatedAt: note.updatedAt, content: await content }, ["noteId"]);
HtmlNoteCacheEntries.upsert({ noteId: note.id, updatedAt: note.updatedAt ?? note.createdAt, content: await content }, ["noteId"]);
}
}

View file

@ -6,7 +6,6 @@ import { generateChannelQuery } from "@/server/api/common/generate-channel-query
import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js";
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
import { generateMutedNoteQuery } from "@/server/api/common/generate-muted-note-query.js";
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
@ -43,7 +42,6 @@ export class TimelineHelpers {
generateRepliesQuery(query, true, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);
@ -88,7 +86,6 @@ export class TimelineHelpers {
generateRepliesQuery(query, true, user);
if (user) {
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);
}
@ -158,7 +155,6 @@ export class TimelineHelpers {
generateRepliesQuery(query, true, user);
if (user) {
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);
}

View file

@ -1,11 +1,11 @@
import { MastodonStream } from "../channel.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { Note } from "@/models/entities/note.js";
import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { StreamMessages } from "@/server/api/stream/types.js";
import { Packed } from "@/misc/schema.js";
import { User } from "@/models/entities/user.js";
import { UserListJoinings } from "@/models/index.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class MastodonStreamList extends MastodonStream {
public static shouldShare = false;
@ -75,7 +75,7 @@ export class MastodonStreamList extends MastodonStream {
if (!this.listUsers.includes(note.userId)) return false;
if (note.channelId) return false;
if (note.renoteId !== null && !note.text && this.renoteMuting.has(note.userId)) return false;
if (this.userProfile && (await getWordHardMute(note, this.user, this.userProfile.mutedWords))) return false;
if (this.userProfile && (await isFiltered(note as Note, this.user, this.userProfile))) return false;
if (note.visibility === "specified") return !!note.visibleUserIds?.includes(this.user.id);
if (note.visibility === "followers") return this.following.has(note.userId);
return true;

View file

@ -1,5 +1,4 @@
import { MastodonStream } from "../channel.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import { Note } from "@/models/entities/note.js";
@ -7,6 +6,7 @@ import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { StreamMessages } from "@/server/api/stream/types.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import isQuote from "@/misc/is-quote.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class MastodonStreamPublic extends MastodonStream {
public static shouldShare = true;
@ -72,7 +72,7 @@ export class MastodonStreamPublic extends MastodonStream {
if (isUserRelated(note, this.muting)) return false;
if (isUserRelated(note, this.blocking)) return false;
if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false;
if (this.userProfile && (await getWordHardMute(note, this.user, this.userProfile.mutedWords))) return false;
if (this.userProfile && (await isFiltered(note as Note, this.user, this.userProfile))) return false;
return true;
}

View file

@ -1,11 +1,11 @@
import { MastodonStream } from "../channel.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import { Note } from "@/models/entities/note.js";
import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { StreamMessages } from "@/server/api/stream/types.js";
import isQuote from "@/misc/is-quote.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class MastodonStreamTag extends MastodonStream {
public static shouldShare = false;
@ -64,7 +64,7 @@ export class MastodonStreamTag extends MastodonStream {
if (isUserRelated(note, this.muting)) return false;
if (isUserRelated(note, this.blocking)) return false;
if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false;
if (this.userProfile && (await getWordHardMute(note, this.user, this.userProfile.mutedWords))) return false;
if (this.userProfile && (await isFiltered(note as Note, this.user, this.userProfile))) return false;
return true;
}

View file

@ -1,5 +1,4 @@
import { MastodonStream } from "../channel.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import { Note } from "@/models/entities/note.js";
@ -8,6 +7,7 @@ import { StreamMessages } from "@/server/api/stream/types.js";
import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js";
import { AnnouncementConverter } from "@/server/api/mastodon/converters/announcement.js";
import isQuote from "@/misc/is-quote.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class MastodonStreamUser extends MastodonStream {
public static shouldShare = true;
@ -99,7 +99,7 @@ export class MastodonStreamUser extends MastodonStream {
if (isUserRelated(note, this.blocking)) return false;
if (isUserRelated(note, this.hidden)) return false;
if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false;
if (this.userProfile && (await getWordHardMute(note, this.user, this.userProfile.mutedWords))) return false;
if (this.userProfile && (await isFiltered(note as Note, this.user, this.userProfile))) return false;
return true;
}

View file

@ -1,9 +1,10 @@
import Channel from "../channel.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import type { Packed } from "@/misc/schema.js";
import { isFiltered } from "@/misc/is-filtered.js";
import { Note } from "@/models/entities/note.js";
export default class extends Channel {
public readonly chName = "globalTimeline";
@ -68,7 +69,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await isFiltered(note as unknown as Note, this.user, this.userProfile))
)
return;

View file

@ -1,8 +1,9 @@
import Channel from "../channel.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import type { Packed } from "@/misc/schema.js";
import { isFiltered } from "@/misc/is-filtered.js";
import { Note } from "@/models/entities/note.js";
export default class extends Channel {
public readonly chName = "homeTimeline";
@ -69,7 +70,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await isFiltered(note as unknown as Note, this.user, this.userProfile))
)
return;

View file

@ -1,9 +1,10 @@
import Channel from "../channel.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import type { Packed } from "@/misc/schema.js";
import { isFiltered } from "@/misc/is-filtered.js";
import { Note } from "@/models/entities/note.js";
export default class extends Channel {
public readonly chName = "hybridTimeline";
@ -86,7 +87,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await isFiltered(note as unknown as Note, this.user, this.userProfile))
)
return;

View file

@ -1,8 +1,9 @@
import Channel from "../channel.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import type { Packed } from "@/misc/schema.js";
import { isFiltered } from "@/misc/is-filtered.js";
import { Note } from "@/models/entities/note.js";
export default class extends Channel {
public readonly chName = "localTimeline";
@ -60,7 +61,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await isFiltered(note as unknown as Note, this.user, this.userProfile))
)
return;

View file

@ -1,9 +1,10 @@
import Channel from "../channel.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import type { Packed } from "@/misc/schema.js";
import { isFiltered } from "@/misc/is-filtered.js";
import { Note } from "@/models/entities/note.js";
export default class extends Channel {
public readonly chName = "recommendedTimeline";
@ -82,7 +83,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await isFiltered(note as unknown as Note, this.user, this.userProfile))
)
return;

View file

@ -27,7 +27,6 @@ import {
Notes,
Instances,
UserProfiles,
MutedNotes,
Channels,
ChannelFollowings,
NoteThreadMutings,
@ -48,7 +47,6 @@ import { Poll } from "@/models/entities/poll.js";
import { createNotification } from "../create-notification.js";
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
import { checkHitAntenna } from "@/misc/check-hit-antenna.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { addNoteToAntenna } from "../add-note-to-antenna.js";
import { countSameRenotes } from "@/misc/count-same-renotes.js";
import { deliverToRelays, getCachedRelays } from "../relay.js";
@ -57,8 +55,6 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js";
import { getAntennas } from "@/misc/antenna-cache.js";
import { endedPollNotificationQueue } from "@/queue/queues.js";
import { webhookDeliver } from "@/queue/index.js";
import { Cache } from "@/misc/cache.js";
import type { UserProfile } from "@/models/entities/user-profile.js";
import { db } from "@/db/postgre.js";
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
@ -67,10 +63,6 @@ import { Mutex } from "redis-semaphore";
import { RecursionLimiter } from "@/models/repositories/user-profile.js";
import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
>("mutedWords", 60 * 5);
type NotificationType = "reply" | "renote" | "quote" | "mention";
class NotificationManager {
@ -367,33 +359,6 @@ export default async (
// Increment notes count (user)
incNotesCountOfUser(user);
// Word mute
mutedWordsCache
.fetch(null, () =>
UserProfiles.find({
where: {
enableWordMute: true,
},
select: ["userId", "mutedWords"],
}),
)
.then((us) => {
for (const u of us) {
getWordHardMute(data, { id: u.userId }, u.mutedWords).then(
(shouldMute) => {
if (shouldMute) {
MutedNotes.insert({
id: genId(),
userId: u.userId,
noteId: note.id,
reason: "word",
});
}
},
);
}
});
// Antenna
for (const antenna of await getAntennas()) {
checkHitAntenna(antenna, note, user).then((hit) => {

View file

@ -21,6 +21,4 @@ export const noteVisibilities = [
"hidden",
] as const;
export const mutedNoteReasons = ["word", "manual", "spam", "other"] as const;
export const ffVisibility = ["public", "followers", "private"] as const;

View file

@ -8,7 +8,7 @@ export default defineComponent({
props: {
items: {
type: Array as PropType<
{ id: string; createdAt: string; _shouldInsertAd_: boolean }[]
{ id: string; createdAt: string; }[]
>,
required: true,
},

View file

@ -162,17 +162,13 @@ const init = async (): Promise<void> => {
redisPaginationStr = res.pagination;
res = res.notes;
}
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - 2) item._shouldInsertAd_ = true;
} else {
if (i === 3) item._shouldInsertAd_ = true;
}
}
const length = res.length;
res = (res as Item[]).filter(p => !p.isFiltered);
if (
!props.pagination.noPaging &&
res.length > (props.pagination.limit || 10)
length > (props.pagination.limit || 10)
) {
res.pop();
items.value = props.pagination.reversed
@ -201,7 +197,7 @@ const reload = (): void => {
init();
};
const refresh = async (): void => {
const refresh = async (): Promise<void> => {
const params = props.pagination.params
? isRef(props.pagination.params)
? props.pagination.params.value
@ -290,15 +286,10 @@ const fetchMore = async (): Promise<void> => {
res = res.notes;
}
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - 9) item._shouldInsertAd_ = true;
} else {
if (i === 10) item._shouldInsertAd_ = true;
}
}
if (res.length > SECOND_FETCH_LIMIT) {
const length = res.length;
res = (res as Item[]).filter(p => !p.isFiltered);
if (length > SECOND_FETCH_LIMIT) {
res.pop();
items.value = props.pagination.reversed
? [...res].reverse().concat(items.value)
@ -361,7 +352,9 @@ const fetchMoreAhead = async (): Promise<void> => {
res = res.notes;
}
if (res.length > SECOND_FETCH_LIMIT) {
const length = res.length;
res = (res as Item[]).filter(p => !p.isFiltered);
if (length > SECOND_FETCH_LIMIT) {
res.pop();
items.value = props.pagination.reversed
? [...res].reverse().concat(items.value)
@ -383,6 +376,7 @@ const fetchMoreAhead = async (): Promise<void> => {
};
const prepend = (item: Item): void => {
if (item.isFiltered) return;
if (props.pagination.reversed) {
if (rootEl.value) {
const container = getScrollContainer(rootEl.value);
@ -446,6 +440,7 @@ const prepend = (item: Item): void => {
};
const append = (item: Item): void => {
if (item.isFiltered) return;
items.value.push(item);
};

View file

@ -31,15 +31,6 @@
}}</template
>
</FormTextarea>
<MkKeyValue
v-if="hardWordMutedNotesCount != null"
class="_formBlock"
>
<template #key>{{ i18n.ts._wordMute.mutedNotes }}</template>
<template #value>{{
number(hardWordMutedNotesCount)
}}</template>
</MkKeyValue>
</div>
</div>
<MkButton primary inline :disabled="!changed" @click="save()"
@ -77,13 +68,8 @@ const render = (mutedWords) =>
const tab = ref("soft");
const softMutedWords = ref(render(defaultStore.state.mutedWords));
const hardMutedWords = ref(render($i!.mutedWords));
const hardWordMutedNotesCount = ref(null);
const changed = ref(false);
os.api("i/get-word-muted-notes-count", {}).then((response) => {
hardWordMutedNotesCount.value = response?.count;
});
watch(softMutedWords, () => {
changed.value = true;
});

View file

@ -28,7 +28,6 @@
| Variable | Description |
| --- | --- |
| [ffVisibility](./iceshrimp-js.ffvisibility.md) | |
| [mutedNoteReasons](./iceshrimp-js.mutednotereasons.md) | |
| [noteVisibilities](./iceshrimp-js.notevisibilities.md) | |
| [notificationTypes](./iceshrimp-js.notificationtypes.md) | |
| [permissions](./iceshrimp-js.permissions.md) | |

View file

@ -90,10 +90,6 @@ export type Endpoints = {
"admin/update-meta": { req: TODO; res: TODO };
"admin/vacuum": { req: TODO; res: TODO };
"admin/accounts/create": { req: TODO; res: TODO };
"admin/ad/create": { req: TODO; res: TODO };
"admin/ad/delete": { req: { id: Ad["id"] }; res: null };
"admin/ad/list": { req: TODO; res: TODO };
"admin/ad/update": { req: TODO; res: TODO };
"admin/announcements/create": { req: TODO; res: TODO };
"admin/announcements/delete": { req: { id: Announcement["id"] }; res: null };
"admin/announcements/list": { req: TODO; res: TODO };
@ -630,7 +626,6 @@ export type Endpoints = {
};
"i/gallery/likes": { req: TODO; res: TODO };
"i/gallery/posts": { req: TODO; res: TODO };
"i/get-word-muted-notes-count": { req: TODO; res: TODO };
"i/import-following": { req: TODO; res: TODO };
"i/import-user-lists": { req: TODO; res: TODO };
"i/move": { req: TODO; res: TODO };

View file

@ -20,8 +20,6 @@ export const noteVisibilities = [
"specified",
] as const;
export const mutedNoteReasons = ["word", "manual", "spam", "other"] as const;
export const ffVisibility = ["public", "followers", "private"] as const;
export const permissions = [

View file

@ -9,7 +9,6 @@ export { Endpoints, Stream, Connection as ChannelConnection, Channels, Acct };
export const permissions = consts.permissions;
export const notificationTypes = consts.notificationTypes;
export const noteVisibilities = consts.noteVisibilities;
export const mutedNoteReasons = consts.mutedNoteReasons;
export const ffVisibility = consts.ffVisibility;
// api extractor not supported yet