Compare commits
No commits in common. "e6a8ef5f9ed511ab565e5453157903628e4a228d" and "bfe1c4fe0ab7b7be2166a46562d49a9bc817bc9e" have entirely different histories.
e6a8ef5f9e
...
bfe1c4fe0a
@ -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", {
|
||||||
|
12
src/index.js
12
src/index.js
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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");
|
||||||
|
@ -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: `;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user