separate components/remove api backend setting/wip oauth

This commit is contained in:
rushiiMachine 2024-06-29 13:09:19 -07:00
parent 964801da3a
commit 1fd506da73
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
7 changed files with 210 additions and 150 deletions

View file

@ -7,15 +7,12 @@
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 { OAuth2AuthorizeModal, showToast, Toasts, UserStore } from "@webpack/common";
import { ReviewDBAuth } from "./entities";
const DATA_STORE_KEY = "rdb-auth";
const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
export let Auth: ReviewDBAuth = {};
export async function initAuth() {

View file

@ -6,33 +6,33 @@
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { Logger } from "@utils/Logger";
import { UserStore } from "@webpack/common";
import settings from "./settings";
export const DEFAULT_API = "https://timezonedb.catvibers.me/api";
export const API_URL: string = "https://timezonedb.catvibers.me/api";
export type Snowflake = string;
type ApiError = { error: string; };
type UserFetchResponse = ApiError | { timezoneId: string }
type BulkFetchResponse = ApiError | Record<Snowflake, { timezoneId: string | null }>;
export async function verifyApi(url: string): Promise<boolean> {
if (url === DEFAULT_API) return true;
const res = await fetch(url, {
export async function verifyLogin(token: string): Promise<boolean> {
const res = await fetch(API_URL, {
method: "GET",
headers: {
"User-Agent": VENCORD_USER_AGENT,
"Authorization": token,
},
});
return "logged_in" in await res.json();
const json: { logged_in?: boolean } = await res.json();
return !!json.logged_in;
}
export async function fetchTimezonesBulk(ids: Snowflake[]): Promise<Record<Snowflake, string | null> | undefined> {
try {
const { apiUrl } = settings.store;
const req = await fetch(`${apiUrl}/user/bulk`, {
const req = await fetch(`${API_URL}/user/bulk`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -58,8 +58,7 @@ export async function fetchTimezonesBulk(ids: Snowflake[]): Promise<Record<Snowf
export async function fetchTimezone(userId: Snowflake): Promise<string | null | undefined> {
try {
const { apiUrl } = settings.store;
const req = await fetch(`${apiUrl}/user/${userId}`, {
const req = await fetch(`${API_URL}/user/${userId}`, {
method: "GET",
headers: {
"User-Agent": VENCORD_USER_AGENT,
@ -80,3 +79,10 @@ export async function fetchTimezone(userId: Snowflake): Promise<string | null |
return undefined;
}
}
export function getCurrentToken(): string | undefined {
const userId = UserStore.getCurrentUser().id;
const { tokens } = settings.store;
return tokens[userId];
}

View file

@ -0,0 +1,95 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "../styles.css";
import { ErrorBoundary } from "@components/index";
import { findByPropsLazy } from "@webpack";
import { React, Tooltip, useEffect, useState } from "@webpack/common";
import { Snowflake } from "../api";
import { getUserTimezone } from "../cache";
import { formatTimestamp } from "../utils";
import { openTimezoneOverrideModal } from "./SetTimezoneOverrideModal";
// Based on Syncxv's vc-timezones user plugin //
const messageClasses = findByPropsLazy("timestamp", "compact", "contentOnly");
interface LocalTimestampProps {
userId: Snowflake;
timestamp?: Date;
type: "message" | "profile";
}
export function LocalTimestamp(props: LocalTimestampProps): JSX.Element {
return <ErrorBoundary noop={true} wrappedProps={props}>
<LocalTimestampInner {...props} />
</ErrorBoundary>;
}
function LocalTimestampInner(props: LocalTimestampProps): JSX.Element | null {
const [timezone, setTimezone] = useState<string | null>();
const [timestamp, setTimestamp] = useState(props.timestamp ?? Date.now());
useEffect(() => {
if (!timezone) {
getUserTimezone(props.userId, props.type === "profile").then(setTimezone);
return;
}
let timer: NodeJS.Timeout;
if (props.type === "profile") {
setTimestamp(Date.now());
const now = new Date();
const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds();
timer = setTimeout(() => setTimestamp(Date.now()), delay);
}
return () => timer && clearTimeout(timer);
}, [timezone, timestamp]);
if (!timezone) return null;
const longTime = formatTimestamp(timezone, timestamp, true);
const shortTime = formatTimestamp(timezone, timestamp, false);
if (props.type === "message" && !shortTime)
return null;
const shortTimeFormatted = props.type === "message"
? `${shortTime}`
: shortTime ?? "Error";
const classes = props.type === "message"
? `vc-timezones-message-display ${messageClasses.timestamp}`
: "vc-timezones-profile-display";
return <Tooltip
position="top"
// @ts-ignore
delay={750}
allowOverflow={false}
spacing={8}
hideOnClick={true}
tooltipClassName="vc-timezones-tooltip"
hide={!longTime}
text={longTime}
>
{toolTipProps => <>
<span {...toolTipProps}
className={classes}
onClick={() => {
toolTipProps.onClick();
openTimezoneOverrideModal(props.userId);
}}>
{shortTimeFormatted}
</span>
</>}
</Tooltip>;
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import "../styles.css";
import { ErrorBoundary, Link } from "@components/index";
import { Margins } from "@utils/margins";
@ -18,102 +18,27 @@ import {
ModalRoot,
openModal,
} from "@utils/modal";
import { findByPropsLazy } from "@webpack";
import { Button, Forms, React, SearchableSelect, Text, Tooltip, useEffect, useState } from "@webpack/common";
import { Button, Forms, React, SearchableSelect, Text, useEffect, useState } from "@webpack/common";
import { SelectOption } from "@webpack/types";
import { Snowflake } from "./api";
import { getUserTimezone } from "./cache";
import settings, { TimezoneOverrides } from "./settings";
import { formatTimestamp, getTimezonesLazy } from "./utils";
import settings, { TimezoneOverrides } from "../settings";
import { getTimezonesLazy } from "../utils";
import { openTimezoneDBAuthModal } from "./TimezoneDBAuthModal";
// Based on Syncxv's vc-timezones user plugin //
const messageClasses = findByPropsLazy("timestamp", "compact", "contentOnly");
interface LocalTimestampProps {
userId: Snowflake;
timestamp?: Date;
type: "message" | "profile";
export function openTimezoneOverrideModal(userId: string) {
openModal(modalProps => <>
<ErrorBoundary>
<SetTimezoneOverrideModal userId={userId} modalProps={modalProps} />
</ErrorBoundary>
</>);
}
export function LocalTimestamp(props: LocalTimestampProps): JSX.Element {
return <ErrorBoundary noop={true} wrappedProps={props}>
<LocalTimestampInner {...props} />
</ErrorBoundary>;
}
function LocalTimestampInner(props: LocalTimestampProps): JSX.Element | null {
const [timezone, setTimezone] = useState<string | null>();
const [timestamp, setTimestamp] = useState(props.timestamp ?? Date.now());
useEffect(() => {
if (!timezone) {
getUserTimezone(props.userId, props.type === "profile").then(setTimezone);
return;
}
let timer: NodeJS.Timeout;
if (props.type === "profile") {
setTimestamp(Date.now());
const now = new Date();
const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds();
timer = setTimeout(() => setTimestamp(Date.now()), delay);
}
return () => timer && clearTimeout(timer);
}, [timezone, timestamp]);
if (!timezone) return null;
const longTime = formatTimestamp(timezone, timestamp, true);
const shortTime = formatTimestamp(timezone, timestamp, false);
if (props.type === "message" && !shortTime)
return null;
const shortTimeFormatted = props.type === "message"
? `${shortTime}`
: shortTime ?? "Error";
const classes = props.type === "message"
? `vc-timezones-message-display ${messageClasses.timestamp}`
: "vc-timezones-profile-display";
return <>
<Tooltip
position="top"
// @ts-ignore
delay={750}
allowOverflow={false}
spacing={8}
hideOnClick={true}
tooltipClassName="vc-timezones-tooltip"
hide={!longTime}
text={longTime}
>
{toolTipProps => <>
<span {...toolTipProps}
className={classes}
onClick={() => {
toolTipProps.onClick();
openTimezoneOverrideModal(props.userId);
}}>
{shortTimeFormatted}
</span>
</>}
</Tooltip>
</>;
}
interface TimezoneOverrideModalProps {
interface SetTimezoneOverrideModalProps {
userId: string,
modalProps: ModalProps,
}
function SetTimezoneOverrideModal(props: TimezoneOverrideModalProps) {
function SetTimezoneOverrideModal(props: SetTimezoneOverrideModalProps) {
const [availableTimezones, setAvailableTimezones] = useState<SelectOption[]>();
const [timezone, setTimezone] = useState<string | "NONE" | undefined>();
@ -174,7 +99,7 @@ function SetTimezoneOverrideModal(props: TimezoneOverrideModalProps) {
<br />
<br />
To set your own Timezone for other users to see,
click <Link onClick={/* TODO */ _ => _}>here</Link> to
click <Link onClick={openTimezoneDBAuthModal}>here</Link> to
authorize the public TimezoneDB API.
</Text>
@ -207,11 +132,3 @@ function SetTimezoneOverrideModal(props: TimezoneOverrideModalProps) {
</ModalFooter>
</ModalRoot>;
}
export function openTimezoneOverrideModal(userId: string) {
openModal(modalProps => <>
<ErrorBoundary>
<SetTimezoneOverrideModal userId={userId} modalProps={modalProps} />
</ErrorBoundary>
</>);
}

View file

@ -0,0 +1,67 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { Logger } from "@utils/Logger";
import { ModalProps, openModal } from "@utils/modal";
import { OAuth2AuthorizeModal, showToast, Toasts, UserStore } from "@webpack/common";
import { API_URL, getCurrentToken, verifyLogin } from "../api";
import settings from "../settings";
const REDIRECT_URI: string = `${API_URL}/auth`;
const CLIENT_ID: string = "922650528821940224";
const SCOPES: string[] = ["identify"];
export async function openTimezoneDBAuthModal() {
const token = getCurrentToken();
if (token && await verifyLogin(token)) return; // TODO: open set current user modal
openModal(modalProps => <>
<ErrorBoundary>
<TimezoneDBAuthModal {...modalProps} />
</ErrorBoundary>
</>);
}
function TimezoneDBAuthModal(modalProps: ModalProps): JSX.Element {
return <OAuth2AuthorizeModal
{...modalProps}
scopes={SCOPES}
responseType="code"
redirectUri={REDIRECT_URI}
permissions={0n}
clientId={CLIENT_ID}
cancelCompletesFlow={false}
callback={async (response: { location: string }) => {
try {
const res = await fetch(response.location, {
redirect: "manual",
headers: {
"Content-Type": VENCORD_USER_AGENT,
},
});
const { token } = await res.json() as { token: string };
if (!await verifyLogin(token)) {
throw "Returned token was invalid!";
}
settings.store.tokens = {
[UserStore.getCurrentUser().id]: token,
...settings.store.tokens,
};
showToast("Successfully connected to TimezoneDB!", Toasts.Type.SUCCESS);
} catch (e) {
showToast("Failed to authorize TimezoneDB!", Toasts.Type.FAILURE);
new Logger("Timezones").error("Failed to authorize TimezoneDB", e);
}
}}
/>;
}

View file

@ -7,14 +7,12 @@
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { CogWheel } from "@components/Icons";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { Menu, UserStore } from "@webpack/common";
import { Message, User } from "discord-types/general";
import { Promisable } from "type-fest";
import { verifyApi } from "./api";
import { LocalTimestamp, openTimezoneOverrideModal } from "./components";
import { LocalTimestamp } from "./components/LocalTimestamp";
import { openTimezoneOverrideModal } from "./components/SetTimezoneOverrideModal";
import settings, { SettingsComponent } from "./settings";
const contextMenuPatch: NavContextMenuPatchCallback = (children, { user }: { user: User }) => {
@ -57,7 +55,7 @@ export default definePlugin({
},
})),
{
find: '"Message Username"',
find: "\"Message Username\"",
replacement: {
match: /(?<=isVisibleOnlyOnHover.+?)id:.{1,11},timestamp.{1,50}}\),/,
replace: "$&,$self.renderMessageTimezone(arguments[0]),",
@ -70,20 +68,6 @@ export default definePlugin({
"user-profile-overflow-menu": contextMenuPatch,
},
beforeSave(options: Record<string, any>): Promisable<true | string> {
// Check that API url is valid
const { apiUrl } = options;
if (!apiUrl) return "Invalid API url!";
return verifyApi(apiUrl).then(success => {
if (success) return true;
return "Failed to verify API!";
}).catch(err => {
new Logger("Timezones").info("Failed to verify API url", err);
return "Failed to verify API!";
});
},
renderProfileTimezone: (props?: { user?: User; }) => {
if (!settings.store.displayInProfile || !props?.user?.id) return null;

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
@ -24,25 +23,24 @@ import { classes } from "@utils/misc";
import { IPluginOptionComponentProps, OptionType } from "@utils/types";
import { Text } from "@webpack/common";
import { DEFAULT_API, Snowflake } from "./api";
import { TimezoneCache } from "./cache";
import { Snowflake } from "./api";
/** A mapping of each user id to the override, being either a timezone or "disabled" */
export type TimezoneOverrides = Record<Snowflake, string | null>;
/* A mapping of each authorized user id to the JWT token returned by the API. */
export type TimezoneDBTokens = Record<Snowflake, string>;
const settings = definePluginSettings({
enableApi: {
type: OptionType.BOOLEAN,
description: "Fetch user timezones from TimezoneDB when a local override does not exist",
default: true,
},
apiUrl: {
type: OptionType.STRING,
description: "The TimezoneDB API instance",
default: DEFAULT_API,
placeholder: DEFAULT_API,
onChange(_: string) {
DataStore.clear(TimezoneCache).catch(_ => _);
},
tokens: {
type: OptionType.COMPONENT,
description: "Authorization with TimezoneDB",
component: props => <AuthorizeTimezoneDBSetting {...props} />,
},
displayInChat: {
type: OptionType.BOOLEAN,
@ -57,21 +55,13 @@ const settings = definePluginSettings({
timezoneOverrides: {
type: OptionType.COMPONENT,
description: "Local overrides for users' timezones",
component: props => <>
<TimezoneOverridesSetting
setValue={props.setValue}
setError={props.setError}
option={props.option} />
</>,
component: props => <TimezoneOverridesSetting {...props} />,
},
});
export default settings;
export function SettingsComponent(): JSX.Element {
// const { apiUrl } = settings.use(["apiUrl"]);
// const url = `${apiUrl}/../?client_mod=${encodeURIComponent(VENCORD_USER_AGENT)}`;
return <>
<Text variant="text-md/normal" className={classes(Margins.top16, Margins.bottom20)}>
This plugin supports setting your own timezone publicly for others to
@ -84,3 +74,7 @@ export function SettingsComponent(): JSX.Element {
function TimezoneOverridesSetting(props: IPluginOptionComponentProps): JSX.Element {
return <></>;
}
function AuthorizeTimezoneDBSetting(props: IPluginOptionComponentProps): JSX.Element {
return <></>;
}