Compare commits

..

No commits in common. "e6a8ef5f9ed511ab565e5453157903628e4a228d" and "bfe1c4fe0ab7b7be2166a46562d49a9bc817bc9e" have entirely different histories.

5 changed files with 31 additions and 79 deletions

View File

@ -53,8 +53,7 @@ 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

@ -58,10 +58,14 @@ 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' }, function (request, payload, done) { server.addContentTypeParser(['text/xml', 'application/xml', 'application/atom+xml'], { parseAs: 'string' }, async (request, payload, done) => {
xml2js.parseString(payload, function (err, body) { try {
done(err, body) let parsed = await xml2js.parseStringPromise(payload);
}) done(null, parsed)
} catch(e) {
console.log(e);
done(e, undefined)
}
}) })
// Routes // Routes

View File

@ -6,8 +6,6 @@ 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
@ -30,7 +28,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(desc(articlesTable.created_at)).limit(5); .orderBy(articlesTable.created_at).limit(5);
const recentSignups = await db.select({ const recentSignups = await db.select({
email: signupsTable.email, email: signupsTable.email,
@ -340,9 +338,6 @@ export const dashboardRoutes = (fastify, _, done) => {
}, },
domain: { domain: {
type: ["string", "null"] type: ["string", "null"]
},
auto_publish: {
type: "boolean"
} }
}, },
required: ["id"] required: ["id"]
@ -369,21 +364,6 @@ 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,10 +2,8 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { articles, sites, users } from "../db/schemas.js"; import { users } from "../db/schemas.js";
import { getVideoWithCaptions } from "../utils/youtube.js"; import { authMiddleware } from "../modules/middleware.js";
import { createBlogFromCaptions } from "../utils/ai.js";
import { createArticleSlug } from "../utils/index.js";
/** /**
* *
@ -29,10 +27,8 @@ 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"] && req.query["hub.verify_token"] === "FQNI4Suzih") { if (req.query["hub.challenge"]) {
// 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
@ -43,45 +39,19 @@ 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') {
try { // Parse the XML payload
// Parse the XML payload const { feed } = body;
console.log(body) // Example processing: log the video IDs of new videos
const feed = body["feed"]; feed.entry.forEach(entry => {
// Example processing: log the video IDs of new videos
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");
if (user.tokens < 3) throw new Error("Not enough tokens");
const videoId = entry["yt:videoId"][0]; const videoId = entry["yt:videoId"][0];
const videoURL = `https://youtu.be/${videoId}`; console.log(`New video uploaded: ${videoId}`);
reply.code(200).send(); });
const video_data = await getVideoWithCaptions(videoURL); // Respond with a success status
const blog_content_json = await createBlogFromCaptions(video_data.captions, { title: video_data.title, description: video_data.description }); return reply.code(200).send();
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,14 +71,13 @@ 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 = {}) { }, {
const { length,
length, language,
language, format,
format, tone,
tone, faq
faq } = { length: 700, language: "English", format: "summary", tone: "informal", faq: false }) {
} = 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: `;