[mastodon-client] Add basic support for filters

Currently you have to configure these in the web ui, but this will eventually be implemented as well
This commit is contained in:
Laura Hausmann 2023-11-27 20:47:42 +01:00
parent ef3463e8dc
commit 03cdf4ec4a
Signed by: zotan
GPG key ID: D044E84C5BE01605
20 changed files with 99 additions and 49 deletions

View file

@ -5,12 +5,20 @@ import { getWordHardMute } from "@/misc/check-word-mute.js";
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
import { unique } from "@/prelude/array.js"; import { unique } from "@/prelude/array.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import { UserProfiles } from "@/models/index.js";
const filteredNoteCache = new Cache<boolean>("filteredNote", config.wordMuteCache?.ttlSeconds ?? 60 * 60 * 24); const filteredNoteCache = new Cache<boolean>("filteredNote", config.wordMuteCache?.ttlSeconds ?? 60 * 60 * 24);
const mutedWordsCache = new Cache<UserProfile["mutedWords"]>("mutedWords", 60 * 5);
export function isFiltered(note: Note, user: { id: User["id"] } | null | undefined, profile: { mutedWords: UserProfile["mutedWords"] } | null): boolean | Promise<boolean> { export async function isFiltered(note: Note, user: { id: User["id"] } | null | undefined, profile?: { mutedWords: UserProfile["mutedWords"] } | null): Promise<boolean> {
if (!user || !profile) return false; if (!user) return false;
if (profile.mutedWords.length < 1) return false; if (profile === undefined)
return filteredNoteCache.fetch(`${note.id}:${(note.updatedAt ?? note.createdAt).getTime()}:${user.id}`, profile = { mutedWords: await mutedWordsCache.fetch(user.id, async () =>
() => getWordHardMute(note, user, unique(profile.mutedWords))); UserProfiles.findOneBy({ userId: user.id }).then(p => p?.mutedWords ?? [])) };
if (!profile || profile.mutedWords.length < 1) return false;
const ts = (note.updatedAt ?? note.createdAt) as Date | string;
const identifier = (typeof ts === "string" ? new Date(ts) : ts)?.getTime() ?? '0';
return filteredNoteCache.fetch(`${note.id}:${ts}:${user.id}`,
() => getWordHardMute(note, user, unique(profile!.mutedWords)));
} }

View file

@ -33,8 +33,6 @@ import { isFiltered } from "@/misc/is-filtered.js";
import { UserProfile } from "@/models/entities/user-profile.js"; import { UserProfile } from "@/models/entities/user-profile.js";
import { Cache } from "@/misc/cache.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) { export async function populatePoll(note: Note, meId: User["id"] | null) {
const poll = await Polls.findOneByOrFail({ noteId: note.id }); const poll = await Polls.findOneByOrFail({ noteId: note.id });
const choices = poll.choices.map((c) => ({ const choices = poll.choices.map((c) => ({
@ -180,7 +178,6 @@ export const NoteRepository = db.getRepository(Note).extend({
}; };
}, },
userCache: PackedUserCache = Users.getFreshPackedUserCache(), userCache: PackedUserCache = Users.getFreshPackedUserCache(),
profile?: { mutedWords: UserProfile["mutedWords"] } | null,
): Promise<Packed<"Note">> { ): Promise<Packed<"Note">> {
const opts = Object.assign( const opts = Object.assign(
{ {
@ -193,11 +190,6 @@ export const NoteRepository = db.getRepository(Note).extend({
const note = const note =
typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
const host = note.userHost; 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))) { if (!(await this.isVisibleForMe(note, meId))) {
throw new IdentifiableError( throw new IdentifiableError(
@ -269,7 +261,7 @@ export const NoteRepository = db.getRepository(Note).extend({
? { ? {
myReaction: populateMyReaction(note, meId, options?._hint_), myReaction: populateMyReaction(note, meId, options?._hint_),
isRenoted: populateIsRenoted(note, meId, options?._hint_), isRenoted: populateIsRenoted(note, meId, options?._hint_),
isFiltered: isFiltered(note, me, await meProfile), isFiltered: isFiltered(note, me),
} }
: {}), : {}),
@ -338,7 +330,6 @@ export const NoteRepository = db.getRepository(Note).extend({
options?: { options?: {
detail?: boolean; detail?: boolean;
}, },
profile?: { mutedWords: UserProfile["mutedWords"] } | null,
) { ) {
if (notes.length === 0) return []; if (notes.length === 0) return [];
@ -374,10 +365,6 @@ export const NoteRepository = db.getRepository(Note).extend({
!!myRenotes.find(p => p.renoteId == target), !!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)); await prefetchEmojis(aggregateNoteEmojis(notes));
@ -390,7 +377,7 @@ export const NoteRepository = db.getRepository(Note).extend({
myReactions: myReactionsMap, myReactions: myReactionsMap,
myRenotes: myRenotesMap myRenotes: myRenotesMap
}, },
}, undefined, profile), }),
), ),
); );

View file

@ -33,6 +33,7 @@ import { unique } from "@/prelude/array.js";
import { NoteReaction } from "@/models/entities/note-reaction.js"; import { NoteReaction } from "@/models/entities/note-reaction.js";
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js"; import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class NoteConverter { export class NoteConverter {
private static noteContentHtmlCache = new Cache<string | null>('html:note:content', config.htmlCache?.ttlSeconds ?? 60 * 60); private static noteContentHtmlCache = new Cache<string | null>('html:note:content', config.htmlCache?.ttlSeconds ?? 60 * 60);
@ -138,6 +139,21 @@ export class NoteConverter {
const reblog = Promise.resolve(renote).then(renote => recurseCounter > 0 && renote ? this.encode(renote, ctx, isQuote(renote) && !isQuote(note) ? --recurseCounter : 0) : null); const reblog = Promise.resolve(renote).then(renote => recurseCounter > 0 && renote ? this.encode(renote, ctx, isQuote(renote) && !isQuote(note) ? --recurseCounter : 0) : null);
const filtered = isFiltered(note, user).then(res => {
if (!res || ctx.filterContext == null || !['home', 'public'].includes(ctx.filterContext)) return null;
return [{
filter: {
id: '0',
title: 'Hard word mutes',
context: ['home', 'public'],
expires_at: null,
filter_action: 'hide',
keywords: [],
statuses: [],
}
} as MastodonEntity.FilterResult];
});
// noinspection ES6MissingAwait // noinspection ES6MissingAwait
return await awaitAll({ return await awaitAll({
id: note.id, id: note.id,
@ -172,7 +188,8 @@ export class NoteConverter {
reactions: populated.then(populated => Promise.resolve(reaction).then(reaction => this.encodeReactions(note.reactions, reaction?.reaction, populated))), reactions: populated.then(populated => Promise.resolve(reaction).then(reaction => this.encodeReactions(note.reactions, reaction?.reaction, populated))),
bookmarked: isBookmarked, bookmarked: isBookmarked,
quote: reblog.then(reblog => isQuote(note) ? reblog : null), quote: reblog.then(reblog => isQuote(note) ? reblog : null),
edited_at: note.updatedAt?.toISOString() ?? null edited_at: note.updatedAt?.toISOString() ?? null,
filtered: filtered,
}); });
} }
@ -293,8 +310,8 @@ export class NoteConverter {
}).filter(r => r.count > 0); }).filter(r => r.count > 0);
} }
public static async encodeEvent(note: Note, user: ILocalUser | undefined): Promise<MastodonEntity.Status> { public static async encodeEvent(note: Note, user: ILocalUser | undefined, filterContext?: string): Promise<MastodonEntity.Status> {
const ctx = getStubMastoContext(user); const ctx = getStubMastoContext(user, filterContext);
NoteHelpers.fixupEventNote(note); NoteHelpers.fixupEventNote(note);
return NoteConverter.encode(note, ctx); return NoteConverter.encode(note, ctx);
} }

View file

@ -95,8 +95,8 @@ export class NotificationConverter {
} }
} }
public static async encodeEvent(target: Notification["id"], user: ILocalUser): Promise<MastodonEntity.Notification | null> { public static async encodeEvent(target: Notification["id"], user: ILocalUser, filterContext?: string): Promise<MastodonEntity.Notification | null> {
const ctx = getStubMastoContext(user); const ctx = getStubMastoContext(user, filterContext);
const notification = await Notifications.findOneByOrFail({ id: target }); const notification = await Notifications.findOneByOrFail({ id: target });
return this.encode(notification, ctx).catch(_ => null); return this.encode(notification, ctx).catch(_ => null);
} }

View file

@ -6,6 +6,7 @@ import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
import { ListHelpers } from "@/server/api/mastodon/helpers/list.js"; import { ListHelpers } from "@/server/api/mastodon/helpers/list.js";
import { auth } from "@/server/api/mastodon/middleware/auth.js"; import { auth } from "@/server/api/mastodon/middleware/auth.js";
import { SearchHelpers } from "@/server/api/mastodon/helpers/search.js"; import { SearchHelpers } from "@/server/api/mastodon/helpers/search.js";
import { filterContext } from "@/server/api/mastodon/middleware/filter-context.js";
export function setupEndpointsAccount(router: Router): void { export function setupEndpointsAccount(router: Router): void {
router.get("/v1/accounts/verify_credentials", router.get("/v1/accounts/verify_credentials",
@ -52,6 +53,7 @@ export function setupEndpointsAccount(router: Router): void {
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>(
"/v1/accounts/:id/statuses", "/v1/accounts/:id/statuses",
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
filterContext('account'),
async (ctx) => { async (ctx) => {
const query = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx); const query = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query))); const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query)));

View file

@ -4,6 +4,7 @@ import { argsToBools, limitToInt } from "@/server/api/mastodon/endpoints/timelin
import { Announcements } from "@/models/index.js"; import { Announcements } from "@/models/index.js";
import { auth } from "@/server/api/mastodon/middleware/auth.js"; import { auth } from "@/server/api/mastodon/middleware/auth.js";
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
import { filterContext } from "@/server/api/mastodon/middleware/filter-context.js";
export function setupEndpointsMisc(router: Router): void { export function setupEndpointsMisc(router: Router): void {
router.get("/v1/custom_emojis", router.get("/v1/custom_emojis",
@ -48,6 +49,7 @@ export function setupEndpointsMisc(router: Router): void {
); );
router.get("/v1/trends/statuses", router.get("/v1/trends/statuses",
filterContext('public'),
async (ctx) => { async (ctx) => {
const args = limitToInt(ctx.query); const args = limitToInt(ctx.query);
ctx.body = await MiscHelpers.getTrendingStatuses(args.limit, args.offset, ctx); ctx.body = await MiscHelpers.getTrendingStatuses(args.limit, args.offset, ctx);

View file

@ -3,10 +3,12 @@ import { limitToInt, normalizeUrlQuery } from "./timeline.js";
import { NotificationHelpers } from "@/server/api/mastodon/helpers/notification.js"; import { NotificationHelpers } from "@/server/api/mastodon/helpers/notification.js";
import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js"; import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js";
import { auth } from "@/server/api/mastodon/middleware/auth.js"; import { auth } from "@/server/api/mastodon/middleware/auth.js";
import { filterContext } from "@/server/api/mastodon/middleware/filter-context.js";
export function setupEndpointsNotifications(router: Router): void { export function setupEndpointsNotifications(router: Router): void {
router.get("/v1/notifications", router.get("/v1/notifications",
auth(true, ['read:notifications']), auth(true, ['read:notifications']),
filterContext('notifications'),
async (ctx) => { async (ctx) => {
const args = normalizeUrlQuery(limitToInt(ctx.query), ['types[]', 'exclude_types[]']); const args = normalizeUrlQuery(limitToInt(ctx.query), ['types[]', 'exclude_types[]']);
const res = await NotificationHelpers.getNotifications(args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id, ctx); const res = await NotificationHelpers.getNotifications(args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id, ctx);
@ -16,6 +18,7 @@ export function setupEndpointsNotifications(router: Router): void {
router.get("/v1/notifications/:id", router.get("/v1/notifications/:id",
auth(true, ['read:notifications']), auth(true, ['read:notifications']),
filterContext('notifications'),
async (ctx) => { async (ctx) => {
const notification = await NotificationHelpers.getNotificationOr404(ctx.params.id, ctx); const notification = await NotificationHelpers.getNotificationOr404(ctx.params.id, ctx);
ctx.body = await NotificationConverter.encode(notification, ctx); ctx.body = await NotificationConverter.encode(notification, ctx);

View file

@ -7,6 +7,7 @@ import { PollHelpers } from "@/server/api/mastodon/helpers/poll.js";
import { toArray } from "@/prelude/array.js"; import { toArray } from "@/prelude/array.js";
import { auth } from "@/server/api/mastodon/middleware/auth.js"; import { auth } from "@/server/api/mastodon/middleware/auth.js";
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
import { filterContext } from "@/server/api/mastodon/middleware/filter-context.js";
export function setupEndpointsStatus(router: Router): void { export function setupEndpointsStatus(router: Router): void {
router.post("/v1/statuses", router.post("/v1/statuses",
@ -40,6 +41,7 @@ export function setupEndpointsStatus(router: Router): void {
); );
router.get<{ Params: { id: string } }>("/v1/statuses/:id", router.get<{ Params: { id: string } }>("/v1/statuses/:id",
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
filterContext('thread'),
async (ctx) => { async (ctx) => {
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx); const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
@ -57,6 +59,7 @@ export function setupEndpointsStatus(router: Router): void {
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>(
"/v1/statuses/:id/context", "/v1/statuses/:id/context",
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
filterContext('thread'),
async (ctx) => { async (ctx) => {
//FIXME: determine final limits within helper functions instead of here //FIXME: determine final limits within helper functions instead of here
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx); const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);

View file

@ -5,6 +5,7 @@ import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { UserLists } from "@/models/index.js"; import { UserLists } from "@/models/index.js";
import { auth } from "@/server/api/mastodon/middleware/auth.js"; import { auth } from "@/server/api/mastodon/middleware/auth.js";
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
import { filterContext } from "@/server/api/mastodon/middleware/filter-context.js";
//TODO: Move helper functions to a helper class //TODO: Move helper functions to a helper class
export function limitToInt(q: ParsedUrlQuery, additional: string[] = []) { export function limitToInt(q: ParsedUrlQuery, additional: string[] = []) {
@ -53,6 +54,7 @@ export function normalizeUrlQuery(q: ParsedUrlQuery, arrayKeys: string[] = []):
export function setupEndpointsTimeline(router: Router): void { export function setupEndpointsTimeline(router: Router): void {
router.get("/v1/timelines/public", router.get("/v1/timelines/public",
auth(true, ['read:statuses']), auth(true, ['read:statuses']),
filterContext('public'),
async (ctx, reply) => { async (ctx, reply) => {
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query))); const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query)));
const res = await TimelineHelpers.getPublicTimeline(args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote, ctx); const res = await TimelineHelpers.getPublicTimeline(args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote, ctx);
@ -61,6 +63,7 @@ export function setupEndpointsTimeline(router: Router): void {
router.get<{ Params: { hashtag: string } }>( router.get<{ Params: { hashtag: string } }>(
"/v1/timelines/tag/:hashtag", "/v1/timelines/tag/:hashtag",
auth(false, ['read:statuses']), auth(false, ['read:statuses']),
filterContext('public'),
async (ctx, reply) => { async (ctx, reply) => {
const tag = (ctx.params.hashtag ?? '').trim().toLowerCase(); const tag = (ctx.params.hashtag ?? '').trim().toLowerCase();
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query)), ['any[]', 'all[]', 'none[]']); const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query)), ['any[]', 'all[]', 'none[]']);
@ -70,6 +73,7 @@ export function setupEndpointsTimeline(router: Router): void {
); );
router.get("/v1/timelines/home", router.get("/v1/timelines/home",
auth(true, ['read:statuses']), auth(true, ['read:statuses']),
filterContext('home'),
async (ctx, reply) => { async (ctx, reply) => {
const args = normalizeUrlQuery(limitToInt(ctx.query)); const args = normalizeUrlQuery(limitToInt(ctx.query));
const res = await TimelineHelpers.getHomeTimeline(args.max_id, args.since_id, args.min_id, args.limit, ctx); const res = await TimelineHelpers.getHomeTimeline(args.max_id, args.since_id, args.min_id, args.limit, ctx);
@ -78,6 +82,7 @@ export function setupEndpointsTimeline(router: Router): void {
router.get<{ Params: { listId: string } }>( router.get<{ Params: { listId: string } }>(
"/v1/timelines/list/:listId", "/v1/timelines/list/:listId",
auth(true, ['read:lists']), auth(true, ['read:lists']),
filterContext('home'),
async (ctx, reply) => { async (ctx, reply) => {
const list = await UserLists.findOneBy({ userId: ctx.user.id, id: ctx.params.listId }); const list = await UserLists.findOneBy({ userId: ctx.user.id, id: ctx.params.listId });
if (!list) throw new MastoApiError(404); if (!list) throw new MastoApiError(404);

View file

@ -1,12 +1,13 @@
namespace MastodonEntity { namespace MastodonEntity {
export type Filter = { export type Filter = {
id: string; id: string;
phrase: string; title: string;
context: Array<FilterContext>; context: Array<FilterContext>;
expires_at: string | null; expires_at: string | null;
irreversible: boolean; filter_action: 'warn' | 'hide';
whole_word: boolean; keywords: FilterKeyword[];
statuses: FilterStatus[];
}; };
export type FilterContext = string; export type FilterContext = 'home' | 'notifications' | 'public' | 'thread' | 'account';
} }

View file

@ -0,0 +1,7 @@
namespace MastodonEntity {
export type FilterKeyword = {
id: string;
keyword: string;
whole_word: boolean;
};
}

View file

@ -0,0 +1,7 @@
namespace MastodonEntity {
export type FilterResult = {
filter: Filter;
keyword_matches?: string[];
status_matches?: string[];
};
}

View file

@ -0,0 +1,6 @@
namespace MastodonEntity {
export type FilterStatus = {
id: string;
status_id: string;
};
}

View file

@ -43,6 +43,7 @@ namespace MastodonEntity {
quote: Status | null; quote: Status | null;
bookmarked: boolean; bookmarked: boolean;
edited_at: string | null; edited_at: string | null;
filtered: Array<FilterResult> | null;
}; };
export type StatusCreationRequest = { export type StatusCreationRequest = {

View file

@ -50,9 +50,10 @@ function setupMiddleware(router: Router): void {
router.use(CacheMiddleware); router.use(CacheMiddleware);
} }
export function getStubMastoContext(user: ILocalUser | null | undefined): any { export function getStubMastoContext(user: ILocalUser | null | undefined, filterContext?: string): any {
return { return {
user: user ?? null, user: user ?? null,
cache: UserHelpers.getFreshAccountCache() cache: UserHelpers.getFreshAccountCache(),
filterContext: filterContext,
}; };
} }

View file

@ -0,0 +1,8 @@
import { MastoContext } from "@/server/api/mastodon/index.js";
export function filterContext(context: string) {
return async function filterContext(ctx: MastoContext, next: () => Promise<any>) {
ctx.filterContext = context;
await next();
};
}

View file

@ -5,7 +5,6 @@ import { StreamMessages } from "@/server/api/stream/types.js";
import { Packed } from "@/misc/schema.js"; import { Packed } from "@/misc/schema.js";
import { User } from "@/models/entities/user.js"; import { User } from "@/models/entities/user.js";
import { UserListJoinings } from "@/models/index.js"; import { UserListJoinings } from "@/models/index.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class MastodonStreamList extends MastodonStream { export class MastodonStreamList extends MastodonStream {
public static shouldShare = false; public static shouldShare = false;
@ -50,7 +49,7 @@ export class MastodonStreamList extends MastodonStream {
private async onNote(note: Note) { private async onNote(note: Note) {
if (!await this.shouldProcessNote(note)) return; if (!await this.shouldProcessNote(note)) return;
const encoded = await NoteConverter.encodeEvent(note, this.user) const encoded = await NoteConverter.encodeEvent(note, this.user, 'home')
this.connection.send(this.chName, "update", encoded); this.connection.send(this.chName, "update", encoded);
} }
@ -60,7 +59,7 @@ export class MastodonStreamList extends MastodonStream {
switch (data.type) { switch (data.type) {
case "updated": case "updated":
const encoded = await NoteConverter.encodeEvent(note, this.user); const encoded = await NoteConverter.encodeEvent(note, this.user, 'home');
this.connection.send(this.chName, "status.update", encoded); this.connection.send(this.chName, "status.update", encoded);
break; break;
case "deleted": case "deleted":
@ -75,7 +74,6 @@ export class MastodonStreamList extends MastodonStream {
if (!this.listUsers.includes(note.userId)) return false; if (!this.listUsers.includes(note.userId)) return false;
if (note.channelId) return false; if (note.channelId) return false;
if (note.renoteId !== null && !note.text && this.renoteMuting.has(note.userId)) return false; if (note.renoteId !== null && !note.text && this.renoteMuting.has(note.userId)) 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 === "specified") return !!note.visibleUserIds?.includes(this.user.id);
if (note.visibility === "followers") return this.following.has(note.userId); if (note.visibility === "followers") return this.following.has(note.userId);
return true; return true;

View file

@ -6,7 +6,6 @@ import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { StreamMessages } from "@/server/api/stream/types.js"; import { StreamMessages } from "@/server/api/stream/types.js";
import { fetchMeta } from "@/misc/fetch-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js";
import isQuote from "@/misc/is-quote.js"; import isQuote from "@/misc/is-quote.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class MastodonStreamPublic extends MastodonStream { export class MastodonStreamPublic extends MastodonStream {
public static shouldShare = true; public static shouldShare = true;
@ -40,7 +39,7 @@ export class MastodonStreamPublic extends MastodonStream {
private async onNote(note: Note) { private async onNote(note: Note) {
if (!await this.shouldProcessNote(note)) return; if (!await this.shouldProcessNote(note)) return;
const encoded = await NoteConverter.encodeEvent(note, this.user) const encoded = await NoteConverter.encodeEvent(note, this.user, 'public')
this.connection.send(this.chName, "update", encoded); this.connection.send(this.chName, "update", encoded);
} }
@ -50,7 +49,7 @@ export class MastodonStreamPublic extends MastodonStream {
switch (data.type) { switch (data.type) {
case "updated": case "updated":
const encoded = await NoteConverter.encodeEvent(note, this.user); const encoded = await NoteConverter.encodeEvent(note, this.user, 'public');
this.connection.send(this.chName, "status.update", encoded); this.connection.send(this.chName, "status.update", encoded);
break; break;
case "deleted": case "deleted":
@ -72,7 +71,6 @@ export class MastodonStreamPublic extends MastodonStream {
if (isUserRelated(note, this.muting)) return false; if (isUserRelated(note, this.muting)) return false;
if (isUserRelated(note, this.blocking)) return false; if (isUserRelated(note, this.blocking)) return false;
if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false; if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false;
if (this.userProfile && (await isFiltered(note as Note, this.user, this.userProfile))) return false;
return true; return true;
} }

View file

@ -5,7 +5,6 @@ import { Note } from "@/models/entities/note.js";
import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { StreamMessages } from "@/server/api/stream/types.js"; import { StreamMessages } from "@/server/api/stream/types.js";
import isQuote from "@/misc/is-quote.js"; import isQuote from "@/misc/is-quote.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class MastodonStreamTag extends MastodonStream { export class MastodonStreamTag extends MastodonStream {
public static shouldShare = false; public static shouldShare = false;
@ -34,7 +33,7 @@ export class MastodonStreamTag extends MastodonStream {
private async onNote(note: Note) { private async onNote(note: Note) {
if (!await this.shouldProcessNote(note)) return; if (!await this.shouldProcessNote(note)) return;
const encoded = await NoteConverter.encodeEvent(note, this.user) const encoded = await NoteConverter.encodeEvent(note, this.user, 'public')
this.connection.send(this.chName, "update", encoded); this.connection.send(this.chName, "update", encoded);
} }
@ -44,7 +43,7 @@ export class MastodonStreamTag extends MastodonStream {
switch (data.type) { switch (data.type) {
case "updated": case "updated":
const encoded = await NoteConverter.encodeEvent(note, this.user); const encoded = await NoteConverter.encodeEvent(note, this.user, 'public');
this.connection.send(this.chName, "status.update", encoded); this.connection.send(this.chName, "status.update", encoded);
break; break;
case "deleted": case "deleted":
@ -64,7 +63,6 @@ export class MastodonStreamTag extends MastodonStream {
if (isUserRelated(note, this.muting)) return false; if (isUserRelated(note, this.muting)) return false;
if (isUserRelated(note, this.blocking)) return false; if (isUserRelated(note, this.blocking)) return false;
if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false; if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false;
if (this.userProfile && (await isFiltered(note as Note, this.user, this.userProfile))) return false;
return true; return true;
} }

View file

@ -7,7 +7,6 @@ import { StreamMessages } from "@/server/api/stream/types.js";
import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js"; import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js";
import { AnnouncementConverter } from "@/server/api/mastodon/converters/announcement.js"; import { AnnouncementConverter } from "@/server/api/mastodon/converters/announcement.js";
import isQuote from "@/misc/is-quote.js"; import isQuote from "@/misc/is-quote.js";
import { isFiltered } from "@/misc/is-filtered.js";
export class MastodonStreamUser extends MastodonStream { export class MastodonStreamUser extends MastodonStream {
public static shouldShare = true; public static shouldShare = true;
@ -40,7 +39,7 @@ export class MastodonStreamUser extends MastodonStream {
private async onNote(note: Note) { private async onNote(note: Note) {
if (!await this.shouldProcessNote(note)) return; if (!await this.shouldProcessNote(note)) return;
const encoded = await NoteConverter.encodeEvent(note, this.user) const encoded = await NoteConverter.encodeEvent(note, this.user, 'home')
this.connection.send(this.chName, "update", encoded); this.connection.send(this.chName, "update", encoded);
} }
@ -50,7 +49,7 @@ export class MastodonStreamUser extends MastodonStream {
switch (data.type) { switch (data.type) {
case "updated": case "updated":
const encoded = await NoteConverter.encodeEvent(note, this.user); const encoded = await NoteConverter.encodeEvent(note, this.user, 'home');
this.connection.send(this.chName, "status.update", encoded); this.connection.send(this.chName, "status.update", encoded);
break; break;
case "deleted": case "deleted":
@ -64,7 +63,7 @@ export class MastodonStreamUser extends MastodonStream {
private async onUserEvent(data: StreamMessages["main"]["payload"]) { private async onUserEvent(data: StreamMessages["main"]["payload"]) {
switch (data.type) { switch (data.type) {
case "notification": case "notification":
const encoded = await NotificationConverter.encodeEvent(data.body.id, this.user); const encoded = await NotificationConverter.encodeEvent(data.body.id, this.user, 'notifications');
if (encoded) this.connection.send(this.chName, "notification", encoded); if (encoded) this.connection.send(this.chName, "notification", encoded);
break; break;
default: default:
@ -99,7 +98,6 @@ export class MastodonStreamUser extends MastodonStream {
if (isUserRelated(note, this.blocking)) return false; if (isUserRelated(note, this.blocking)) return false;
if (isUserRelated(note, this.hidden)) return false; if (isUserRelated(note, this.hidden)) return false;
if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false; if (note.renoteId !== null && !isQuote(note) && this.renoteMuting.has(note.userId)) return false;
if (this.userProfile && (await isFiltered(note as Note, this.user, this.userProfile))) return false;
return true; return true;
} }