Compare commits

...

4 Commits

Author SHA1 Message Date
Tom
dc4f9f6bfb Minor changes 2025-01-07 15:55:49 +00:00
Tom
6847ea6c9d Added Veadotube redemption 2025-01-07 15:53:54 +00:00
Tom
d2352288f2 Added ad break redemptions 2025-01-07 15:53:16 +00:00
Tom
dd8530b589 Fixed a few things 2025-01-07 15:52:47 +00:00
9 changed files with 107 additions and 57 deletions

View File

@ -19,8 +19,12 @@ const obsTransformations = [
const customTwitchRedemptions = [
{
id: 'adbreak',
title: 'Adbreak (TTS redemption)'
id: 'adbreak_begin',
title: 'Adbreak Begin (TTS redemption)'
},
{
id: 'adbreak_end',
title: 'Adbreak End (TTS redemption)'
},
{
id: 'follow',

View File

@ -3,10 +3,11 @@ import { NextResponse } from "next/server";
export async function GET(req: Request) {
return NextResponse.json({
major_version: 4,
minor_version: 3,
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"
minor_version: 5,
download: "https://drive.proton.me/urls/7JMTHDQ7VM#6h85HeLaxXgr",
changelog: "Added Veadotube integration.\nFixed voice changes via redemptions.\nMessages in queue for longer than a minute will be skipped automatically."
//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."

View File

@ -73,7 +73,7 @@ export async function GET(req: Request) {
const groupIdSchema = z.string({
required_error: "Group ID should be available.",
invalid_type_error: "Group ID must be a string"
}).regex(/^[\w\-\=]{1,32}$/, "Group ID must contain only letters, numbers, dashes, underscores & equal signs.")
}).regex(/^[\w\-\=]{1,40}$/, "Group ID must contain only letters, numbers, dashes, underscores & equal signs.")
export async function POST(req: Request) {
try {

View File

@ -6,7 +6,7 @@ import { z } from "zod";
const groupIdSchema = z.string({
required_error: "Group ID should be available.",
invalid_type_error: "Group ID must be a string"
}).regex(/^[\w\-\=]{1,32}$/, "Group ID must contain only letters, numbers, dashes, underscores & equal signs.")
}).regex(/^[\w\-\=]{1,40}$/, "Group ID must contain only letters, numbers, dashes, underscores & equal signs.")
export async function GET(req: Request) {
try {

View File

@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { ActionType, Prisma } from "@prisma/client";
import { z } from "zod";
import { redirect } from "next/dist/server/api-utils";
export async function GET(req: Request) {
try {
@ -28,18 +29,15 @@ const nameSchema = z.string({
invalid_type_error: "Name must be a string"
}).regex(/^[\w\-\s]{1,32}$/, "Name must contain only letters, numbers, spaces, dashes, and underscores.")
async function common(req: Request, action: (id: string, name: string, type: ActionType, data: any) => void) {
async function common(req: Request, action: (id: string, name: string, type: ActionType, values: any) => void) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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();
const { name, type, data }: { name: string, type: ActionType, data: any } = await req.json();
if (!name)
return NextResponse.json({ message: 'name is required.', error: null, value: null }, { status: 400 });
const nameValidation = nameSchema.safeParse(name)
@ -47,48 +45,51 @@ async function common(req: Request, action: (id: string, name: string, type: Act
return NextResponse.json({ message: 'name must follow some requirements.', error: nameValidation.error, value: null }, { status: 400 });
if (!type)
return NextResponse.json({ message: 'type is required', error: null, value: null }, { status: 400 });
if (type == ActionType.OBS_TRANSFORM && (!scene_name || !scene_item_name || !rotation && !position_x && !position_y))
if (type == ActionType.OBS_TRANSFORM && (!data.scene_name || !data.scene_item_name || !data.rotation && !data.position_x && !data.position_y))
return NextResponse.json({ message: '"scene_name", "scene_item_name" and one of "rotation", "position_x", "position_y" are required.', error: null, value: null }, { status: 400 });
if ((type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) && (!file_path || !file_content))
if ((type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) && (!data.file_path || !data.file_content))
return NextResponse.json({ message: '"scene_name", "scene_item_name", "file_path" & "file_content" are required.', error: null, value: null }, { status: 400 });
if (type == ActionType.AUDIO_FILE && !file_path)
if (type == ActionType.AUDIO_FILE && !data.file_path)
return NextResponse.json({ message: '"scene_name", "scene_item_name" & "file_path" are required.', error: null, value: null }, { 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))
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 || !data.oauth_type))
return NextResponse.json({ message: '"oauth_name" & "oauth_type" are required.', error: null, value: null }, { status: 400 });
if ([ActionType.VEADOTUBE_POP_STATE, ActionType.VEADOTUBE_PUSH_STATE, ActionType.VEADOTUBE_SET_STATE].some(t => t == type) && !data.state)
return NextResponse.json({ message: '"state" is required.', error: null, value: null }, { status: 400 });
let data: any = {}
let d: any = {}
if (type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) {
data = { file_path, file_content }
d = { file_path: data.file_path, file_content: data.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 }
d = { scene_name: data.scene_name, scene_item_name: data.scene_item_name }
if (!!data.rotation)
d = { rotation: data.rotation, ...data }
if (!!data.position_x)
d = { position_x: data.position_x, ...data }
if (!!data.position_y)
d = { position_y: data.position_y, ...data }
} else if (type == ActionType.AUDIO_FILE) {
data = { file_path }
d = { file_path: data.file_path }
} else if (type == ActionType.SPECIFIC_TTS_VOICE) {
data = { tts_voice }
d = { tts_voice: data.tts_voice }
} else if (type == ActionType.TOGGLE_OBS_VISIBILITY) {
data = { scene_name, scene_item_name }
d = { scene_name: data.scene_name, scene_item_name: data.scene_item_name }
} else if (type == ActionType.SPECIFIC_OBS_VISIBILITY) {
data = { scene_name, scene_item_name, obs_visible }
d = { scene_name: data.scene_name, scene_item_name: data.scene_item_name, obs_visible: data.obs_visible }
} else if (type == ActionType.SPECIFIC_OBS_INDEX) {
data = { scene_name, scene_item_name, obs_index }
d = { scene_name: data.scene_name, scene_item_name: data.scene_item_name, obs_index: data.obs_index }
} else if (type == ActionType.SLEEP) {
data = { sleep }
d = { 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
d = {
oauth_name: data.oauth_name,
oauth_type: data.oauth_type
}
} else if ([ActionType.VEADOTUBE_POP_STATE, ActionType.VEADOTUBE_PUSH_STATE, ActionType.VEADOTUBE_SET_STATE].some(t => t == type)) {
d = { state: data.state }
}
action(user.id, name, type, data)
return NextResponse.json({ message: null, error: null, value: null }, { status: 200 });
const dd = action(user.id, name, type, d)
return NextResponse.json({ message: null, error: null, value: dd }, { status: 200 });
} catch (error: any) {
return NextResponse.json({ message: null, error: error, value: null }, { status: 500 });
}
@ -96,7 +97,7 @@ async function common(req: Request, action: (id: string, name: string, type: Act
export async function POST(req: Request) {
return common(req, async (id, name, type, data) => {
await db.action.create({
return await db.action.create({
data: {
userId: id,
name,
@ -109,7 +110,7 @@ export async function POST(req: Request) {
export async function PUT(req: Request) {
return common(req, async (id, name, type, data) => {
await db.action.update({
return await db.action.update({
where: {
userId_name: {
userId: id,
@ -117,7 +118,6 @@ export async function PUT(req: Request) {
}
},
data: {
name,
type,
data: data as Prisma.JsonObject
}

View File

@ -240,6 +240,19 @@ const actionTypes = [
}
]
},
{
"name": "Veadotube - Set State",
"value": ActionType.VEADOTUBE_SET_STATE,
"inputs": [
{
"type": "text",
"label": "State",
"key": "state",
"placeholder": "state #1",
"required": true
}
]
},
]
const nameSchema = z.string({
@ -321,7 +334,6 @@ const RedemptionAction = ({
}
info.data = actionData
if (isNew) {
axios.post("/api/settings/redemptions/actions", info)
.then(d => {

View File

@ -8,6 +8,7 @@ export async function updateTwitchToken(userId: string) {
}
})
if (!connection) {
console.log('Connections do not exist.')
return null
}
@ -48,6 +49,7 @@ export async function updateTwitchToken(userId: string) {
const { access_token, expires_in, refresh_token, token_type } = token
if (!access_token || !refresh_token || token_type !== "bearer") {
console.log('Failed to grab token.')
return null
}

View File

@ -2,6 +2,19 @@
const nextConfig = {
reactStrictMode: false,
output: 'standalone',
headers: async () => {
return [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" }, // replace this your actual origin
{ key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT" },
{ key: "Access-Control-Allow-Headers", value: "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" },
]
}
]
}
}
module.exports = nextConfig

View File

@ -30,20 +30,21 @@ model User {
impersonationSources Impersonation[] @relation(name: "impersonationSources")
impersonationTargets Impersonation[] @relation(name: "impersonationTargets")
apiKeys ApiKey[]
accounts Account[]
twitchConnections TwitchConnection[]
Connection Connection[]
ConnectionState ConnectionState[]
ttsUsernameFilter TtsUsernameFilter[]
ttsWordFilter TtsWordFilter[]
ttsChatVoices TtsChatVoice[]
ttsVoiceStates TtsVoiceState[]
actions Action[]
redemptions Redemption[]
groups Group[]
chatterGroups ChatterGroup[]
groupPermissions GroupPermission[]
apiKeys ApiKey[]
accounts Account[]
twitchConnections TwitchConnection[]
Connection Connection[]
ConnectionState ConnectionState[]
ttsUsernameFilter TtsUsernameFilter[]
ttsWordFilter TtsWordFilter[]
ttsChatVoices TtsChatVoice[]
ttsVoiceStates TtsVoiceState[]
actions Action[]
redemptions Redemption[]
groups Group[]
chatterGroups ChatterGroup[]
groupPermissions GroupPermission[]
GroupPermissionPolicy GroupPermissionPolicy[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -105,7 +106,7 @@ model Connection {
grantType String
scope String
expiresAt DateTime
default Boolean @default(false)
default Boolean @default(false)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@ -223,6 +224,20 @@ model GroupPermission {
@@index([userId])
}
model GroupPermissionPolicy {
id String @id @default(uuid()) @db.Uuid
userId String
groupId String @db.Uuid
path String
count Int
timespan Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, groupId, path])
@@index([userId])
}
model Chatter {
id BigInt
name String
@ -273,6 +288,9 @@ enum ActionType {
NIGHTBOT_CLEAR_PLAYLIST
NIGHTBOT_CLEAR_QUEUE
TWITCH_OAUTH
VEADOTUBE_SET_STATE
VEADOTUBE_PUSH_STATE
VEADOTUBE_POP_STATE
}
model Action {