Compare commits

..

3 Commits

Author SHA1 Message Date
e6a8ef5f9e added logic for auto-publish 2024-06-13 14:45:43 +02:00
8412cbc572 fix xml parser 2024-06-13 14:45:06 +02:00
b4c44e7622 fix createBlogFromCaptions function default values 2024-06-13 14:44:55 +02:00
5 changed files with 80 additions and 32 deletions

View File

@ -53,7 +53,8 @@ export const sites = pgTable("sites", {
subdomain_slug: text("subdomain_slug").$defaultFn(() => { subdomain_slug: text("subdomain_slug").$defaultFn(() => {
return makeid(10); return makeid(10);
}), }),
social_medias: jsonb("social_medias") social_medias: jsonb("social_medias"),
auto_publish: boolean("auto_publish").default(false)
}); });
export const articles = pgTable("articles", { export const articles = pgTable("articles", {

View File

@ -36,7 +36,7 @@ export const main = async () => {
origin: true, origin: true,
credentials: true, credentials: true,
}); });
server.register(oauth, { server.register(oauth, {
name: 'googleOAuth2', name: 'googleOAuth2',
scope: ['https://www.googleapis.com/auth/youtube.readonly', 'https://www.googleapis.com/auth/youtube.force-ssl', "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"], scope: ['https://www.googleapis.com/auth/youtube.readonly', 'https://www.googleapis.com/auth/youtube.force-ssl', "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"],
@ -58,15 +58,11 @@ export const main = async () => {
callbackUri: `${env.PUBLIC_API_URL}/auth/google/callback` callbackUri: `${env.PUBLIC_API_URL}/auth/google/callback`
}); });
server.addContentTypeParser(['text/xml', 'application/xml', 'application/atom+xml'], { parseAs: 'string' }, async (request, payload, done) => { server.addContentTypeParser(['text/xml', 'application/xml', 'application/atom+xml'], { parseAs: 'string' }, function (request, payload, done) {
try { xml2js.parseString(payload, function (err, body) {
let parsed = await xml2js.parseStringPromise(payload); done(err, body)
done(null, parsed) })
} catch(e) { })
console.log(e);
done(e, undefined)
}
})
// Routes // Routes
server.register(channelRoutes, { server.register(channelRoutes, {

View File

@ -6,6 +6,8 @@ import { articles, articles as articlesTable, signups as signupsTable, sites, us
import { authMiddleware, authMiddlewareFn } from "../modules/middleware.js"; import { authMiddleware, authMiddlewareFn } from "../modules/middleware.js";
import { jsonToCsv, createBlogFromCaptions, createArticleSlug, getVideoById, env, getWhisperCaptions, getVideoWithCaptions } from "../utils/index.js"; import { jsonToCsv, createBlogFromCaptions, createArticleSlug, getVideoById, env, getWhisperCaptions, getVideoWithCaptions } from "../utils/index.js";
const websubVerifyToken = "FQNI4Suzih";
/** /**
* *
* @param {FastifyInstance} fastify * @param {FastifyInstance} fastify
@ -28,7 +30,7 @@ export const dashboardRoutes = (fastify, _, done) => {
created_at: articlesTable.created_at created_at: articlesTable.created_at
}).from(articlesTable) }).from(articlesTable)
.where(eq(articlesTable.site_id, site_id)) .where(eq(articlesTable.site_id, site_id))
.orderBy(articlesTable.created_at).limit(5); .orderBy(desc(articlesTable.created_at)).limit(5);
const recentSignups = await db.select({ const recentSignups = await db.select({
email: signupsTable.email, email: signupsTable.email,
@ -338,6 +340,9 @@ export const dashboardRoutes = (fastify, _, done) => {
}, },
domain: { domain: {
type: ["string", "null"] type: ["string", "null"]
},
auto_publish: {
type: "boolean"
} }
}, },
required: ["id"] required: ["id"]
@ -364,6 +369,21 @@ export const dashboardRoutes = (fastify, _, done) => {
}); });
} }
if(site.auto_publish !== req.body.auto_publish) {
const [user] = await db.select().from(users).where(eq(users.id, req.session.user_id));
if(!user) throw new Error("Problem getting user");
let form = new FormData();
form.set("hub.callback", env.PUBLIC_API_URL + "/webhooks/youtube");
form.set("hub.topic", "https://www.youtube.com/xml/feeds/videos.xml?channel_id=" + user.channel_id);
form.set("hub.verify", "async");
form.set("hub.mode", auto_publish ? "subscribe" : "unsubscribe");
form.set("hub.verify_token", websubVerifyToken);
await fetch("https://pubsubhubbub.appspot.com/subscribe", {
method: "POST",
body: form
});
}
const data = structuredClone(req.body); const data = structuredClone(req.body);
delete data.id; delete data.id;

View File

@ -2,8 +2,10 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { users } from "../db/schemas.js"; import { articles, sites, users } from "../db/schemas.js";
import { authMiddleware } from "../modules/middleware.js"; import { getVideoWithCaptions } from "../utils/youtube.js";
import { createBlogFromCaptions } from "../utils/ai.js";
import { createArticleSlug } from "../utils/index.js";
/** /**
* *
@ -27,8 +29,10 @@ export const webhookRoutes = (fastify, _, done) => {
fastify.get("/youtube", async (req, reply) => { fastify.get("/youtube", async (req, reply) => {
// Check if the request contains the 'hub.challenge' query parameter // Check if the request contains the 'hub.challenge' query parameter
if (req.query["hub.challenge"]) { if (req.query["hub.challenge"] && req.query["hub.verify_token"] === "FQNI4Suzih") {
// Respond with the challenge to verify the subscription // Respond with the challenge to verify the subscription
console.log(req.query)
console.log("verifying...", req.query["hub.challenge"]);
return reply.send(req.query["hub.challenge"]); return reply.send(req.query["hub.challenge"]);
} else { } else {
// Handle other cases or errors // Handle other cases or errors
@ -39,19 +43,45 @@ export const webhookRoutes = (fastify, _, done) => {
fastify.post("/youtube", async (req, reply) => { fastify.post("/youtube", async (req, reply) => {
const { headers, body } = req; const { headers, body } = req;
const contentType = headers['content-type']; const contentType = headers['content-type'];
console.log(JSON.stringify(body.feed.contry))
// Check if the content type is 'application/atom+xml' // Check if the content type is 'application/atom+xml'
if (contentType === 'application/atom+xml') { if (contentType === 'application/atom+xml') {
// Parse the XML payload try {
const { feed } = body; // Parse the XML payload
// Example processing: log the video IDs of new videos console.log(body)
feed.entry.forEach(entry => { const feed = body["feed"];
const videoId = entry["yt:videoId"][0]; // Example processing: log the video IDs of new videos
console.log(`New video uploaded: ${videoId}`); const entry = feed.entry[0];
}); const [{users: user, sites: site}] = await db.select().from(users).leftJoin(sites, eq(users.id, sites.user_id)).where(eq(users.channel_id, "UC" + feed["yt:channelId"][0]));
if (!user || !site) throw new Error("User not found");
// Respond with a success status if (user.tokens < 3) throw new Error("Not enough tokens");
return reply.code(200).send(); const videoId = entry["yt:videoId"][0];
const videoURL = `https://youtu.be/${videoId}`;
reply.code(200).send();
const video_data = await getVideoWithCaptions(videoURL);
const blog_content_json = await createBlogFromCaptions(video_data.captions, { title: video_data.title, description: video_data.description });
await db.insert(articles).values({
site_id: site.id,
title: blog_content_json.title,
content: blog_content_json.content,
meta_title: blog_content_json.meta_title,
meta_desc: blog_content_json.meta_desc,
excerp: blog_content_json.excerp,
source_video_id: videoId,
seo_slug: createArticleSlug(blog_content_json.title),
is_public: false
}).returning({ id: articles.id });
await db.update(users).set({
tokens: user.tokens - 3
}).where(eq(users.id, user.id));
// Respond with a success status
} catch (e) {
console.log(e)
return reply.code(500).send(e);
}
} else { } else {
// Respond with an error status if the content type is not expected // Respond with an error status if the content type is not expected
return reply.code(400).send("Bad Request"); return reply.code(400).send("Bad Request");

View File

@ -71,13 +71,14 @@ async function promptGPT(prompt, { model, is_json } = { model: "gpt-3.5-turbo",
export async function createBlogFromCaptions(captions, { export async function createBlogFromCaptions(captions, {
title, title,
description description
}, { }, options = {}) {
length, const {
language, length,
format, language,
tone, format,
faq tone,
} = { length: 700, language: "English", format: "summary", tone: "informal", faq: false }) { faq
} = Object.assign({ length: 700, language: "English", format: "summary", tone: "informal", faq: false }, options);
// const prompt = `Convert the following video transcript into a blog post. The approximate length should be around ${length || 500} characters, written in ${language || "English"}. The desired format of the blog post is a ${format || "summary"}. Please ensure the blog post has a ${tone || "informal"} tone throughout. Use markdown to format the article. You must always respond in the following json fromat: {"title": string, "content": string, "seo_friendly_slug": string}. \nHere is the transcript: ` // const prompt = `Convert the following video transcript into a blog post. The approximate length should be around ${length || 500} characters, written in ${language || "English"}. The desired format of the blog post is a ${format || "summary"}. Please ensure the blog post has a ${tone || "informal"} tone throughout. Use markdown to format the article. You must always respond in the following json fromat: {"title": string, "content": string, "seo_friendly_slug": string}. \nHere is the transcript: `
// const prompt = `Convert the following video transcript into an engaging blog post. You must always respond in the following json fromat: {"title": string, "body": string, "seo_friendly_slug": string}. Do not, under any circumstance, include the title inside the body, it should only be reserved for the body of the article. Use markdown to format the article. Use "\\n" to add line-breaks. \nHere is the transcript: `; // const prompt = `Convert the following video transcript into an engaging blog post. You must always respond in the following json fromat: {"title": string, "body": string, "seo_friendly_slug": string}. Do not, under any circumstance, include the title inside the body, it should only be reserved for the body of the article. Use markdown to format the article. Use "\\n" to add line-breaks. \nHere is the transcript: `;