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 = [ const customTwitchRedemptions = [
{ {
id: 'adbreak', id: 'adbreak_begin',
title: 'Adbreak (TTS redemption)' title: 'Adbreak Begin (TTS redemption)'
},
{
id: 'adbreak_end',
title: 'Adbreak End (TTS redemption)'
}, },
{ {
id: 'follow', id: 'follow',

View File

@ -3,10 +3,11 @@ import { NextResponse } from "next/server";
export async function GET(req: Request) { export async function GET(req: Request) {
return NextResponse.json({ return NextResponse.json({
major_version: 4, major_version: 4,
minor_version: 3, minor_version: 5,
download: "https://drive.proton.me/urls/YH86153EWM#W6VTyaoAVHKP", download: "https://drive.proton.me/urls/7JMTHDQ7VM#6h85HeLaxXgr",
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: "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: "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: "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 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({ const groupIdSchema = z.string({
required_error: "Group ID should be available.", required_error: "Group ID should be available.",
invalid_type_error: "Group ID must be a string" 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) { export async function POST(req: Request) {
try { try {

View File

@ -6,7 +6,7 @@ import { z } from "zod";
const groupIdSchema = z.string({ const groupIdSchema = z.string({
required_error: "Group ID should be available.", required_error: "Group ID should be available.",
invalid_type_error: "Group ID must be a string" 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) { export async function GET(req: Request) {
try { try {

View File

@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation"; import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { ActionType, Prisma } from "@prisma/client"; import { ActionType, Prisma } from "@prisma/client";
import { z } from "zod"; import { z } from "zod";
import { redirect } from "next/dist/server/api-utils";
export async function GET(req: Request) { export async function GET(req: Request) {
try { try {
@ -28,18 +29,15 @@ const nameSchema = z.string({
invalid_type_error: "Name must be a string" invalid_type_error: "Name must be a string"
}).regex(/^[\w\-\s]{1,32}$/, "Name must contain only letters, numbers, spaces, dashes, and underscores.") }).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 { try {
const user = await fetchUserWithImpersonation(req) const user = await fetchUserWithImpersonation(req)
if (!user) { if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); 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 }: const { name, type, data }: { name: string, type: ActionType, data: any } = await req.json();
{
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) if (!name)
return NextResponse.json({ message: 'name is required.', error: null, value: null }, { status: 400 }); return NextResponse.json({ message: 'name is required.', error: null, value: null }, { status: 400 });
const nameValidation = nameSchema.safeParse(name) 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 }); return NextResponse.json({ message: 'name must follow some requirements.', error: nameValidation.error, value: null }, { status: 400 });
if (!type) if (!type)
return NextResponse.json({ message: 'type is required', error: null, value: null }, { status: 400 }); 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 }); 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 }); 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 }); 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 }); 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) { 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) { } else if (type == ActionType.OBS_TRANSFORM) {
data = { scene_name, scene_item_name } d = { scene_name: data.scene_name, scene_item_name: data.scene_item_name }
if (!!rotation) if (!!data.rotation)
data = { rotation, ...data } d = { rotation: data.rotation, ...data }
if (!!position_x) if (!!data.position_x)
data = { position_x, ...data } d = { position_x: data.position_x, ...data }
if (!!position_y) if (!!data.position_y)
data = { position_y, ...data } d = { position_y: data.position_y, ...data }
} else if (type == ActionType.AUDIO_FILE) { } else if (type == ActionType.AUDIO_FILE) {
data = { file_path } d = { file_path: data.file_path }
} else if (type == ActionType.SPECIFIC_TTS_VOICE) { } else if (type == ActionType.SPECIFIC_TTS_VOICE) {
data = { tts_voice } d = { tts_voice: data.tts_voice }
} else if (type == ActionType.TOGGLE_OBS_VISIBILITY) { } 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) { } 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) { } 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) { } 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)) { } 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 = { d = {
oauth_name, oauth_name: data.oauth_name,
oauth_type 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) const dd = action(user.id, name, type, d)
return NextResponse.json({ message: null, error: null, value: dd }, { status: 200 });
return NextResponse.json({ message: null, error: null, value: null }, { status: 200 });
} catch (error: any) { } catch (error: any) {
return NextResponse.json({ message: null, error: error, value: null }, { status: 500 }); 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) { export async function POST(req: Request) {
return common(req, async (id, name, type, data) => { return common(req, async (id, name, type, data) => {
await db.action.create({ return await db.action.create({
data: { data: {
userId: id, userId: id,
name, name,
@ -109,7 +110,7 @@ export async function POST(req: Request) {
export async function PUT(req: Request) { export async function PUT(req: Request) {
return common(req, async (id, name, type, data) => { return common(req, async (id, name, type, data) => {
await db.action.update({ return await db.action.update({
where: { where: {
userId_name: { userId_name: {
userId: id, userId: id,
@ -117,7 +118,6 @@ export async function PUT(req: Request) {
} }
}, },
data: { data: {
name,
type, type,
data: data as Prisma.JsonObject 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({ const nameSchema = z.string({
@ -321,7 +334,6 @@ const RedemptionAction = ({
} }
info.data = actionData info.data = actionData
if (isNew) { if (isNew) {
axios.post("/api/settings/redemptions/actions", info) axios.post("/api/settings/redemptions/actions", info)
.then(d => { .then(d => {

View File

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

View File

@ -2,6 +2,19 @@
const nextConfig = { const nextConfig = {
reactStrictMode: false, reactStrictMode: false,
output: 'standalone', 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 module.exports = nextConfig

View File

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