ReviewDB: proper multi account support

This commit is contained in:
Vendicated 2023-10-06 19:40:53 +02:00
parent 664dd0a992
commit c0f2c97458
No known key found for this signature in database
GPG key ID: D66986BAF75ECF18
9 changed files with 143 additions and 90 deletions

View file

@ -0,0 +1,78 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { findByPropsLazy } from "@webpack";
import { showToast, Toasts, UserStore } from "@webpack/common";
import { ReviewDBAuth } from "./entities";
const DATA_STORE_KEY = "rdb-auth";
const OAuth = findByPropsLazy("OAuth2AuthorizeModal");
export let Auth: ReviewDBAuth = {};
export async function initAuth() {
Auth = await getAuth() ?? {};
}
export async function getAuth(): Promise<ReviewDBAuth | undefined> {
const auth = await DataStore.get(DATA_STORE_KEY);
return auth?.[UserStore.getCurrentUser()?.id];
}
export async function getToken() {
const auth = await getAuth();
return auth?.token;
}
export async function updateAuth(newAuth: ReviewDBAuth) {
return DataStore.update(DATA_STORE_KEY, auth => {
auth ??= {};
Auth = auth[UserStore.getCurrentUser().id] ??= {};
if (newAuth.token) Auth.token = newAuth.token;
if (newAuth.user) Auth.user = newAuth.user;
return auth;
});
}
export function authorize(callback?: any) {
openModal(props =>
<OAuth.OAuth2AuthorizeModal
{...props}
scopes={["identify"]}
responseType="code"
redirectUri="https://manti.vendicated.dev/api/reviewdb/auth"
permissions={0n}
clientId="915703782174752809"
cancelCompletesFlow={false}
callback={async (response: any) => {
try {
const url = new URL(response.location);
url.searchParams.append("clientMod", "vencord");
const res = await fetch(url, {
headers: new Headers({ Accept: "application/json" })
});
const { token, success } = await res.json();
if (success) {
updateAuth({ token });
showToast("Successfully logged in!");
callback?.();
} else if (res.status === 1) {
showToast("An Error occurred while logging in.", Toasts.Type.FAILURE);
}
} catch (e) {
new Logger("ReviewDB").error("Failed to authorize", e);
}
}}
/>
);
}

View file

@ -20,12 +20,12 @@ import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react"; import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack"; import { filters, findBulk } from "@webpack";
import { Alerts, moment, Parser, Timestamp } from "@webpack/common"; import { Alerts, moment, Parser, showToast, Timestamp } from "@webpack/common";
import { Review, ReviewType } from "../entities"; import { Review, ReviewType } from "../entities";
import { deleteReview, reportReview } from "../reviewDbApi"; import { deleteReview, reportReview } from "../reviewDbApi";
import { settings } from "../settings"; import { settings } from "../settings";
import { canDeleteReview, cl, showToast } from "../utils"; import { canDeleteReview, cl } from "../utils";
import { DeleteButton, ReportButton } from "./MessageButton"; import { DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge"; import ReviewBadge from "./ReviewBadge";

View file

@ -21,8 +21,8 @@ import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, Mo
import { useForceUpdater } from "@utils/react"; import { useForceUpdater } from "@utils/react";
import { Paginator, Text, useRef, useState } from "@webpack/common"; import { Paginator, Text, useRef, useState } from "@webpack/common";
import { Auth } from "../auth";
import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings";
import { cl } from "../utils"; import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent"; import ReviewComponent from "./ReviewComponent";
import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
@ -35,7 +35,7 @@ function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: st
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const reviewCount = data?.reviewCount; const reviewCount = data?.reviewCount;
const ownReview = data?.reviews.find(r => r.sender.discordID === settings.store.user?.discordID); const ownReview = data?.reviews.find(r => r.sender.discordID === Auth.user?.discordID);
return ( return (
<ErrorBoundary> <ErrorBoundary>
@ -68,6 +68,7 @@ function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: st
<ReviewComponent <ReviewComponent
refetch={refetch} refetch={refetch}
review={ownReview} review={ownReview}
profileId={discordId}
/> />
)} )}
<ReviewsInputComponent <ReviewsInputComponent

View file

@ -18,12 +18,13 @@
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react"; import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
import { find, findByPropsLazy } from "@webpack"; import { find, findByPropsLazy } from "@webpack";
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common"; import { Forms, React, RelationshipStore, showToast, useRef, UserStore } from "@webpack/common";
import { Auth, authorize } from "../auth";
import { Review } from "../entities"; import { Review } from "../entities";
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings"; import { settings } from "../settings";
import { authorize, cl, showToast } from "../utils"; import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent"; import ReviewComponent from "./ReviewComponent";
@ -120,7 +121,7 @@ function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch():
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) { export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
const { token } = settings.store; const { token } = Auth;
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const inputType = InputTypes.FORM; const inputType = InputTypes.FORM;
inputType.disableAutoFocus = true; inputType.disableAutoFocus = true;

View file

@ -36,6 +36,11 @@ export const enum NotificationType {
Warning = 3 Warning = 3
} }
export interface ReviewDBAuth {
token?: string;
user?: ReviewDBUser;
}
export interface Badge { export interface Badge {
name: string; name: string;
description: string; description: string;

View file

@ -23,16 +23,17 @@ import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader"; import ExpandableHeader from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons"; import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Alerts, Menu, Parser, useState } from "@webpack/common"; import { Alerts, Menu, Parser, showToast, useState } from "@webpack/common";
import { Guild, User } from "discord-types/general"; import { Guild, User } from "discord-types/general";
import { Auth, initAuth, updateAuth } from "./auth";
import { openReviewsModal } from "./components/ReviewModal"; import { openReviewsModal } from "./components/ReviewModal";
import ReviewsView from "./components/ReviewsView"; import ReviewsView from "./components/ReviewsView";
import { NotificationType } from "./entities"; import { NotificationType } from "./entities";
import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings"; import { settings } from "./settings";
import { showToast } from "./utils";
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => { const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
children.push( children.push(
@ -62,31 +63,48 @@ export default definePlugin({
} }
], ],
async start() { flux: {
const s = settings.store; CONNECTION_OPEN: initAuth,
const { token, lastReviewId, notifyReviews } = s; },
if (!notifyReviews || !token) return; async start() {
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
const s = settings.store;
const { lastReviewId, notifyReviews } = s;
const legacy = s as any as { token?: string; };
if (legacy.token) {
await updateAuth({ token: legacy.token });
legacy.token = undefined;
new Logger("ReviewDB").info("Migrated legacy settings");
}
await initAuth();
setTimeout(async () => { setTimeout(async () => {
const user = await getCurrentUserInfo(token); if (!Auth.token) return;
if (lastReviewId && lastReviewId < user.lastReviewID) {
s.lastReviewId = user.lastReviewID;
if (user.lastReviewID !== 0)
showToast("You have new reviews on your profile!");
}
addContextMenuPatch("guild-header-popout", guildPopoutPatch); const user = await getCurrentUserInfo(Auth.token);
updateAuth({ user });
if (notifyReviews) {
if (lastReviewId && lastReviewId < user.lastReviewID) {
s.lastReviewId = user.lastReviewID;
if (user.lastReviewID !== 0)
showToast("You have new reviews on your profile!");
}
}
if (user.notification) { if (user.notification) {
const props = user.notification.type === NotificationType.Ban ? { const props = user.notification.type === NotificationType.Ban ? {
cancelText: "Appeal", cancelText: "Appeal",
confirmText: "Ok", confirmText: "Ok",
onCancel: () => onCancel: async () =>
VencordNative.native.openExternal( VencordNative.native.openExternal(
"https://reviewdb.mantikafasi.dev/api/redirect?" "https://reviewdb.mantikafasi.dev/api/redirect?"
+ new URLSearchParams({ + new URLSearchParams({
token: settings.store.token!, token: Auth.token!,
page: "dashboard/appeal" page: "dashboard/appeal"
}) })
) )
@ -105,7 +123,6 @@ export default definePlugin({
readNotification(user.notification.id); readNotification(user.notification.id);
} }
s.user = user;
}, 4000); }, 4000);
}, },

View file

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { showToast, Toasts } from "@webpack/common";
import { authorize, getToken } from "./auth";
import { Review, ReviewDBUser } from "./entities"; import { Review, ReviewDBUser } from "./entities";
import { settings } from "./settings"; import { settings } from "./settings";
import { authorize, showToast } from "./utils";
const API_URL = "https://manti.vendicated.dev"; const API_URL = "https://manti.vendicated.dev";
@ -57,7 +59,7 @@ export async function getReviews(id: string, offset = 0): Promise<Response> {
}; };
if (!res.success) { if (!res.success) {
showToast(res.message); showToast(res.message, Toasts.Type.FAILURE);
return { return {
...res, ...res,
reviews: [ reviews: [
@ -82,7 +84,7 @@ export async function getReviews(id: string, offset = 0): Promise<Response> {
} }
export async function addReview(review: any): Promise<Response | null> { export async function addReview(review: any): Promise<Response | null> {
review.token = settings.store.token; review.token = await getToken();
if (!review.token) { if (!review.token) {
showToast("Please authorize to add a review."); showToast("Please authorize to add a review.");
@ -104,7 +106,7 @@ export async function addReview(review: any): Promise<Response | null> {
}); });
} }
export function deleteReview(id: number): Promise<Response> { export async function deleteReview(id: number): Promise<Response> {
return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, { return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, {
method: "DELETE", method: "DELETE",
headers: new Headers({ headers: new Headers({
@ -112,7 +114,7 @@ export function deleteReview(id: number): Promise<Response> {
Accept: "application/json", Accept: "application/json",
}), }),
body: JSON.stringify({ body: JSON.stringify({
token: settings.store.token, token: await getToken(),
reviewid: id reviewid: id
}) })
}).then(r => r.json()); }).then(r => r.json());
@ -127,7 +129,7 @@ export async function reportReview(id: number) {
}), }),
body: JSON.stringify({ body: JSON.stringify({
reviewid: id, reviewid: id,
token: settings.store.token token: await getToken()
}) })
}).then(r => r.json()) as Response; }).then(r => r.json()) as Response;
@ -141,11 +143,11 @@ export function getCurrentUserInfo(token: string): Promise<ReviewDBUser> {
}).then(r => r.json()); }).then(r => r.json());
} }
export function readNotification(id: number) { export async function readNotification(id: number) {
return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, { return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, {
method: "PATCH", method: "PATCH",
headers: { headers: {
"Authorization": settings.store.token || "", "Authorization": await getToken() || "",
}, },
}); });
} }

View file

@ -20,8 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types"; import { OptionType } from "@utils/types";
import { Button } from "@webpack/common"; import { Button } from "@webpack/common";
import { ReviewDBUser } from "./entities"; import { authorize, getToken } from "./auth";
import { authorize } from "./utils";
export const settings = definePluginSettings({ export const settings = definePluginSettings({
authorize: { authorize: {
@ -57,10 +56,11 @@ export const settings = definePluginSettings({
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "ReviewDB website", description: "ReviewDB website",
component: () => ( component: () => (
<Button onClick={() => { <Button onClick={async () => {
let url = "https://reviewdb.mantikafasi.dev/"; let url = "https://reviewdb.mantikafasi.dev/";
if (settings.store.token) const token = await getToken();
url += "/api/redirect?token=" + encodeURIComponent(settings.store.token); if (token)
url += "/api/redirect?token=" + encodeURIComponent(token);
VencordNative.native.openExternal(url); VencordNative.native.openExternal(url);
}}> }}>
@ -80,8 +80,6 @@ export const settings = definePluginSettings({
) )
} }
}).withPrivateSettings<{ }).withPrivateSettings<{
token?: string;
user?: ReviewDBUser;
lastReviewId?: number; lastReviewId?: number;
reviewsDropdownState?: boolean; reviewsDropdownState?: boolean;
}>(); }>();

View file

@ -17,67 +17,18 @@
*/ */
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger"; import { UserStore } from "@webpack/common";
import { openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { React, Toasts, UserStore } from "@webpack/common";
import { Auth } from "./auth";
import { Review, UserType } from "./entities"; import { Review, UserType } from "./entities";
import { settings } from "./settings";
export const cl = classNameFactory("vc-rdb-"); export const cl = classNameFactory("vc-rdb-");
export function authorize(callback?: any) {
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
openModal((props: any) =>
<OAuth2AuthorizeModal
{...props}
scopes={["identify"]}
responseType="code"
redirectUri="https://manti.vendicated.dev/api/reviewdb/auth"
permissions={0n}
clientId="915703782174752809"
cancelCompletesFlow={false}
callback={async (response: any) => {
try {
const url = new URL(response.location);
url.searchParams.append("clientMod", "vencord");
const res = await fetch(url, {
headers: new Headers({ Accept: "application/json" })
});
const { token, success } = await res.json();
if (success) {
settings.store.token = token;
showToast("Successfully logged in!");
callback?.();
} else if (res.status === 1) {
showToast("An Error occurred while logging in.");
}
} catch (e) {
new Logger("ReviewDB").error("Failed to authorize", e);
}
}}
/>
);
}
export function showToast(text: string) {
Toasts.show({
type: Toasts.Type.MESSAGE,
message: text,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
},
});
}
export function canDeleteReview(profileId: string, review: Review) { export function canDeleteReview(profileId: string, review: Review) {
const myId = UserStore.getCurrentUser().id; const myId = UserStore.getCurrentUser().id;
return ( return (
myId === profileId myId === profileId
|| review.sender.discordID === profileId || review.sender.discordID === profileId
|| settings.store.user?.type === UserType.Admin || Auth.user?.type === UserType.Admin
); );
} }