2024-05-12 20:05:06 +00:00
|
|
|
/** @typedef {import("fastify").FastifyInstance} FastifyInstance */
|
|
|
|
|
2024-06-07 10:39:11 +00:00
|
|
|
import { and, desc, eq, getTableColumns, sql } from "drizzle-orm";
|
2024-05-12 20:05:06 +00:00
|
|
|
import { db } from "../db/index.js";
|
|
|
|
import { articles, articles as articlesTable, signups as signupsTable, sites, users } from "../db/schemas.js";
|
|
|
|
import { authMiddleware, authMiddlewareFn } from "../modules/middleware.js";
|
2024-06-10 19:11:17 +00:00
|
|
|
import { jsonToCsv, createBlogFromCaptions, createArticleSlug, getVideoById, env, getWhisperCaptions, getVideoWithCaptions } from "../utils/index.js";
|
2024-05-12 20:05:06 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {FastifyInstance} fastify
|
|
|
|
* @param {unknown} _
|
|
|
|
* @param {() => void} done
|
|
|
|
*/
|
|
|
|
export const dashboardRoutes = (fastify, _, done) => {
|
|
|
|
fastify.register(authMiddleware);
|
|
|
|
|
|
|
|
fastify.get("/", async (req, response) => {
|
|
|
|
try {
|
|
|
|
const [{ site_id, user_id }] = await db.select({
|
|
|
|
site_id: sites.id,
|
|
|
|
user_id: sites.user_id
|
|
|
|
}).from(sites).where(eq(sites.user_id, req.session.user_id));
|
|
|
|
|
|
|
|
const recentArticles = await db.select({
|
|
|
|
title: articlesTable.title,
|
|
|
|
views: articlesTable.views,
|
|
|
|
created_at: articlesTable.created_at
|
|
|
|
}).from(articlesTable)
|
|
|
|
.where(eq(articlesTable.site_id, site_id))
|
|
|
|
.orderBy(articlesTable.created_at).limit(5);
|
|
|
|
|
|
|
|
const recentSignups = await db.select({
|
|
|
|
email: signupsTable.email,
|
|
|
|
created_at: signupsTable.created_at
|
|
|
|
}).from(signupsTable)
|
|
|
|
.where(eq(signupsTable.site_id, site_id))
|
|
|
|
.orderBy(desc(signupsTable.created_at)).limit(8);
|
|
|
|
|
2024-05-29 19:35:18 +00:00
|
|
|
const [{ totalArticles }] = await db.select({ totalArticles: sql`count(*)` }).from(articlesTable).where(eq(articlesTable.site_id, site_id));
|
2024-05-30 10:24:10 +00:00
|
|
|
const [{ totalViews }] = await db.select({ totalViews: sql`COALESCE(sum(${articlesTable.views}), 0)` }).from(articlesTable).where(eq(articlesTable.site_id, site_id));
|
2024-05-29 19:35:18 +00:00
|
|
|
const [{ totalEmails }] = await db.select({ totalEmails: sql`count(*)` }).from(signupsTable).where(eq(signupsTable.site_id, site_id));
|
2024-05-12 20:05:06 +00:00
|
|
|
|
|
|
|
response.send({
|
|
|
|
success: true,
|
|
|
|
recentArticles,
|
2024-05-29 19:35:18 +00:00
|
|
|
recentSignups,
|
|
|
|
totalArticles,
|
|
|
|
totalEmails,
|
|
|
|
totalViews
|
2024-05-12 20:05:06 +00:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
response.status(500).send({
|
|
|
|
success: false,
|
|
|
|
message: "Failed to get dashboard data",
|
|
|
|
log: e.message
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
fastify.get("/email-export", async (req, response) => {
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
fastify.get("/signups", async (req, reply) => {
|
|
|
|
const site = await db.select().from(sites).where(eq(sites.user_id, req.session.user_id));
|
|
|
|
|
|
|
|
if (site[0]?.user_id != req.session.user_id) {
|
|
|
|
reply.status(403).send({ success: false });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const signups = await db.select().from(signupsTable).where(eq(signupsTable.site_id, site[0].id)).orderBy(desc(signupsTable.created_at));
|
|
|
|
|
|
|
|
reply.send({
|
|
|
|
success: true,
|
|
|
|
signups
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
fastify.get("/signups/export", async (req, reply) => {
|
|
|
|
try {
|
|
|
|
|
|
|
|
const [{ site_id }] = await db.select({
|
|
|
|
site_id: sites.id
|
|
|
|
}).from(sites).where(eq(sites.user_id, req.session.user_id));
|
|
|
|
|
|
|
|
const signups = await db.select({
|
|
|
|
email: signupsTable.email,
|
|
|
|
source: sql`COALESCE(source, 'Home')`,
|
|
|
|
created_at: signupsTable.created_at
|
|
|
|
}).from(signupsTable).where(eq(signupsTable.site_id, site_id)).orderBy(desc(signupsTable.created_at));
|
|
|
|
|
|
|
|
let result = jsonToCsv(signups);
|
|
|
|
|
|
|
|
reply.send({
|
|
|
|
success: true,
|
|
|
|
data: result
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
fastify.put("/article", {
|
|
|
|
schema: {
|
|
|
|
body: {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
id: {
|
|
|
|
type: "string"
|
|
|
|
},
|
|
|
|
is_public: {
|
|
|
|
type: "boolean"
|
|
|
|
},
|
|
|
|
content: {
|
|
|
|
type: "string"
|
|
|
|
},
|
|
|
|
title: {
|
|
|
|
type: "string"
|
|
|
|
},
|
|
|
|
},
|
|
|
|
required: ["id"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, async (req, reply) => {
|
|
|
|
const [article] = await db.select(getTableColumns(articles)).from(articles).leftJoin(sites, eq(sites.id, articles.site_id)).where(eq(sites.user_id, req.session.user_id));
|
|
|
|
|
|
|
|
if (!article) {
|
|
|
|
reply.status(404).send({
|
|
|
|
success: false,
|
|
|
|
message: "This article does not exist."
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Object.keys(req.body).length > 1) {
|
|
|
|
await db.update(articles).set(JSON.parse(JSON.stringify({ ...req.body, id: undefined }))).where(eq(articles.id, req.body.id));
|
|
|
|
}
|
|
|
|
reply.send({
|
|
|
|
success: true
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
|
2024-06-07 10:39:11 +00:00
|
|
|
fastify.delete("/article", {
|
|
|
|
schema: {
|
|
|
|
querystring: {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
id: {
|
|
|
|
type: "string"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
required: ["id"]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, async (req, reply) => {
|
|
|
|
const [article] = await db.select(getTableColumns(articles)).from(articles).leftJoin(sites, eq(sites.id, articles.site_id)).where(and(eq(sites.user_id, req.session.user_id), eq(articles.id, req.query.id)));
|
|
|
|
|
|
|
|
if (!article) {
|
|
|
|
reply.status(404).send({
|
|
|
|
success: false,
|
|
|
|
message: "This article does not exist."
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await db.delete(articles).where(eq(articles.id, article.id));
|
|
|
|
|
|
|
|
reply.send({
|
|
|
|
success: true
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
|
2024-05-12 20:05:06 +00:00
|
|
|
fastify.post("/create", {
|
|
|
|
schema: {
|
|
|
|
body: {
|
|
|
|
type: "object",
|
|
|
|
required: ["video_id"],
|
|
|
|
properties: {
|
|
|
|
video_id: {
|
|
|
|
type: "string",
|
|
|
|
},
|
|
|
|
length: {
|
|
|
|
type: "number",
|
|
|
|
},
|
|
|
|
format: {
|
|
|
|
type: "string"
|
|
|
|
},
|
2024-05-29 19:35:18 +00:00
|
|
|
faq: {
|
|
|
|
type: "boolean"
|
|
|
|
},
|
2024-05-12 20:05:06 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}, async (req, reply) => {
|
|
|
|
try {
|
|
|
|
const [{ tokens }] = await db.select({
|
|
|
|
tokens: users.tokens
|
|
|
|
}).from(users).where(eq(users.id, req.session.user_id));
|
|
|
|
|
|
|
|
if (tokens < 1) {
|
|
|
|
reply.send({
|
|
|
|
success: false,
|
|
|
|
code: "insufficient_tokens",
|
|
|
|
message: "Insufficient tokens"
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-06-10 19:11:17 +00:00
|
|
|
// const access_token = await getAccessToken(fastify, req);
|
2024-06-09 22:14:41 +00:00
|
|
|
|
|
|
|
let urlRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=|\?v=)([^#\&\?]*).*/;
|
|
|
|
let match = req.body.video_id.match(urlRegex);
|
|
|
|
if (!match) {
|
|
|
|
reply.status(400).send({
|
|
|
|
success: false,
|
|
|
|
message: "Invalid Youtube URL"
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// youtube-dl --write-sub --sub-lang en --skip-download URL
|
|
|
|
|
2024-06-10 19:11:17 +00:00
|
|
|
|
|
|
|
const video_data = await getVideoWithCaptions(req.body.video_id);
|
2024-05-12 20:05:06 +00:00
|
|
|
|
|
|
|
reply.send({
|
|
|
|
success: true
|
|
|
|
});
|
|
|
|
|
2024-06-10 19:11:17 +00:00
|
|
|
// const video_data = await getVideoById(access_token, req.body.video_id);
|
2024-05-29 19:35:18 +00:00
|
|
|
|
2024-06-10 19:11:17 +00:00
|
|
|
const blog_content_json = await createBlogFromCaptions(video_data.captions, { title: video_data.title, description: video_data.description }, req.body);
|
2024-05-12 20:05:06 +00:00
|
|
|
// TODO: once I add multiple sites per user, this should come from the client
|
|
|
|
const site = await db.select().from(sites).where(eq(sites.user_id, req.session.user_id));
|
|
|
|
|
|
|
|
await db.insert(articlesTable).values({
|
|
|
|
site_id: site[0].id,
|
|
|
|
title: blog_content_json.title,
|
2024-05-30 13:21:55 +00:00
|
|
|
content: blog_content_json.content,
|
|
|
|
meta_title: blog_content_json.meta_title,
|
|
|
|
meta_desc: blog_content_json.meta_desc,
|
|
|
|
excerp: blog_content_json.excerp,
|
2024-05-12 20:05:06 +00:00
|
|
|
source_video_id: req.body.video_id,
|
|
|
|
seo_slug: createArticleSlug(blog_content_json.title),
|
|
|
|
is_public: false
|
|
|
|
}).returning({ id: articlesTable.id });
|
|
|
|
|
|
|
|
await db.update(users).set({
|
|
|
|
tokens: tokens - 1
|
2024-05-29 19:35:18 +00:00
|
|
|
}).where(eq(users.id, req.session.user_id));
|
|
|
|
|
2024-05-12 20:05:06 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-06-02 00:36:39 +00:00
|
|
|
fastify.post("/recreate", {
|
|
|
|
schema: {
|
|
|
|
body: {
|
|
|
|
type: "object",
|
|
|
|
required: ["video_id"],
|
|
|
|
properties: {
|
|
|
|
video_id: {
|
|
|
|
type: "string",
|
|
|
|
},
|
|
|
|
length: {
|
|
|
|
type: "number",
|
|
|
|
},
|
|
|
|
format: {
|
|
|
|
type: "string"
|
|
|
|
},
|
|
|
|
faq: {
|
|
|
|
type: "boolean"
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}, async (req, reply) => {
|
|
|
|
|
|
|
|
})
|
|
|
|
|
2024-05-12 20:05:06 +00:00
|
|
|
fastify.put("/website", {
|
|
|
|
schema: {
|
|
|
|
body: {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
id: {
|
|
|
|
type: "string"
|
|
|
|
},
|
|
|
|
name: {
|
|
|
|
type: "string"
|
|
|
|
},
|
|
|
|
primary_color_hex: {
|
|
|
|
type: "string",
|
|
|
|
maxLength: 6,
|
|
|
|
minLength: 6
|
|
|
|
},
|
|
|
|
secondary_color_hex: {
|
|
|
|
type: "string",
|
|
|
|
maxLength: 6,
|
|
|
|
minLength: 6
|
|
|
|
},
|
|
|
|
text_color_hex: {
|
|
|
|
type: "string",
|
|
|
|
maxLength: 6,
|
|
|
|
minLength: 6
|
2024-05-29 19:35:18 +00:00
|
|
|
},
|
|
|
|
use_freebie: {
|
|
|
|
type: "boolean"
|
|
|
|
},
|
|
|
|
freebie_name: {
|
2024-06-07 19:34:52 +00:00
|
|
|
type: ["string", "null"]
|
2024-05-29 19:35:18 +00:00
|
|
|
},
|
|
|
|
freebie_url: {
|
2024-06-07 19:34:52 +00:00
|
|
|
type: ["string", "null"],
|
2024-05-29 19:35:18 +00:00
|
|
|
format: "uri"
|
|
|
|
},
|
|
|
|
freebie_image_url: {
|
2024-06-07 19:34:52 +00:00
|
|
|
type: ["string", "null"],
|
2024-05-29 19:35:18 +00:00
|
|
|
format: "uri"
|
|
|
|
},
|
|
|
|
freebie_text: {
|
2024-06-07 19:34:52 +00:00
|
|
|
type: ["string", "null"]
|
2024-05-29 19:35:18 +00:00
|
|
|
},
|
|
|
|
title: {
|
|
|
|
type: "string",
|
|
|
|
},
|
|
|
|
subtitle: {
|
|
|
|
type: "string",
|
2024-06-02 00:36:39 +00:00
|
|
|
},
|
|
|
|
domain: {
|
2024-06-07 19:34:52 +00:00
|
|
|
type: ["string", "null"]
|
2024-05-12 20:05:06 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
required: ["id"]
|
|
|
|
}
|
|
|
|
},
|
|
|
|
preValidation: authMiddlewareFn
|
|
|
|
}, async (req, reply) => {
|
2024-05-29 19:35:18 +00:00
|
|
|
try {
|
|
|
|
const [site] = await db.select().from(sites).where(eq(sites.id, req.body.id));
|
2024-05-12 20:05:06 +00:00
|
|
|
|
2024-05-29 19:35:18 +00:00
|
|
|
if (site.user_id !== req.session.user_id) {
|
|
|
|
reply.status(403).send({
|
|
|
|
success: false
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2024-05-12 20:05:06 +00:00
|
|
|
|
2024-06-02 08:47:01 +00:00
|
|
|
if (env.CERTS_URL && site.domain !== req.body.domain) {
|
2024-06-02 08:32:48 +00:00
|
|
|
await fetch(`http://${env.CERTS_URL}/provision`, {
|
2024-06-02 00:36:39 +00:00
|
|
|
method: "POST",
|
2024-06-02 08:30:56 +00:00
|
|
|
body: JSON.stringify({
|
2024-06-02 00:36:39 +00:00
|
|
|
domain: req.body.domain
|
2024-06-02 08:30:56 +00:00
|
|
|
})
|
2024-06-02 00:36:39 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-06-07 20:00:08 +00:00
|
|
|
const data = structuredClone(req.body.id);
|
|
|
|
|
|
|
|
delete data.id;
|
|
|
|
await db.update(sites).set(data).where(eq(sites.id, req.body.id));
|
2024-05-12 20:05:06 +00:00
|
|
|
|
2024-05-29 19:35:18 +00:00
|
|
|
return {
|
|
|
|
success: true
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
2024-05-12 20:05:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
done();
|
|
|
|
};
|
2024-06-09 22:14:41 +00:00
|
|
|
|
|
|
|
function convertSRV1ToPlainText(transcript) {
|
|
|
|
// Define regular expression to extract text content
|
|
|
|
const textRegex = /<text[^>]+>(.*?)<\/text>/g;
|
|
|
|
|
|
|
|
// Initialize an empty array to hold the plain text lines
|
|
|
|
const plainTextLines = [];
|
|
|
|
|
|
|
|
// Match text segments using regular expression
|
|
|
|
let match;
|
|
|
|
while ((match = textRegex.exec(transcript)) !== null) {
|
|
|
|
// Extract text content and remove any HTML tags
|
|
|
|
const textContent = match[1].replace(/<[^>]+>/g, '');
|
|
|
|
|
|
|
|
// Add text content to the plain text lines array
|
|
|
|
plainTextLines.push(textContent);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Join the plain text lines into a single string and return it
|
|
|
|
return plainTextLines.join('\n');
|
|
|
|
}
|