Added redemptions & redeemable actions. Fixed a few bugs.

This commit is contained in:
Tom
2024-06-24 22:16:55 +00:00
parent 68df045c54
commit 6548ce33e0
35 changed files with 1787 additions and 471 deletions

View File

@ -1,22 +1,20 @@
import axios from 'axios'
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUser from '@/lib/fetch-user';
import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation';
export async function GET(req: Request) {
try {
// Verify state against user id in user table.
const key = await db.apiKey.findFirst({
where: {
id: req.headers.get('x-api-key') as string
}
})
if (!key) {
return new NextResponse("Forbidden", { status: 403 });
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const connection = await db.twitchConnection.findFirst({
where: {
userId: key.userId
userId: user.id
}
})
if (!connection) {
@ -29,8 +27,22 @@ export async function GET(req: Request) {
Authorization: 'OAuth ' + connection.accessToken
}
})).data;
if (expires_in > 3600)
return new NextResponse("", { status: 201 });
if (expires_in > 3600) {
let data = await db.twitchConnection.findFirst({
where: {
userId: user.id
}
})
let dataFormatted = {
user_id: user.id,
access_token: data?.accessToken,
refresh_token: data?.refreshToken,
broadcaster_id: connection.broadcasterId
}
return NextResponse.json(dataFormatted, { status: 201 });
}
} catch (error) {
}
@ -51,14 +63,22 @@ export async function GET(req: Request) {
await db.twitchConnection.update({
where: {
userId: key.userId
userId: user.id
},
data: {
accessToken: access_token
accessToken: access_token,
refreshToken: refresh_token
}
})
const data = {
user_id: user.id,
access_token,
refresh_token,
broadcaster_id: connection.broadcasterId
}
return new NextResponse("", { status: 200 });
return NextResponse.json(data)
} catch (error) {
console.log("[ACCOUNT]", error);
return new NextResponse("Internal Error", { status: 500 });

View File

@ -0,0 +1,35 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation';
import axios from "axios";
import { updateTwitchToken } from "@/data/twitch-reauthorize";
export async function GET(req: Request) {
try {
if (!process.env.TWITCH_BOT_CLIENT_ID)
return new NextResponse("Internal Error", { status: 500 });
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const auth = await updateTwitchToken(user.id)
if (!auth)
return new NextResponse("Bad Request", { status: 400 })
const redemptions = await axios.get("https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + auth.broadcaster_id,
{
headers: {
"Client-Id": process.env.TWITCH_BOT_CLIENT_ID,
"Authorization": "Bearer " + auth.access_token
}
}
)
return NextResponse.json(redemptions.data);
} catch (error) {
console.log("[REDEMPTIONS/ACTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,9 @@ import fetchUser from "@/lib/fetch-user";
export async function GET(req: Request) {
try {
return NextResponse.json(await fetchUser(req))
const user = await fetchUser(req)
if (!user) return new NextResponse("Internal Error", { status: 401 })
return NextResponse.json(user)
} catch (error) {
console.log("[ACCOUNT]", error);
return new NextResponse("Internal Error", { status: 500 });

View File

@ -0,0 +1,13 @@
import { NextResponse } from "next/server";
export async function GET(req: Request) {
return NextResponse.json({
major_version: 3,
minor_version: 3,
download: "https://drive.proton.me/urls/KVGW0ZKE9C#2Y0WGGt5uHFZ",
changelog: "Revised the redeem system, activated via channel point redeems.\nAdded OBS transformation to redeems.\nLogs changed & writes to logs folder as well."
//changelog: "Added new command for mods: !refresh <username_filters|word_filters|default_voice> - Used to refresh data if done via website.\nAdded new command for mods: !tts <voice_name> <remove|enable|disable> - To delete, enable, or disable a specific voice."
//changelog: "Save TTS voices set by chatters.\nAdded more options for TTS voices." 3.1
//changelog: "Added a message when new updates are available.\nFixed 7tv renames not being applied correctly." 3.0
});
}

View File

@ -0,0 +1,150 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { ActionType, Prisma } from "@prisma/client";
import { JsonSerializer } from "typescript-json-serializer";
export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const actions = await db.action.findMany({
where: {
userId: user.id
}
})
return NextResponse.json(actions.map(({userId, ...attrs}) => attrs));
} catch (error) {
console.log("[REDEMPTIONS/ACTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { name, type, scene_name, scene_item_name, rotation, position_x, position_y, file_path, file_content }: { name: string, type: ActionType, scene_name: string, scene_item_name: string, rotation: string, position_x: string, position_y: string, file_path: string, file_content: string } = await req.json();
if (!name && !type)
return new NextResponse("Bad Request", { status: 400 });
if (type == ActionType.OBS_TRANSFORM && (!scene_name || !scene_item_name || !rotation && !position_x && !position_y))
return new NextResponse("Bad Request", { status: 400 });
if ((type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) && (!file_path || !file_content))
return new NextResponse("Bad Request", { status: 400 });
if (type == ActionType.AUDIO_FILE && !file_path)
return new NextResponse("Bad Request", { status: 400 });
let data:any = { }
if (type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) {
data = { file_path, file_content, ...data }
} else if (type == ActionType.OBS_TRANSFORM) {
data = { scene_name, scene_item_name, ...data }
if (!!rotation)
data = { rotation, ...data }
if (!!position_x)
data = { position_x, ...data }
if (!!position_y)
data = { position_y, ...data }
} else if (type == ActionType.AUDIO_FILE) {
data = { file_path, ...data }
}
await db.action.create({
data: {
userId: user.id,
name,
type,
data: data as Prisma.JsonObject
}
});
return new NextResponse("", { status: 200 });
} catch (error) {
console.log("[REDEMPTIONS/ACTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function PUT(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { name, type, scene_name, scene_item_name, rotation, position_x, position_y, file_path, file_content }: { name: string, type: ActionType, scene_name: string, scene_item_name: string, rotation: string, position_x: string, position_y: string, file_path: string, file_content: string } = await req.json();
if (!name && !type)
return new NextResponse("Bad Request", { status: 400 });
if (type == ActionType.OBS_TRANSFORM && (!scene_name || !scene_item_name || !rotation && !position_x && !position_y))
return new NextResponse("Bad Request", { status: 400 });
if ((type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) && (!file_path || !file_content))
return new NextResponse("Bad Request", { status: 400 });
if (type == ActionType.AUDIO_FILE && !file_path)
return new NextResponse("Bad Request", { status: 400 });
let data:any = { }
if (type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) {
data = { file_path, file_content, ...data }
} else if (type == ActionType.OBS_TRANSFORM) {
data = { scene_name, scene_item_name, ...data }
if (!!rotation)
data = { rotation, ...data }
if (!!position_x)
data = { position_x, ...data }
if (!!position_y)
data = { position_y, ...data }
} else if (type == ActionType.AUDIO_FILE) {
data = { file_path, ...data }
}
await db.action.update({
where: {
userId_name: {
userId: user.id,
name
}
},
data: {
type,
data: data as Prisma.JsonObject
}
});
return new NextResponse("", { status: 200 });
} catch (error) {
console.log("[REDEMPTIONS/ACTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
const name = searchParams.get('action_name') as string
const redemptions = await db.action.delete({
where: {
userId_name: {
userId: user.id,
name
}
}
})
return NextResponse.json(redemptions);
} catch (error) {
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -0,0 +1,143 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const redemptions = await db.redemption.findMany({
where: {
userId: user.id
}
})
return NextResponse.json(redemptions.map(({userId, ...attrs}) => attrs));
} catch (error) {
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { actionName, redemptionId, order, state }: { actionName: string, redemptionId: string, order: number, state: boolean } = await req.json();
if (!redemptionId || !actionName && !order && !state)
return new NextResponse("Bad Request", { status: 400 });
const action = await db.action.findFirst({
where: {
name: actionName
}
})
if (!action)
return new NextResponse("Bad Request", { status: 400 });
let data:any = {
actionName,
order,
state
}
if (!data.actionName)
data = { actionName, ...data }
if (!data.order)
data = { order, ...data }
if (!data.state)
data = { state, ...data }
const res = await db.redemption.create({
data: {
userId: user.id,
redemptionId,
order,
state: true,
...data
}
});
return NextResponse.json(res, { status: 200 });
} catch (error) {
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function PUT(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { id, actionName, redemptionId, order, state }: { id: string, actionName: string, redemptionId: string, order: number, state: boolean } = await req.json();
if (!redemptionId || !actionName && !order && !state)
return new NextResponse("Bad Request", { status: 400 });
const action = await db.action.findFirst({
where: {
name: actionName
}
})
if (!action)
return new NextResponse("Bad Request", { status: 400 });
let data:any = {
actionName,
redemptionId,
order,
state
}
if (!data.actionName)
data = { actionName, ...data }
if (!data.order)
data = { order, ...data }
if (!data.state)
data = { state, ...data }
if (!data.redemptionId)
data = { redemptionId, ...data }
const res = await db.redemption.update({
where: {
id
},
data: data
});
return NextResponse.json(res, { status: 200 });
} catch (error) {
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
const id = searchParams.get('id') as string
const redemptions = await db.redemption.delete({
where: {
id
}
})
return NextResponse.json(redemptions);
} catch (error) {
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -5,19 +5,16 @@ import voices from "@/data/tts";
export async function GET(req: Request) {
try {
if (!voices) {
return new NextResponse("Voices not available.", { status: 500 });
}
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const u = await db.user.findFirst({
where: {
id: user.id
}
});
const voice = voices.find(v => v.value == new String(u?.ttsDefaultVoice))
return NextResponse.json(voice);
return NextResponse.json(user.ttsDefaultVoice);
} catch (error) {
console.log("[TTS/FILTER/DEFAULT]", error);
return new NextResponse("Internal Error", { status: 500 });
@ -26,28 +23,32 @@ export async function GET(req: Request) {
export async function POST(req: Request) {
try {
if (!voices) {
return new NextResponse("Voices not available.", { status: 500 });
}
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { voice } = await req.json();
if (!voice || !voices.map(v => v.label.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 });
if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 });
const v = voices.find(v => v.label.toLowerCase() == voice.toLowerCase())
const v = voices.find(v => v.toLowerCase() == voice.toLowerCase())
await db.user.update({
where: {
id: user.id
},
data: {
ttsDefaultVoice: Number.parseInt(v.value)
ttsDefaultVoice: v
}
});
return new NextResponse("", { status: 200 });
} catch (error) {
console.log("[TTS/FILTER/DEFAULT]", error);
return new NextResponse("Internal Error", { status: 500 });
}
return new NextResponse("Internal Error", { status: 500 });
}

View File

@ -31,7 +31,7 @@ export async function POST(req: Request) {
const { search, replace } = await req.json();
if (!search || search.length < 4 || search.length > 200) return new NextResponse("Bad Request", { status: 400 });
if (!replace) return new NextResponse("Bad Request", { status: 400 });
if (replace == null) return new NextResponse("Bad Request", { status: 400 });
const filter = await db.ttsWordFilter.create({
data: {
@ -58,7 +58,7 @@ export async function PUT(req: Request) {
const { id, search, replace } = await req.json();
if (!id || id.length < 1) return new NextResponse("Bad Request", { status: 400 });
if (!search || search.length < 4 || search.length > 200) return new NextResponse("Bad Request", { status: 400 });
if (!replace) return new NextResponse("Bad Request", { status: 400 });
if (replace == null) return new NextResponse("Bad Request", { status: 400 });
const filter = await db.ttsWordFilter.update({
where: {

View File

@ -1,7 +1,6 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import voices from "@/data/tts";
export async function GET(req: Request) {
try {
@ -10,23 +9,18 @@ export async function GET(req: Request) {
return new NextResponse("Unauthorized", { status: 401 });
}
let list : {
value: string;
label: string;
gender: string;
language: string;
}[] = []
const enabled = user.ttsEnabledVoice
for (let v of voices) {
var n = Number.parseInt(v.value) - 1
if ((enabled & (1 << n)) > 0) {
list.push(v)
}
}
const voiceStates = await db.ttsVoiceState.findMany({
where: {
userId: user.id
}
});
return NextResponse.json(list);
const voiceNames = await db.ttsVoice.findMany();
const voiceNamesMapped: { [id: string]: string } = Object.assign({}, ...voiceNames.map(v => ({ [v.id]: v.name })))
return NextResponse.json(voiceStates.filter(v => v.state).map(v => voiceNamesMapped[v.ttsVoiceId]));
} catch (error) {
console.log("[TTS/FILTER/USER]", error);
console.log("[TTS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -38,16 +32,29 @@ export async function POST(req: Request) {
return new NextResponse("Unauthorized", { status: 401 });
}
let { voice } = await req.json();
voice = voice & ((1 << voices.length) - 1)
const { voice, state }: { voice: string, state: boolean } = await req.json();
await db.user.update({
where: {
id: user.id
},
data: {
ttsEnabledVoice: voice
}
const voiceIds = await db.ttsVoice.findMany();
const voiceIdsMapped: { [voice: string]: string } = Object.assign({}, ...voiceIds.map(v => ({ [v.name.toLowerCase()]: v.id })));
if (!voiceIdsMapped[voice.toLowerCase()]) {
return new NextResponse("Bad Request", { status: 400 });
}
await db.ttsVoiceState.upsert({
where: {
userId_ttsVoiceId: {
userId: user.id,
ttsVoiceId: voiceIdsMapped[voice.toLowerCase()]
}
},
update: {
state
},
create: {
userId: user.id,
ttsVoiceId: voiceIdsMapped[voice.toLowerCase()],
state
}
});
return new NextResponse("", { status: 200 });

View File

@ -0,0 +1,71 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import voices from "@/data/tts";
export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const selected = await db.ttsChatVoice.findMany({
where: {
userId: user.id
}
})
const voices = await db.ttsVoice.findMany();
const voiceNamesMapped: { [id: string]: string } = Object.assign({}, ...voices.map(v => ({ [v.id]: v.name })))
const data = selected.map(s => ({ chatter_id: new Number(s.chatterId), voice: voiceNamesMapped[s.ttsVoiceId] }))
return NextResponse.json(data);
} catch (error) {
console.log("[TTS/SELECTED]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { voice, chatterId }: { voice: string, chatterId: number } = await req.json();
if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 });
const v = voices.find(v => v.toLowerCase() == voice.toLowerCase())
const voiceData = await db.ttsVoice.findFirst({
where: {
name: v
}
})
if (!voiceData)
return new NextResponse("Bad Request", { status: 400 });
await db.ttsChatVoice.upsert({
where: {
userId_chatterId: {
userId: user.id,
chatterId
}
},
create: {
userId: user.id,
chatterId,
ttsVoiceId: voiceData.id
},
update: {
ttsVoiceId: voiceData.id
}
});
return new NextResponse("", { status: 200 });
} catch (error) {
console.log("[TTS/SELECTED]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -30,7 +30,6 @@ export async function GET(req: Request, { params } : { params: { id: string } })
export async function DELETE(req: Request, { params } : { params: { id: string } }) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}

View File

@ -4,20 +4,24 @@ import { NextResponse } from "next/server";
export async function GET(req: Request) {
try {
console.log("ABC 1")
const user = await fetchUserWithImpersonation(req);
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
console.log("ABC 2")
const api = await db.twitchConnection.findFirst({
where: {
userId: user.id
}
})
console.log("ABC 3")
if (!api) {
return new NextResponse("Forbidden", { status: 403 });
}
console.log("ABC 4")
const data = {
client_id: process.env.TWITCH_BOT_CLIENT_ID,
client_secret: process.env.TWITCH_BOT_CLIENT_SECRET,
@ -25,6 +29,7 @@ export async function GET(req: Request) {
refresh_token: api.refreshToken,
broadcaster_id: api.broadcasterId
}
console.log("ABC 5", data)
return NextResponse.json(data);
} catch (error) {
console.log("[TOKENS/GET]", error);

View File

@ -20,11 +20,11 @@ export async function POST(req: Request) {
const id = generateToken()
const token = await db.apiKey.create({
data: {
id,
label,
userId: userId as string
}
data: {
id,
label,
userId: userId as string
}
});
return NextResponse.json(token);
@ -41,16 +41,16 @@ export async function DELETE(req: Request) {
return new NextResponse("Unauthorized", { status: 401 });
}
let { id } = await req.json();
const { id } = await req.json();
if (!id) {
return NextResponse.json(null)
}
const token = await db.apiKey.delete({
where: {
id,
userId: user?.id
}
where: {
id,
userId: user?.id
}
});
return NextResponse.json(token);
@ -60,12 +60,12 @@ export async function DELETE(req: Request) {
}
}
export function generateToken() {
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
var string_length = 32;
var randomstring = '';
for (var i = 0; i < string_length; i++) {
var rnum = Math.floor(Math.random() * chars.length);
function generateToken() {
let chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
let string_length = 32;
let randomstring = '';
for (let i = 0; i < string_length; i++) {
let rnum = Math.floor(Math.random() * chars.length);
randomstring += chars[rnum];
}
return randomstring;

View File

@ -1,28 +1,23 @@
import fetchUser from "@/lib/fetch-user";
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url)
let userId = searchParams.get('userId')
if (userId == null) {
const user = await fetchUser(req);
if (user != null) {
userId = user.id as string;
}
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const tokens = await db.apiKey.findMany({
where: {
userId: userId as string
userId: user.id
}
});
return NextResponse.json(tokens);
} catch (error) {
console.log("[TOKENS/GET]", error);
return new NextResponse("Internal Error", { status: 500});
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -24,7 +24,7 @@ export async function GET(req: Request) {
return NextResponse.json(users)
}
if (id) {
const users = await db.user.findUnique({
const users = await db.user.findFirst({
where: {
id: id
}
@ -35,7 +35,7 @@ export async function GET(req: Request) {
const users = await db.user.findMany();
return NextResponse.json(users)
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
console.log("[USERS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -3,63 +3,46 @@
import axios from "axios";
import { Button } from "@/components/ui/button";
import * as React from 'react';
import { ApiKey, User } from "@prisma/client";
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
const SettingsPage = () => {
const { data: session, status } = useSession();
const [apiKeyViewable, setApiKeyViewable] = useState(0)
const [apiKeyChanges, setApiKeyChanges] = useState(0)
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
const ApiKeyPage = () => {
const [apiKeyViewable, setApiKeyViewable] = useState<number>(-1)
const [apiKeys, setApiKeys] = useState<{ id: string, label: string, userId: string }[]>([])
useEffect(() => {
const fetchData = async () => {
try {
const keys = (await axios.get("/api/tokens")).data ?? {};
setApiKeys(keys)
} catch (error) {
console.log("ERROR", error)
}
await axios.get("/api/tokens")
.then(d => setApiKeys(d.data ?? []))
.catch(console.error)
};
fetchData().catch(console.error);
}, [apiKeyChanges]);
fetchData();
}, []);
const onApiKeyAdd = async () => {
try {
await axios.post("/api/token", {
label: "Key label"
});
setApiKeyChanges(apiKeyChanges + 1)
} catch (error) {
console.log("ERROR", error)
}
const onApiKeyAdd = async (label: string) => {
await axios.post("/api/token", { label })
.then(d => setApiKeys(apiKeys.concat([d.data])))
.catch(console.error)
}
const onApiKeyDelete = async (id: string) => {
try {
await axios.delete("/api/token/" + id);
setApiKeyChanges(apiKeyChanges - 1)
} catch (error) {
console.log("ERROR", error)
}
await axios.delete("/api/token/" + id)
.then((d) => setApiKeys(apiKeys.filter(k => k.id != d.data.id)))
.catch(console.error)
}
return (
<div>
<div className="px-10 py-5 mx-5 my-10">
<div>
<div className="text-xl justify-left mt-10">API Keys</div>
<Table className="max-w-2xl">
<div className="text-xl justify-left mt-10 text-center">API Keys</div>
<Table>
<TableCaption>A list of your secret API keys.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Label</TableHead>
<TableHead>Token</TableHead>
<TableHead>View</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
@ -67,20 +50,20 @@ const SettingsPage = () => {
{apiKeys.map((key, index) => (
<TableRow key={key.id}>
<TableCell className="font-medium">{key.label}</TableCell>
<TableCell>{(apiKeyViewable & (1 << index)) > 0 ? key.id : "*".repeat(key.id.length)}</TableCell>
<TableCell>{apiKeyViewable == index ? key.id : "*".repeat(key.id.length)}</TableCell>
<TableCell>
<Button onClick={() => setApiKeyViewable((v) => v ^ (1 << index))}>
{(apiKeyViewable & (1 << index)) > 0 ? "HIDE" : "VIEW"}
<Button onClick={() => setApiKeyViewable((v) => v != index ? index : -1)}>
{apiKeyViewable == index ? "HIDE" : "VIEW"}
</Button>
<Button onClick={() => onApiKeyDelete(key.id)} className="ml-[10px] bg-red-500 hover:bg-red-700">DELETE</Button>
</TableCell>
<TableCell><Button onClick={() => onApiKeyDelete(key.id)}>DEL</Button></TableCell>
<TableCell></TableCell>
</TableRow>
))}
<TableRow key="ADD">
<TableCell className="font-medium"></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell><Button onClick={onApiKeyAdd}>ADD</Button></TableCell>
<TableCell><Button onClick={() => onApiKeyAdd("Key label")}>ADD</Button></TableCell>
</TableRow>
</TableBody>
</Table>
@ -90,4 +73,4 @@ const SettingsPage = () => {
);
}
export default SettingsPage;
export default ApiKeyPage;

View File

@ -10,7 +10,7 @@ import Link from "next/link";
import { cn } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
const SettingsPage = () => {
const ConnectionsPage = () => {
const { data: session, status } = useSession();
const [previousUsername, setPreviousUsername] = useState<string>()
const [userId, setUserId] = useState<string>()
@ -24,7 +24,7 @@ const SettingsPage = () => {
setPreviousUsername(session.user?.name as string)
if (session.user?.name) {
const fetchData = async () => {
var connection: User = (await axios.get("/api/account")).data
let connection: User = (await axios.get("/api/account")).data
setUserId(connection.id)
setLoading(false)
}
@ -36,7 +36,7 @@ const SettingsPage = () => {
const [twitchUser, setTwitchUser] = useState<TwitchConnection | null>(null)
useEffect(() => {
const fetchData = async () => {
var connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data
let connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data
setTwitchUser(connection)
}
@ -97,4 +97,4 @@ const SettingsPage = () => {
);
}
export default SettingsPage;
export default ConnectionsPage;

View File

@ -3,8 +3,11 @@ import { cn } from "@/lib/utils";
import { headers } from 'next/headers';
import React from "react";
const SettingsLayout = async (
{ children } : { children:React.ReactNode } ) => {
const SettingsLayout = async ({
children
} : {
children:React.ReactNode
} ) => {
const headersList = headers();
const header_url = headersList.get('x-url') || "";
@ -14,7 +17,7 @@ const SettingsLayout = async (
header_url.endsWith("/settings") && "flex h-full w-full md:w-[250px] z-30 flex-col fixed inset-y-0")}>
<SettingsNavigation />
</div>
<main className={cn("md:pl-[250px] h-full", header_url.endsWith("/settings") && "hidden")}>
<main className={"md:pl-[250px] h-full"}>
{children}
</main>
</div>

View File

@ -0,0 +1,143 @@
"use client";
import axios from "axios";
import * as React from 'react';
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import RedeemptionAction from "@/components/elements/redeemable-action";
import OBSRedemption from "@/components/elements/redemption";
import { ActionType } from "@prisma/client";
import InfoNotice from "@/components/elements/info-notice";
const obsTransformations = [
{ label: "scene_name", description: "", placeholder: "Name of the OBS scene" },
{ label: "scene_item_name", description: "", placeholder: "Name of the OBS scene item / source" },
{ label: "rotation", description: "", placeholder: "An expression using x as the previous value" },
{ label: "position_x", description: "", placeholder: "An expression using x as the previous value" },
{ label: "position_y", description: "", placeholder: "An expression using x as the previous value" }
]
const RedemptionsPage = () => {
const { data: session, status } = useSession();
const [previousUsername, setPreviousUsername] = useState<string | null>()
const [loading, setLoading] = useState<boolean>(true)
const [open, setOpen] = useState(false)
const [actions, setActions] = useState<{ name: string, type: string, data: any }[]>([])
const [twitchRedemptions, setTwitchRedemptions] = useState<{ id: string, title: string }[]>([])
const [redemptions, setRedemptions] = useState<{ id: string, redemptionId: string, actionName: string, order: number }[]>([])
function addAction(name: string, type: ActionType, data: { [key: string]: string }) {
setActions([...actions, { name, type, data }])
}
function removeAction(action: { name: string, type: string, data: any }) {
setActions(actions.filter(a => a.name != action.name))
}
function addRedemption(id: string, actionName: string, redemptionId: string, order: number) {
setRedemptions([...redemptions, { id, redemptionId, actionName, order }])
}
function removeRedemption(redemption: { id: string, redemptionId: string, actionName: string, order: number }) {
setRedemptions(redemptions.filter(r => r.id != redemption.id))
}
useEffect(() => {
if (status !== "authenticated" || previousUsername == session.user?.name) {
return
}
setPreviousUsername(session.user?.name)
axios.get("/api/settings/redemptions/actions")
.then(d => {
setActions(d.data)
})
axios.get("/api/account/redemptions")
.then(d => {
const rs = d.data.data?.map(r => ({ id: r.id, title: r.title })) ?? []
setTwitchRedemptions(rs)
axios.get("/api/settings/redemptions")
.then(d => {
setRedemptions(d.data)
})
})
}, [session])
return (
<div>
<div className="text-2xl text-center pt-[50px]">Redemption Actions</div>
<InfoNotice
message="Redemption actions are activated when specific Twitch channel point redeems have been activated. Aforementioned redeem need to be linked in the redemption part, together with the action, for the action to activate."
hidden={false} />
{actions.map(action =>
<div
className="px-10 py-3 w-full h-full flex-grow inset-y-1/2"
key={action.name}>
<RedeemptionAction
name={action.name}
type={action.type}
data={action.data}
edit={false}
showEdit={true}
isNew={false}
obsTransformations={obsTransformations}
adder={addAction}
remover={removeAction} />
</div>
)}
<div
className="px-10 py-3 w-full h-full flex-grow inset-y-1/2">
<RedeemptionAction
name=""
type={undefined}
data={{}}
edit={true}
showEdit={false}
isNew={true}
obsTransformations={obsTransformations}
adder={addAction}
remover={removeAction} />
</div>
<div className="text-2xl text-center pt-[50px]">Redemptions</div>
<InfoNotice
message="Redemptions are just a way to link specific actions to actual Twitch channel point redeems."
hidden={false} />
{redemptions.map(redemption =>
<div
className="px-10 py-3 w-full h-full flex-grow inset-y-1/2"
key={redemption.id}>
<OBSRedemption
id={redemption.id}
redemptionId={redemption.redemptionId}
actionName={redemption.actionName}
edit={false}
showEdit={true}
isNew={false}
actions={actions.map(a => a.name)}
twitchRedemptions={twitchRedemptions}
adder={addRedemption}
remover={removeRedemption} />
</div>
)}
<div
className="px-10 py-3 w-full h-full flex-grow inset-y-1/2">
<OBSRedemption
id={undefined}
redemptionId={undefined}
actionName=""
edit={true}
showEdit={false}
isNew={true}
actions={actions.map(a => a.name)}
twitchRedemptions={twitchRedemptions}
adder={addRedemption}
remover={removeRedemption} />
</div>
</div>
);
}
export default RedemptionsPage;

View File

@ -230,7 +230,7 @@ const TTSFiltersPage = () => {
<Form {...usernameFilteredForm}>
<form onSubmit={usernameFilteredForm.handleSubmit(onAdd)}>
<div className="flex w-full items-center justify-between rounded-md border px-4 py-2 gap-3 mt-2">
<Label className="rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground ">
<Label className="rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground">
{tag}
</Label>
<FormField

View File

@ -3,10 +3,8 @@
import axios from "axios";
import * as React from 'react';
import { Check, ChevronsUpDown } from "lucide-react"
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { useEffect, useReducer, useState } from "react";
import { cn } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button"
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
@ -16,49 +14,50 @@ import voices from "@/data/tts";
import InfoNotice from "@/components/elements/info-notice";
const TTSVoiceFiltersPage = () => {
const { data: session, status } = useSession();
const [loading, setLoading] = useState<boolean>(true)
const [open, setOpen] = useState(false)
const [value, setValue] = useState(0)
const [enabled, setEnabled] = useState(0)
const [defaultVoice, setDefaultVoice] = useState("")
const [loading, setLoading] = useState(true)
function enabledVoicesReducer(enabledVoices: { [voice: string]: boolean }, action: { type: string, value: string }) {
if (action.type == "enable") {
return { ...enabledVoices, [action.value]: true }
} else if (action.type == "disable") {
return { ...enabledVoices, [action.value]: false }
}
return enabledVoices
}
const [enabledVoices, dispatchEnabledVoices] = useReducer(enabledVoicesReducer, Object.assign({}, ...voices.map(v => ({[v]: false}) )))
useEffect(() => {
axios.get("/api/settings/tts/default")
.then((voice) => {
setValue(Number.parseInt(voice.data.value))
setDefaultVoice(voice.data)
})
axios.get("/api/settings/tts")
.then((d) => {
const total = d.data.reduce((acc: number, item: {value: number, label: string, gender: string, language: string}) => acc |= 1 << (item.value - 1), 0)
setEnabled(total)
const data: string[] = d.data;
data.forEach(d => dispatchEnabledVoices({ type: "enable", value: d }))
setLoading(false)
})
}, [])
const onDefaultChange = (voice: string) => {
try {
axios.post("/api/settings/tts/default", { voice })
.then(d => {
console.log(d)
})
.catch(e => console.error(e))
} catch (error) {
console.log("[TTS/DEFAULT]", error);
return;
}
}
const onEnabledChanged = (val: number) => {
const onEnabledChanged = (voice: string, state: boolean) => {
try {
axios.post("/api/settings/tts", { voice: val })
.then(d => {
console.log(d)
})
axios.post("/api/settings/tts", { voice: voice, state: state })
.catch(e => console.error(e))
} catch (error) {
console.log("[TTS]", error);
return;
console.log("[TTS/ENABLED]", error);
}
}
@ -78,7 +77,7 @@ const TTSVoiceFiltersPage = () => {
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between">
{value ? voices.find(v => Number.parseInt(v.value) == value)?.label : "Select voice..."}
{defaultVoice ? voices.find(v => v == defaultVoice) : "Select voice..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@ -89,20 +88,20 @@ const TTSVoiceFiltersPage = () => {
<CommandGroup>
{voices.map((voice) => (
<CommandItem
key={voice.value + "-" + voice.label}
value={voice.value}
onSelect={(currentValue) => {
setValue(Number.parseInt(currentValue))
onDefaultChange(voice.label)
key={voice}
value={voice}
onSelect={(currentVoice) => {
setDefaultVoice(voice)
onDefaultChange(voice)
setOpen(false)
}}>
<Check
className={cn(
"mr-2 h-4 w-4",
value === Number.parseInt(voice.value) ? "opacity-100" : "opacity-0"
defaultVoice === voice ? "opacity-100" : "opacity-0"
)}
/>
{voice.label}
{voice}
</CommandItem>
))}
</CommandGroup>
@ -116,14 +115,14 @@ const TTSVoiceFiltersPage = () => {
<InfoNotice message="Voices can be disabled from being used. Default voice will always work." hidden={false} />
<div className="grid grid-cols-4 grid-flow-row gap-4 pt-[20px]">
{voices.map((v, i) => (
<div key={v.label + "-enabled"} className="h-[30px] row-span-1 col-span-1 align-middle flex items-center justify-center">
<div key={v + "-enabled"} className="h-[30px] row-span-1 col-span-1 align-middle flex items-center justify-center">
<Checkbox onClick={() => {
const newVal = enabled ^ (1 << (Number.parseInt(v.value) - 1))
setEnabled(newVal)
onEnabledChanged(newVal)
dispatchEnabledVoices({ type: enabledVoices[v] ? "disable" : "enable", value: v })
onEnabledChanged(v, !enabledVoices[v])
}}
checked={(enabled & (1 << (Number.parseInt(v.value) - 1))) > 0} />
<div className="pl-[5px]">{v.label}</div>
disabled={loading}
checked={enabledVoices[v]} />
<div className="pl-[5px]">{v}</div>
</div>
))}
</div>

58
app/socket/page.tsx Normal file
View File

@ -0,0 +1,58 @@
"use client"
import { Button } from '@/components/ui/button';
import { v4 } from "uuid"
import { useEffect, useState } from 'react';
import useWebSocket from 'react-use-websocket';
const socketUrl = 'wss://echo.websocket.org';
const SocketPage = () => {
const [mounted, setMounted] = useState(false)
const {
sendMessage,
sendJsonMessage,
lastMessage,
lastJsonMessage,
readyState,
getWebSocket,
} = useWebSocket(socketUrl, {
onOpen: () => console.log('opened'),
onMessage: (e) => console.log("MESSAGE", e),
onError: (e) => console.error(e),
shouldReconnect: (closeEvent) => { console.log("Reconnect"); return true; },
});
const [messageHistory, setMessageHistory] = useState<MessageEvent[]>([]);
useEffect(() => {
if (lastMessage !== null) {
console.log("LAST", lastMessage)
setMessageHistory((prev) => prev.concat(lastMessage));
}
}, [lastMessage, setMessageHistory]);
useEffect(() => {
if (!mounted) {
setMounted(true)
}
}, [])
return (<div className="w-full bg-blue-300">
<p>Hello</p>
<p>{readyState}</p>
<Button onClick={() => sendMessage("uisdhnishdadasdfsd " + v4())}>
Click on me
</Button>
<div>
{lastMessage ? <span>Last message: {lastMessage.data}</span> : null}
<ul>
{messageHistory.map((message, idx) => (
<p className='block' key={idx}>{message ? message.data : null}</p>
))}
</ul>
</div>
</div>)
}
export default SocketPage