+ //
+ //
+ // {userTags.map((user, index) => (
+ //
+ //
+ //
+ // {user.tag}
+ //
+ // {user.username}
+ //
+ //
0} onOpenChange={() => setMoreOpen(v => v ^ (1 << index))}>
+ //
+ //
+ //
+ //
+ // Actions
+ //
+ //
+ //
+ //
+ // Apply label
+ //
+ //
+ //
+ //
+ //
+ // No label found.
+ //
+ // {tags.map((tag) => (
+ // {
+ // onAddExtended({ username: userTags[index].username, tag: value}, false)
+ // setMoreOpen(0)
+ // }}
+ // >
+ // {tag}
+ //
+ // ))}
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // Delete
+ //
+ //
+ //
+ //
+ //
+ // ))}
+ //
+ //
+ //
+
+ );
+}
+
+export default TTSFiltersPage;
\ No newline at end of file
diff --git a/app/settings/tts/voices/page.tsx b/app/(protected)/settings/tts/voices/page.tsx
similarity index 100%
rename from app/settings/tts/voices/page.tsx
rename to app/(protected)/settings/tts/voices/page.tsx
diff --git a/app/api/account/authorize/route.ts b/app/api/account/authorize/route.ts
index 9146688..6d73447 100644
--- a/app/api/account/authorize/route.ts
+++ b/app/api/account/authorize/route.ts
@@ -15,22 +15,22 @@ export async function GET(req: Request) {
// Verify state against user id in user table.
const user = await db.user.findFirst({
- where: {
- id: state
- }
+ where: {
+ id: state
+ }
})
if (!user) {
- return new NextResponse("Bad Request", { status: 400 });
+ return new NextResponse("Bad Request", { status: 400 });
}
// 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", {
+ 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,
code: code,
grant_type: "authorization_code",
- redirect_uri: "https://hermes.goblincaves.com/api/account/authorize"
+ redirect_uri: "https://tomtospeech.com/api/account/authorize"
})).data
// Fetch values from token.
@@ -49,17 +49,17 @@ export async function GET(req: Request) {
const broadcasterId = info.data.data[0]['id']
await db.twitchConnection.create({
- data: {
- broadcasterId: broadcasterId,
- accessToken: access_token,
- refreshToken: refresh_token,
- userId: state
- }
+ data: {
+ broadcasterId: broadcasterId,
+ accessToken: access_token,
+ refreshToken: refresh_token,
+ userId: state
+ }
})
-
+
return new NextResponse("", { status: 200 });
} catch (error) {
- console.log("[ACCOUNT]", error);
+ console.log("[ACCOUNT/AUTHORIZE]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
\ No newline at end of file
diff --git a/app/api/account/reauthorize/route.ts b/app/api/account/reauthorize/route.ts
index b9f9d93..4669731 100644
--- a/app/api/account/reauthorize/route.ts
+++ b/app/api/account/reauthorize/route.ts
@@ -39,7 +39,8 @@ export async function GET(req: Request) {
user_id: user.id,
access_token: data?.accessToken,
refresh_token: data?.refreshToken,
- broadcaster_id: connection.broadcasterId
+ broadcaster_id: connection.broadcasterId,
+ expires_in
}
return NextResponse.json(dataFormatted, { status: 201 });
}
@@ -75,7 +76,8 @@ export async function GET(req: Request) {
user_id: user.id,
access_token,
refresh_token,
- broadcaster_id: connection.broadcasterId
+ broadcaster_id: connection.broadcasterId,
+ expires_in
}
return NextResponse.json(data)
diff --git a/app/api/account/redemptions/route.ts b/app/api/account/redemptions/route.ts
index 6ca7b51..ae30834 100644
--- a/app/api/account/redemptions/route.ts
+++ b/app/api/account/redemptions/route.ts
@@ -18,16 +18,21 @@ export async function GET(req: Request) {
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
+ try {
+ 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);
+ return NextResponse.json(redemptions.data);
+ } catch (error: any) {
+ console.error('Fetching Twitch channel redemptions:', error.response.data)
+ }
+ return NextResponse.json([]);
} catch (error) {
console.log("[REDEMPTIONS/ACTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
diff --git a/app/api/account/route.ts b/app/api/account/route.ts
index 0d31abb..b7f1eb5 100644
--- a/app/api/account/route.ts
+++ b/app/api/account/route.ts
@@ -6,9 +6,16 @@ import fetchUser from "@/lib/fetch-user";
export async function GET(req: Request) {
try {
- const user = await fetchUser(req)
- if (!user) return new NextResponse("Internal Error", { status: 401 })
- return NextResponse.json(user)
+ const user = await fetchUser(req)
+ if (!user) return new NextResponse("Internal Error", { status: 401 })
+
+ const account = await db.account.findFirst({
+ where: {
+ userId: user.id
+ }
+ });
+
+ return NextResponse.json({ ... user, broadcasterId: account?.providerAccountId })
} catch (error) {
console.log("[ACCOUNT]", error);
return new NextResponse("Internal Error", { status: 500 });
@@ -22,29 +29,29 @@ export async function POST(req: Request) {
if (!user) {
return new NextResponse("Internal Error", { status: 401 })
}
-
+
const exist = await db.user.findFirst({
- where: {
- name: user
- }
+ where: {
+ name: user
+ }
});
-
+
if (exist) {
return NextResponse.json({
- id: exist.id,
- username: exist.name
+ id: exist.id,
+ username: exist.name
});
}
-
+
const newUser = await db.user.create({
- data: {
- name: user,
- }
+ data: {
+ name: user,
+ }
});
return NextResponse.json({
- id: newUser.id,
- username: newUser.name
+ id: newUser.id,
+ username: newUser.name
});
} catch (error) {
console.log("[ACCOUNT]", error);
diff --git a/app/api/connection/authorize/route.ts b/app/api/connection/authorize/route.ts
new file mode 100644
index 0000000..ee7b273
--- /dev/null
+++ b/app/api/connection/authorize/route.ts
@@ -0,0 +1,92 @@
+import { db } from "@/lib/db"
+import { NextResponse } from "next/server";
+import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation';
+import axios from "axios";
+
+export async function POST(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req);
+ if (!user)
+ return NextResponse.json({ error: null, message: "Unauthorized", success: false }, { status: 401 });
+
+ let { access_token, expires_in, token_type, scope, state } = await req.json();
+
+ if (!token_type)
+ return NextResponse.json({ error: null, message: 'No token type given for the authorization.', success: false }, { status: 400 })
+
+ if (!access_token)
+ return NextResponse.json({ error: null, message: 'No access token given for the authorization.', success: false }, { status: 400 })
+
+ if (!scope)
+ return NextResponse.json({ error: null, message: 'No scope given for the authorization.', success: false }, { status: 400 })
+
+ if (!state)
+ return NextResponse.json({ error: null, message: 'No state given for the authorization.', success: false }, { status: 400 })
+
+ // Fetch connection state data
+ const info = await db.connectionState.findUnique({
+ where: {
+ state: state
+ }
+ })
+ if (!info)
+ return NextResponse.json({ error: null, message: 'No authorization code was received previously.', success: false }, { status: 400 })
+
+ if (info.type == "twitch") {
+ const response = await axios.get("https://id.twitch.tv/oauth2/validate", {
+ headers: {
+ Authorization: 'OAuth ' + access_token
+ }
+ })
+ expires_in = response.data.expires_in
+ }
+ if (!expires_in)
+ return NextResponse.json({ error: null, message: 'No expiration given for the authorization.', success: false }, { status: 400 })
+
+ let expiration = new Date()
+ expiration.setSeconds(expiration.getSeconds() + parseInt(expires_in) - 600);
+
+ await db.connection.upsert({
+ where: {
+ userId_name: {
+ userId: info.userId,
+ name: info.name
+ }
+ },
+ create: {
+ userId: info.userId,
+ name: info.name,
+ type: info.type,
+ clientId: info.clientId,
+ accessToken: access_token,
+ scope,
+ grantType: token_type,
+ expiresAt: expiration
+ },
+ update: {
+ clientId: info.clientId,
+ accessToken: access_token,
+ scope,
+ grantType: token_type,
+ expiresAt: expiration
+ }
+ })
+
+ await db.connectionState.delete({
+ where: {
+ userId_name: {
+ userId: user.id,
+ name: info.name
+ }
+ }
+ })
+
+ return NextResponse.json({ error: null, message: "", success: true }, { status: 200 });
+ } catch (error: any) {
+ if (error.name == 'PrismaClientKnownRequestError') {
+ if (error.code == 'P2002')
+ return NextResponse.json({ error, message: "Connection already saved.", success: false }, { status: 500 });
+ }
+ return NextResponse.json({ error, message: "Failed to save connection", success: false }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/connection/default/route.ts b/app/api/connection/default/route.ts
new file mode 100644
index 0000000..c66f121
--- /dev/null
+++ b/app/api/connection/default/route.ts
@@ -0,0 +1,75 @@
+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 NextResponse.json({ error: null, message: "Unauthorized", success: false }, { status: 401 });
+ }
+
+ const data = await db.connection.findMany({
+ where: {
+ userId: user.id,
+ default: true
+ }
+ })
+
+ return NextResponse.json({ error: null, message: "", success: true, data }, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json({ error, message: "Failed to get default connection", success: false }, { status: 500 });
+ }
+}
+
+export async function PUT(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req);
+ if (!user) {
+ return NextResponse.json({ error: null, message: "Unauthorized", success: false }, { status: 401 });
+ }
+
+ const { name } = await req.json();
+ if (!name)
+ return NextResponse.json({ error: null, message: 'Requires "name" param to be passed in - name of the connection.', success: false }, { status: 400 })
+
+ const connection = await db.connection.findFirst({
+ where: {
+ userId: user.id,
+ name
+ }
+ })
+
+ if (!connection) {
+ return NextResponse.json({ error: null, message: 'Connection with that name does not exist.', success: false }, { status: 400 })
+ }
+
+ await db.connection.updateMany({
+ where: {
+ userId: user.id,
+ type: connection.type,
+ default: true
+ },
+ data: {
+ default: false
+ }
+ })
+
+ const data = await db.connection.update({
+ where: {
+ userId_name: {
+ userId: user.id,
+ name,
+ }
+ },
+ data: {
+ default: true as boolean
+ }
+ })
+
+ return NextResponse.json({ error: null, message: "", success: true, data }, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json({ error, message: "Failed to update default connection", success: false }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/connection/prepare/route.ts b/app/api/connection/prepare/route.ts
new file mode 100644
index 0000000..cebdcc3
--- /dev/null
+++ b/app/api/connection/prepare/route.ts
@@ -0,0 +1,59 @@
+import { db } from "@/lib/db"
+import { NextResponse } from "next/server";
+import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation';
+
+export async function POST(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req);
+ if (!user) {
+ return NextResponse.json({ error: null, message: "Unauthorized", success: false }, { status: 401 });
+ }
+
+ let { state, name, type, grantType, clientId } = await req.json();
+
+ if (!clientId)
+ return NextResponse.json({ error: null, message: 'No client id given for the authorization.', success: false }, { status: 400 })
+
+ if (!type)
+ return NextResponse.json({ error: null, message: 'No type given for the authorization.', success: false }, { status: 400 })
+
+ if (!state)
+ return NextResponse.json({ error: null, message: 'No state given for the authorization.', success: false }, { status: 400 })
+
+ if (!name)
+ return NextResponse.json({ error: null, message: 'No name given for the authorization.', success: false }, { status: 400 })
+
+ if (!grantType)
+ return NextResponse.json({ error: null, message: 'No grant type given for the authorization.', success: false }, { status: 400 })
+
+ await db.connectionState.upsert({
+ where: {
+ userId_name: {
+ userId: user.id,
+ name
+ }
+ },
+ create: {
+ userId: user.id,
+ name,
+ type,
+ state,
+ grantType,
+ clientId
+ },
+ update: {
+ name,
+ state,
+ grantType,
+ }
+ })
+
+ return NextResponse.json({ error: null, message: "", success: true }, { status: 200 });
+ } catch (error: any) {
+ if (error.name == 'PrismaClientKnownRequestError') {
+ if (error.code == 'P2002')
+ return NextResponse.json({ error, message: "Connection already prepared.", success: false }, { status: 500 });
+ }
+ return NextResponse.json({ error, message: "Failed to prepare connection", success: false }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/connection/route.ts b/app/api/connection/route.ts
new file mode 100644
index 0000000..f265f9d
--- /dev/null
+++ b/app/api/connection/route.ts
@@ -0,0 +1,74 @@
+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 NextResponse.json({ error: null, message: "Unauthorized", success: false }, { status: 401 });
+ }
+
+ //const { searchParams } = new URL(req.url)
+ const data = await db.connection.findMany({
+ where: {
+ userId: user.id
+ }
+ })
+
+ return NextResponse.json({ error: null, message: "", success: true, data }, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json({ error, message: "Failed to fetch connections", success: false }, { status: 500 });
+ }
+}
+
+export async function DELETE(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req);
+ if (!user) {
+ return NextResponse.json({ error: null, message: "Unauthorized", success: false }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(req.url)
+ const name = searchParams.get('name') as string
+
+ if (!name)
+ return NextResponse.json({ error: null, message: 'Requires "name" param to be passed in - name of the service.', success: false }, { status: 400 })
+
+ const data = await db.connection.delete({
+ where: {
+ userId_name: {
+ userId: user.id,
+ name: name
+ }
+ }
+ })
+
+ const connections = await db.connection.findMany({
+ where: {
+ userId: user.id,
+ type: data.type
+ }
+ })
+
+ if (connections.length > 0 && connections.every(c => !c.default)) {
+ const connectionName = connections[0].name
+ await db.connection.update({
+ where: {
+ userId_name: {
+ userId: user.id,
+ name: connectionName
+ }
+ },
+ data: {
+ default: true
+ }
+ })
+ }
+
+ return NextResponse.json({ error: null, message: "", success: true, data }, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json({ error, message: "Failed to delete connection", success: false }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/info/version/route.ts b/app/api/info/version/route.ts
index aead9be..f35b1cb 100644
--- a/app/api/info/version/route.ts
+++ b/app/api/info/version/route.ts
@@ -2,10 +2,22 @@ import { NextResponse } from "next/server";
export async function GET(req: Request) {
return NextResponse.json({
- major_version: 3,
+ major_version: 4,
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."
+ download: "https://drive.proton.me/urls/YH86153EWM#W6VTyaoAVHKP",
+ changelog: "Reconnecting should be fixed.\nNew TTS messages when queue is empty will be played faster, up to 200 ms.\nRemoved subscriptions errors when reconnecting on Twitch\nAdded !refresh connections"
+
+ //changelog: "When using multiple voices (ex: brian: hello amy: world), TTS messages are now merged as a single TTS message.\nNightbot integration\nTwitch authentication changed. Need to refresh connection every 30-60 days.\nFixed raid spam, probably."
+ //changelog: "Added raid spam prevention (lasts 30 seconds; works on joined chats as well).\nAdded permission check for chat messages with bits."
+ //changelog: "Fixed group permissions.\nRemoved default permissions.\nSome commands may have additional permission requirements, which are more strict.\nAdded support for redeemable actions via adbreak, follow, subscription.\nMessage deletion and bans automatically remove TTS messages from queue and playing.\nAdded support to read TTS from multiple chats via !tts join.\nFixed some reconnection problems."
+ //changelog: "Fixed several requests for data.\nRe-did the command system.\nAdded !skip all (!skipall still works)"
+ //changelog: "Fixed initial TTS voice change.\nRandom TTS voice ensures a different voice.\nBetter connection handling."
+ //changelog: "Fixed permissions.\nInternal changes."
+ //changelog: "Added groups (custom + subscribers/vip/moderators/etc).\nAdded group permissions.\nFixed 7tv reconnection.\nNew website link. Old one will be on July 19, 2024.\nAdded !refresh permissions & !refresh username_filters"
+ //changelog: "Fixed toggle OBS scene item visibility. Added more logs. Added version data to login."
+ //changelog: "Added ability to change or toggle OBS visibility via actions.\nAdded sleep via actions.\nAdded ability to change OBS index."
+ //changelog: "Fix OBS crashing the app.\nAdded '!refresh redemptions' command.\nAdded TTS voice randomizer and specifc TTS voice redemptions."
+ //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
- Used to refresh data if done via website.\nAdded new command for mods: !tts - 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
diff --git a/app/api/settings/groups/chatters/route.ts b/app/api/settings/groups/chatters/route.ts
new file mode 100644
index 0000000..be43191
--- /dev/null
+++ b/app/api/settings/groups/chatters/route.ts
@@ -0,0 +1,127 @@
+import { db } from "@/lib/db"
+import { NextResponse } from "next/server";
+import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
+import axios from "axios";
+import { env } from "process";
+import { TwitchUpdateAuthorization } from "@/lib/twitch";
+
+export async function GET(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req)
+ if (!user)
+ return new NextResponse("Unauthorized", { status: 401 });
+
+ const { searchParams } = new URL(req.url)
+ const groupId = searchParams.get('groupId') as string
+ const pageString = searchParams.get('page') as string
+ const search = searchParams.get('search') as string
+
+ if (!groupId && search != 'all')
+ return new NextResponse("Bad Request", { status: 400 })
+
+ let page = parseInt(pageString)
+ if (isNaN(page) || page === undefined || page === null)
+ page = 0
+
+ let chatters: { userId: string, groupId: string, chatterId: bigint, chatterLabel: string }[]
+
+ if (search != 'all')
+ chatters = await db.chatterGroup.findMany({
+ where: {
+ userId: user.id,
+ groupId
+ }
+ })
+ else
+ chatters = await db.chatterGroup.findMany({
+ where: {
+ userId: user.id
+ }
+ })
+
+ const paginated = search == 'all' ? chatters : chatters.slice(page * 50, (page + 1) * 50)
+ if (!paginated || paginated.length == 0) {
+ console.log('No users returned from db')
+ return NextResponse.json([])
+ }
+
+ const ids = chatters.map(c => c.chatterId)
+ const idsString = 'id=' + ids.map(i => i.toString()).reduce((a, b) => a + '&id=' + b)
+
+ const auth = await TwitchUpdateAuthorization(user.id)
+ if (!auth) {
+ return new NextResponse("", { status: 403 })
+ }
+
+ const users = await axios.get("https://api.twitch.tv/helix/users?" + idsString, {
+ headers: {
+ "Authorization": "Bearer " + auth.access_token,
+ "Client-Id": env.TWITCH_BOT_CLIENT_ID
+ }
+ })
+
+ if (!users) {
+ return new NextResponse("", { status: 400 })
+ }
+
+ if (users.data.data.length == 0) {
+ console.log('No users returned from twitch')
+ return NextResponse.json([])
+ }
+
+ return NextResponse.json(users.data.data.map((u: any) => ({ id: u.id, username: u.login })));
+ } catch (error) {
+ console.log("[GROUPS/USERS]", 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 { groupId, users }: { groupId: string, users: { id: number, username: string }[] } = await req.json();
+ if (!groupId || !users)
+ return new NextResponse("Bad Request", { status: 400 });
+
+ const chatters = await db.chatterGroup.createMany({
+ data: users.map(u => ({ userId: user.id, chatterId: u.id, groupId, chatterLabel: u.username }))
+ });
+
+ return NextResponse.json(chatters, { status: 200 });
+ } catch (error) {
+ console.log("[GROUPS/USERS]", 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 groupId = searchParams.get('groupId') as string
+ const ids = searchParams.get('ids') as string
+ if (!groupId || !ids)
+ return new NextResponse("Bad Request", { status: 400 });
+
+ const chatters = await db.chatterGroup.deleteMany({
+ where: {
+ userId: user.id,
+ chatterId: {
+ in: ids.split(',').map(i => parseInt(i)).filter(i => !i || !isNaN(i))
+ },
+ groupId
+ }
+ })
+
+ return NextResponse.json(chatters);
+ } catch (error) {
+ console.log("[GROUPS/USERS]", error);
+ return new NextResponse("Internal Error", { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/settings/groups/permissions/route.ts b/app/api/settings/groups/permissions/route.ts
new file mode 100644
index 0000000..53bb50e
--- /dev/null
+++ b/app/api/settings/groups/permissions/route.ts
@@ -0,0 +1,101 @@
+import { db } from "@/lib/db"
+import { NextResponse } from "next/server";
+import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
+import { ActionType, Prisma } from "@prisma/client";
+
+export async function GET(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req)
+ if (!user)
+ return new NextResponse("Unauthorized", { status: 401 });
+
+ const commands = await db.groupPermission.findMany({
+ where: {
+ userId: user.id
+ }
+ })
+
+ return NextResponse.json(commands.map(({userId, ...attrs}) => attrs));
+ } catch (error) {
+ console.log("[GROUPS/PERMISSIONS]", 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 { path, allow, groupId }: { path: string, allow: boolean, groupId: string } = await req.json();
+ if (!path)
+ return new NextResponse("Bad Request", { status: 400 });
+
+ const permission = await db.groupPermission.create({
+ data: {
+ userId: user.id,
+ path,
+ allow,
+ groupId
+ }
+ });
+
+ return NextResponse.json(permission, { status: 200 });
+ } catch (error) {
+ console.log("[GROUPS/PERMISSIONS]", 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, path, allow }: { id: string, path: string, allow: boolean|null } = await req.json();
+ if (!id)
+ return new NextResponse("Bad Request", { status: 400 });
+ if (!path)
+ return new NextResponse("Bad Request", { status: 400 });
+
+ let data: any = {}
+ if (!!path)
+ data = { ...data, path }
+ data = { ...data, allow }
+
+ const permission = await db.groupPermission.update({
+ where: {
+ id
+ },
+ data: data
+ });
+
+ return NextResponse.json(permission, { status: 200 });
+ } catch (error) {
+ console.log("[GROUPS/PERMISSIONS]", 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 permission = await db.groupPermission.delete({
+ where: {
+ id
+ }
+ })
+
+ return NextResponse.json(permission);
+ } catch (error) {
+ console.log("[GROUPS/PERMISSIONS]", error);
+ return new NextResponse("Internal Error", { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/settings/groups/route.ts b/app/api/settings/groups/route.ts
new file mode 100644
index 0000000..ea6b7be
--- /dev/null
+++ b/app/api/settings/groups/route.ts
@@ -0,0 +1,105 @@
+import { db } from "@/lib/db"
+import { NextResponse } from "next/server";
+import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
+import { ActionType, Prisma } from "@prisma/client";
+
+export async function GET(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req)
+ if (!user) {
+ return new NextResponse("Unauthorized", { status: 401 });
+ }
+
+ const actions = await db.group.findMany({
+ where: {
+ userId: user.id
+ }
+ })
+
+ return NextResponse.json(actions.map(({userId, ...attrs}) => attrs));
+ } catch (error) {
+ console.log("[GROUPS]", 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, priority }: { name: string, priority: number } = await req.json();
+ if (!name)
+ return new NextResponse("Bad Request", { status: 400 });
+
+ const group = await db.group.create({
+ data: {
+ userId: user.id,
+ name: name.toLowerCase(),
+ priority
+ }
+ });
+
+ return NextResponse.json(group, { status: 200 });
+ } catch (error) {
+ console.log("[GROUPS]", 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, name, priority }: { id: string, name: string, priority: number } = await req.json();
+ if (!id)
+ return new NextResponse("Bad Request", { status: 400 });
+ if (!name && !priority)
+ return new NextResponse("Bad Request", { status: 400 });
+
+ let data: any = {}
+ if (!!name)
+ data = { ...data, name: name.toLowerCase() }
+ if (!!priority)
+ data = { ...data, priority }
+
+ const group = await db.group.update({
+ where: {
+ id
+ },
+ data: data
+ });
+
+ return NextResponse.json(group, { status: 200 });
+ } catch (error) {
+ console.log("[GROUPS]", 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 group = await db.group.delete({
+ where: {
+ id
+ }
+ })
+
+ return NextResponse.json(group);
+ } catch (error) {
+ console.log("[GROUPS]", error);
+ return new NextResponse("Internal Error", { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/settings/groups/twitchchatters/route.ts b/app/api/settings/groups/twitchchatters/route.ts
new file mode 100644
index 0000000..1439058
--- /dev/null
+++ b/app/api/settings/groups/twitchchatters/route.ts
@@ -0,0 +1,55 @@
+import { NextResponse } from "next/server";
+import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
+import axios from "axios";
+import { env } from "process";
+import { TwitchUpdateAuthorization } from "@/lib/twitch";
+
+export async function GET(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req)
+ if (!user)
+ return new NextResponse("Unauthorized", { status: 401 });
+
+ const { searchParams } = new URL(req.url)
+ const logins = (searchParams.get('logins') as string)?.split(',').reduce((a,b) => a + ',&login=' + b)
+ const ids = (searchParams.get('ids') as string)?.split(',').reduce((a,b) => a + ',&id=' + b)
+ if (!logins && !ids) {
+ return new NextResponse("Bad Request", { status: 400 });
+ }
+
+ let suffix = ""
+ if (!!logins)
+ suffix += "&login=" + logins
+ if (!!ids)
+ suffix += "&id=" + ids
+ if (!!suffix)
+ suffix = "?" + suffix.substring(1)
+
+ const auth = await TwitchUpdateAuthorization(user.id)
+ if (!auth) {
+ return new NextResponse("", { status: 403 })
+ }
+
+ console.log('TWITCH URL:', 'https://api.twitch.tv/helix/users' + suffix)
+ console.log("AUTH", auth)
+ const users = await axios.get("https://api.twitch.tv/helix/users" + suffix, {
+ headers: {
+ "Authorization": "Bearer " + auth.access_token,
+ "Client-Id": env.TWITCH_BOT_CLIENT_ID
+ }
+ })
+
+ if (!users || !users.data) {
+ return new NextResponse("", { status: 400 })
+ }
+
+ if (users.data.data.length == 0) {
+ return NextResponse.json([])
+ }
+
+ return NextResponse.json(users.data.data.map((u: any) => ({ id: u.id, username: u.login })));
+ } catch (error) {
+ console.log("[GROUPS/USERS]", error);
+ return new NextResponse("Internal Error", { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/settings/groups/users/route.ts b/app/api/settings/groups/users/route.ts
new file mode 100644
index 0000000..50534ee
--- /dev/null
+++ b/app/api/settings/groups/users/route.ts
@@ -0,0 +1,40 @@
+import { db } from "@/lib/db"
+import { NextResponse } from "next/server";
+import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
+import axios from "axios";
+import { env } from "process";
+import { TwitchUpdateAuthorization } from "@/lib/twitch";
+
+export async function GET(req: Request) {
+ try {
+ const user = await fetchUserWithImpersonation(req)
+ if (!user)
+ return new NextResponse("Unauthorized", { status: 401 });
+
+ const { searchParams } = new URL(req.url)
+ const groupId = searchParams.get('groupId') as string
+
+ let chatters: { userId: string, groupId: string, chatterId: bigint, chatterLabel: string }[]
+
+ if (!!groupId)
+ chatters = await db.chatterGroup.findMany({
+ where: {
+ userId: user.id,
+ groupId
+ }
+ })
+ else
+ chatters = await db.chatterGroup.findMany({
+ where: {
+ userId: user.id
+ }
+ })
+
+ return NextResponse.json(chatters.map(u => ({ ...u, chatterId: Number(u.chatterId) }))
+ .map(({userId, chatterLabel, ...attrs}) => attrs))
+
+ } catch (error) {
+ console.log("[GROUPS/USERS]", error);
+ return new NextResponse("Internal Error", { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/settings/redemptions/actions/route.ts b/app/api/settings/redemptions/actions/route.ts
index b5bc884..6ab5615 100644
--- a/app/api/settings/redemptions/actions/route.ts
+++ b/app/api/settings/redemptions/actions/route.ts
@@ -2,7 +2,6 @@ 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 {
@@ -17,111 +16,104 @@ export async function GET(req: Request) {
}
})
- return NextResponse.json(actions.map(({userId, ...attrs}) => attrs));
+ return NextResponse.json(actions.map(({ userId, ...attrs }) => attrs));
} catch (error) {
console.log("[REDEMPTIONS/ACTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
+async function common(req: Request, action: (id: string, name: string, type: ActionType, data: any) => void) {
+ 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, tts_voice, obs_visible, obs_index, sleep, oauth_name, oauth_type }:
+ {
+ 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, tts_voice: string, obs_visible: boolean, obs_index: number, sleep: number,
+ oauth_name: string, oauth_type: 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 });
+ if ([ActionType.OAUTH, ActionType.NIGHTBOT_PLAY, ActionType.NIGHTBOT_PAUSE, ActionType.NIGHTBOT_SKIP, ActionType.NIGHTBOT_CLEAR_PLAYLIST, ActionType.NIGHTBOT_CLEAR_QUEUE, ActionType.TWITCH_OAUTH].some(t => t == type) && (!oauth_name || !oauth_type))
+ 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 }
+ } else if (type == ActionType.OBS_TRANSFORM) {
+ data = { scene_name, scene_item_name }
+ 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 }
+ } else if (type == ActionType.SPECIFIC_TTS_VOICE) {
+ data = { tts_voice }
+ } else if (type == ActionType.TOGGLE_OBS_VISIBILITY) {
+ data = { scene_name, scene_item_name }
+ } else if (type == ActionType.SPECIFIC_OBS_VISIBILITY) {
+ data = { scene_name, scene_item_name, obs_visible }
+ } else if (type == ActionType.SPECIFIC_OBS_INDEX) {
+ data = { scene_name, scene_item_name, obs_index }
+ } else if (type == ActionType.SLEEP) {
+ data = { sleep }
+ } else if ([ActionType.OAUTH, ActionType.NIGHTBOT_PLAY, ActionType.NIGHTBOT_PAUSE, ActionType.NIGHTBOT_SKIP, ActionType.NIGHTBOT_CLEAR_PLAYLIST, ActionType.NIGHTBOT_CLEAR_QUEUE, ActionType.TWITCH_OAUTH].some(t => t == type)) {
+ data = {
+ oauth_name,
+ oauth_type
+ }
+ }
+
+ action(user.id, name, type, data)
+
+ return new NextResponse("", { status: 200 });
+ } catch (error: any) {
+ //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 }
- }
-
+ return common(req, async (id, name, type, data) => {
await db.action.create({
data: {
- userId: user.id,
+ userId: 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 }
- }
-
+ return common(req, async (id, name, type, data) => {
await db.action.update({
where: {
userId_name: {
- userId: user.id,
+ userId: id,
name
}
},
data: {
+ 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 DELETE(req: Request) {
diff --git a/app/api/token/bot/route.ts b/app/api/token/bot/route.ts
index ec2723e..cca9cdf 100644
--- a/app/api/token/bot/route.ts
+++ b/app/api/token/bot/route.ts
@@ -4,24 +4,20 @@ 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,
@@ -29,7 +25,6 @@ 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);
diff --git a/app/layout.tsx b/app/layout.tsx
index 44d94d4..5f19241 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,38 +1,21 @@
-import './globals.css'
+import '@/app/globals.css'
import type { Metadata } from 'next'
-import { Open_Sans } from 'next/font/google'
-import AuthProvider from './context/auth-provider'
-import { ThemeProvider } from '@/components/providers/theme-provider'
-import { cn } from '@/lib/utils'
-
-const font = Open_Sans({ subsets: ['latin'] })
export const metadata: Metadata = {
- title: 'Hermes',
- description: '',
+ title: 'Tom-to-Speech',
+ description: '',
}
export default function RootLayout({
- children,
+ children,
}: {
- children: React.ReactNode
+ children: React.ReactNode
}) {
- return (
-
-
-
-
- {children}
-
-
-
-
- )
-}
+ return (
+
+
+ {children}
+
+
+ )
+}
\ No newline at end of file
diff --git a/app/settings/connections/page.tsx b/app/settings/connections/page.tsx
deleted file mode 100644
index 387e64c..0000000
--- a/app/settings/connections/page.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-"use client";
-
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import axios from "axios";
-import * as React from 'react';
-import { ApiKey, TwitchConnection, User } from "@prisma/client";
-import { useEffect, useState } from "react";
-import { useSession } from "next-auth/react";
-import Link from "next/link";
-import { cn } from "@/lib/utils";
-import { Skeleton } from "@/components/ui/skeleton";
-
-const ConnectionsPage = () => {
- const { data: session, status } = useSession();
- const [previousUsername, setPreviousUsername] = useState()
- const [userId, setUserId] = useState()
- const [loading, setLoading] = useState(true)
-
- useEffect(() => {
- if (status !== "authenticated" || previousUsername == session.user?.name) {
- return
- }
-
- setPreviousUsername(session.user?.name as string)
- if (session.user?.name) {
- const fetchData = async () => {
- let connection: User = (await axios.get("/api/account")).data
- setUserId(connection.id)
- setLoading(false)
- }
-
- fetchData().catch(console.error)
- }
- }, [session])
-
- const [twitchUser, setTwitchUser] = useState(null)
- useEffect(() => {
- const fetchData = async () => {
- let connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data
- setTwitchUser(connection)
- }
-
- fetchData().catch(console.error);
- }, [])
-
- const OnTwitchConnectionDelete = async () => {
- try {
- await axios.post("/api/settings/connections/twitch/delete")
- setTwitchUser(null)
- } catch (error) {
- console.log("ERROR", error)
- }
- }
-
- return (
-
-
Connections
-
-
-
-
-
-
-
Twitch
-
- Connect your Twitch account!
-
-
-
{twitchUser?.broadcasterId}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default ConnectionsPage;
\ No newline at end of file
diff --git a/app/settings/tts/filters/page.tsx b/app/settings/tts/filters/page.tsx
deleted file mode 100644
index 66ee753..0000000
--- a/app/settings/tts/filters/page.tsx
+++ /dev/null
@@ -1,345 +0,0 @@
-"use client";
-
-import axios from "axios";
-import * as React from 'react';
-import { InfoIcon, MoreHorizontal, Plus, Save, Tags, Trash } from "lucide-react"
-import { useEffect, useState } from "react";
-import { useSession } from "next-auth/react";
-import { Button } from "@/components/ui/button"
-import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
-import { Input } from "@/components/ui/input";
-import { useRouter } from "next/navigation";
-import { useForm } from "react-hook-form";
-import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
-import * as z from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
-import { Label } from "@/components/ui/label";
-import { ToastAction } from "@/components/ui/toast"
-import { useToast } from "@/components/ui/use-toast"
-import InfoNotice from "@/components/elements/info-notice";
-import { Toaster } from "@/components/ui/toaster";
-import { stringifyError } from "next/dist/shared/lib/utils";
-
-
-const TTSFiltersPage = () => {
- const { data: session, status } = useSession();
- const [moreOpen, setMoreOpen] = useState(0)
- const [tag, setTag] = useState("blacklisted")
- const [open, setOpen] = useState(false)
- const [userTags, setUserTag] = useState<{ username: string, tag: string }[]>([])
- const { toast } = useToast()
- const [error, setError] = useState("")
- const router = useRouter();
-
- const tags = [
- "blacklisted",
- "priority"
- ]
-
- const toasting = (title: string, error: Error) => {
- toast({
- title: title,
- description: error.message,
- variant: "error"
- })
- }
-
- const success = (title: string, description: string) => {
- toast({
- title: title,
- description: description,
- variant: "success"
- })
- }
-
- // Username blacklist
- const usernameFilteredFormSchema = z.object({
- username: z.string().trim().min(4).max(25).regex(new RegExp("[a-zA-Z0-9][a-zA-Z0-9\_]{3,24}"), "Must be a valid twitch username."),
- tag: z.string().trim()
- });
-
- const usernameFilteredForm = useForm({
- resolver: zodResolver(usernameFilteredFormSchema),
- defaultValues: {
- username: "",
- tag: ""
- }
- });
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- const userFiltersData = await axios.get("/api/settings/tts/filter/users")
- setUserTag(userFiltersData.data ?? [])
- } catch (error) {
- toasting("Failed to fetch all the username filters.", error as Error)
- }
-
- try {
- const replacementData = await axios.get("/api/settings/tts/filter/words")
- setReplacements(replacementData.data ?? [])
- } catch (error) {
- toasting("Failed to fetch all the word filters.", error as Error)
- }
- };
-
- fetchData().catch((error) => toasting("Failed to fetch all the username filters.", error as Error));
- }, []);
-
- const onDelete = () => {
- const username = userTags[Math.log2(moreOpen)].username
- axios.delete("/api/settings/tts/filter/users?username=" + username)
- .then(() => {
- setUserTag(userTags.filter((u) => u.username != username))
- success("Username filter deleted", `"${username.toLowerCase()}" is now back to normal.`)
- }).catch((error) => toasting("Failed to delete the username filter.", error as Error))
- }
-
- const isSubmitting = usernameFilteredForm.formState.isSubmitting;
-
- const onAddExtended = (values: z.infer, test: boolean = true) => {
- const original = userTags.find(u => u.username.toLowerCase() == values.username.toLowerCase())
-
- if (test)
- values.tag = tag
-
- axios.post("/api/settings/tts/filter/users", values)
- .then((d) => {
- if (original == null) {
- userTags.push({ username: values.username.toLowerCase(), tag: values.tag })
- } else {
- original.tag = values.tag
- }
- setUserTag(userTags)
-
- usernameFilteredForm.reset();
- router.refresh();
- if (values.tag == "blacklisted")
- success("Username filter added", `"${values.username.toLowerCase()}" will be blocked.`)
- else if (values.tag == "priority")
- success("Username filter added", `"${values.username.toLowerCase()}" will be taking priority.`)
- }).catch(error => toasting("Failed to add the username filter.", error as Error))
- }
-
- const onAdd = (values: z.infer) => {
- onAddExtended(values, true)
- }
-
- // Word replacement
- const [replacements, setReplacements] = useState<{ id: string, search: string, replace: string, userId: string }[]>([])
-
- const onReplaceAdd = async () => {
- if (search.length <= 0) {
- toasting("Unable to add the word filter.", new Error("Search must not be empty."))
- return
- }
-
- await axios.post("/api/settings/tts/filter/words", { search, replace })
- .then(d => {
- replacements.push({ id: d.data.id, search: d.data.search, replace: d.data.replace, userId: d.data.userId })
- setReplacements(replacements)
- setSearch("")
- success("Word filter added", `"${d.data.search}" will be replaced.`)
- }).catch(error => toasting("Failed to add the word filter.", error as Error))
- }
-
- const onReplaceUpdate = async (data: { id: string, search: string, replace: string, userId: string }) => {
- await axios.put("/api/settings/tts/filter/words", data)
- .then(() => success("Word filter updated", ""))
- .catch(error => toasting("Failed to update the word filter.", error as Error))
- }
-
- const onReplaceDelete = async (id: string) => {
- await axios.delete("/api/settings/tts/filter/words?id=" + id)
- .then(d => {
- const r = replacements.filter(r => r.id != d.data.id)
- setReplacements(r)
- success("Word filter deleted", `No more filter for "${d.data.search}"`)
- }).catch(error => toasting("Failed to delete the word filter.", error as Error))
- }
-
- let [search, setSearch] = useState("")
- let [replace, setReplace] = useState("")
- let [searchInfo, setSearchInfo] = useState("")
-
- return (
-
-
TTS Filters
-
-
-
- {userTags.map((user, index) => (
-
-
-
- {user.tag}
-
- {user.username}
-
-
0} onOpenChange={() => setMoreOpen(v => v ^ (1 << index))}>
-
-
-
-
- Actions
-
-
-
-
- Apply label
-
-
-
-
-
- No label found.
-
- {tags.map((tag) => (
- {
- onAddExtended({ username: userTags[index].username, tag: value}, false)
- setMoreOpen(0)
- }}
- >
- {tag}
-
- ))}
-
-
-
-
-
-
-
-
- Delete
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
- );
-}
-
-export default TTSFiltersPage;
\ No newline at end of file
diff --git a/components/elements/connection-default.tsx b/components/elements/connection-default.tsx
new file mode 100644
index 0000000..3c09b04
--- /dev/null
+++ b/components/elements/connection-default.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Button } from "../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 axios from "axios";
+
+export interface ConnectionDefault {
+ type: string,
+ connections: { name: string, clientId: string, token: string, type: string, scope: string, expiresAt: Date }[]
+}
+
+export const ConnectionDefaultElement = ({
+ type,
+ connections,
+}: ConnectionDefault) => {
+ const [connection, setConnection] = useState<{ name: string, clientId: string, token: string, type: string, scope: string, expiresAt: Date } | undefined>(undefined)
+ const [open, setOpen] = useState(false)
+
+ const OnDefaultConnectionUpdate = function (con: { name: string, clientId: string, token: string, type: string, scope: string, expiresAt: Date }) {
+ if (connection && con.name == connection.name)
+ return;
+
+ axios.put('/api/connection/default', { name: con.name, type: con.type })
+ .then(d => {
+ setConnection(con)
+ })
+ }
+
+ useEffect(() => {
+ const con = connections.filter((c: any) => c.type == type && c.default)
+ if (con.length > 0)
+ OnDefaultConnectionUpdate(con[0])
+ console.log('default', type, connections.filter(c => c.type == type).length > 0)
+ }, [])
+
+ return (
+ c.type == type).length > 0 ? 'visible' : 'hidden')}>
+
+
+
+
+
+
+
+
+
+
+
+ No action found.
+
+ {connections.filter(c => c.type == type).map(c => (
+ {
+ OnDefaultConnectionUpdate(c)
+ setOpen(false)
+ }}>
+ {c.name}
+
+ ))}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/elements/connection.tsx b/components/elements/connection.tsx
new file mode 100644
index 0000000..9ec4631
--- /dev/null
+++ b/components/elements/connection.tsx
@@ -0,0 +1,223 @@
+"use client";
+
+import axios from "axios";
+import { useState } from "react";
+import { Button } from "../ui/button";
+import { useRouter } from "next/navigation";
+import { v4 } from "uuid";
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Input } from "../ui/input";
+import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
+import { env } from "process";
+
+export interface Connection {
+ name: string
+ type: string
+ clientId: string
+ scope: string
+ expiresAt: Date
+ remover: (name: string) => void
+}
+
+const AUTHORIZATION_DATA: { [service: string]: { type: string, endpoint: string, grantType: string, scopes: string[], redirect: string } } = {
+ 'nightbot': {
+ type: 'nightbot',
+ endpoint: 'https://api.nightbot.tv/oauth2/authorize',
+ grantType: 'token',
+ scopes: ['song_requests', 'song_requests_queue', 'song_requests_playlist'],
+ redirect: 'https://tomtospeech.com/connection/authorize'
+ },
+ 'twitch': {
+ type: 'twitch',
+ endpoint: 'https://id.twitch.tv/oauth2/authorize',
+ grantType: 'token',
+ scopes: [
+ 'chat:read',
+ 'bits:read',
+ 'channel:read:polls',
+ 'channel:read:predictions',
+ 'channel:read:subscriptions',
+ 'channel:read:vips',
+ 'moderator:read:blocked_terms',
+ 'chat:read',
+ 'channel:moderate',
+ 'channel:read:redemptions',
+ 'channel:manage:redemptions',
+ 'channel:manage:predictions',
+ 'user:read:chat',
+ 'channel:bot',
+ 'moderator:read:followers',
+ 'channel:read:ads',
+ 'moderator:read:chatters',
+ ],
+ redirect: 'https://tomtospeech.com/connection/authorize'
+ },
+ // 'twitch tts bot': {
+ // type: 'twitch',
+ // endpoint: 'https://id.twitch.tv/oauth2/authorize',
+ // grantType: 'token',
+ // scopes: [
+ // 'chat:read',
+ // 'bits:read',
+ // 'channel:read:polls',
+ // 'channel:read:predictions',
+ // 'channel:read:subscriptions',
+ // 'channel:read:vips',
+ // 'moderator:read:blocked_terms',
+ // 'chat:read',
+ // 'channel:moderate',
+ // 'channel:read:redemptions',
+ // 'channel:manage:redemptions',
+ // 'channel:manage:predictions',
+ // 'user:read:chat',
+ // 'channel:bot',
+ // 'moderator:read:followers',
+ // 'channel:read:ads',
+ // 'moderator:read:chatters',
+ // ],
+ // redirect: 'https://tomtospeech.com/connection/authorize'
+ // }
+}
+
+function AddOrRenew(name: string, type: string | undefined, clientId: string, router: AppRouterInstance) {
+ if (type === undefined)
+ return
+ if (!(type in AUTHORIZATION_DATA))
+ return
+
+ console.log(type)
+ const data = AUTHORIZATION_DATA[type]
+ const state = v4()
+ const clientIdUpdated = type == 'twitch tts bot' ? process.env.NEXT_PUBLIC_TWITCH_TTS_CLIENT_ID : clientId
+ axios.post("/api/connection/prepare", {
+ name: name,
+ type: data.type,
+ clientId: clientIdUpdated,
+ grantType: data.grantType,
+ state: state
+ }).then(_ => {
+ const url = data.endpoint + '?client_id=' + clientIdUpdated + '&redirect_uri=' + data.redirect + '&response_type=' + data.grantType
+ + '&scope=' + data.scopes.join('%20') + '&state=' + state + '&force_verify=true'
+ router.push(url)
+ })
+}
+
+export const ConnectionElement = ({
+ name,
+ type,
+ clientId,
+ expiresAt,
+ remover,
+}: Connection) => {
+ const router = useRouter()
+ const expirationHours = (new Date(expiresAt).getTime() - new Date().getTime()) / 1000 / 60 / 60
+ const expirationDays = expirationHours / 24
+
+ function Delete() {
+ axios.delete("/api/connection?name=" + name)
+ .then(d => {
+ remover(d.data.data.name)
+ })
+ }
+
+ return (
+
+
+ {name}
+
+ {expirationDays > 1 && Math.floor(expirationDays) + " days - " + type}
+ {expirationDays <= 1 && Math.floor(expirationHours) + " hours - " + type}
+
+
+
+
+
+
+
+
+ );
+}
+
+export const ConnectionAdderElement = () => {
+ const router = useRouter()
+ const [name, setName] = useState('')
+ const [type, setType] = useState(undefined)
+ const [clientId, setClientId] = useState('')
+ const [open, setOpen] = useState(false)
+
+ return (
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/elements/group-permission.tsx b/components/elements/group-permission.tsx
new file mode 100644
index 0000000..8a6ab34
--- /dev/null
+++ b/components/elements/group-permission.tsx
@@ -0,0 +1,211 @@
+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"
+import { Checkbox } from "../ui/checkbox";
+
+interface Permission {
+ id: string|undefined
+ path: string
+ allow: boolean|null
+ groupId: string
+ edit: boolean
+ showEdit: boolean
+ isNew: boolean
+ permissionPaths: { path: string, description: string }[]
+ adder: (id: string, path: string, allow: boolean|null) => void
+ remover: (redemption: { id: string, path: string, allow: boolean|null }) => void
+}
+
+const GroupPermission = ({
+ id,
+ path,
+ allow,
+ groupId,
+ edit,
+ showEdit,
+ isNew,
+ permissionPaths,
+ adder,
+ remover
+}: Permission) => {
+ const [pathOpen, setPathOpen] = useState(false)
+ const [isEditable, setIsEditable] = useState(edit)
+ const [oldData, setOldData] = useState<{ path: string, allow: boolean|null } | undefined>(undefined)
+ const [permission, setPermission] = useState<{ id: string|undefined, path: string, allow: boolean|null }>({ id, path, allow });
+
+ function Save() {
+ if (!permission || !permission.path)
+ return
+
+ if (isNew) {
+ axios.post("/api/settings/groups/permissions", {
+ path: permission.path,
+ allow: permission.allow,
+ groupId: groupId
+ }).then(d => {
+ if (!d || !d.data)
+ return
+
+ adder(d.data.id, permission.path, permission.allow)
+ setPermission({ id: undefined, path: "", allow: true })
+ })
+ } else {
+ axios.put("/api/settings/groups/permissions", {
+ id: permission.id,
+ path: permission.path,
+ allow: permission.allow
+ }).then(d => {
+ setIsEditable(false)
+ })
+ }
+ }
+
+ function Cancel() {
+ if (!oldData)
+ return
+
+ setPermission({ ...oldData, id: permission.id })
+ setIsEditable(false)
+ setOldData(undefined)
+ }
+
+ function Delete() {
+ axios.delete("/api/settings/groups/permissions?id=" + permission.id)
+ .then(d => {
+ remover(d.data)
+ })
+ }
+
+ return (
+
+
+ {!isEditable &&
+
+ || isEditable &&
+
+
+
+
+
+
+
+
+ No permission found.
+
+ {permissionPaths.map((p) => (
+ {
+ setPermission({ ...permission, path: permissionPaths.find(v => v.path.toLowerCase() == value.toLowerCase())?.path ?? value.toLowerCase()})
+ setPathOpen(false)
+ }}>
+
+
+ {p.path}
+
+
+ {p.description}
+
+
+
+
+ ))}
+
+
+
+
+
+ }
+
+
+
+ {
+ if (permission.allow === null)
+ setPermission({ ...permission, allow: false })
+ else
+ setPermission({ ...permission, allow: null })
+ }}
+ disabled={!isEditable} />
+
+ {
+ setPermission({ ...permission, allow: !permission.allow })
+ }}
+ disabled={!isEditable || permission === undefined} />
+
+
+ {isEditable &&
+
+ }
+ {isEditable && !isNew &&
+
+ }
+ {showEdit && !isEditable &&
+
+ }
+ {!isEditable &&
+
+ }
+
+
+ );
+}
+
+export default GroupPermission;
\ No newline at end of file
diff --git a/components/elements/group.tsx b/components/elements/group.tsx
new file mode 100644
index 0000000..b2e4742
--- /dev/null
+++ b/components/elements/group.tsx
@@ -0,0 +1,276 @@
+import axios from "axios";
+import { useState } from "react";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Label } from "../ui/label";
+import { Maximize2, Minimize2, Trash2Icon } from "lucide-react";
+import GroupPermission from "./group-permission";
+import { z } from "zod";
+import UserList from "./user-list-group";
+
+interface Group {
+ id: string | undefined
+ name: string
+ priority: number
+ permissionsLoaded: { id: string, path: string, allow: boolean | null }[]
+ edit: boolean
+ showEdit: boolean
+ isNewGroup: boolean
+ permissionPaths: { path: string, description: string }[]
+ specialGroups: string[]
+ adder: (id: string, name: string, priority: number) => void
+ remover: (group: { id: string, name: string, priority: number }) => void
+}
+
+const GroupElement = ({
+ id,
+ name,
+ priority,
+ permissionsLoaded,
+ edit,
+ showEdit,
+ isNewGroup,
+ permissionPaths,
+ specialGroups,
+ adder,
+ remover
+}: Group) => {
+ const [isEditable, setIsEditable] = useState(edit)
+ const [isNew, setIsNew] = useState(isNewGroup)
+ const [isMinimized, setIsMinimized] = useState(true)
+ const [oldData, setOldData] = useState<{ name: string, priority: number } | undefined>(undefined)
+ const [group, setGroup] = useState<{ id: string | undefined, name: string, priority: number }>({ id, name, priority })
+ const [permissions, setPermissions] = useState<{ id: string, path: string, allow: boolean | null }[]>(permissionsLoaded);
+ const isSpecial = (isEditable || oldData === undefined) && !!group && specialGroups.includes(group?.name)
+ const [error, setError] = useState(undefined)
+
+
+ function addPermission(id: string, path: string, allow: boolean | null) {
+ setPermissions([...permissions, { id, path, allow }])
+ }
+
+ function removePermission(permission: { id: string, path: string, allow: boolean | null }) {
+ setPermissions(permissions.filter(p => p.id != permission.id))
+ }
+
+ const nameSchema = z.string({
+ required_error: "Name is required.",
+ invalid_type_error: "Name must be a string"
+ }).regex(/^[\w\-\s]{1,20}$/, "Name must contain only letters, numbers, dashes, and underscores.")
+ const prioritySchema = z.string().regex(/^-?\d{1,5}$/, "Priority must be a valid number.")
+
+ function Save() {
+ setError(undefined)
+ if (!isNew && !id)
+ return
+
+ const nameValidation = nameSchema.safeParse(group.name)
+ if (!nameValidation.success) {
+ setError(JSON.parse(nameValidation.error['message'])[0].message)
+ return
+ }
+
+ const priorityValidation = prioritySchema.safeParse(group.priority.toString())
+ if (!priorityValidation.success) {
+ setError(JSON.parse(priorityValidation.error['message'])[0].message)
+ return
+ }
+
+ if (isNew || group.id?.startsWith('$')) {
+ axios.post("/api/settings/groups", {
+ name: group.name,
+ priority: group.priority
+ }).then(d => {
+ if (!d) {
+ setError("Something went wrong.")
+ return
+ }
+ console.log("DATA", d.data)
+
+ if (specialGroups.includes(group.name)) {
+ setIsNew(false)
+ setIsEditable(false)
+ setGroup({ id: d.data.id, name: d.data.name, priority: d.data.priority })
+ } else {
+ adder(d.data.id, group.name.toLowerCase(), group.priority)
+ setGroup({ id: undefined, name: "", priority: 0 })
+ }
+ }).catch(() => {
+ setError("Potential group name duplicate.")
+ })
+ } else {
+ axios.put("/api/settings/groups", {
+ id: group.id,
+ name: group.name,
+ priority: group.priority
+ }).then(d => {
+ console.log("DATA", d.data)
+ setIsEditable(false)
+ }).catch(() => {
+ setError("Potential group name duplicate.")
+ })
+ }
+ }
+
+ function Cancel() {
+ setError(undefined)
+ if (!oldData)
+ return
+
+ setGroup({ ...oldData, id: group.id })
+ setIsEditable(false)
+ setOldData(undefined)
+ }
+
+ function Delete() {
+ axios.delete("/api/settings/groups?id=" + group.id)
+ .then(d => {
+ if (specialGroups.includes(group.name)) {
+ setPermissions([])
+ setIsMinimized(true)
+ setOldData(undefined)
+ setIsNew(true)
+ setIsEditable(true)
+ } else
+ remover(d.data)
+ })
+ }
+
+ return (
+
+
+
+
+
+ {isSpecial &&
+
+ auto-generated
+
+ }
+
setGroup({ ...group, name: e.target.value })}
+ readOnly={isSpecial || !isEditable} />
+
+
+
+ setGroup(d => {
+ let temp = { ...group }
+ const v = parseInt(e.target.value)
+ if (e.target.value.length == 0) {
+ temp.priority = 0
+ } else if (!Number.isNaN(v) && Number.isSafeInteger(v)) {
+ temp.priority = v
+ } else if (Number.isNaN(v)) {
+ temp.priority = 0
+ }
+ return temp
+ })}
+ readOnly={!isEditable} />
+
+
+
+ {error}
+
+
+ {isEditable &&
+
+ }
+ {isEditable && !isNew &&
+
+ }
+ {showEdit && !isEditable &&
+
+ }
+ {!isEditable && !isNew &&
+
+ }
+ {!isNew && !group?.id?.startsWith('$') &&
+
+ }
+ {!isNew && !isSpecial &&
+
+ }
+
+
+ {!isNew && !isMinimized &&
+
+ {permissions.map(permission =>
+
+
+
+ )}
+
+
+
+
+ }
+
+ );
+}
+
+export default GroupElement;
\ No newline at end of file
diff --git a/components/elements/redeemable-action.tsx b/components/elements/redeemable-action.tsx
index 2569b97..18e514c 100644
--- a/components/elements/redeemable-action.tsx
+++ b/components/elements/redeemable-action.tsx
@@ -7,24 +7,218 @@ 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";
+import { boolean } from "zod";
const actionTypes = [
{
"name": "Overwrite local file content",
- "value": ActionType.WRITE_TO_FILE
+ "value": ActionType.WRITE_TO_FILE,
+ "inputs": [
+ {
+ "type": "text",
+ "label": "File path",
+ "key": "file_path",
+ "placeholder": "Enter local file path, relative or full."
+ },
+ {
+ "type": "text",
+ "label": "File content",
+ "key": "file_content",
+ "placeholder": "Enter the text to write to the file."
+ }
+ ]
},
{
"name": "Append to local file",
- "value": ActionType.APPEND_TO_FILE
+ "value": ActionType.APPEND_TO_FILE,
+ "inputs": [
+ {
+ "type": "text",
+ "label": "File path",
+ "key": "file_path",
+ "placeholder": "Enter local file path, relative or full."
+ },
+ {
+ "type": "text",
+ "label": "File content",
+ "key": "file_content",
+ "placeholder": "Enter the text to append to the file."
+ }
+ ]
},
{
"name": "Cause a transformation on OBS scene item",
- "value": ActionType.OBS_TRANSFORM
+ "value": ActionType.OBS_TRANSFORM,
+ "inputs": []
},
{
"name": "Play an audio file locally",
- "value": ActionType.AUDIO_FILE
+ "value": ActionType.AUDIO_FILE,
+ "inputs": [
+ {
+ "type": "text",
+ "label": "File path",
+ "key": "file_path",
+ "placeholder": "Enter local file path, relative or full."
+ }
+ ]
+ },
+ {
+ "name": "User gets a random TTS voice that is enabled",
+ "value": ActionType.RANDOM_TTS_VOICE,
+ "inputs": []
+ },
+ {
+ "name": "User gets a specific TTS voice",
+ "value": ActionType.SPECIFIC_TTS_VOICE,
+ "inputs": [
+ {
+ "type": "text",
+ "label": "TTS Voice",
+ "key": "tts_voice",
+ "placeholder": "Name of an enabled TTS voice",
+ }
+ ]
+ },
+ {
+ "name": "Toggle OBS scene item visibility",
+ "value": ActionType.TOGGLE_OBS_VISIBILITY,
+ "inputs": [
+ {
+ "type": "text",
+ "label": "Scene Name",
+ "key": "scene_name",
+ "placeholder": "Name of the OBS scene"
+ },
+ {
+ "type": "text",
+ "label": "Scene Item Name",
+ "key": "scene_item_name",
+ "placeholder": "Name of the OBS scene item / source"
+ }
+ ]
+ },
+ {
+ "name": "Set OBS scene item visibility",
+ "value": ActionType.SPECIFIC_OBS_VISIBILITY,
+ "inputs": [
+ {
+ "type": "text",
+ "label": "Scene Name",
+ "key": "scene_name",
+ "placeholder": "Name of the OBS scene"
+ },
+ {
+ "type": "text",
+ "label": "Scene Item Name",
+ "key": "scene_item_name",
+ "placeholder": "Name of the OBS scene item / source"
+ },
+ {
+ "type": "text-values",
+ "label": "Visible",
+ "key": "obs_visible",
+ "placeholder": "true for visible; false otherwise",
+ "values": ["true", "false"]
+ }
+ ]
+ },
+ {
+ "name": "Set OBS scene item's index",
+ "value": ActionType.SPECIFIC_OBS_INDEX,
+ "inputs": [
+ {
+ "type": "text",
+ "label": "Scene Name",
+ "key": "scene_name",
+ "placeholder": "Name of the OBS scene"
+ },
+ {
+ "type": "text",
+ "label": "Scene Item Name",
+ "key": "scene_item_name",
+ "placeholder": "Name of the OBS scene item / source"
+ },
+ {
+ "type": "number",
+ "label": "Index",
+ "key": "obs_index",
+ "placeholder": "index, starting from 0."
+ }
+ ]
+ },
+ {
+ "name": "Sleep - do nothing",
+ "value": ActionType.SLEEP,
+ "inputs": [
+ {
+ "type": "number",
+ "label": "Sleep",
+ "key": "sleep",
+ "placeholder": "Time in milliseconds to do nothing",
+ }
+ ]
+ },
+ {
+ "name": "Nightbot - Play",
+ "value": ActionType.NIGHTBOT_PLAY,
+ "inputs": [
+ {
+ "type": "oauth.nightbot.play",
+ "label": "nightbot.play",
+ "key": "nightbot_play",
+ "placeholder": "",
+ }
+ ]
+ },
+ {
+ "name": "Nightbot - Pause",
+ "value": ActionType.NIGHTBOT_PAUSE,
+ "inputs": [
+ {
+ "type": "oauth.nightbot.pause",
+ "label": "nightbot.pause",
+ "key": "nightbot_pause",
+ "placeholder": "",
+ }
+ ]
+ },
+ {
+ "name": "Nightbot - Skip",
+ "value": ActionType.NIGHTBOT_SKIP,
+ "inputs": [
+ {
+ "type": "oauth.nightbot.skip",
+ "label": "nightbot.skip",
+ "key": "nightbot_skip",
+ "placeholder": "",
+ }
+ ]
+ },
+ {
+ "name": "Nightbot - Clear Playlist",
+ "value": ActionType.NIGHTBOT_CLEAR_PLAYLIST,
+ "inputs": [
+ {
+ "type": "oauth.nightbot.clear_playlist",
+ "label": "nightbot.clear_playlist",
+ "key": "nightbot_clear_playlist",
+ "placeholder": "",
+ }
+ ]
+ },
+ {
+ "name": "Nightbot - Clear Queue",
+ "value": ActionType.NIGHTBOT_CLEAR_QUEUE,
+ "inputs": [
+ {
+ "type": "oauth.nightbot.clear_queue",
+ "label": "nightbot.clear_queue",
+ "key": "nightbot_clear_queue",
+ "placeholder": "",
+ }
+ ]
},
]
@@ -37,6 +231,7 @@ interface RedeemableAction {
showEdit?: boolean
isNew: boolean
obsTransformations: { label: string, placeholder: string, description: string }[]
+ connections: { name: string, type: string }[]
adder: (name: string, type: ActionType, data: { [key: string]: string }) => void
remover: (action: { name: string, type: string, data: any }) => void
}
@@ -50,12 +245,13 @@ const RedemptionAction = ({
showEdit = true,
isNew = false,
obsTransformations = [],
+ connections = [],
adder,
remover
}: RedeemableAction) => {
- const [open, setOpen] = useState(false)
+ const [open, setOpen] = useState<{ [key: string]: boolean }>({ 'actions': false, 'oauth': false, 'oauth.nightbot': false, 'oauth.twitch': false })
const [actionName, setActionName] = useState(name)
- const [actionType, setActionType] = useState<{ name: string, value: ActionType } | undefined>(actionTypes.find(a => a.value == type?.toUpperCase()))
+ const [actionType, setActionType] = useState<{ name: string, value: ActionType, inputs: any[] } | 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)
@@ -66,6 +262,7 @@ const RedemptionAction = ({
if (!name) {
return
}
+ console.log('typeeee', type)
if (!type) {
return
}
@@ -96,7 +293,7 @@ const RedemptionAction = ({
}
}
- function Cancel(data: { n: string, t: ActionType | undefined, d: { [k: string]: string } } | undefined) {
+ function Cancel(data: { n: string, t: ActionType | undefined, d: { [k: string]: any } } | undefined) {
if (!data)
return
@@ -126,7 +323,7 @@ const RedemptionAction = ({
{actionName}
@@ -160,13 +357,13 @@ const RedemptionAction = ({
readOnly />
|| isEditable &&
+ open={open['actions']}
+ onOpenChange={() => setOpen({ ...open, 'actions': !open['actions'] })}>
@@ -184,7 +381,7 @@ const RedemptionAction = ({
key={action.value}
onSelect={(value) => {
setActionType(actionTypes.find(v => v.name.toLowerCase() == value.toLowerCase()))
- setOpen(false)
+ setOpen({ ...open, 'actions': false })
}}>
{action.name}
@@ -197,38 +394,138 @@ const RedemptionAction = ({
}