Compare commits
4 Commits
624b3fa63b
...
dc4f9f6bfb
Author | SHA1 | Date | |
---|---|---|---|
dc4f9f6bfb | |||
6847ea6c9d | |||
d2352288f2 | |||
dd8530b589 |
@ -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',
|
||||
|
@ -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."
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user