diff --git a/Makefile b/Makefile index 92f4748..32f91c4 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ staticcheck: staticcheck ./... web: - yarn dev + cd web && yarn dev deploy: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo -o gempbot main.go diff --git a/internal/server/overlay.go b/internal/server/overlay.go new file mode 100644 index 0000000..e3720b5 --- /dev/null +++ b/internal/server/overlay.go @@ -0,0 +1,70 @@ +package server + +import ( + "net/http" + "strings" + + "github.com/gempir/gempbot/internal/api" + "github.com/gempir/gempbot/internal/store" + "github.com/google/uuid" + "github.com/teris-io/shortid" +) + +func (a *Api) OverlayHandler(w http.ResponseWriter, r *http.Request) { + authResp, _, apiErr := a.authClient.AttemptAuth(r, w) + if apiErr != nil { + return + } + userID := authResp.Data.UserID + + if r.URL.Query().Get("managing") != "" { + userID, apiErr = a.userAdmin.CheckEditor(r, a.userAdmin.GetUserConfig(userID)) + if apiErr != nil { + http.Error(w, apiErr.Error(), apiErr.Status()) + return + } + } + + if r.Method == http.MethodGet { + if r.URL.Query().Get("roomId") != "" { + overlay := a.db.GetOverlayByRoomId(r.URL.Query().Get("roomId")) + api.WriteJson(w, overlay, http.StatusOK) + return + } + + if r.URL.Query().Get("id") != "" { + overlay := a.db.GetOverlay(r.URL.Query().Get("id"), userID) + api.WriteJson(w, overlay, http.StatusOK) + return + } + + overlays := a.db.GetOverlays(userID) + api.WriteJson(w, overlays, http.StatusOK) + } else if r.Method == http.MethodPost { + overlay := store.Overlay{} + overlay.OwnerTwitchID = userID + overlay.ID = shortid.MustGenerate() + // long string so you cant read addressbar easily + var roomID []string + for i := 0; i < 16; i++ { + roomID = append(roomID, uuid.New().String()) + } + overlay.RoomID = strings.Join(roomID, "-") + + err := a.db.SaveOverlay(overlay) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + api.WriteJson(w, overlay, http.StatusCreated) + + } else if r.Method == http.MethodDelete { + if r.URL.Query().Get("id") == "" { + http.Error(w, "missing id", http.StatusBadRequest) + } + + a.db.DeleteOverlay(r.URL.Query().Get("id")) + w.WriteHeader(http.StatusOK) + } +} diff --git a/internal/store/db.go b/internal/store/db.go index 08110c7..7b9def7 100644 --- a/internal/store/db.go +++ b/internal/store/db.go @@ -38,6 +38,11 @@ type Store interface { CountNominationDownvotes(ctx context.Context, channelTwitchID string, voteBy string) (int, error) CountNominationVotes(ctx context.Context, channelTwitchID string, voteBy string) (int, error) IsAlreadyNominated(ctx context.Context, channelTwitchID string, emoteID string) (bool, error) + GetOverlays(userID string) []Overlay + GetOverlay(ID string, userID string) Overlay + GetOverlayByRoomId(roomID string) Overlay + DeleteOverlay(ID string) + SaveOverlay(overlay Overlay) error } type Database struct { @@ -77,6 +82,7 @@ func (db *Database) Migrate() { Nomination{}, NominationVote{}, NominationDownvote{}, + Overlay{}, ) if err != nil { panic("Failed to migrate, " + err.Error()) diff --git a/internal/store/overlay.go b/internal/store/overlay.go new file mode 100644 index 0000000..1bc0935 --- /dev/null +++ b/internal/store/overlay.go @@ -0,0 +1,45 @@ +package store + +import "gorm.io/gorm/clause" + +type Overlay struct { + ID string `gorm:"primaryKey"` + OwnerTwitchID string `gorm:"index"` + RoomID string `gorm:"index"` +} + +func (db *Database) GetOverlays(userID string) []Overlay { + var overlays []Overlay + + db.Client.Where("owner_twitch_id = ?", userID).Find(&overlays) + + return overlays +} + +func (db *Database) GetOverlay(ID string, userID string) Overlay { + var overlay Overlay + + db.Client.Where("id = ? AND owner_twitch_id = ?", ID, userID).First(&overlay) + + return overlay +} + +func (db *Database) GetOverlayByRoomId(roomID string) Overlay { + var overlay Overlay + + db.Client.Where("room_id = ?", roomID).First(&overlay) + + return overlay +} + +func (db *Database) DeleteOverlay(ID string) { + db.Client.Delete(&Overlay{}, "id = ?", ID) +} + +func (db *Database) SaveOverlay(overlay Overlay) error { + update := db.Client.Clauses(clause.OnConflict{ + UpdateAll: true, + }).Create(&overlay) + + return update.Error +} diff --git a/main.go b/main.go index 2a63406..cd9c52b 100644 --- a/main.go +++ b/main.go @@ -68,6 +68,7 @@ func main() { mux.HandleFunc("/api/reward", apiHandlers.RewardHandler) mux.HandleFunc("/api/subscriptions", apiHandlers.SubscriptionsHandler) mux.HandleFunc("/api/userconfig", apiHandlers.UserConfigHandler) + mux.HandleFunc("/api/overlay", apiHandlers.OverlayHandler) mux.HandleFunc("/api/ws", wsHandler.HandleWs) handler := cors.New(cors.Options{ diff --git a/web/src/components/Overlay/Editor.tsx b/web/src/components/Overlay/Editor.tsx index c5d0084..03eda71 100644 --- a/web/src/components/Overlay/Editor.tsx +++ b/web/src/components/Overlay/Editor.tsx @@ -5,14 +5,14 @@ import { useStore } from '../../store'; type Props = { - overlayId: string; + roomId: string; readonly?: boolean; } export function Editor(props: Partial & Props) { const yjsWsUrl = useStore(state => state.yjsWsUrl); const store = useYjsStore({ - roomId: props.overlayId, + roomId: props.roomId, hostUrl: yjsWsUrl, }); const editor = useEditor(); diff --git a/web/src/components/Overlay/IframeOverlayPage.tsx b/web/src/components/Overlay/IframeOverlayPage.tsx index 95862a1..1b1feba 100644 --- a/web/src/components/Overlay/IframeOverlayPage.tsx +++ b/web/src/components/Overlay/IframeOverlayPage.tsx @@ -7,7 +7,9 @@ import { useParams } from 'next/navigation'; const Editor = dynamic(async () => (await import('./Editor')).Editor, { ssr: false }) export function IframeOverlayPage() { - const { overlayId } = useParams<{ overlayId: string }>(); + const params = useParams<{ roomId: string }>(); + + console.log("Joining", params.roomId); return (
@@ -22,7 +24,7 @@ export function IframeOverlayPage() { } `} - +
); } \ No newline at end of file diff --git a/web/src/components/Overlay/OverlayEditPage.tsx b/web/src/components/Overlay/OverlayEditPage.tsx new file mode 100644 index 0000000..861da13 --- /dev/null +++ b/web/src/components/Overlay/OverlayEditPage.tsx @@ -0,0 +1,15 @@ +const Editor = dynamic(async () => (await import('./Editor')).Editor, { ssr: false }) +import dynamic from "next/dynamic"; +import { useOverlay } from "../../hooks/useOverlays"; +import { useParams } from "next/navigation"; + +export function OverlayEditPage() { + const params = useParams<{ overlayId: string }>(); + const [overlay] = useOverlay(params.overlayId); + + console.log("Joining", overlay?.RoomID); + + return
+ {overlay?.RoomID && } +
; +} \ No newline at end of file diff --git a/web/src/components/Overlay/OverlaysPage.tsx b/web/src/components/Overlay/OverlaysPage.tsx index baf2182..633349f 100644 --- a/web/src/components/Overlay/OverlaysPage.tsx +++ b/web/src/components/Overlay/OverlaysPage.tsx @@ -1,14 +1,29 @@ -import dynamic from "next/dynamic"; -import { useUserConfig } from "../../hooks/useUserConfig"; -const Editor = dynamic(async () => (await import('./Editor')).Editor, { ssr: false }) +import Link from "next/link"; +import { useOverlays } from "../../hooks/useOverlays"; export function OverlaysPage() { - const [userCfg, setUserConfig, , loading, errorMessage] = useUserConfig(); - if (!userCfg) { - return null; - } + const [overlays, addOverlay, deleteOverlay, errorMessage, loading] = useOverlays(); - return
- Table with overlays + + return
+
+ +
+ {overlays.map(overlay =>
+
+ +
+
{overlay.ID}
+
+ Edit +
+
+ +
+
)} +
+
; } \ No newline at end of file diff --git a/web/src/components/Sidebar/Sidebar.tsx b/web/src/components/Sidebar/Sidebar.tsx index 48144eb..8e3b854 100644 --- a/web/src/components/Sidebar/Sidebar.tsx +++ b/web/src/components/Sidebar/Sidebar.tsx @@ -30,7 +30,7 @@ export function Sidebar() { Bot {isDev && Overlays } diff --git a/web/src/hooks/useOverlays.ts b/web/src/hooks/useOverlays.ts new file mode 100644 index 0000000..c5f1fa7 --- /dev/null +++ b/web/src/hooks/useOverlays.ts @@ -0,0 +1,81 @@ +import { useEffect, useState } from "react"; +import { Method, doFetch } from "../service/doFetch"; +import { useStore } from "../store"; + +type Overlay = { + ID: string; + RoomID: string; +} + +export function useOverlays(): [Overlay[], () => void, (id: string) => void, string | null, boolean] { + const [overlays, setOverlays] = useState([]); + const [errorMessage, setErrorMessage] = useState(null); + const [loading, setLoading] = useState(false); + const managing = useStore(state => state.managing); + const apiBaseUrl = useStore(state => state.apiBaseUrl); + const scToken = useStore(state => state.scToken); + + const fetchOverlays = () => { + setLoading(true); + const endPoint = "/api/overlay"; + + doFetch({ apiBaseUrl, managing, scToken }, Method.GET, endPoint).then(setOverlays).catch(setErrorMessage).finally(() => setLoading(false)); + } + + useEffect(fetchOverlays, []); + + const addOverlay = () => { + setLoading(true); + const endPoint = "/api/overlay"; + + doFetch({ apiBaseUrl, managing, scToken }, Method.POST, endPoint).then(fetchOverlays).catch(setErrorMessage).finally(() => setLoading(false)); + } + + const deleteOverlay = (id: string) => { + setLoading(true); + const endPoint = "/api/overlay"; + + doFetch({ apiBaseUrl, managing, scToken }, Method.DELETE, endPoint, new URLSearchParams({id})).then(() => setErrorMessage(null)).then(fetchOverlays).catch(setErrorMessage).finally(() => setLoading(false)); + } + + return [overlays, addOverlay, deleteOverlay, errorMessage, loading]; +} + + +export function useOverlay(id: string): [Overlay|null, boolean] { + const [overlay, setOverlay] = useState(null); + const [loading, setLoading] = useState(false); + const managing = useStore(state => state.managing); + const apiBaseUrl = useStore(state => state.apiBaseUrl); + const scToken = useStore(state => state.scToken); + + const fetchOverlay = () => { + setLoading(true); + const endPoint = "/api/overlay"; + + doFetch({ apiBaseUrl, managing, scToken }, Method.GET, endPoint, new URLSearchParams({id})).then(setOverlay).finally(() => setLoading(false)); + } + + useEffect(fetchOverlay, [id]); + + return [overlay, loading]; +} + +export function useOverlayByRoomId(roomId: string): [Overlay|null, boolean] { + const [overlay, setOverlay] = useState(null); + const [loading, setLoading] = useState(false); + const managing = useStore(state => state.managing); + const apiBaseUrl = useStore(state => state.apiBaseUrl); + const scToken = useStore(state => state.scToken); + + const fetchOverlay = () => { + setLoading(true); + const endPoint = "/api/overlay"; + + doFetch({ apiBaseUrl, managing, scToken }, Method.GET, endPoint, new URLSearchParams({roomId})).then(setOverlay).finally(() => setLoading(false)); + } + + useEffect(fetchOverlay, [roomId]); + + return [overlay, loading]; +} \ No newline at end of file diff --git a/web/src/pages/overlay/[overlayId].tsx b/web/src/pages/overlay/[roomId].tsx similarity index 100% rename from web/src/pages/overlay/[overlayId].tsx rename to web/src/pages/overlay/[roomId].tsx diff --git a/web/src/pages/overlay/edit/[overlayId].tsx b/web/src/pages/overlay/edit/[overlayId].tsx index 1862d1f..c3bc241 100644 --- a/web/src/pages/overlay/edit/[overlayId].tsx +++ b/web/src/pages/overlay/edit/[overlayId].tsx @@ -1,13 +1,8 @@ -'use client'; - -import dynamic from "next/dynamic"; -import { useParams } from "next/navigation"; -const Editor = dynamic(async () => (await import('../../../components/Overlay/Editor')).Editor, { ssr: false }) +import { OverlayEditPage } from "../../../components/Overlay/OverlayEditPage"; +import { initializeStore } from "../../../service/initializeStore"; export default function OverlaysEditPage() { - const params = useParams<{ overlayId: string }>(); + return +} - return
- -
; -} \ No newline at end of file +export const getServerSideProps = initializeStore; \ No newline at end of file