Added redemptions & redeemable actions. Fixed a few bugs.
This commit is contained in:
parent
68df045c54
commit
6548ce33e0
@ -1,22 +1,20 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { db } from "@/lib/db"
|
import { db } from "@/lib/db"
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import fetchUser from '@/lib/fetch-user';
|
||||||
|
import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation';
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
// Verify state against user id in user table.
|
// Verify state against user id in user table.
|
||||||
const key = await db.apiKey.findFirst({
|
const user = await fetchUserWithImpersonation(req)
|
||||||
where: {
|
if (!user) {
|
||||||
id: req.headers.get('x-api-key') as string
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!key) {
|
|
||||||
return new NextResponse("Forbidden", { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const connection = await db.twitchConnection.findFirst({
|
const connection = await db.twitchConnection.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: key.userId
|
userId: user.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
@ -29,8 +27,22 @@ export async function GET(req: Request) {
|
|||||||
Authorization: 'OAuth ' + connection.accessToken
|
Authorization: 'OAuth ' + connection.accessToken
|
||||||
}
|
}
|
||||||
})).data;
|
})).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) {
|
} catch (error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,14 +63,22 @@ export async function GET(req: Request) {
|
|||||||
|
|
||||||
await db.twitchConnection.update({
|
await db.twitchConnection.update({
|
||||||
where: {
|
where: {
|
||||||
userId: key.userId
|
userId: user.id
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
accessToken: access_token
|
accessToken: access_token,
|
||||||
|
refreshToken: refresh_token
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return new NextResponse("", { status: 200 });
|
const data = {
|
||||||
|
user_id: user.id,
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
broadcaster_id: connection.broadcasterId
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[ACCOUNT]", error);
|
console.log("[ACCOUNT]", error);
|
||||||
return new NextResponse("Internal Error", { status: 500 });
|
return new NextResponse("Internal Error", { status: 500 });
|
||||||
|
35
app/api/account/redemptions/route.ts
Normal file
35
app/api/account/redemptions/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,9 @@ import fetchUser from "@/lib/fetch-user";
|
|||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.log("[ACCOUNT]", error);
|
console.log("[ACCOUNT]", error);
|
||||||
return new NextResponse("Internal Error", { status: 500 });
|
return new NextResponse("Internal Error", { status: 500 });
|
||||||
|
13
app/api/info/version/route.ts
Normal file
13
app/api/info/version/route.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
150
app/api/settings/redemptions/actions/route.ts
Normal file
150
app/api/settings/redemptions/actions/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
143
app/api/settings/redemptions/route.ts
Normal file
143
app/api/settings/redemptions/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
@ -5,19 +5,16 @@ import voices from "@/data/tts";
|
|||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
|
if (!voices) {
|
||||||
|
return new NextResponse("Voices not available.", { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
const user = await fetchUserWithImpersonation(req)
|
const user = await fetchUserWithImpersonation(req)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const u = await db.user.findFirst({
|
return NextResponse.json(user.ttsDefaultVoice);
|
||||||
where: {
|
|
||||||
id: user.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const voice = voices.find(v => v.value == new String(u?.ttsDefaultVoice))
|
|
||||||
return NextResponse.json(voice);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[TTS/FILTER/DEFAULT]", error);
|
console.log("[TTS/FILTER/DEFAULT]", error);
|
||||||
return new NextResponse("Internal Error", { status: 500 });
|
return new NextResponse("Internal Error", { status: 500 });
|
||||||
@ -26,28 +23,32 @@ export async function GET(req: Request) {
|
|||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
|
if (!voices) {
|
||||||
|
return new NextResponse("Voices not available.", { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
const user = await fetchUserWithImpersonation(req)
|
const user = await fetchUserWithImpersonation(req)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { voice } = await req.json();
|
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({
|
await db.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: user.id
|
id: user.id
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
ttsDefaultVoice: Number.parseInt(v.value)
|
ttsDefaultVoice: v
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new NextResponse("", { status: 200 });
|
return new NextResponse("", { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[TTS/FILTER/DEFAULT]", error);
|
console.log("[TTS/FILTER/DEFAULT]", error);
|
||||||
return new NextResponse("Internal Error", { status: 500 });
|
|
||||||
}
|
}
|
||||||
|
return new NextResponse("Internal Error", { status: 500 });
|
||||||
}
|
}
|
@ -31,7 +31,7 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
const { search, replace } = await req.json();
|
const { search, replace } = await req.json();
|
||||||
if (!search || search.length < 4 || search.length > 200) 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.create({
|
const filter = await db.ttsWordFilter.create({
|
||||||
data: {
|
data: {
|
||||||
@ -58,7 +58,7 @@ export async function PUT(req: Request) {
|
|||||||
const { id, search, replace } = await req.json();
|
const { id, search, replace } = await req.json();
|
||||||
if (!id || id.length < 1) return new NextResponse("Bad Request", { status: 400 });
|
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 (!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({
|
const filter = await db.ttsWordFilter.update({
|
||||||
where: {
|
where: {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { db } from "@/lib/db"
|
import { db } from "@/lib/db"
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
|
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
|
||||||
import voices from "@/data/tts";
|
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
@ -10,23 +9,18 @@ export async function GET(req: Request) {
|
|||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let list : {
|
const voiceStates = await db.ttsVoiceState.findMany({
|
||||||
value: string;
|
where: {
|
||||||
label: string;
|
userId: user.id
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.log("[TTS/FILTER/USER]", error);
|
console.log("[TTS]", error);
|
||||||
return new NextResponse("Internal Error", { status: 500 });
|
return new NextResponse("Internal Error", { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,15 +32,28 @@ export async function POST(req: Request) {
|
|||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let { voice } = await req.json();
|
const { voice, state }: { voice: string, state: boolean } = await req.json();
|
||||||
voice = voice & ((1 << voices.length) - 1)
|
|
||||||
|
|
||||||
await db.user.update({
|
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: {
|
where: {
|
||||||
id: user.id
|
userId_ttsVoiceId: {
|
||||||
|
userId: user.id,
|
||||||
|
ttsVoiceId: voiceIdsMapped[voice.toLowerCase()]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data: {
|
update: {
|
||||||
ttsEnabledVoice: voice
|
state
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId: user.id,
|
||||||
|
ttsVoiceId: voiceIdsMapped[voice.toLowerCase()],
|
||||||
|
state
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
71
app/api/settings/tts/selected/route.ts
Normal file
71
app/api/settings/tts/selected/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,6 @@ export async function GET(req: Request, { params } : { params: { id: string } })
|
|||||||
export async function DELETE(req: Request, { params } : { params: { id: string } }) {
|
export async function DELETE(req: Request, { params } : { params: { id: string } }) {
|
||||||
try {
|
try {
|
||||||
const user = await fetchUserWithImpersonation(req)
|
const user = await fetchUserWithImpersonation(req)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
@ -4,20 +4,24 @@ import { NextResponse } from "next/server";
|
|||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
|
console.log("ABC 1")
|
||||||
const user = await fetchUserWithImpersonation(req);
|
const user = await fetchUserWithImpersonation(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
console.log("ABC 2")
|
||||||
|
|
||||||
const api = await db.twitchConnection.findFirst({
|
const api = await db.twitchConnection.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id
|
userId: user.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
console.log("ABC 3")
|
||||||
if (!api) {
|
if (!api) {
|
||||||
return new NextResponse("Forbidden", { status: 403 });
|
return new NextResponse("Forbidden", { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("ABC 4")
|
||||||
const data = {
|
const data = {
|
||||||
client_id: process.env.TWITCH_BOT_CLIENT_ID,
|
client_id: process.env.TWITCH_BOT_CLIENT_ID,
|
||||||
client_secret: process.env.TWITCH_BOT_CLIENT_SECRET,
|
client_secret: process.env.TWITCH_BOT_CLIENT_SECRET,
|
||||||
@ -25,6 +29,7 @@ export async function GET(req: Request) {
|
|||||||
refresh_token: api.refreshToken,
|
refresh_token: api.refreshToken,
|
||||||
broadcaster_id: api.broadcasterId
|
broadcaster_id: api.broadcasterId
|
||||||
}
|
}
|
||||||
|
console.log("ABC 5", data)
|
||||||
return NextResponse.json(data);
|
return NextResponse.json(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[TOKENS/GET]", error);
|
console.log("[TOKENS/GET]", error);
|
||||||
|
@ -41,7 +41,7 @@ export async function DELETE(req: Request) {
|
|||||||
return new NextResponse("Unauthorized", { status: 401 });
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let { id } = await req.json();
|
const { id } = await req.json();
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return NextResponse.json(null)
|
return NextResponse.json(null)
|
||||||
}
|
}
|
||||||
@ -60,12 +60,12 @@ export async function DELETE(req: Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateToken() {
|
function generateToken() {
|
||||||
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
|
let chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
|
||||||
var string_length = 32;
|
let string_length = 32;
|
||||||
var randomstring = '';
|
let randomstring = '';
|
||||||
for (var i = 0; i < string_length; i++) {
|
for (let i = 0; i < string_length; i++) {
|
||||||
var rnum = Math.floor(Math.random() * chars.length);
|
let rnum = Math.floor(Math.random() * chars.length);
|
||||||
randomstring += chars[rnum];
|
randomstring += chars[rnum];
|
||||||
}
|
}
|
||||||
return randomstring;
|
return randomstring;
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
import fetchUser from "@/lib/fetch-user";
|
|
||||||
import { db } from "@/lib/db"
|
import { db } from "@/lib/db"
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(req.url)
|
const user = await fetchUserWithImpersonation(req)
|
||||||
let userId = searchParams.get('userId')
|
if (!user) {
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
if (userId == null) {
|
|
||||||
const user = await fetchUser(req);
|
|
||||||
if (user != null) {
|
|
||||||
userId = user.id as string;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = await db.apiKey.findMany({
|
const tokens = await db.apiKey.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: userId as string
|
userId: user.id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(tokens);
|
return NextResponse.json(tokens);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[TOKENS/GET]", error);
|
console.log("[TOKENS/GET]", error);
|
||||||
return new NextResponse("Internal Error", { status: 500});
|
return new NextResponse("Internal Error", { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -24,7 +24,7 @@ export async function GET(req: Request) {
|
|||||||
return NextResponse.json(users)
|
return NextResponse.json(users)
|
||||||
}
|
}
|
||||||
if (id) {
|
if (id) {
|
||||||
const users = await db.user.findUnique({
|
const users = await db.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: id
|
id: id
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export async function GET(req: Request) {
|
|||||||
const users = await db.user.findMany();
|
const users = await db.user.findMany();
|
||||||
return NextResponse.json(users)
|
return NextResponse.json(users)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
|
console.log("[USERS]", error);
|
||||||
return new NextResponse("Internal Error", { status: 500 });
|
return new NextResponse("Internal Error", { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,63 +3,46 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ApiKey, User } from "@prisma/client";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
|
||||||
const SettingsPage = () => {
|
const ApiKeyPage = () => {
|
||||||
const { data: session, status } = useSession();
|
const [apiKeyViewable, setApiKeyViewable] = useState<number>(-1)
|
||||||
|
const [apiKeys, setApiKeys] = useState<{ id: string, label: string, userId: string }[]>([])
|
||||||
const [apiKeyViewable, setApiKeyViewable] = useState(0)
|
|
||||||
const [apiKeyChanges, setApiKeyChanges] = useState(0)
|
|
||||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
await axios.get("/api/tokens")
|
||||||
const keys = (await axios.get("/api/tokens")).data ?? {};
|
.then(d => setApiKeys(d.data ?? []))
|
||||||
setApiKeys(keys)
|
.catch(console.error)
|
||||||
} catch (error) {
|
|
||||||
console.log("ERROR", error)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData().catch(console.error);
|
fetchData();
|
||||||
}, [apiKeyChanges]);
|
}, []);
|
||||||
|
|
||||||
const onApiKeyAdd = async () => {
|
const onApiKeyAdd = async (label: string) => {
|
||||||
try {
|
await axios.post("/api/token", { label })
|
||||||
await axios.post("/api/token", {
|
.then(d => setApiKeys(apiKeys.concat([d.data])))
|
||||||
label: "Key label"
|
.catch(console.error)
|
||||||
});
|
|
||||||
setApiKeyChanges(apiKeyChanges + 1)
|
|
||||||
} catch (error) {
|
|
||||||
console.log("ERROR", error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onApiKeyDelete = async (id: string) => {
|
const onApiKeyDelete = async (id: string) => {
|
||||||
try {
|
await axios.delete("/api/token/" + id)
|
||||||
await axios.delete("/api/token/" + id);
|
.then((d) => setApiKeys(apiKeys.filter(k => k.id != d.data.id)))
|
||||||
setApiKeyChanges(apiKeyChanges - 1)
|
.catch(console.error)
|
||||||
} catch (error) {
|
|
||||||
console.log("ERROR", error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="px-10 py-5 mx-5 my-10">
|
<div className="px-10 py-5 mx-5 my-10">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl justify-left mt-10">API Keys</div>
|
<div className="text-xl justify-left mt-10 text-center">API Keys</div>
|
||||||
<Table className="max-w-2xl">
|
<Table>
|
||||||
<TableCaption>A list of your secret API keys.</TableCaption>
|
<TableCaption>A list of your secret API keys.</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Label</TableHead>
|
<TableHead>Label</TableHead>
|
||||||
<TableHead>Token</TableHead>
|
<TableHead>Token</TableHead>
|
||||||
<TableHead>View</TableHead>
|
|
||||||
<TableHead>Action</TableHead>
|
<TableHead>Action</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@ -67,20 +50,20 @@ const SettingsPage = () => {
|
|||||||
{apiKeys.map((key, index) => (
|
{apiKeys.map((key, index) => (
|
||||||
<TableRow key={key.id}>
|
<TableRow key={key.id}>
|
||||||
<TableCell className="font-medium">{key.label}</TableCell>
|
<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>
|
<TableCell>
|
||||||
<Button onClick={() => setApiKeyViewable((v) => v ^ (1 << index))}>
|
<Button onClick={() => setApiKeyViewable((v) => v != index ? index : -1)}>
|
||||||
{(apiKeyViewable & (1 << index)) > 0 ? "HIDE" : "VIEW"}
|
{apiKeyViewable == index ? "HIDE" : "VIEW"}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => onApiKeyDelete(key.id)} className="ml-[10px] bg-red-500 hover:bg-red-700">DELETE</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell><Button onClick={() => onApiKeyDelete(key.id)}>DEL</Button></TableCell>
|
<TableCell></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
<TableRow key="ADD">
|
<TableRow key="ADD">
|
||||||
<TableCell className="font-medium"></TableCell>
|
<TableCell className="font-medium"></TableCell>
|
||||||
<TableCell></TableCell>
|
<TableCell></TableCell>
|
||||||
<TableCell></TableCell>
|
<TableCell><Button onClick={() => onApiKeyAdd("Key label")}>ADD</Button></TableCell>
|
||||||
<TableCell><Button onClick={onApiKeyAdd}>ADD</Button></TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@ -90,4 +73,4 @@ const SettingsPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsPage;
|
export default ApiKeyPage;
|
@ -10,7 +10,7 @@ import Link from "next/link";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
const SettingsPage = () => {
|
const ConnectionsPage = () => {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const [previousUsername, setPreviousUsername] = useState<string>()
|
const [previousUsername, setPreviousUsername] = useState<string>()
|
||||||
const [userId, setUserId] = useState<string>()
|
const [userId, setUserId] = useState<string>()
|
||||||
@ -24,7 +24,7 @@ const SettingsPage = () => {
|
|||||||
setPreviousUsername(session.user?.name as string)
|
setPreviousUsername(session.user?.name as string)
|
||||||
if (session.user?.name) {
|
if (session.user?.name) {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
var connection: User = (await axios.get("/api/account")).data
|
let connection: User = (await axios.get("/api/account")).data
|
||||||
setUserId(connection.id)
|
setUserId(connection.id)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ const SettingsPage = () => {
|
|||||||
const [twitchUser, setTwitchUser] = useState<TwitchConnection | null>(null)
|
const [twitchUser, setTwitchUser] = useState<TwitchConnection | null>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
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)
|
setTwitchUser(connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,4 +97,4 @@ const SettingsPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsPage;
|
export default ConnectionsPage;
|
@ -3,8 +3,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const SettingsLayout = async (
|
const SettingsLayout = async ({
|
||||||
{ children } : { children:React.ReactNode } ) => {
|
children
|
||||||
|
} : {
|
||||||
|
children:React.ReactNode
|
||||||
|
} ) => {
|
||||||
const headersList = headers();
|
const headersList = headers();
|
||||||
const header_url = headersList.get('x-url') || "";
|
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")}>
|
header_url.endsWith("/settings") && "flex h-full w-full md:w-[250px] z-30 flex-col fixed inset-y-0")}>
|
||||||
<SettingsNavigation />
|
<SettingsNavigation />
|
||||||
</div>
|
</div>
|
||||||
<main className={cn("md:pl-[250px] h-full", header_url.endsWith("/settings") && "hidden")}>
|
<main className={"md:pl-[250px] h-full"}>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
143
app/settings/redemptions/page.tsx
Normal file
143
app/settings/redemptions/page.tsx
Normal 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;
|
@ -230,7 +230,7 @@ const TTSFiltersPage = () => {
|
|||||||
<Form {...usernameFilteredForm}>
|
<Form {...usernameFilteredForm}>
|
||||||
<form onSubmit={usernameFilteredForm.handleSubmit(onAdd)}>
|
<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">
|
<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}
|
{tag}
|
||||||
</Label>
|
</Label>
|
||||||
<FormField
|
<FormField
|
||||||
|
@ -3,10 +3,8 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Check, ChevronsUpDown } from "lucide-react"
|
import { Check, ChevronsUpDown } from "lucide-react"
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useReducer, useState } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command"
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command"
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||||
@ -16,49 +14,50 @@ import voices from "@/data/tts";
|
|||||||
import InfoNotice from "@/components/elements/info-notice";
|
import InfoNotice from "@/components/elements/info-notice";
|
||||||
|
|
||||||
const TTSVoiceFiltersPage = () => {
|
const TTSVoiceFiltersPage = () => {
|
||||||
const { data: session, status } = useSession();
|
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [value, setValue] = useState(0)
|
const [defaultVoice, setDefaultVoice] = useState("")
|
||||||
const [enabled, setEnabled] = useState(0)
|
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(() => {
|
useEffect(() => {
|
||||||
axios.get("/api/settings/tts/default")
|
axios.get("/api/settings/tts/default")
|
||||||
.then((voice) => {
|
.then((voice) => {
|
||||||
setValue(Number.parseInt(voice.data.value))
|
setDefaultVoice(voice.data)
|
||||||
})
|
})
|
||||||
|
|
||||||
axios.get("/api/settings/tts")
|
axios.get("/api/settings/tts")
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
const total = d.data.reduce((acc: number, item: {value: number, label: string, gender: string, language: string}) => acc |= 1 << (item.value - 1), 0)
|
const data: string[] = d.data;
|
||||||
setEnabled(total)
|
data.forEach(d => dispatchEnabledVoices({ type: "enable", value: d }))
|
||||||
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onDefaultChange = (voice: string) => {
|
const onDefaultChange = (voice: string) => {
|
||||||
try {
|
try {
|
||||||
axios.post("/api/settings/tts/default", { voice })
|
axios.post("/api/settings/tts/default", { voice })
|
||||||
.then(d => {
|
|
||||||
console.log(d)
|
|
||||||
})
|
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[TTS/DEFAULT]", error);
|
console.log("[TTS/DEFAULT]", error);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEnabledChanged = (val: number) => {
|
const onEnabledChanged = (voice: string, state: boolean) => {
|
||||||
try {
|
try {
|
||||||
axios.post("/api/settings/tts", { voice: val })
|
axios.post("/api/settings/tts", { voice: voice, state: state })
|
||||||
.then(d => {
|
|
||||||
console.log(d)
|
|
||||||
})
|
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[TTS]", error);
|
console.log("[TTS/ENABLED]", error);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +77,7 @@ const TTSVoiceFiltersPage = () => {
|
|||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="w-[200px] justify-between">
|
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" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@ -89,20 +88,20 @@ const TTSVoiceFiltersPage = () => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{voices.map((voice) => (
|
{voices.map((voice) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={voice.value + "-" + voice.label}
|
key={voice}
|
||||||
value={voice.value}
|
value={voice}
|
||||||
onSelect={(currentValue) => {
|
onSelect={(currentVoice) => {
|
||||||
setValue(Number.parseInt(currentValue))
|
setDefaultVoice(voice)
|
||||||
onDefaultChange(voice.label)
|
onDefaultChange(voice)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}>
|
}}>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4",
|
"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>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
@ -116,14 +115,14 @@ const TTSVoiceFiltersPage = () => {
|
|||||||
<InfoNotice message="Voices can be disabled from being used. Default voice will always work." hidden={false} />
|
<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]">
|
<div className="grid grid-cols-4 grid-flow-row gap-4 pt-[20px]">
|
||||||
{voices.map((v, i) => (
|
{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={() => {
|
<Checkbox onClick={() => {
|
||||||
const newVal = enabled ^ (1 << (Number.parseInt(v.value) - 1))
|
dispatchEnabledVoices({ type: enabledVoices[v] ? "disable" : "enable", value: v })
|
||||||
setEnabled(newVal)
|
onEnabledChanged(v, !enabledVoices[v])
|
||||||
onEnabledChanged(newVal)
|
|
||||||
}}
|
}}
|
||||||
checked={(enabled & (1 << (Number.parseInt(v.value) - 1))) > 0} />
|
disabled={loading}
|
||||||
<div className="pl-[5px]">{v.label}</div>
|
checked={enabledVoices[v]} />
|
||||||
|
<div className="pl-[5px]">{v}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
58
app/socket/page.tsx
Normal file
58
app/socket/page.tsx
Normal 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
|
316
components/elements/redeemable-action.tsx
Normal file
316
components/elements/redeemable-action.tsx
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import { Maximize2, Minimize2, Trash2Icon } from "lucide-react";
|
||||||
|
import { ActionType } from "@prisma/client";
|
||||||
|
|
||||||
|
|
||||||
|
const actionTypes = [
|
||||||
|
{
|
||||||
|
"name": "Overwrite local file content",
|
||||||
|
"value": ActionType.WRITE_TO_FILE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Append to local file",
|
||||||
|
"value": ActionType.APPEND_TO_FILE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cause a transformation on OBS scene item",
|
||||||
|
"value": ActionType.OBS_TRANSFORM
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Play an audio file locally",
|
||||||
|
"value": ActionType.AUDIO_FILE
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
interface RedeemableAction {
|
||||||
|
name: string
|
||||||
|
type: string | undefined
|
||||||
|
data: { [key: string]: string }
|
||||||
|
edit?: boolean
|
||||||
|
showEdit?: boolean
|
||||||
|
isNew: boolean
|
||||||
|
obsTransformations: { label: string, placeholder: string, description: string }[]
|
||||||
|
adder: (name: string, type: ActionType, data: { [key: string]: string }) => void
|
||||||
|
remover: (action: { name: string, type: string, data: any }) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const RedemptionAction = ({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
edit,
|
||||||
|
showEdit = true,
|
||||||
|
isNew = false,
|
||||||
|
obsTransformations = [],
|
||||||
|
adder,
|
||||||
|
remover
|
||||||
|
}: RedeemableAction) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [actionName, setActionName] = useState(name)
|
||||||
|
const [actionType, setActionType] = useState<{ name: string, value: ActionType } | undefined>(actionTypes.find(a => a.value == type?.toUpperCase()))
|
||||||
|
const [actionData, setActionData] = useState<{ [key: string]: string }>(data)
|
||||||
|
const [isEditable, setIsEditable] = useState(edit)
|
||||||
|
const [isMinimized, setIsMinimized] = useState(!isNew)
|
||||||
|
const [oldData, setOldData] = useState<{ n: string, t: ActionType | undefined, d: { [k: string]: string } } | undefined>(undefined)
|
||||||
|
|
||||||
|
function Save(name: string, type: ActionType | undefined, data: { [key: string]: string }, isNew: boolean) {
|
||||||
|
// TODO: validation
|
||||||
|
if (!name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let info: any = {
|
||||||
|
name,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
info = { ...info, ...data }
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
axios.post("/api/settings/redemptions/actions", info)
|
||||||
|
.then(d => {
|
||||||
|
adder(name, type, data)
|
||||||
|
setActionName("")
|
||||||
|
setActionType(undefined)
|
||||||
|
setActionData({})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
axios.put("/api/settings/redemptions/actions", info)
|
||||||
|
.then(d => {
|
||||||
|
setIsEditable(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Cancel(data: { n: string, t: ActionType | undefined, d: { [k: string]: string } } | undefined) {
|
||||||
|
if (!data)
|
||||||
|
return
|
||||||
|
|
||||||
|
setActionName(data.n)
|
||||||
|
setActionType(actionTypes.find(a => a.value == data.t))
|
||||||
|
setActionData(data.d)
|
||||||
|
setIsEditable(false)
|
||||||
|
setOldData(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Delete() {
|
||||||
|
axios.delete("/api/settings/redemptions/actions?action_name=" + name)
|
||||||
|
.then(d => {
|
||||||
|
remover(d.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-orange-300 p-3 border-2 border-orange-400 rounded-lg w-[830px]">
|
||||||
|
{isMinimized &&
|
||||||
|
<div
|
||||||
|
className="flex">
|
||||||
|
<Label
|
||||||
|
className="mr-2 grow text-lg align-middle"
|
||||||
|
htmlFor="name">
|
||||||
|
{actionName}
|
||||||
|
</Label>
|
||||||
|
<Button
|
||||||
|
className="flex inline-block self-end"
|
||||||
|
onClick={e => setIsMinimized(!isMinimized)}>
|
||||||
|
{isMinimized ? <Maximize2 /> : <Minimize2 />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|| !isMinimized &&
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="pb-3">
|
||||||
|
<Label
|
||||||
|
className="mr-2"
|
||||||
|
htmlFor="name">
|
||||||
|
Action name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="inline-block w-[300px]"
|
||||||
|
id="name"
|
||||||
|
placeholder="Enter a name for this action"
|
||||||
|
onChange={e => setActionName(e.target.value)}
|
||||||
|
value={actionName}
|
||||||
|
readOnly={!isNew} />
|
||||||
|
<Label
|
||||||
|
className="ml-10 mr-2"
|
||||||
|
htmlFor="type">
|
||||||
|
Action type
|
||||||
|
</Label>
|
||||||
|
{!isEditable &&
|
||||||
|
<Input
|
||||||
|
className="inline-block w-[300px] justify-between"
|
||||||
|
name="type"
|
||||||
|
value={actionType?.name}
|
||||||
|
readOnly />
|
||||||
|
|| isEditable &&
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="w-[300px] justify-between"
|
||||||
|
>{!actionType ? "Select one..." : actionType.name}</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Filter actions..."
|
||||||
|
autoFocus={true} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No action found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{actionTypes.map((action) => (
|
||||||
|
<CommandItem
|
||||||
|
value={action.name}
|
||||||
|
key={action.value}
|
||||||
|
onSelect={(value) => {
|
||||||
|
setActionType(actionTypes.find(v => v.name.toLowerCase() == value.toLowerCase()))
|
||||||
|
setOpen(false)
|
||||||
|
}}>
|
||||||
|
{action.name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{actionType && (actionType.value == ActionType.WRITE_TO_FILE || actionType.value == ActionType.APPEND_TO_FILE) &&
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
className="mr-2"
|
||||||
|
htmlFor="file_path">
|
||||||
|
File path
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="w-[300px] justify-between inline-block"
|
||||||
|
name="file_path"
|
||||||
|
placeholder={actionType.value == ActionType.WRITE_TO_FILE ? "Enter the local file path to the file to overwrite" : "Enter the local file path to the file to append to"}
|
||||||
|
value={actionData["file_path"]}
|
||||||
|
onChange={e => setActionData({ ...actionData, "file_path": e.target.value })}
|
||||||
|
readOnly={!isEditable} />
|
||||||
|
<Label
|
||||||
|
className="ml-10 mr-2"
|
||||||
|
htmlFor="file_content">
|
||||||
|
File content
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="w-[300px] justify-between inline-block"
|
||||||
|
name="file_content"
|
||||||
|
placeholder="Enter the content that should be written"
|
||||||
|
value={actionData["file_content"]}
|
||||||
|
onChange={e => setActionData({ ...actionData, "file_content": e.target.value })}
|
||||||
|
readOnly={!isEditable} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{actionType && actionType.value == ActionType.OBS_TRANSFORM &&
|
||||||
|
<div>
|
||||||
|
{obsTransformations.map(t =>
|
||||||
|
<div
|
||||||
|
className="mt-3">
|
||||||
|
<Label
|
||||||
|
className="mr-2"
|
||||||
|
htmlFor={t.label.toLowerCase()}>
|
||||||
|
{t.label.split("_").map(w => w.substring(0, 1).toUpperCase() + w.substring(1).toLowerCase()).join(" ")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="w-[300px] justify-between inline-block"
|
||||||
|
name={t.label.toLowerCase()}
|
||||||
|
placeholder={t.placeholder}
|
||||||
|
value={actionData[t.label]}
|
||||||
|
onChange={e => {
|
||||||
|
let c = { ...actionData }
|
||||||
|
c[t.label] = e.target.value
|
||||||
|
setActionData(c)
|
||||||
|
}}
|
||||||
|
readOnly={!isEditable} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{actionType && actionType.value == ActionType.AUDIO_FILE &&
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
className="mr-2"
|
||||||
|
htmlFor="file_path">
|
||||||
|
File path
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="w-[300px] justify-between inline-block"
|
||||||
|
name="file_path"
|
||||||
|
placeholder={"Enter the local file path where the audio file is at"}
|
||||||
|
value={actionData["file_path"]}
|
||||||
|
onChange={e => setActionData({ ...actionData, "file_path": e.target.value })}
|
||||||
|
readOnly={!isEditable} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isEditable &&
|
||||||
|
<Button
|
||||||
|
className="m-3"
|
||||||
|
onClick={() => Save(actionName, actionType?.value, actionData, isNew)}>
|
||||||
|
{isNew ? "Add" : "Save"}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{isEditable && !isNew &&
|
||||||
|
<Button
|
||||||
|
className="m-3"
|
||||||
|
onClick={() => Cancel(oldData)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{showEdit && !isEditable &&
|
||||||
|
<Button
|
||||||
|
className="m-3"
|
||||||
|
onClick={() => {
|
||||||
|
setOldData({ n: actionName, t: actionType?.value, d: actionData })
|
||||||
|
setIsEditable(true)
|
||||||
|
}}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{!isEditable &&
|
||||||
|
<Button
|
||||||
|
className="m-3 bg-red-500 hover:bg-red-600 align-bottom"
|
||||||
|
onClick={() => Delete()}>
|
||||||
|
<Trash2Icon />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{!isNew &&
|
||||||
|
<Button
|
||||||
|
className="m-3 align-middle"
|
||||||
|
onClick={e => setIsMinimized(!isMinimized)}>
|
||||||
|
{isMinimized ? <Maximize2 /> : <Minimize2 />}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RedemptionAction;
|
274
components/elements/redemption.tsx
Normal file
274
components/elements/redemption.tsx
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import { HelpCircleIcon, Trash2Icon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "../ui/tooltip"
|
||||||
|
|
||||||
|
interface Redemption {
|
||||||
|
id: string | undefined
|
||||||
|
redemptionId: string | undefined
|
||||||
|
actionName: string
|
||||||
|
edit: boolean
|
||||||
|
showEdit: boolean
|
||||||
|
isNew: boolean
|
||||||
|
actions: string[]
|
||||||
|
twitchRedemptions: { id: string, title: string }[]
|
||||||
|
adder: (id: string, actionName: string, redemptionId: string, order: number) => void
|
||||||
|
remover: (redemption: { id: string, redemptionId: string, actionName: string, order: number }) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const OBSRedemption = ({
|
||||||
|
id,
|
||||||
|
redemptionId,
|
||||||
|
actionName,
|
||||||
|
edit,
|
||||||
|
showEdit,
|
||||||
|
isNew,
|
||||||
|
actions,
|
||||||
|
twitchRedemptions,
|
||||||
|
adder,
|
||||||
|
remover
|
||||||
|
}: Redemption) => {
|
||||||
|
const [actionOpen, setActionOpen] = useState(false)
|
||||||
|
const [redemptionOpen, setRedemptionOpen] = useState(false)
|
||||||
|
const [twitchRedemption, setTwitchRedemption] = useState<{ id: string, title: string } | undefined>(undefined)
|
||||||
|
const [action, setAction] = useState<string | undefined>(actionName)
|
||||||
|
const [order, setOrder] = useState<number>(0)
|
||||||
|
const [isEditable, setIsEditable] = useState(edit)
|
||||||
|
const [oldData, setOldData] = useState<{ r: { id: string, title: string } | undefined, a: string | undefined, o: number } | undefined>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("TR:", twitchRedemptions, redemptionId, twitchRedemptions.find(r => r.id == redemptionId))
|
||||||
|
setTwitchRedemption(twitchRedemptions.find(r => r.id == redemptionId))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function Save() {
|
||||||
|
// TODO: validation
|
||||||
|
if (!isNew && !id)
|
||||||
|
return
|
||||||
|
if (!action || !twitchRedemption)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
axios.post("/api/settings/redemptions", {
|
||||||
|
actionName: action,
|
||||||
|
redemptionId: twitchRedemption?.id,
|
||||||
|
order: order,
|
||||||
|
state: true
|
||||||
|
}).then(d => {
|
||||||
|
adder(d.data.id, action, twitchRedemption.id, 0)
|
||||||
|
setAction(undefined)
|
||||||
|
setTwitchRedemption(undefined)
|
||||||
|
setOrder(0)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
axios.put("/api/settings/redemptions", {
|
||||||
|
id: id,
|
||||||
|
actionName: action,
|
||||||
|
redemptionId: twitchRedemption?.id,
|
||||||
|
order: order,
|
||||||
|
state: true
|
||||||
|
}).then(d => {
|
||||||
|
setIsEditable(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Cancel() {
|
||||||
|
if (!oldData)
|
||||||
|
return
|
||||||
|
|
||||||
|
setAction(oldData.a)
|
||||||
|
setTwitchRedemption(oldData.r)
|
||||||
|
setOrder(oldData.o)
|
||||||
|
setIsEditable(false)
|
||||||
|
setOldData(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Delete() {
|
||||||
|
axios.delete("/api/settings/redemptions?id=" + id)
|
||||||
|
.then(d => {
|
||||||
|
remover(d.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-orange-300 p-5 border-2 border-orange-400 rounded-lg w-[830px]">
|
||||||
|
<div
|
||||||
|
className="pb-4">
|
||||||
|
<Label
|
||||||
|
className="mr-2"
|
||||||
|
htmlFor="redemption">
|
||||||
|
Twitch Redemption
|
||||||
|
</Label>
|
||||||
|
{!isEditable &&
|
||||||
|
<Input
|
||||||
|
className="inline-block w-[290px] justify-between"
|
||||||
|
name="redemption"
|
||||||
|
value={twitchRedemption?.title}
|
||||||
|
readOnly />
|
||||||
|
|| isEditable &&
|
||||||
|
<Popover
|
||||||
|
open={redemptionOpen}
|
||||||
|
onOpenChange={setRedemptionOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={actionOpen}
|
||||||
|
className="w-[290px] justify-between"
|
||||||
|
>{!twitchRedemption ? "Select one..." : twitchRedemption.title}</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Filter redemptions..."
|
||||||
|
autoFocus={true} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No redemption found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{twitchRedemptions.map((redemption) => (
|
||||||
|
<CommandItem
|
||||||
|
value={redemption.title}
|
||||||
|
key={redemption.id}
|
||||||
|
onSelect={(value) => {
|
||||||
|
setTwitchRedemption(twitchRedemptions.find(v => v.title.toLowerCase() == value.toLowerCase()))
|
||||||
|
setRedemptionOpen(false)
|
||||||
|
}}>
|
||||||
|
{redemption.title}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
}
|
||||||
|
<Label
|
||||||
|
className="ml-10 mr-2"
|
||||||
|
htmlFor="action">
|
||||||
|
Action
|
||||||
|
</Label>
|
||||||
|
{!isEditable &&
|
||||||
|
<Input
|
||||||
|
className="inline-block w-[290px] justify-between"
|
||||||
|
name="action"
|
||||||
|
value={action}
|
||||||
|
readOnly />
|
||||||
|
|| isEditable &&
|
||||||
|
<Popover
|
||||||
|
open={actionOpen}
|
||||||
|
onOpenChange={setActionOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={actionOpen}
|
||||||
|
className="w-[290px] justify-between">
|
||||||
|
{!action ? "Select one..." : action}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Filter actions..."
|
||||||
|
autoFocus={true} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No action found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{actions.map((action) => (
|
||||||
|
<CommandItem
|
||||||
|
value={action}
|
||||||
|
key={action}
|
||||||
|
onSelect={(value) => {
|
||||||
|
let a = actions.find(v => v == action)
|
||||||
|
if (a)
|
||||||
|
setAction(a)
|
||||||
|
setActionOpen(false)
|
||||||
|
}}>
|
||||||
|
{action}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="pb-4">
|
||||||
|
<Label
|
||||||
|
className="mr-2"
|
||||||
|
htmlFor="order">
|
||||||
|
Order
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="inline-block w-[300px]"
|
||||||
|
id="name"
|
||||||
|
placeholder="Enter an order number for this action"
|
||||||
|
onChange={e => setOrder(e.target.value.length == 0 ? 0 : parseInt(e.target.value))}
|
||||||
|
value={order}
|
||||||
|
readOnly={!isEditable} />
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircleIcon
|
||||||
|
className="inline-block ml-3"/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>This decides when this action will be done relative to other actions for this Twitch redemption.<br/>
|
||||||
|
Action start from lowest to highest order number.<br/>
|
||||||
|
Equal order numbers cannot be guaranteed proper order.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isEditable &&
|
||||||
|
<Button
|
||||||
|
className="m-3"
|
||||||
|
onClick={() => Save()}>
|
||||||
|
{isNew ? "Add" : "Save"}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{isEditable && !isNew &&
|
||||||
|
<Button
|
||||||
|
className="m-3"
|
||||||
|
onClick={() => Cancel()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{showEdit && !isEditable &&
|
||||||
|
<Button
|
||||||
|
className="m-3"
|
||||||
|
onClick={() => {
|
||||||
|
setOldData({ a: actionName, r: twitchRedemption, o: order })
|
||||||
|
setIsEditable(true)
|
||||||
|
}}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{!isEditable &&
|
||||||
|
<Button
|
||||||
|
className="m-3 bg-red-500 hover:bg-red-600 align-bottom"
|
||||||
|
onClick={() => Delete()}>
|
||||||
|
<Trash2Icon />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OBSRedemption;
|
@ -16,24 +16,16 @@ const AdminProfile = () => {
|
|||||||
const session = useSession();
|
const session = useSession();
|
||||||
const [impersonation, setImpersonation] = useState<string | null>(null)
|
const [impersonation, setImpersonation] = useState<string | null>(null)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const [users, setUsers] = useState<User[]>([])
|
const [users, setUsers] = useState<User[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetch = async (userId: string | undefined) => {
|
const fetch = async (userId: string | undefined) => {
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
|
|
||||||
await axios.get<User>("/api/users?id=" + userId)
|
await axios.get("/api/users?id=" + userId)
|
||||||
.then(u => {
|
.then(u => setImpersonation(u.data?.name))
|
||||||
setImpersonation(u.data?.name)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(session)
|
|
||||||
fetch(session?.data?.user?.impersonation?.id)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
await axios.get<User[]>("/api/users")
|
await axios.get<User[]>("/api/users")
|
||||||
.then((u) => {
|
.then((u) => {
|
||||||
@ -41,12 +33,14 @@ const AdminProfile = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch(session?.data?.user?.impersonation?.id)
|
||||||
fetchUsers()
|
fetchUsers()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onImpersonationChange = async (userId: string, name: string) => {
|
const onImpersonationChange = async (userId: string, name: string) => {
|
||||||
|
console.log("IMPERSONATION", impersonation)
|
||||||
if (impersonation) {
|
if (impersonation) {
|
||||||
if (impersonation == session.data?.user.impersonation?.name) {
|
if (impersonation == name) {
|
||||||
await axios.delete("/api/account/impersonate")
|
await axios.delete("/api/account/impersonate")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setImpersonation(null)
|
setImpersonation(null)
|
||||||
|
@ -47,6 +47,17 @@ const SettingsNavigation = async () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li className="text-xs text-gray-200">
|
||||||
|
Twitch
|
||||||
|
</li>
|
||||||
|
<li className="">
|
||||||
|
<Link href={"/settings/redemptions"}>
|
||||||
|
<Button variant="ghost" className="w-full text-lg">
|
||||||
|
Channel Redemptions
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li className="text-xs text-gray-200">
|
<li className="text-xs text-gray-200">
|
||||||
API
|
API
|
||||||
</li>
|
</li>
|
||||||
|
@ -22,7 +22,7 @@ const UserProfile = () => {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
if (user) return
|
if (user) return
|
||||||
|
|
||||||
var userData = (await axios.get("/api/account")).data
|
let userData = (await axios.get("/api/account")).data
|
||||||
setUser(userData)
|
setUser(userData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
30
components/ui/tooltip.tsx
Normal file
30
components/ui/tooltip.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
126
data/tts.ts
126
data/tts.ts
@ -1,126 +1,4 @@
|
|||||||
let voices_data = [
|
const voices_data = ["Filiz", "Astrid", "Tatyana", "Maxim", "Carmen", "Ines", "Cristiano", "Vitoria", "Ricardo", "Maja", "Jan", "Jacek", "Ewa", "Ruben", "Lotte", "Liv", "Seoyeon", "Takumi", "Mizuki", "Giorgio", "Carla", "Bianca", "Karl", "Dora", "Mathieu", "Celine", "Chantal", "Penelope", "Miguel", "Mia", "Enrique", "Conchita", "Geraint", "Salli", "Matthew", "Kimberly", "Kendra", "Justin", "Joey", "Joanna", "Ivy", "Raveena", "Aditi", "Emma", "Brian", "Amy", "Russell", "Nicole", "Vicki", "Marlene", "Hans", "Naja", "Mads", "Gwyneth", "Zhiyu", "es-ES-Standard-A", "it-IT-Standard-A", "it-IT-Wavenet-A", "ja-JP-Standard-A", "ja-JP-Wavenet-A", "ko-KR-Standard-A", "ko-KR-Wavenet-A", "pt-BR-Standard-A", "tr-TR-Standard-A", "sv-SE-Standard-A", "nl-NL-Standard-A", "nl-NL-Wavenet-A", "en-US-Wavenet-A", "en-US-Wavenet-B", "en-US-Wavenet-C", "en-US-Wavenet-D", "en-US-Wavenet-E", "en-US-Wavenet-F", "en-GB-Standard-A", "en-GB-Standard-B", "en-GB-Standard-C", "en-GB-Standard-D", "en-GB-Wavenet-A", "en-GB-Wavenet-B", "en-GB-Wavenet-C", "en-GB-Wavenet-D", "en-US-Standard-B", "en-US-Standard-C", "en-US-Standard-D", "en-US-Standard-E", "de-DE-Standard-A", "de-DE-Standard-B", "de-DE-Wavenet-A", "de-DE-Wavenet-B", "de-DE-Wavenet-C", "de-DE-Wavenet-D", "en-AU-Standard-A", "en-AU-Standard-B", "en-AU-Wavenet-A", "en-AU-Wavenet-B", "en-AU-Wavenet-C", "en-AU-Wavenet-D", "en-AU-Standard-C", "en-AU-Standard-D", "fr-CA-Standard-A", "fr-CA-Standard-B", "fr-CA-Standard-C", "fr-CA-Standard-D", "fr-FR-Standard-C", "fr-FR-Standard-D", "fr-FR-Wavenet-A", "fr-FR-Wavenet-B", "fr-FR-Wavenet-C", "fr-FR-Wavenet-D", "da-DK-Wavenet-A", "pl-PL-Wavenet-A", "pl-PL-Wavenet-B", "pl-PL-Wavenet-C", "pl-PL-Wavenet-D", "pt-PT-Wavenet-A", "pt-PT-Wavenet-B", "pt-PT-Wavenet-C", "pt-PT-Wavenet-D", "ru-RU-Wavenet-A", "ru-RU-Wavenet-B", "ru-RU-Wavenet-C", "ru-RU-Wavenet-D", "sk-SK-Wavenet-A", "tr-TR-Wavenet-A", "tr-TR-Wavenet-B", "tr-TR-Wavenet-C", "tr-TR-Wavenet-D", "tr-TR-Wavenet-E", "uk-UA-Wavenet-A", "ar-XA-Wavenet-A", "ar-XA-Wavenet-B", "ar-XA-Wavenet-C", "cs-CZ-Wavenet-A", "nl-NL-Wavenet-B", "nl-NL-Wavenet-C", "nl-NL-Wavenet-D", "nl-NL-Wavenet-E", "en-IN-Wavenet-A", "en-IN-Wavenet-B", "en-IN-Wavenet-C", "fil-PH-Wavenet-A", "fi-FI-Wavenet-A", "el-GR-Wavenet-A", "hi-IN-Wavenet-A", "hi-IN-Wavenet-B", "hi-IN-Wavenet-C", "hu-HU-Wavenet-A", "id-ID-Wavenet-A", "id-ID-Wavenet-B", "id-ID-Wavenet-C", "it-IT-Wavenet-B", "it-IT-Wavenet-C", "it-IT-Wavenet-D", "ja-JP-Wavenet-B", "ja-JP-Wavenet-C", "ja-JP-Wavenet-D", "cmn-CN-Wavenet-A", "cmn-CN-Wavenet-B", "cmn-CN-Wavenet-C", "cmn-CN-Wavenet-D", "nb-no-Wavenet-E", "nb-no-Wavenet-A", "nb-no-Wavenet-B", "nb-no-Wavenet-C", "nb-no-Wavenet-D", "vi-VN-Wavenet-A", "vi-VN-Wavenet-B", "vi-VN-Wavenet-C", "vi-VN-Wavenet-D", "sr-rs-Standard-A", "lv-lv-Standard-A", "is-is-Standard-A", "bg-bg-Standard-A", "af-ZA-Standard-A", "Tracy", "Danny", "Huihui", "Yaoyao", "Kangkang", "HanHan", "Zhiwei", "Asaf", "An", "Stefanos", "Filip", "Ivan", "Heidi", "Herena", "Kalpana", "Hemant", "Matej", "Andika", "Rizwan", "Lado", "Valluvar", "Linda", "Heather", "Sean", "Michael", "Karsten", "Guillaume", "Pattara", "Jakub", "Szabolcs", "Hoda", "Naayf"]
|
||||||
{
|
const voices = voices_data.filter(v => !v.includes("-")).sort()
|
||||||
value: "1",
|
|
||||||
label: "Brian",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "2",
|
|
||||||
label: "Amy",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "3",
|
|
||||||
label: "Emma",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "4",
|
|
||||||
label: "Geraint",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "5",
|
|
||||||
label: "Russel",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "6",
|
|
||||||
label: "Nicole",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "7",
|
|
||||||
label: "Joey",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "8",
|
|
||||||
label: "Justin",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "9",
|
|
||||||
label: "Matthew",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "10",
|
|
||||||
label: "Ivy",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "11",
|
|
||||||
label: "Joanna",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "12",
|
|
||||||
label: "Kendra",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "13",
|
|
||||||
label: "Kimberly",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "14",
|
|
||||||
label: "Salli",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "15",
|
|
||||||
label: "Raveena",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "16",
|
|
||||||
label: "Carter",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "17",
|
|
||||||
label: "Paul",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "18",
|
|
||||||
label: "Evelyn",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "19",
|
|
||||||
label: "Liam",
|
|
||||||
gender: "Male",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "20",
|
|
||||||
label: "Jasmine",
|
|
||||||
gender: "Female",
|
|
||||||
language: "en"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const voices = voices_data.sort((a, b) => a.label < b.label ? -1 : a.label > b.label ? 1 : 0)
|
|
||||||
|
|
||||||
export default voices
|
export default voices
|
72
data/twitch-reauthorize.ts
Normal file
72
data/twitch-reauthorize.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { db } from "@/lib/db"
|
||||||
|
|
||||||
|
export async function updateTwitchToken(userId: string) {
|
||||||
|
const connection = await db.twitchConnection.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!connection) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { expires_in }: { client_id: string, login: string, scopes: string[], user_id: string, expires_in: number } = (await axios.get("https://id.twitch.tv/oauth2/validate", {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'OAuth ' + connection.accessToken
|
||||||
|
}
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
if (expires_in > 3600) {
|
||||||
|
let data = await db.twitchConnection.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let dataFormatted = {
|
||||||
|
user_id: userId,
|
||||||
|
access_token: data?.accessToken,
|
||||||
|
refresh_token: data?.refreshToken,
|
||||||
|
broadcaster_id: connection.broadcasterId
|
||||||
|
}
|
||||||
|
return dataFormatted
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post to https://id.twitch.tv/oauth2/token
|
||||||
|
const token: { access_token: string, expires_in: number, refresh_token: string, token_type: string, scope: string[] } = (await axios.post("https://id.twitch.tv/oauth2/token", {
|
||||||
|
client_id: process.env.TWITCH_BOT_CLIENT_ID,
|
||||||
|
client_secret: process.env.TWITCH_BOT_CLIENT_SECRET,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: connection.refreshToken
|
||||||
|
})).data
|
||||||
|
|
||||||
|
// Fetch values from token.
|
||||||
|
const { access_token, expires_in, refresh_token, token_type } = token
|
||||||
|
|
||||||
|
if (!access_token || !refresh_token || token_type !== "bearer") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.twitchConnection.update({
|
||||||
|
where: {
|
||||||
|
userId: userId
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
accessToken: access_token,
|
||||||
|
refreshToken: refresh_token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
user_id: userId,
|
||||||
|
access_token,
|
||||||
|
refresh_token,
|
||||||
|
broadcaster_id: connection.broadcasterId
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
0
hooks/ApiKeyHooks.tsx
Normal file
0
hooks/ApiKeyHooks.tsx
Normal file
@ -12,7 +12,7 @@ export default async function fetchUserWithImpersonation(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = req.headers?.get('x-api-key')
|
const token = req.headers?.get('x-api-key')
|
||||||
if (token === null || token === undefined)
|
if (!token)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const key = await db.apiKey.findFirst({
|
const key = await db.apiKey.findFirst({
|
||||||
@ -21,7 +21,8 @@ export default async function fetchUserWithImpersonation(req: Request) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!key) return null
|
if (!key)
|
||||||
|
return null
|
||||||
|
|
||||||
return fetch(key.userId)
|
return fetch(key.userId)
|
||||||
}
|
}
|
||||||
@ -35,6 +36,7 @@ const fetch = async (userId: string) => {
|
|||||||
|
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
||||||
|
// Only admins can impersonate others.
|
||||||
if (user.role == "ADMIN") {
|
if (user.role == "ADMIN") {
|
||||||
const impersonation = await db.impersonation.findFirst({
|
const impersonation = await db.impersonation.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
@ -12,7 +12,7 @@ export default async function fetchUser(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = req.headers?.get('x-api-key')
|
const token = req.headers?.get('x-api-key')
|
||||||
if (token === null || token === undefined)
|
if (!token)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const key = await db.apiKey.findFirst({
|
const key = await db.apiKey.findFirst({
|
||||||
@ -38,6 +38,6 @@ const fetch = async (userId: string) => {
|
|||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.name,
|
username: user.name,
|
||||||
role: user.role
|
role: user.role,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,7 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {}
|
const nextConfig = {
|
||||||
|
reactStrictMode: false,
|
||||||
|
output: 'standalone',
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig
|
||||||
|
@ -2,10 +2,15 @@ generator client {
|
|||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// datasource db {
|
||||||
|
// provider = "mysql"
|
||||||
|
// url = env("DATABASE_URL")
|
||||||
|
// relationMode = "prisma"
|
||||||
|
// }
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "mysql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
relationMode = "prisma"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserRole {
|
enum UserRole {
|
||||||
@ -20,8 +25,7 @@ model User {
|
|||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
role UserRole @default(USER)
|
role UserRole @default(USER)
|
||||||
image String?
|
image String?
|
||||||
ttsDefaultVoice Int @default(1)
|
ttsDefaultVoice String @default("Brian")
|
||||||
ttsEnabledVoice Int @default(1048575)
|
|
||||||
|
|
||||||
impersonationSources Impersonation[] @relation(name: "impersonationSources")
|
impersonationSources Impersonation[] @relation(name: "impersonationSources")
|
||||||
impersonationTargets Impersonation[] @relation(name: "impersonationTargets")
|
impersonationTargets Impersonation[] @relation(name: "impersonationTargets")
|
||||||
@ -31,6 +35,10 @@ model User {
|
|||||||
twitchConnections TwitchConnection[]
|
twitchConnections TwitchConnection[]
|
||||||
ttsUsernameFilter TtsUsernameFilter[]
|
ttsUsernameFilter TtsUsernameFilter[]
|
||||||
ttsWordFilter TtsWordFilter[]
|
ttsWordFilter TtsWordFilter[]
|
||||||
|
ttsChatVoices TtsChatVoice[]
|
||||||
|
ttsVoiceStates TtsVoiceState[]
|
||||||
|
actions Action[]
|
||||||
|
redemptions Redemption[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@ -50,18 +58,17 @@ model Account {
|
|||||||
id_token String? @db.Text
|
id_token String? @db.Text
|
||||||
session_state String?
|
session_state String?
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
@@unique([provider, providerAccountId])
|
@@unique([provider, providerAccountId])
|
||||||
@@index([userId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Impersonation {
|
model Impersonation {
|
||||||
sourceId String
|
sourceId String
|
||||||
targetId String
|
targetId String
|
||||||
|
|
||||||
source User @relation(name: "impersonationSources", fields: [sourceId], references: [id], onDelete: Cascade)
|
source User @relation(name: "impersonationSources", fields: [sourceId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
target User @relation(name: "impersonationTargets", fields: [targetId], references: [id], onDelete: Cascade)
|
target User @relation(name: "impersonationTargets", fields: [targetId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
@@id([sourceId])
|
@@id([sourceId])
|
||||||
@@index([sourceId])
|
@@index([sourceId])
|
||||||
@ -73,9 +80,7 @@ model ApiKey {
|
|||||||
label String
|
label String
|
||||||
|
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
@@index([userId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model TwitchConnection {
|
model TwitchConnection {
|
||||||
@ -84,9 +89,7 @@ model TwitchConnection {
|
|||||||
refreshToken String
|
refreshToken String
|
||||||
|
|
||||||
userId String @id
|
userId String @id
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
@@index([userId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model TtsUsernameFilter {
|
model TtsUsernameFilter {
|
||||||
@ -94,9 +97,8 @@ model TtsUsernameFilter {
|
|||||||
tag String
|
tag String
|
||||||
|
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
@@index([userId])
|
|
||||||
@@id([userId, username])
|
@@id([userId, username])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,8 +108,115 @@ model TtsWordFilter {
|
|||||||
replace String
|
replace String
|
||||||
|
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
@@index([userId])
|
|
||||||
@@unique([userId, search])
|
@@unique([userId, search])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model TtsVoice {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
|
||||||
|
ttsChatVoices TtsChatVoice[]
|
||||||
|
ttsVoiceStates TtsVoiceState[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model TtsChatVoice {
|
||||||
|
userId String
|
||||||
|
chatterId BigInt
|
||||||
|
ttsVoiceId String
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
voice TtsVoice @relation(fields: [ttsVoiceId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
@@id([userId, chatterId])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model TtsVoiceState {
|
||||||
|
userId String
|
||||||
|
ttsVoiceId String
|
||||||
|
state Boolean @default(true)
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
voice TtsVoice @relation(fields: [ttsVoiceId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
@@id([userId, ttsVoiceId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Chatter {
|
||||||
|
id BigInt
|
||||||
|
name String
|
||||||
|
ban DateTime @default(dbgenerated("'1970-01-01 00:00:00.000'"))
|
||||||
|
|
||||||
|
//history EmoteUsageHistory[]
|
||||||
|
|
||||||
|
@@id([id])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Emote {
|
||||||
|
id String
|
||||||
|
name String
|
||||||
|
|
||||||
|
//history EmoteUsageHistory[]
|
||||||
|
|
||||||
|
@@id([id])
|
||||||
|
}
|
||||||
|
|
||||||
|
model EmoteUsageHistory {
|
||||||
|
timestamp DateTime
|
||||||
|
broadcasterId BigInt
|
||||||
|
emoteId String
|
||||||
|
chatterId BigInt
|
||||||
|
|
||||||
|
//emote Emote @relation(fields: [emoteId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
//chatter Chatter @relation(fields: [chatterId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
@@id([timestamp, emoteId, chatterId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActionType {
|
||||||
|
WRITE_TO_FILE
|
||||||
|
APPEND_TO_FILE
|
||||||
|
AUDIO_FILE
|
||||||
|
OBS_TRANSFORM
|
||||||
|
}
|
||||||
|
|
||||||
|
model Action {
|
||||||
|
userId String
|
||||||
|
name String @unique
|
||||||
|
type ActionType
|
||||||
|
data Json
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([userId, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Redemption {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
userId String
|
||||||
|
redemptionId String
|
||||||
|
actionName String
|
||||||
|
order Int
|
||||||
|
state Boolean
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Quest {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
type Int
|
||||||
|
target Int
|
||||||
|
start DateTime
|
||||||
|
end DateTime
|
||||||
|
|
||||||
|
@@unique([type, start])
|
||||||
|
}
|
||||||
|
|
||||||
|
model QuestProgression {
|
||||||
|
chatterId BigInt
|
||||||
|
questId Int
|
||||||
|
counter Int
|
||||||
|
|
||||||
|
@@id([chatterId, questId])
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user