import { safeUserIdentity, type UserIdentity, } from "$lib/Data/AniList/identity"; import { addUserBadge, getUserBadges, removeAllUserBadges, removeUserBadge, setShadowHidden, setShadowHiddenBadge, updateUserBadge, type Badge as DatabaseBadge, } from "$lib/Database/SB/User/badges"; import type { WithIndex } from "../$types"; import type { Resolvers as RootResolvers, Badge } from "../$types"; import type { RequestEvent } from "@sveltejs/kit"; import { getUserPreferences, setBiography, setCSS, setPinnedBadgeWallCategories, toggleHideAWCBadges, toggleHideMissingBadges, toggleHololiveStreamPinning, togglePinnedBadgeWallCategory, type UserPreferences, } from "$lib/Database/SB/User/preferences"; import privilegedUser from "$lib/Utility/privilegedUser"; import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; type Context = RequestEvent>, string | null>; type UserResolvers = Pick< RootResolvers, "Query" | "Mutation" | "User" | "Badge" | "Preferences" >; const toGraphQLBadges = (databaseBadges: DatabaseBadge[]): Badge[] => databaseBadges.map((databaseBadge) => ({ id: databaseBadge.id ?? 0, post: databaseBadge.post ?? "", image: databaseBadge.image ?? "", time: databaseBadge.time ?? new Date().toISOString(), hidden: databaseBadge.hidden ?? false, shadow_hidden: databaseBadge.shadow_hidden ?? false, click_count: databaseBadge.click_count ?? 0, category: databaseBadge.category ?? null, description: databaseBadge.description ?? null, source: databaseBadge.source ?? null, designer: databaseBadge.designer ?? null, })); const auth = async (context: Context) => { const userCookie = context.cookies.get("user"); if (!userCookie) return Error("Unauthorised"); const user = decodeAuthCookieOrNull(userCookie); if (!user) return Error("Unauthorised"); return (await safeUserIdentity(user)) ?? Error("Unauthorised"); }; const authenticatedBadgesOperation = async ( context: Context, operation: (identity: UserIdentity, authorised: boolean) => Promise, ) => { const identity = await auth(context); if (identity instanceof Error) throw new Error("Unauthorized"); const authorised = privilegedUser(identity.id); await operation(identity, authorised); const databaseBadges = await getUserBadges(identity.id); const badges = toGraphQLBadges(databaseBadges); return { id: identity.id, badges, preferences: null, badgesCount: badges.length, }; }; const authenticatedPreferencesOperation = async ( context: Context, operation: ( identity: UserIdentity, authorised: boolean, ) => Promise, ) => { const identity = await auth(context); if (identity instanceof Error) throw new Error("Unauthorized"); const authorised = privilegedUser(identity.id); return { id: identity.id, badges: [] as Badge[], preferences: await operation(identity, authorised), badgesCount: 0, }; }; const ensureOwnerOrPrivileged = ( identity: UserIdentity, authorised: boolean, targetUserId: number, ) => { if (!authorised && identity.id !== targetUserId) throw new Error("Unauthorized"); }; const ensureBadgeOwnerOrPrivileged = async ( identity: UserIdentity, authorised: boolean, badgeId: number, ) => { if (authorised) return; const ownsBadge = (await getUserBadges(identity.id)).some( (badge) => badge.id === badgeId, ); if (!ownsBadge) throw new Error("Unauthorized"); }; export const resolvers: WithIndex = { Query: { User: async (_, args) => { if (!args.id) return null; const databaseBadges = await getUserBadges(args.id); const badges = toGraphQLBadges(databaseBadges); return { id: args.id, badges, preferences: await getUserPreferences(args.id), badgesCount: badges.length, }; }, badges: async (_, args) => { if (!args.id) return []; const databaseBadges = await getUserBadges( args.id, args.page || 0, args.size || 0, ); return toGraphQLBadges(databaseBadges); }, }, Mutation: { shadowHideBadges: async (_, args, context) => await authenticatedBadgesOperation( context, async (identity, authorised) => { ensureOwnerOrPrivileged(identity, authorised, args.userId); await setShadowHidden(args.userId, authorised); }, ), shadowHideBadge: async (_, args, context) => await authenticatedBadgesOperation( context, async (identity, authorised) => { await ensureBadgeOwnerOrPrivileged(identity, authorised, args.id); await setShadowHiddenBadge( args.id, args.state == null ? true : args.state, ); }, ), hideBadge: async (_, args, context) => await authenticatedBadgesOperation(context, async (identity) => { const allBadges = await getUserBadges(identity.id); const category = args.category || ""; await Promise.all( allBadges .filter((badge) => badge.category === category) .map(async (badge) => { await updateUserBadge(identity.id, badge.id as number, { ...badge, hidden: allBadges .filter((badge) => badge.category === category) .filter((badge) => badge.hidden).length > allBadges.filter((badge) => badge.category === category) .length / 2 ? false : true, }); }), ); }), updateBadge: async (_, args, context) => await authenticatedBadgesOperation(context, async (identity) => { const badge = { post: args.post || undefined, image: args.image || undefined, description: args.description || null, time: args.time || undefined, category: args.category || null, hidden: args.hidden || false, source: args.source || null, designer: args.designer || null, }; if ( (await getUserBadges(identity.id)).find( (badge) => badge.id === args.id, ) ) { await updateUserBadge(identity.id, args.id as number, badge); } else { await addUserBadge(identity.id, badge); } }), deleteBadge: async (_, args, context) => await authenticatedBadgesOperation( context, async (identity) => await removeUserBadge(identity.id, args.id), ), pruneUserBadges: async (_, __, context) => await authenticatedBadgesOperation( context as Context, async (identity) => await removeAllUserBadges(identity.id), ), toggleHideMissingBadges: async (_, _args, context) => await authenticatedPreferencesOperation( context as Context, async (identity) => await toggleHideMissingBadges(identity.id), ), toggleHideAWCBadges: async (_, _args, context) => await authenticatedPreferencesOperation( context as Context, async (identity) => await toggleHideAWCBadges(identity.id), ), setBadgeWallCSS: async (_, args, context) => await authenticatedPreferencesOperation( context as Context, async (identity) => await setCSS(identity.id, args.css), ), togglePinnedBadgeWallCategory: async (_, args, context) => await authenticatedPreferencesOperation( context as Context, async (identity) => await togglePinnedBadgeWallCategory(identity.id, args.category), ), setPinnedBadgeWallCategories: async (_, args, context) => await authenticatedPreferencesOperation( context as Context, async (identity) => await setPinnedBadgeWallCategories(identity.id, args.categories), ), setBiography: async (_, args, context) => await authenticatedPreferencesOperation( context as Context, async (identity) => await setBiography(identity.id, args.biography.slice(0, 3000)), ), togglePinnedHololiveStream: async (_, args, context) => await authenticatedPreferencesOperation( context as Context, async (identity) => await toggleHololiveStreamPinning(identity.id, args.stream), ), }, };