diff --git a/src/index.ts b/src/index.ts index f26f599..ec3b774 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ -import { Injector, common, webpack } from "replugged"; +import { Injector, common, webpack, Logger } from "replugged"; import { subscriptions as UserDecorationsStoreSubscriptions, + useUserDecorAvatarDecoration, useUsersDecorationsStore, } from "./lib/stores/UserDecorationsStore"; import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants"; @@ -10,26 +11,32 @@ const AvatarURL = webpack.getByProps("getUserAvatarURL"); const { isAnimatedAvatarDecoration } = webpack.getByProps("isAnimatedAvatarDecoration"); const inject = new Injector(); +export const logger = Logger.plugin("UserDecorations"); export interface AvatarDecoration { asset: string; skuId: string; } -UserDecorationsStoreSubscriptions; + export async function start(): Promise { - inject.after(users, "getUser", (args, res) => { + useUserDecorAvatarDecoration; + UserDecorationsStoreSubscriptions; + + inject.after(users, "getUser", (_, res) => { const store = useUsersDecorationsStore.getState(); - if (res && store.has(res.id)) { + + if (res && store.has(res?.id)) { const decor = store.get(res.id); if (decor && res.avatarDecoration?.skuId !== SKU_ID) { - users.avatarDecoration = { + res.avatarDecoration = { asset: decor, skuId: SKU_ID, }; - } else if (!decor && res?.avatarDecoration?.skuId === SKU_ID) { - users.avatarDecoration = null; + + } else if (!decor && res.avatarDecoration && res.avatarDecoration?.skuId === SKU_ID) { + //res.avatarDecoration = null; } res.avatarDecorationData = res?.avatarDecoration; diff --git a/src/lib/api.ts b/src/lib/api.ts index b92e992..d555669 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,9 +1,45 @@ import { API_URL } from "./constants"; +import { useAuthorizationStore } from "./stores/AuthorizationStore"; + +export interface Decoration { + hash: string; + animated: boolean; + alt: string | null; + authorId: string | null; + reviewed: boolean | null; + presetId: string | null; +} + +export interface NewDecoration { + uri: string; + fileName: string; + fileType: string; + alt: string | null; +} + +export async function fetchApi(url: RequestInfo, options?: RequestInit) { + const res = await fetch(url, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${useAuthorizationStore.getState().token}`, + }, + }); + + if (res.ok) return res; + else throw new Error(await res.text()); +} export const getUsersDecorations = async (ids: string[] | undefined = undefined) => { - if (ids && ids.length === 0) return {} + if (ids && ids.length === 0) return {}; const url = new URL(API_URL + "/users"); if (ids && ids.length !== 0) url.searchParams.set("ids", JSON.stringify(ids)); - return (await fetch(url).then(c => c.json())) as Record; + return (await fetch(url).then((c) => c.json())) as Record; }; + +export const getUserDecorations = async (id: string = "@me"): Promise => + fetchApi(API_URL + `/users/${id}/decorations`).then((c) => c.json()); + +export const getUserDecoration = async (id: string = "@me"): Promise => + fetchApi(API_URL + `/users/${id}/decoration`).then((c) => c.json()); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index dce403c..d1bf4b9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -7,3 +7,4 @@ export const SKU_ID = "100101099111114"; // decor in ascii numbers export const RAW_SKU_ID = "11497119"; // raw in ascii numbers export const GUILD_ID = "1096357702931841148"; export const INVITE_KEY = "dXp2SdxDcP"; +export const DECORATION_FETCH_COOLDOWN = 1000 * 60 * 60 * 4; // 4 hours diff --git a/src/lib/stores/AuthorizationStore.ts b/src/lib/stores/AuthorizationStore.ts new file mode 100644 index 0000000..ade8230 --- /dev/null +++ b/src/lib/stores/AuthorizationStore.ts @@ -0,0 +1,59 @@ +import type { StateStorage } from "zustand/middleware"; +import { authorizationToken } from "../utils/settings"; +import { persist, create } from "../zustand"; +import { common } from "replugged"; +import showAuthorizationModal from "../utils/showAuthorizationModal"; + +const { users } = common; + +interface AuthorizationState { + token: string | null; + tokens: Record; + init: () => void; + authorize: () => Promise; + setToken: (token: string) => void; + remove: (id: string) => void; + isAuthorized: () => boolean; +} + +const indexedDBStorage: StateStorage = { + async getItem(name: string): Promise { + return (await authorizationToken).get(name).then((v) => v ?? null); + }, + async setItem(name: string, value: string): Promise { + await (await authorizationToken).set(name, value); + }, + async removeItem(name: string): Promise { + await authorizationToken.del(name); + }, +}; + +export const useAuthorizationStore = create( + persist( + (set, get) => ({ + token: null, + tokens: {}, + init: () => { + set({ token: get().tokens[users.getCurrentUser().id] ?? null }); + }, + setToken: (token: string) => + set({ token, tokens: { ...get().tokens, [users.getCurrentUser().id]: token } }), + remove: (id: string) => { + const { tokens, init } = get(); + const newTokens = { ...tokens }; + delete newTokens[id]; + set({ tokens: newTokens }); + + init(); + }, + authorize: () => void showAuthorizationModal(), + isAuthorized: () => !!get().token, + }), + { + name: "decor-auth", + getStorage: () => indexedDBStorage, + partialize: (state) => ({ tokens: state.tokens }), + onRehydrateStorage: () => (state) => state?.init(), + }, + ), +); diff --git a/src/lib/stores/CurrentUserDecorationsStore.ts b/src/lib/stores/CurrentUserDecorationsStore.ts new file mode 100644 index 0000000..21207c6 --- /dev/null +++ b/src/lib/stores/CurrentUserDecorationsStore.ts @@ -0,0 +1,43 @@ +import { common, lodash } from "replugged"; +import { Decoration, NewDecoration, getUserDecoration, getUserDecorations } from "../api"; +import discordifyDecoration from "../utils/discordifyDecoration"; +import { useUsersDecorationsStore } from "./UserDecorationsStore"; +import decorationToString from "../utils/decorationToString"; +import { create } from "../zustand"; + +const { lodash, users, fluxDispatcher } = common; + +interface CurrentUserDecorationsState { + decorations: Decoration[]; + selectedDecoration: Decoration | null; + fetched: boolean; + fetch: () => Promise; + delete: (decoration: Decoration | string) => Promise; + create: (decoration: NewDecoration) => Promise; + select: (decoration: Decoration | null) => Promise; + clear: () => void; +} + +function updateCurrentUserAvatarDecoration(decoration: Decoration | null) { + const user = users.getCurrentUser(); + user.avatarDecoration = decoration ? discordifyDecoration(decoration) : null; + user.avatarDecorationData = user.avatarDecoration; + + useUsersDecorationsStore + .getState() + .set(user.id, decoration ? decorationToString(decoration) : null); + fluxDispatcher.dispatch({ type: "CURRENT_USER_UPDATE", user }); + fluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SUBMIT_SUCCESS" }); +} + +export const useCurrentUserDecorationsStore = create((set, get) => ({ + decorations: [], + selectedDecoration: null, + async fetch() { + const decorations = await getUserDecorations(); + const selectedDecoration = await getUserDecoration(); + + set({ decorations, selectedDecoration }); + }, + clear: () => set({ decorations: [], selectedDecoration: null }) +})); diff --git a/src/lib/stores/UserDecorationsStore.ts b/src/lib/stores/UserDecorationsStore.ts index b7d803d..c850143 100644 --- a/src/lib/stores/UserDecorationsStore.ts +++ b/src/lib/stores/UserDecorationsStore.ts @@ -1,30 +1,37 @@ import { common } from "replugged"; import { create } from "../zustand"; import { getUsersDecorations } from "../api"; -import { SKU_ID } from "../constants"; +import { DECORATION_FETCH_COOLDOWN, SKU_ID } from "../constants"; import { AvatarDecoration } from "../.."; import subscribeToFluxDispatcher from "../utils/subscribeToFluxDispatcher"; +import { useCurrentUserDecorationsStore } from "./CurrentUserDecorationsStore"; +import { useAuthorizationStore } from "./AuthorizationStore"; const { lodash, users, fluxDispatcher, React, channels } = common; +interface UserDecorationData { + asset: string | null; + fetchedAt: Date; +} + interface UsersDecorationsState { - usesDecorations: Map; + usesDecorations: Map; fetchQueue: Set; bulkFetch: () => Promise; fetch: (userId: string, force?: boolean) => Promise; fetchMany: (userIds: string[]) => Promise; getAsset: (userId: string) => string | null | undefined; - get: (userId: string) => string | null | undefined; + get: (userId: string) => UserDecorationData | undefined; has: (userId: string) => boolean; set: (userId: string, decoration: string | null) => void; } export const useUsersDecorationsStore = create((set, get) => ({ - usersDecorations: new Map(), + usersDecorations: new Map(), fetchQueue: new Set(), bulkFetch: lodash.debounce(async () => { const { fetchQueue, usersDecorations } = get(); - set({ fetchQueue: new Set() }); + set({ fetchQueue: new Set() }); const fetchIds = Array.from(fetchQueue); if (fetchIds.length === 0) return; @@ -32,10 +39,11 @@ export const useUsersDecorationsStore = create((set, get) const newUsersDecorations = new Map(usersDecorations); for (const [userId, decoration] of Object.entries(fetchedUsersDecorations)) { - newUsersDecorations.set(userId, decoration); + newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() }); const user = users.getUser(userId) as any; if (user) { + console.log("Bulk: ",decoration) user.avatarDecoration = decoration ? { asset: decoration, skuId: SKU_ID } : null; user.avatarDecorationData = user.avatarDecoration; @@ -54,8 +62,11 @@ export const useUsersDecorationsStore = create((set, get) async fetch(userId: string, force: boolean = false) { const { usersDecorations, fetchQueue, bulkFetch } = get(); - if (!force && usersDecorations.has(userId)) return; - + if (usersDecorations.has(userId)) { + console.log("Fetch: ",usersDecorations.get(userId)) + const { fetchedAt } = usersDecorations.get(userId)!; + if (!force && Date.now() - fetchedAt.getTime() < DECORATION_FETCH_COOLDOWN) return; + } set({ fetchQueue: new Set(fetchQueue).add(userId) }); bulkFetch(); }, @@ -66,9 +77,15 @@ export const useUsersDecorationsStore = create((set, get) const { usersDecorations, fetchQueue, bulkFetch } = get(); const newFetchQueue = new Set(fetchQueue); + for (const userId of userIds) { - if (!usersDecorations.has(userId)) newFetchQueue.add(userId); + if (usersDecorations.has(userId)) { + const { fetchedAt } = usersDecorations.get(userId)!; + if (Date.now() - fetchedAt.getTime() < DECORATION_FETCH_COOLDOWN) continue; + } + newFetchQueue.add(userId); } + set({ fetchQueue: newFetchQueue }); bulkFetch(); }, @@ -86,29 +103,24 @@ export const useUsersDecorationsStore = create((set, get) const { usersDecorations } = get(); const newUsersDecorations = new Map(usersDecorations); - newUsersDecorations.set(userId, decoration); + newUsersDecorations.set(userId, { asset: decoration, fetchedAt: new Date() }); set({ usersDecorations: newUsersDecorations }); }, })); export const subscriptions = [ - subscribeToFluxDispatcher("LOAD_MESSAGES_SUCCESS", ({ messages }) => { - useUsersDecorationsStore.getState().fetchMany(messages.map((m) => m.author.id)); + subscribeToFluxDispatcher("USER_PROFILE_MODAL_OPEN", (data) => { + useUsersDecorationsStore.getState().fetch(data.userId, true); }), + subscribeToFluxDispatcher("CONNECTION_OPEN", () => { + useAuthorizationStore.getState().init(); + useCurrentUserDecorationsStore.getState().clear(); useUsersDecorationsStore.getState().fetch(users.getCurrentUser().id, true); }), - subscribeToFluxDispatcher("MESSAGE_CREATE", (data) => { - const channelId = channels.getChannelId(); - if (data.channelId === channelId) { - useUsersDecorationsStore.getState().fetch(data.message.author.id); - } - }), - subscribeToFluxDispatcher("TYPING_START", (data) => { - const channelId = channels.getChannelId(); - if (data.channelId === channelId) { - useUsersDecorationsStore.getState().fetch(data.userId); - } + + subscribeToFluxDispatcher("LOAD_MESSAGES_SUCCESS", ({ messages }) => { + useUsersDecorationsStore.getState().fetchMany(messages.map((m) => m.author.id)); }), ]; @@ -135,5 +147,7 @@ export function useUserDecorAvatarDecoration(user): AvatarDecoration | null | un fetchUserDecorAvatarDecoration(user.id); }, []); + console.log("Effects", decorAvatarDecoration) + return decorAvatarDecoration ? { asset: decorAvatarDecoration, skuId: SKU_ID } : null; } diff --git a/src/lib/utils/decorationToString.ts b/src/lib/utils/decorationToString.ts new file mode 100644 index 0000000..95844d2 --- /dev/null +++ b/src/lib/utils/decorationToString.ts @@ -0,0 +1,3 @@ +import { Decoration } from "../api"; + +export default (decoration: Decoration) => `${decoration.animated ? 'a_' : ''}${decoration.hash}`; diff --git a/src/lib/utils/discordifyDecoration.ts b/src/lib/utils/discordifyDecoration.ts new file mode 100644 index 0000000..8947bd8 --- /dev/null +++ b/src/lib/utils/discordifyDecoration.ts @@ -0,0 +1,5 @@ +import { Decoration } from "../api"; +import { SKU_ID } from "../constants"; +import decorationToString from "./decorationToString"; + +export default (d: Decoration) => ({ asset: decorationToString(d), skuId: SKU_ID }); diff --git a/src/lib/utils/settings.ts b/src/lib/utils/settings.ts new file mode 100644 index 0000000..c9baf92 --- /dev/null +++ b/src/lib/utils/settings.ts @@ -0,0 +1,4 @@ +import { settings } from "replugged" + +export const defaultSettings = {} +export const authorizationToken = settings.init("decor.auth", defaultSettings) diff --git a/src/lib/utils/showAuthorizationModal.tsx b/src/lib/utils/showAuthorizationModal.tsx new file mode 100644 index 0000000..d0cc161 --- /dev/null +++ b/src/lib/utils/showAuthorizationModal.tsx @@ -0,0 +1,39 @@ +import { webpack, common } from "replugged" +import { useAuthorizationStore } from "../stores/AuthorizationStore"; +import { AUTHORIZE_URL, CLIENT_ID } from "../constants"; +import { logger } from "../.."; + +const { modal: { openModal } } = common +const OAuth = webpack.getByProps("OAuth2AuthorizeModal") + +export default async () => new Promise(r => openModal(props => + { + try { + const url = new URL(response.location); + url.searchParams.append("client", "vencord"); + + const req = await fetch(url); + + if (req?.ok) { + const token = await req.text(); + useAuthorizationStore.getState().setToken(token); + } else { + throw new Error("Request not OK"); + } + r(void 0); + } catch (e) { + logger("Decor").error("Failed to authorize", e); + } + }} + /> +)) + + diff --git a/src/lib/zustand.ts b/src/lib/zustand.ts index 2bc3fc7..d9bebfc 100644 --- a/src/lib/zustand.ts +++ b/src/lib/zustand.ts @@ -1,4 +1,5 @@ -import { webpack } from "replugged" +import { webpack } from "replugged"; - -export const create: typeof import("zustand").default = webpack.getBySource("will be removed in v4") +export const create: typeof import("zustand").default = + webpack.getBySource("will be removed in v4"); +export const { persist } = webpack.getBySource("[zustand persist middleware]");