Compare commits
3 Commits
bfe1c4fe0a
...
e6a8ef5f9e
Author | SHA1 | Date | |
---|---|---|---|
e6a8ef5f9e | |||
8412cbc572 | |||
b4c44e7622 |
@ -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", {
|
||||||
|
16
src/index.js
16
src/index.js
@ -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, {
|
||||||
|
@ -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;
|
||||||
|
@ -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");
|
||||||
|
@ -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: `;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user