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 { db } from "@/lib/db"
|
||||
import { NextResponse } from "next/server";
|
||||
import fetchUser from '@/lib/fetch-user';
|
||||
import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
// Verify state against user id in user table.
|
||||
const key = await db.apiKey.findFirst({
|
||||
where: {
|
||||
id: req.headers.get('x-api-key') as string
|
||||
}
|
||||
})
|
||||
if (!key) {
|
||||
return new NextResponse("Forbidden", { status: 403 });
|
||||
const user = await fetchUserWithImpersonation(req)
|
||||
if (!user) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const connection = await db.twitchConnection.findFirst({
|
||||
where: {
|
||||
userId: key.userId
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
if (!connection) {
|
||||
@ -29,8 +27,22 @@ export async function GET(req: Request) {
|
||||
Authorization: 'OAuth ' + connection.accessToken
|
||||
}
|
||||
})).data;
|
||||
if (expires_in > 3600)
|
||||
return new NextResponse("", { status: 201 });
|
||||
|
||||
if (expires_in > 3600) {
|
||||
let data = await db.twitchConnection.findFirst({
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
|
||||
let dataFormatted = {
|
||||
user_id: user.id,
|
||||
access_token: data?.accessToken,
|
||||
refresh_token: data?.refreshToken,
|
||||
broadcaster_id: connection.broadcasterId
|
||||
}
|
||||
return NextResponse.json(dataFormatted, { status: 201 });
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
@ -51,14 +63,22 @@ export async function GET(req: Request) {
|
||||
|
||||
await db.twitchConnection.update({
|
||||
where: {
|
||||
userId: key.userId
|
||||
userId: user.id
|
||||
},
|
||||
data: {
|
||||
accessToken: access_token
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token
|
||||
}
|
||||
})
|
||||
|
||||
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) {
|
||||
console.log("[ACCOUNT]", error);
|
||||
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) {
|
||||
try {
|
||||
return NextResponse.json(await fetchUser(req))
|
||||
const user = await fetchUser(req)
|
||||
if (!user) return new NextResponse("Internal Error", { status: 401 })
|
||||
return NextResponse.json(user)
|
||||
} catch (error) {
|
||||
console.log("[ACCOUNT]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
|
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) {
|
||||
try {
|
||||
if (!voices) {
|
||||
return new NextResponse("Voices not available.", { status: 500 });
|
||||
}
|
||||
|
||||
const user = await fetchUserWithImpersonation(req)
|
||||
if (!user) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const u = await db.user.findFirst({
|
||||
where: {
|
||||
id: user.id
|
||||
}
|
||||
});
|
||||
|
||||
const voice = voices.find(v => v.value == new String(u?.ttsDefaultVoice))
|
||||
return NextResponse.json(voice);
|
||||
return NextResponse.json(user.ttsDefaultVoice);
|
||||
} catch (error) {
|
||||
console.log("[TTS/FILTER/DEFAULT]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
@ -26,28 +23,32 @@ export async function GET(req: Request) {
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
if (!voices) {
|
||||
return new NextResponse("Voices not available.", { status: 500 });
|
||||
}
|
||||
|
||||
const user = await fetchUserWithImpersonation(req)
|
||||
if (!user) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const { voice } = await req.json();
|
||||
if (!voice || !voices.map(v => v.label.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 });
|
||||
if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 });
|
||||
|
||||
const v = voices.find(v => v.label.toLowerCase() == voice.toLowerCase())
|
||||
const v = voices.find(v => v.toLowerCase() == voice.toLowerCase())
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
id: user.id
|
||||
},
|
||||
data: {
|
||||
ttsDefaultVoice: Number.parseInt(v.value)
|
||||
ttsDefaultVoice: v
|
||||
}
|
||||
});
|
||||
|
||||
return new NextResponse("", { status: 200 });
|
||||
} catch (error) {
|
||||
console.log("[TTS/FILTER/DEFAULT]", error);
|
||||
}
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ export async function POST(req: Request) {
|
||||
|
||||
const { search, replace } = await req.json();
|
||||
if (!search || search.length < 4 || search.length > 200) return new NextResponse("Bad Request", { status: 400 });
|
||||
if (!replace) return new NextResponse("Bad Request", { status: 400 });
|
||||
if (replace == null) return new NextResponse("Bad Request", { status: 400 });
|
||||
|
||||
const filter = await db.ttsWordFilter.create({
|
||||
data: {
|
||||
@ -58,7 +58,7 @@ export async function PUT(req: Request) {
|
||||
const { id, search, replace } = await req.json();
|
||||
if (!id || id.length < 1) return new NextResponse("Bad Request", { status: 400 });
|
||||
if (!search || search.length < 4 || search.length > 200) return new NextResponse("Bad Request", { status: 400 });
|
||||
if (!replace) return new NextResponse("Bad Request", { status: 400 });
|
||||
if (replace == null) return new NextResponse("Bad Request", { status: 400 });
|
||||
|
||||
const filter = await db.ttsWordFilter.update({
|
||||
where: {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { db } from "@/lib/db"
|
||||
import { NextResponse } from "next/server";
|
||||
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
|
||||
import voices from "@/data/tts";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
@ -10,23 +9,18 @@ export async function GET(req: Request) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
let list : {
|
||||
value: string;
|
||||
label: string;
|
||||
gender: string;
|
||||
language: string;
|
||||
}[] = []
|
||||
const enabled = user.ttsEnabledVoice
|
||||
for (let v of voices) {
|
||||
var n = Number.parseInt(v.value) - 1
|
||||
if ((enabled & (1 << n)) > 0) {
|
||||
list.push(v)
|
||||
}
|
||||
const voiceStates = await db.ttsVoiceState.findMany({
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(list);
|
||||
const voiceNames = await db.ttsVoice.findMany();
|
||||
const voiceNamesMapped: { [id: string]: string } = Object.assign({}, ...voiceNames.map(v => ({ [v.id]: v.name })))
|
||||
|
||||
return NextResponse.json(voiceStates.filter(v => v.state).map(v => voiceNamesMapped[v.ttsVoiceId]));
|
||||
} catch (error) {
|
||||
console.log("[TTS/FILTER/USER]", error);
|
||||
console.log("[TTS]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -38,15 +32,28 @@ export async function POST(req: Request) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
let { voice } = await req.json();
|
||||
voice = voice & ((1 << voices.length) - 1)
|
||||
const { voice, state }: { voice: string, state: boolean } = await req.json();
|
||||
|
||||
await db.user.update({
|
||||
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: {
|
||||
id: user.id
|
||||
userId_ttsVoiceId: {
|
||||
userId: user.id,
|
||||
ttsVoiceId: voiceIdsMapped[voice.toLowerCase()]
|
||||
}
|
||||
},
|
||||
data: {
|
||||
ttsEnabledVoice: voice
|
||||
update: {
|
||||
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 } }) {
|
||||
try {
|
||||
const user = await fetchUserWithImpersonation(req)
|
||||
|
||||
if (!user) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
@ -4,20 +4,24 @@ import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
console.log("ABC 1")
|
||||
const user = await fetchUserWithImpersonation(req);
|
||||
if (!user) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
console.log("ABC 2")
|
||||
|
||||
const api = await db.twitchConnection.findFirst({
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
console.log("ABC 3")
|
||||
if (!api) {
|
||||
return new NextResponse("Forbidden", { status: 403 });
|
||||
}
|
||||
|
||||
console.log("ABC 4")
|
||||
const data = {
|
||||
client_id: process.env.TWITCH_BOT_CLIENT_ID,
|
||||
client_secret: process.env.TWITCH_BOT_CLIENT_SECRET,
|
||||
@ -25,6 +29,7 @@ export async function GET(req: Request) {
|
||||
refresh_token: api.refreshToken,
|
||||
broadcaster_id: api.broadcasterId
|
||||
}
|
||||
console.log("ABC 5", data)
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.log("[TOKENS/GET]", error);
|
||||
|
@ -41,7 +41,7 @@ export async function DELETE(req: Request) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
let { id } = await req.json();
|
||||
const { id } = await req.json();
|
||||
if (!id) {
|
||||
return NextResponse.json(null)
|
||||
}
|
||||
@ -60,12 +60,12 @@ export async function DELETE(req: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
export function generateToken() {
|
||||
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
|
||||
var string_length = 32;
|
||||
var randomstring = '';
|
||||
for (var i = 0; i < string_length; i++) {
|
||||
var rnum = Math.floor(Math.random() * chars.length);
|
||||
function generateToken() {
|
||||
let chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
|
||||
let string_length = 32;
|
||||
let randomstring = '';
|
||||
for (let i = 0; i < string_length; i++) {
|
||||
let rnum = Math.floor(Math.random() * chars.length);
|
||||
randomstring += chars[rnum];
|
||||
}
|
||||
return randomstring;
|
||||
|
@ -1,22 +1,17 @@
|
||||
import fetchUser from "@/lib/fetch-user";
|
||||
import { db } from "@/lib/db"
|
||||
import { NextResponse } from "next/server";
|
||||
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
let userId = searchParams.get('userId')
|
||||
|
||||
if (userId == null) {
|
||||
const user = await fetchUser(req);
|
||||
if (user != null) {
|
||||
userId = user.id as string;
|
||||
}
|
||||
const user = await fetchUserWithImpersonation(req)
|
||||
if (!user) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const tokens = await db.apiKey.findMany({
|
||||
where: {
|
||||
userId: userId as string
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -24,7 +24,7 @@ export async function GET(req: Request) {
|
||||
return NextResponse.json(users)
|
||||
}
|
||||
if (id) {
|
||||
const users = await db.user.findUnique({
|
||||
const users = await db.user.findFirst({
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
@ -35,7 +35,7 @@ export async function GET(req: Request) {
|
||||
const users = await db.user.findMany();
|
||||
return NextResponse.json(users)
|
||||
} catch (error) {
|
||||
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
|
||||
console.log("[USERS]", error);
|
||||
return new NextResponse("Internal Error", { status: 500 });
|
||||
}
|
||||
}
|
@ -3,63 +3,46 @@
|
||||
import axios from "axios";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import * as React from 'react';
|
||||
import { ApiKey, User } from "@prisma/client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
const [apiKeyViewable, setApiKeyViewable] = useState(0)
|
||||
const [apiKeyChanges, setApiKeyChanges] = useState(0)
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
|
||||
const ApiKeyPage = () => {
|
||||
const [apiKeyViewable, setApiKeyViewable] = useState<number>(-1)
|
||||
const [apiKeys, setApiKeys] = useState<{ id: string, label: string, userId: string }[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const keys = (await axios.get("/api/tokens")).data ?? {};
|
||||
setApiKeys(keys)
|
||||
} catch (error) {
|
||||
console.log("ERROR", error)
|
||||
}
|
||||
await axios.get("/api/tokens")
|
||||
.then(d => setApiKeys(d.data ?? []))
|
||||
.catch(console.error)
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [apiKeyChanges]);
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const onApiKeyAdd = async () => {
|
||||
try {
|
||||
await axios.post("/api/token", {
|
||||
label: "Key label"
|
||||
});
|
||||
setApiKeyChanges(apiKeyChanges + 1)
|
||||
} catch (error) {
|
||||
console.log("ERROR", error)
|
||||
}
|
||||
const onApiKeyAdd = async (label: string) => {
|
||||
await axios.post("/api/token", { label })
|
||||
.then(d => setApiKeys(apiKeys.concat([d.data])))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const onApiKeyDelete = async (id: string) => {
|
||||
try {
|
||||
await axios.delete("/api/token/" + id);
|
||||
setApiKeyChanges(apiKeyChanges - 1)
|
||||
} catch (error) {
|
||||
console.log("ERROR", error)
|
||||
}
|
||||
await axios.delete("/api/token/" + id)
|
||||
.then((d) => setApiKeys(apiKeys.filter(k => k.id != d.data.id)))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="px-10 py-5 mx-5 my-10">
|
||||
<div>
|
||||
<div className="text-xl justify-left mt-10">API Keys</div>
|
||||
<Table className="max-w-2xl">
|
||||
<div className="text-xl justify-left mt-10 text-center">API Keys</div>
|
||||
<Table>
|
||||
<TableCaption>A list of your secret API keys.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Label</TableHead>
|
||||
<TableHead>Token</TableHead>
|
||||
<TableHead>View</TableHead>
|
||||
<TableHead>Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@ -67,20 +50,20 @@ const SettingsPage = () => {
|
||||
{apiKeys.map((key, index) => (
|
||||
<TableRow key={key.id}>
|
||||
<TableCell className="font-medium">{key.label}</TableCell>
|
||||
<TableCell>{(apiKeyViewable & (1 << index)) > 0 ? key.id : "*".repeat(key.id.length)}</TableCell>
|
||||
<TableCell>{apiKeyViewable == index ? key.id : "*".repeat(key.id.length)}</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={() => setApiKeyViewable((v) => v ^ (1 << index))}>
|
||||
{(apiKeyViewable & (1 << index)) > 0 ? "HIDE" : "VIEW"}
|
||||
<Button onClick={() => setApiKeyViewable((v) => v != index ? index : -1)}>
|
||||
{apiKeyViewable == index ? "HIDE" : "VIEW"}
|
||||
</Button>
|
||||
<Button onClick={() => onApiKeyDelete(key.id)} className="ml-[10px] bg-red-500 hover:bg-red-700">DELETE</Button>
|
||||
</TableCell>
|
||||
<TableCell><Button onClick={() => onApiKeyDelete(key.id)}>DEL</Button></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow key="ADD">
|
||||
<TableCell className="font-medium"></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell><Button onClick={onApiKeyAdd}>ADD</Button></TableCell>
|
||||
<TableCell><Button onClick={() => onApiKeyAdd("Key label")}>ADD</Button></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@ -90,4 +73,4 @@ const SettingsPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsPage;
|
||||
export default ApiKeyPage;
|
@ -10,7 +10,7 @@ import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const SettingsPage = () => {
|
||||
const ConnectionsPage = () => {
|
||||
const { data: session, status } = useSession();
|
||||
const [previousUsername, setPreviousUsername] = useState<string>()
|
||||
const [userId, setUserId] = useState<string>()
|
||||
@ -24,7 +24,7 @@ const SettingsPage = () => {
|
||||
setPreviousUsername(session.user?.name as string)
|
||||
if (session.user?.name) {
|
||||
const fetchData = async () => {
|
||||
var connection: User = (await axios.get("/api/account")).data
|
||||
let connection: User = (await axios.get("/api/account")).data
|
||||
setUserId(connection.id)
|
||||
setLoading(false)
|
||||
}
|
||||
@ -36,7 +36,7 @@ const SettingsPage = () => {
|
||||
const [twitchUser, setTwitchUser] = useState<TwitchConnection | null>(null)
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
var connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data
|
||||
let connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data
|
||||
setTwitchUser(connection)
|
||||
}
|
||||
|
||||
@ -97,4 +97,4 @@ const SettingsPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsPage;
|
||||
export default ConnectionsPage;
|
@ -3,8 +3,11 @@ import { cn } from "@/lib/utils";
|
||||
import { headers } from 'next/headers';
|
||||
import React from "react";
|
||||
|
||||
const SettingsLayout = async (
|
||||
{ children } : { children:React.ReactNode } ) => {
|
||||
const SettingsLayout = async ({
|
||||
children
|
||||
} : {
|
||||
children:React.ReactNode
|
||||
} ) => {
|
||||
const headersList = headers();
|
||||
const header_url = headersList.get('x-url') || "";
|
||||
|
||||
@ -14,7 +17,7 @@ const SettingsLayout = async (
|
||||
header_url.endsWith("/settings") && "flex h-full w-full md:w-[250px] z-30 flex-col fixed inset-y-0")}>
|
||||
<SettingsNavigation />
|
||||
</div>
|
||||
<main className={cn("md:pl-[250px] h-full", header_url.endsWith("/settings") && "hidden")}>
|
||||
<main className={"md:pl-[250px] h-full"}>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
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;
|
@ -3,10 +3,8 @@
|
||||
import axios from "axios";
|
||||
import * as React from 'react';
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
@ -16,49 +14,50 @@ import voices from "@/data/tts";
|
||||
import InfoNotice from "@/components/elements/info-notice";
|
||||
|
||||
const TTSVoiceFiltersPage = () => {
|
||||
const { data: session, status } = useSession();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [value, setValue] = useState(0)
|
||||
const [enabled, setEnabled] = useState(0)
|
||||
const [defaultVoice, setDefaultVoice] = useState("")
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
function enabledVoicesReducer(enabledVoices: { [voice: string]: boolean }, action: { type: string, value: string }) {
|
||||
if (action.type == "enable") {
|
||||
return { ...enabledVoices, [action.value]: true }
|
||||
} else if (action.type == "disable") {
|
||||
return { ...enabledVoices, [action.value]: false }
|
||||
}
|
||||
return enabledVoices
|
||||
}
|
||||
|
||||
const [enabledVoices, dispatchEnabledVoices] = useReducer(enabledVoicesReducer, Object.assign({}, ...voices.map(v => ({[v]: false}) )))
|
||||
|
||||
useEffect(() => {
|
||||
axios.get("/api/settings/tts/default")
|
||||
.then((voice) => {
|
||||
setValue(Number.parseInt(voice.data.value))
|
||||
setDefaultVoice(voice.data)
|
||||
})
|
||||
|
||||
axios.get("/api/settings/tts")
|
||||
.then((d) => {
|
||||
const total = d.data.reduce((acc: number, item: {value: number, label: string, gender: string, language: string}) => acc |= 1 << (item.value - 1), 0)
|
||||
setEnabled(total)
|
||||
const data: string[] = d.data;
|
||||
data.forEach(d => dispatchEnabledVoices({ type: "enable", value: d }))
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onDefaultChange = (voice: string) => {
|
||||
try {
|
||||
axios.post("/api/settings/tts/default", { voice })
|
||||
.then(d => {
|
||||
console.log(d)
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
} catch (error) {
|
||||
console.log("[TTS/DEFAULT]", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const onEnabledChanged = (val: number) => {
|
||||
const onEnabledChanged = (voice: string, state: boolean) => {
|
||||
try {
|
||||
axios.post("/api/settings/tts", { voice: val })
|
||||
.then(d => {
|
||||
console.log(d)
|
||||
})
|
||||
axios.post("/api/settings/tts", { voice: voice, state: state })
|
||||
.catch(e => console.error(e))
|
||||
} catch (error) {
|
||||
console.log("[TTS]", error);
|
||||
return;
|
||||
console.log("[TTS/ENABLED]", error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +77,7 @@ const TTSVoiceFiltersPage = () => {
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-[200px] justify-between">
|
||||
{value ? voices.find(v => Number.parseInt(v.value) == value)?.label : "Select voice..."}
|
||||
{defaultVoice ? voices.find(v => v == defaultVoice) : "Select voice..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@ -89,20 +88,20 @@ const TTSVoiceFiltersPage = () => {
|
||||
<CommandGroup>
|
||||
{voices.map((voice) => (
|
||||
<CommandItem
|
||||
key={voice.value + "-" + voice.label}
|
||||
value={voice.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(Number.parseInt(currentValue))
|
||||
onDefaultChange(voice.label)
|
||||
key={voice}
|
||||
value={voice}
|
||||
onSelect={(currentVoice) => {
|
||||
setDefaultVoice(voice)
|
||||
onDefaultChange(voice)
|
||||
setOpen(false)
|
||||
}}>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === Number.parseInt(voice.value) ? "opacity-100" : "opacity-0"
|
||||
defaultVoice === voice ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{voice.label}
|
||||
{voice}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
@ -116,14 +115,14 @@ const TTSVoiceFiltersPage = () => {
|
||||
<InfoNotice message="Voices can be disabled from being used. Default voice will always work." hidden={false} />
|
||||
<div className="grid grid-cols-4 grid-flow-row gap-4 pt-[20px]">
|
||||
{voices.map((v, i) => (
|
||||
<div key={v.label + "-enabled"} className="h-[30px] row-span-1 col-span-1 align-middle flex items-center justify-center">
|
||||
<div key={v + "-enabled"} className="h-[30px] row-span-1 col-span-1 align-middle flex items-center justify-center">
|
||||
<Checkbox onClick={() => {
|
||||
const newVal = enabled ^ (1 << (Number.parseInt(v.value) - 1))
|
||||
setEnabled(newVal)
|
||||
onEnabledChanged(newVal)
|
||||
dispatchEnabledVoices({ type: enabledVoices[v] ? "disable" : "enable", value: v })
|
||||
onEnabledChanged(v, !enabledVoices[v])
|
||||
}}
|
||||
checked={(enabled & (1 << (Number.parseInt(v.value) - 1))) > 0} />
|
||||
<div className="pl-[5px]">{v.label}</div>
|
||||
disabled={loading}
|
||||
checked={enabledVoices[v]} />
|
||||
<div className="pl-[5px]">{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
58
app/socket/page.tsx
Normal file
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 [impersonation, setImpersonation] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async (userId: string | undefined) => {
|
||||
if (!userId) return
|
||||
|
||||
await axios.get<User>("/api/users?id=" + userId)
|
||||
.then(u => {
|
||||
setImpersonation(u.data?.name)
|
||||
})
|
||||
await axios.get("/api/users?id=" + userId)
|
||||
.then(u => setImpersonation(u.data?.name))
|
||||
}
|
||||
|
||||
console.log(session)
|
||||
fetch(session?.data?.user?.impersonation?.id)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
await axios.get<User[]>("/api/users")
|
||||
.then((u) => {
|
||||
@ -41,12 +33,14 @@ const AdminProfile = () => {
|
||||
})
|
||||
}
|
||||
|
||||
fetch(session?.data?.user?.impersonation?.id)
|
||||
fetchUsers()
|
||||
}, [])
|
||||
|
||||
const onImpersonationChange = async (userId: string, name: string) => {
|
||||
console.log("IMPERSONATION", impersonation)
|
||||
if (impersonation) {
|
||||
if (impersonation == session.data?.user.impersonation?.name) {
|
||||
if (impersonation == name) {
|
||||
await axios.delete("/api/account/impersonate")
|
||||
.then(() => {
|
||||
setImpersonation(null)
|
||||
|
@ -47,6 +47,17 @@ const SettingsNavigation = async () => {
|
||||
</Link>
|
||||
</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">
|
||||
API
|
||||
</li>
|
||||
|
@ -22,7 +22,7 @@ const UserProfile = () => {
|
||||
const fetchData = async () => {
|
||||
if (user) return
|
||||
|
||||
var userData = (await axios.get("/api/account")).data
|
||||
let userData = (await axios.get("/api/account")).data
|
||||
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 = [
|
||||
{
|
||||
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)
|
||||
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()
|
||||
|
||||
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')
|
||||
if (token === null || token === undefined)
|
||||
if (!token)
|
||||
return null
|
||||
|
||||
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)
|
||||
}
|
||||
@ -35,6 +36,7 @@ const fetch = async (userId: string) => {
|
||||
|
||||
if (!user) return null
|
||||
|
||||
// Only admins can impersonate others.
|
||||
if (user.role == "ADMIN") {
|
||||
const impersonation = await db.impersonation.findFirst({
|
||||
where: {
|
||||
|
@ -12,7 +12,7 @@ export default async function fetchUser(req: Request) {
|
||||
}
|
||||
|
||||
const token = req.headers?.get('x-api-key')
|
||||
if (token === null || token === undefined)
|
||||
if (!token)
|
||||
return null
|
||||
|
||||
const key = await db.apiKey.findFirst({
|
||||
@ -38,6 +38,6 @@ const fetch = async (userId: string) => {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.name,
|
||||
role: user.role
|
||||
role: user.role,
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
output: 'standalone',
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
|
@ -2,10 +2,15 @@ generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
// datasource db {
|
||||
// provider = "mysql"
|
||||
// url = env("DATABASE_URL")
|
||||
// relationMode = "prisma"
|
||||
// }
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
relationMode = "prisma"
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
@ -20,8 +25,7 @@ model User {
|
||||
emailVerified DateTime?
|
||||
role UserRole @default(USER)
|
||||
image String?
|
||||
ttsDefaultVoice Int @default(1)
|
||||
ttsEnabledVoice Int @default(1048575)
|
||||
ttsDefaultVoice String @default("Brian")
|
||||
|
||||
impersonationSources Impersonation[] @relation(name: "impersonationSources")
|
||||
impersonationTargets Impersonation[] @relation(name: "impersonationTargets")
|
||||
@ -31,6 +35,10 @@ model User {
|
||||
twitchConnections TwitchConnection[]
|
||||
ttsUsernameFilter TtsUsernameFilter[]
|
||||
ttsWordFilter TtsWordFilter[]
|
||||
ttsChatVoices TtsChatVoice[]
|
||||
ttsVoiceStates TtsVoiceState[]
|
||||
actions Action[]
|
||||
redemptions Redemption[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@ -50,18 +58,17 @@ model Account {
|
||||
id_token String? @db.Text
|
||||
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])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Impersonation {
|
||||
sourceId String
|
||||
targetId String
|
||||
|
||||
source User @relation(name: "impersonationSources", fields: [sourceId], references: [id], onDelete: Cascade)
|
||||
target User @relation(name: "impersonationTargets", fields: [targetId], 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, onUpdate: Cascade)
|
||||
|
||||
@@id([sourceId])
|
||||
@@index([sourceId])
|
||||
@ -73,9 +80,7 @@ model ApiKey {
|
||||
label String
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
}
|
||||
|
||||
model TwitchConnection {
|
||||
@ -84,9 +89,7 @@ model TwitchConnection {
|
||||
refreshToken String
|
||||
|
||||
userId String @id
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
}
|
||||
|
||||
model TtsUsernameFilter {
|
||||
@ -94,9 +97,8 @@ model TtsUsernameFilter {
|
||||
tag 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])
|
||||
}
|
||||
|
||||
@ -106,8 +108,115 @@ model TtsWordFilter {
|
||||
replace 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])
|
||||
}
|
||||
|
||||
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