Loading package-lock.json +543 −886 File changed.Preview size limit exceeded, changes collapsed. Show changes packages/client/package.json +1 −1 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ "@nextui-org/react": "^2.6.11", "@sc07-canvas/lib": "^1.0.0", "@theme-toggles/react": "^4.1.0", "altcha-lib": "^1.2.0", "eventemitter3": "^5.0.1", "framer-motion": "^11.3.2", "lodash.throttle": "^4.1.1", Loading @@ -27,7 +28,6 @@ "vite-plugin-banner": "^0.8.1" }, "devDependencies": { "@types/grecaptcha": "^3.0.9", "@types/lodash.throttle": "^4.1.9", "@types/socket.io-client": "^3.0.0", "eslint-plugin-react": "^7.33.2", Loading packages/client/src/Moderator/Moderator.tsx +9 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,9 @@ import { UserModSidebar } from "./UserModSidebar"; import { KeybindManager } from "../lib/keybinds"; import { ModSidebar } from "./ModSidebar"; import { useHasRole } from "../hooks/useHasRole"; import { ModeratorModule } from "./ModeratorModule"; ModeratorModule.get(); // initialize const context = createContext<{ state: IModeratorContext; Loading Loading @@ -103,6 +106,12 @@ export const ModeratorContext = ({ children }: React.PropsWithChildren) => { ); useEffect(() => { if (isMod) { ModeratorModule.get().connect(); } else { ModeratorModule.get().socket.disconnect(); } const handleKeybind = () => { if (!isMod) { console.warn("TOGGLE_MOD_MENU canceled, user is not a moderator"); Loading packages/client/src/Moderator/ModeratorModule.ts 0 → 100644 +47 −0 Original line number Diff line number Diff line import { Debug } from "@sc07-canvas/lib/src/debug"; import { ModClientToServerEvents, ModServerToClientEvents, } from "@sc07-canvas/lib/src/net"; import { io, Socket } from "socket.io-client"; export class ModeratorModule { private static instance: ModeratorModule; socket: Socket<ModServerToClientEvents, ModClientToServerEvents> = io( "/mod", { autoConnect: false } ); private constructor() { Debug.controllers.set("Moderator", this); this.socket.on("connect", () => { console.log("connected to mod socket"); }); this.socket.on("connect_error", (err) => { if (this.socket.active) { console.log("disconnected temporarily"); } else { console.log("failed to connect", err); } }); this.socket.on("disconnect", () => { console.log("disconnected"); }); this.socket.on("captcha", (...data) => { console.log(...data); }); } static get() { if (!this.instance) this.instance = new ModeratorModule(); return this.instance; } connect() { this.socket.connect(); } } packages/client/src/Moderator/UserModSidebar.tsx +154 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ import { import { CalendarDateTime, parseDateTime } from "@internationalized/date"; import { toast } from "react-toastify"; import { EError } from "@sc07-canvas/lib"; import { ModeratorModule } from "./ModeratorModule"; export const UserModSidebar = () => { const { state, dispatch } = useModerator(); Loading Loading @@ -81,11 +82,164 @@ const Inner = ({ sub }: { sub: string }) => { <AccordionItem title="Send Notice"> <Notice sub={sub} /> </AccordionItem> <AccordionItem title="Sockets & Captcha"> <Captcha sub={sub} /> </AccordionItem> </Accordion> </> ); }; type CaptchaStatus = "WAITING" | "INVALID" | "PASSED"; const CaptchaStatusColors: { [k in CaptchaStatus]: | "default" | "success" | "warning" | "primary" | "secondary" | "danger"; } = { WAITING: "default", INVALID: "danger", PASSED: "success", }; const Captcha = ({ sub }: { sub: string }) => { const [status, setStatus] = useState<{ [k: string]: CaptchaStatus; }>({}); const [socketStatus, setSocketStatus] = useState<{ [k: string]: "NEW" | "GONE"; }>({}); const sockets = useQuery("/mod/user/{sub}/sockets", { params: { path: { sub, }, }, }); const sendCaptcha = useCallback( (socketId?: string) => { oapi.POST( socketId ? "/mod/user/{sub}/captcha/{socket}" : "/mod/user/{sub}/captcha", { params: { path: { sub, socket: socketId, }, }, } ); }, [sub] ); useEffect(() => { console.log("captcha & sockets loaded"); setStatus({}); const handleStatus = ( id: number, userId: string, socketId: string, status: CaptchaStatus ) => { setStatus((v) => ({ ...v, [socketId]: status, })); }; const handleConnect = (id: string) => { setSocketStatus((v) => ({ ...v, [id]: "NEW", })); }; const handleDisconnect = (id: string) => { setSocketStatus((v) => ({ ...v, [id]: "GONE", })); }; const mod = ModeratorModule.get(); mod.socket.emit("join", "captcha"); mod.socket.emit("join", "user:" + sub); mod.socket.on("captcha", handleStatus); mod.socket.on("socketConnect", handleConnect); mod.socket.on("socketDisconnect", handleDisconnect); return () => { console.log("captcha & sockets unloaded"); mod.socket.emit("leave", "captcha"); mod.socket.emit("leave", "user:" + sub); mod.socket.off("captcha", handleStatus); mod.socket.off("socketConnect", handleConnect); mod.socket.off("socketDisconnect", handleDisconnect); }; }, [sub]); return ( <> <Button onPress={() => sendCaptcha()}>Send All</Button> <Accordion> {sockets.data?.sockets.map((socket) => ( <AccordionItem key={socket.id} title={ <> {socket.id} <CaptchaChip status={status[socket.id]} /> <SocketChip status={socketStatus[socket.id]} /> </> } > <Button size="sm" onPress={() => sendCaptcha(socket.id)}> Send Captcha </Button> </AccordionItem> )) || null} </Accordion> </> ); }; const SocketChip = ({ status }: { status?: "NEW" | "GONE" }) => { switch (status) { case "NEW": return ( <Chip size="sm" color="warning"> New </Chip> ); case "GONE": return ( <Chip size="sm" color="danger"> Gone </Chip> ); default: <></>; } }; const CaptchaChip = ({ status }: { status?: CaptchaStatus }) => { if (!status) return <></>; return ( <Chip size="sm" color={CaptchaStatusColors[status]}> {status} </Chip> ); }; const Notice = ({ sub }: { sub: string }) => { const [loading, setLoading] = useState(false); const [title, setTitle] = useState(""); Loading Loading
package-lock.json +543 −886 File changed.Preview size limit exceeded, changes collapsed. Show changes
packages/client/package.json +1 −1 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ "@nextui-org/react": "^2.6.11", "@sc07-canvas/lib": "^1.0.0", "@theme-toggles/react": "^4.1.0", "altcha-lib": "^1.2.0", "eventemitter3": "^5.0.1", "framer-motion": "^11.3.2", "lodash.throttle": "^4.1.1", Loading @@ -27,7 +28,6 @@ "vite-plugin-banner": "^0.8.1" }, "devDependencies": { "@types/grecaptcha": "^3.0.9", "@types/lodash.throttle": "^4.1.9", "@types/socket.io-client": "^3.0.0", "eslint-plugin-react": "^7.33.2", Loading
packages/client/src/Moderator/Moderator.tsx +9 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,9 @@ import { UserModSidebar } from "./UserModSidebar"; import { KeybindManager } from "../lib/keybinds"; import { ModSidebar } from "./ModSidebar"; import { useHasRole } from "../hooks/useHasRole"; import { ModeratorModule } from "./ModeratorModule"; ModeratorModule.get(); // initialize const context = createContext<{ state: IModeratorContext; Loading Loading @@ -103,6 +106,12 @@ export const ModeratorContext = ({ children }: React.PropsWithChildren) => { ); useEffect(() => { if (isMod) { ModeratorModule.get().connect(); } else { ModeratorModule.get().socket.disconnect(); } const handleKeybind = () => { if (!isMod) { console.warn("TOGGLE_MOD_MENU canceled, user is not a moderator"); Loading
packages/client/src/Moderator/ModeratorModule.ts 0 → 100644 +47 −0 Original line number Diff line number Diff line import { Debug } from "@sc07-canvas/lib/src/debug"; import { ModClientToServerEvents, ModServerToClientEvents, } from "@sc07-canvas/lib/src/net"; import { io, Socket } from "socket.io-client"; export class ModeratorModule { private static instance: ModeratorModule; socket: Socket<ModServerToClientEvents, ModClientToServerEvents> = io( "/mod", { autoConnect: false } ); private constructor() { Debug.controllers.set("Moderator", this); this.socket.on("connect", () => { console.log("connected to mod socket"); }); this.socket.on("connect_error", (err) => { if (this.socket.active) { console.log("disconnected temporarily"); } else { console.log("failed to connect", err); } }); this.socket.on("disconnect", () => { console.log("disconnected"); }); this.socket.on("captcha", (...data) => { console.log(...data); }); } static get() { if (!this.instance) this.instance = new ModeratorModule(); return this.instance; } connect() { this.socket.connect(); } }
packages/client/src/Moderator/UserModSidebar.tsx +154 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ import { import { CalendarDateTime, parseDateTime } from "@internationalized/date"; import { toast } from "react-toastify"; import { EError } from "@sc07-canvas/lib"; import { ModeratorModule } from "./ModeratorModule"; export const UserModSidebar = () => { const { state, dispatch } = useModerator(); Loading Loading @@ -81,11 +82,164 @@ const Inner = ({ sub }: { sub: string }) => { <AccordionItem title="Send Notice"> <Notice sub={sub} /> </AccordionItem> <AccordionItem title="Sockets & Captcha"> <Captcha sub={sub} /> </AccordionItem> </Accordion> </> ); }; type CaptchaStatus = "WAITING" | "INVALID" | "PASSED"; const CaptchaStatusColors: { [k in CaptchaStatus]: | "default" | "success" | "warning" | "primary" | "secondary" | "danger"; } = { WAITING: "default", INVALID: "danger", PASSED: "success", }; const Captcha = ({ sub }: { sub: string }) => { const [status, setStatus] = useState<{ [k: string]: CaptchaStatus; }>({}); const [socketStatus, setSocketStatus] = useState<{ [k: string]: "NEW" | "GONE"; }>({}); const sockets = useQuery("/mod/user/{sub}/sockets", { params: { path: { sub, }, }, }); const sendCaptcha = useCallback( (socketId?: string) => { oapi.POST( socketId ? "/mod/user/{sub}/captcha/{socket}" : "/mod/user/{sub}/captcha", { params: { path: { sub, socket: socketId, }, }, } ); }, [sub] ); useEffect(() => { console.log("captcha & sockets loaded"); setStatus({}); const handleStatus = ( id: number, userId: string, socketId: string, status: CaptchaStatus ) => { setStatus((v) => ({ ...v, [socketId]: status, })); }; const handleConnect = (id: string) => { setSocketStatus((v) => ({ ...v, [id]: "NEW", })); }; const handleDisconnect = (id: string) => { setSocketStatus((v) => ({ ...v, [id]: "GONE", })); }; const mod = ModeratorModule.get(); mod.socket.emit("join", "captcha"); mod.socket.emit("join", "user:" + sub); mod.socket.on("captcha", handleStatus); mod.socket.on("socketConnect", handleConnect); mod.socket.on("socketDisconnect", handleDisconnect); return () => { console.log("captcha & sockets unloaded"); mod.socket.emit("leave", "captcha"); mod.socket.emit("leave", "user:" + sub); mod.socket.off("captcha", handleStatus); mod.socket.off("socketConnect", handleConnect); mod.socket.off("socketDisconnect", handleDisconnect); }; }, [sub]); return ( <> <Button onPress={() => sendCaptcha()}>Send All</Button> <Accordion> {sockets.data?.sockets.map((socket) => ( <AccordionItem key={socket.id} title={ <> {socket.id} <CaptchaChip status={status[socket.id]} /> <SocketChip status={socketStatus[socket.id]} /> </> } > <Button size="sm" onPress={() => sendCaptcha(socket.id)}> Send Captcha </Button> </AccordionItem> )) || null} </Accordion> </> ); }; const SocketChip = ({ status }: { status?: "NEW" | "GONE" }) => { switch (status) { case "NEW": return ( <Chip size="sm" color="warning"> New </Chip> ); case "GONE": return ( <Chip size="sm" color="danger"> Gone </Chip> ); default: <></>; } }; const CaptchaChip = ({ status }: { status?: CaptchaStatus }) => { if (!status) return <></>; return ( <Chip size="sm" color={CaptchaStatusColors[status]}> {status} </Chip> ); }; const Notice = ({ sub }: { sub: string }) => { const [loading, setLoading] = useState(false); const [title, setTitle] = useState(""); Loading