mirror of
https://github.com/OneUptime/oneuptime
synced 2024-11-22 15:24:55 +00:00
448 lines
12 KiB
TypeScript
448 lines
12 KiB
TypeScript
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
|
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
|
import URL from "Common/Types/API/URL";
|
|
import OneUptimeDate from "Common/Types/Date";
|
|
import BadDataException from "Common/Types/Exception/BadDataException";
|
|
import { JSONArray, JSONObject, JSONObjectOrArray } from "Common/Types/JSON";
|
|
import JSONFunctions from "Common/Types/JSONFunctions";
|
|
import Text from "Common/Types/Text";
|
|
import API from "Common/Utils/API";
|
|
import LocalCache from "Common/Server/Infrastructure/LocalCache";
|
|
import Markdown, { MarkdownContentType } from "Common/Server/Types/Markdown";
|
|
|
|
export interface BlogPostAuthor {
|
|
username: string;
|
|
githubUrl: string;
|
|
profileImageUrl: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface BlogPostBaseProps {
|
|
title: string;
|
|
description: string;
|
|
|
|
formattedPostDate: string;
|
|
fileName: string;
|
|
tags: string[];
|
|
postDate: string;
|
|
blogUrl: string;
|
|
}
|
|
|
|
export interface BlogPostHeader extends BlogPostBaseProps {
|
|
authorGitHubUsername: string;
|
|
}
|
|
|
|
export interface BlogPost extends BlogPostBaseProps {
|
|
htmlBody: string;
|
|
markdownBody: string;
|
|
socialMediaImageUrl: string;
|
|
author: BlogPostAuthor | null;
|
|
}
|
|
|
|
const GitHubRawUrl: string =
|
|
"https://raw.githubusercontent.com/oneuptime/blog/master";
|
|
|
|
export default class BlogPostUtil {
|
|
public static async getBlogPostList(
|
|
tagName?: string | undefined,
|
|
): Promise<BlogPostHeader[]> {
|
|
const fileUrl: URL = URL.fromString(`${GitHubRawUrl}/Blogs.json`);
|
|
|
|
const fileData:
|
|
| HTTPResponse<
|
|
| JSONObjectOrArray
|
|
| BaseModel
|
|
| BaseModel[]
|
|
| AnalyticsBaseModel
|
|
| AnalyticsBaseModel[]
|
|
>
|
|
| HTTPErrorResponse = await API.get(fileUrl);
|
|
|
|
if (fileData.isFailure()) {
|
|
throw fileData as HTTPErrorResponse;
|
|
}
|
|
|
|
let jsonContent: string | JSONArray =
|
|
(fileData.data as string | JSONArray) || [];
|
|
|
|
if (typeof jsonContent === "string") {
|
|
jsonContent = JSONFunctions.parseJSONArray(jsonContent);
|
|
}
|
|
|
|
const blogs: Array<JSONObject> = JSONFunctions.deserializeArray(
|
|
jsonContent as Array<JSONObject>,
|
|
).reverse(); // reverse so new content comes first
|
|
|
|
const resultList: Array<BlogPostHeader> = [];
|
|
|
|
for (const blog of blogs) {
|
|
const fileName: string = blog["post"] as string;
|
|
const formattedPostDate: string =
|
|
this.getFormattedPostDateFromFileName(fileName);
|
|
const postDate: string = this.getPostDateFromFileName(fileName);
|
|
|
|
resultList.push({
|
|
title: blog["title"] as string,
|
|
description: blog["description"] as string,
|
|
fileName,
|
|
formattedPostDate,
|
|
postDate,
|
|
tags: blog["tags"] as string[],
|
|
authorGitHubUsername: blog["authorGitHubUsername"] as string,
|
|
blogUrl: `/blog/post/${fileName}`,
|
|
});
|
|
}
|
|
|
|
if (tagName) {
|
|
return resultList.filter((blog: BlogPostHeader) => {
|
|
return blog.tags
|
|
.map((item: string) => {
|
|
return Text.replaceAll(item.toLowerCase(), " ", "-");
|
|
})
|
|
.includes(tagName);
|
|
});
|
|
}
|
|
|
|
return resultList;
|
|
}
|
|
|
|
public static async getBlogPost(fileName: string): Promise<BlogPost | null> {
|
|
let blogPost: BlogPost | null = this.getBlogPostFromCache(fileName);
|
|
|
|
if (blogPost) {
|
|
return Promise.resolve(blogPost);
|
|
}
|
|
|
|
blogPost = await this.getBlogPostFromGitHub(fileName);
|
|
|
|
// save this to cache
|
|
LocalCache.setJSON(
|
|
"blog",
|
|
fileName,
|
|
JSONFunctions.serialize(blogPost as any),
|
|
);
|
|
|
|
return blogPost;
|
|
}
|
|
|
|
public static async getNameOfGitHubUser(username: string): Promise<string> {
|
|
const fileUrl: URL = URL.fromString(
|
|
`https://api.github.com/users/${username}`,
|
|
);
|
|
|
|
const fileData:
|
|
| HTTPResponse<
|
|
| JSONObjectOrArray
|
|
| BaseModel
|
|
| BaseModel[]
|
|
| AnalyticsBaseModel
|
|
| AnalyticsBaseModel[]
|
|
>
|
|
| HTTPErrorResponse = await API.get(fileUrl);
|
|
|
|
if (fileData.isFailure()) {
|
|
throw fileData as HTTPErrorResponse;
|
|
}
|
|
|
|
const name: string =
|
|
(fileData.data as JSONObject)?.["name"]?.toString() || "";
|
|
return name;
|
|
}
|
|
|
|
public static async getGitHubMarkdownFileContent(
|
|
githubPath: string,
|
|
): Promise<string | null> {
|
|
const fileUrl: URL = URL.fromString(`${GitHubRawUrl}/${githubPath}`);
|
|
|
|
const fileData:
|
|
| HTTPResponse<
|
|
| JSONObjectOrArray
|
|
| BaseModel
|
|
| BaseModel[]
|
|
| AnalyticsBaseModel
|
|
| AnalyticsBaseModel[]
|
|
>
|
|
| HTTPErrorResponse = await API.get(fileUrl);
|
|
|
|
if (fileData.isFailure()) {
|
|
if ((fileData as HTTPErrorResponse).statusCode === 404) {
|
|
return null;
|
|
}
|
|
|
|
throw fileData as HTTPErrorResponse;
|
|
}
|
|
|
|
const markdownContent: string =
|
|
(fileData.data as JSONObject)?.["data"]?.toString() || "";
|
|
return markdownContent;
|
|
}
|
|
|
|
public static async getTags(): Promise<string[]> {
|
|
// check if tags are in cache
|
|
let tags: string[] = LocalCache.getJSON("blog-tags", "tags") as string[];
|
|
|
|
if (tags && tags.length > 0) {
|
|
return tags;
|
|
}
|
|
|
|
tags = await this.getAllTagsFromGitHub();
|
|
|
|
// save this to cache
|
|
|
|
LocalCache.setJSON(
|
|
"blog-tags",
|
|
"tags",
|
|
JSONFunctions.serialize(tags as any),
|
|
);
|
|
|
|
return tags;
|
|
}
|
|
|
|
public static async getAllTagsFromGitHub(): Promise<string[]> {
|
|
const tagsMarkdownContent: string | null =
|
|
await this.getGitHubMarkdownFileContent("Tags.md");
|
|
|
|
if (!tagsMarkdownContent) {
|
|
return [];
|
|
}
|
|
|
|
const tags: Array<string> = tagsMarkdownContent
|
|
.split("\n")
|
|
.map((tag: string) => {
|
|
return tag.trim();
|
|
})
|
|
.filter((tag: string) => {
|
|
return tag.startsWith("-");
|
|
})
|
|
.map((tag: string) => {
|
|
return tag.replace("-", "").trim();
|
|
});
|
|
|
|
return tags;
|
|
}
|
|
|
|
public static async getBlogPostFromGitHub(
|
|
fileName: string,
|
|
): Promise<BlogPost | null> {
|
|
const fileUrl: URL = URL.fromString(
|
|
`${GitHubRawUrl}/posts/${fileName}/README.md`,
|
|
);
|
|
|
|
const postDate: string = this.getPostDateFromFileName(fileName);
|
|
const formattedPostDate: string =
|
|
this.getFormattedPostDateFromFileName(fileName);
|
|
|
|
const fileData:
|
|
| HTTPResponse<
|
|
| JSONObjectOrArray
|
|
| BaseModel
|
|
| BaseModel[]
|
|
| AnalyticsBaseModel
|
|
| AnalyticsBaseModel[]
|
|
>
|
|
| HTTPErrorResponse = await API.get(fileUrl);
|
|
|
|
if (fileData.isFailure()) {
|
|
if ((fileData as HTTPErrorResponse).statusCode === 404) {
|
|
return null;
|
|
}
|
|
|
|
throw fileData as HTTPErrorResponse;
|
|
}
|
|
|
|
let markdownContent: string =
|
|
(fileData.data as JSONObject)?.["data"]?.toString() || "";
|
|
|
|
const blogPostAuthor: BlogPostAuthor | null =
|
|
await this.getAuthorFromFileContent(markdownContent);
|
|
|
|
const title: string = this.getTitleFromFileContent(markdownContent);
|
|
const description: string =
|
|
this.getDescriptionFromFileContent(markdownContent);
|
|
const tags: Array<string> = this.getTagsFromFileContent(markdownContent);
|
|
|
|
markdownContent = this.getPostFromMarkdown(markdownContent);
|
|
|
|
const htmlBody: string = await Markdown.convertToHTML(
|
|
markdownContent,
|
|
MarkdownContentType.Blog,
|
|
);
|
|
|
|
const blogPost: BlogPost = {
|
|
title,
|
|
description,
|
|
author: blogPostAuthor,
|
|
htmlBody,
|
|
markdownBody: markdownContent,
|
|
fileName,
|
|
tags,
|
|
postDate,
|
|
formattedPostDate,
|
|
socialMediaImageUrl: `${GitHubRawUrl}/posts/${fileName}/social-media.png`,
|
|
blogUrl: `https://oneuptime.com/blog/post/${fileName}`, // this has to be oneuptime.com because its used in twitter cards and faceboomk cards. Please dont change this.
|
|
};
|
|
|
|
return blogPost;
|
|
}
|
|
|
|
private static getPostDateFromFileName(fileName: string): string {
|
|
const year: string | undefined = fileName.split("-")[0];
|
|
const month: string | undefined = fileName.split("-")[1];
|
|
const day: string | undefined = fileName.split("-")[2];
|
|
|
|
if (!year || !month || !day) {
|
|
throw new BadDataException("Invalid file name");
|
|
}
|
|
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
private static getFormattedPostDateFromFileName(fileName: string): string {
|
|
// file name is of the format YYYY-MM-DD-Title.md
|
|
const year: string | undefined = fileName.split("-")[0];
|
|
const month: string | undefined = fileName.split("-")[1];
|
|
const day: string | undefined = fileName.split("-")[2];
|
|
|
|
if (!year || !month || !day) {
|
|
throw new BadDataException("Invalid file name");
|
|
}
|
|
|
|
const date: Date = OneUptimeDate.getDateFromYYYYMMDD(year, month, day);
|
|
return OneUptimeDate.getDateAsLocalFormattedString(date, true);
|
|
}
|
|
|
|
private static getPostFromMarkdown(markdownContent: string): string {
|
|
const authorLine: string | undefined = markdownContent
|
|
.split("\n")
|
|
.find((line: string) => {
|
|
return line.startsWith("Author:");
|
|
});
|
|
const titleLine: string | undefined = markdownContent
|
|
.split("\n")
|
|
.find((line: string) => {
|
|
return line.startsWith("#");
|
|
});
|
|
const descriptionLine: string | undefined =
|
|
markdownContent.split("\n").find((line: string) => {
|
|
return line.startsWith("Description:");
|
|
}) || "";
|
|
|
|
const tagsLine: string | undefined =
|
|
markdownContent.split("\n").find((line: string) => {
|
|
return line.startsWith("Tags:");
|
|
}) || "";
|
|
|
|
if (!authorLine && !titleLine && !descriptionLine && !tagsLine) {
|
|
return markdownContent;
|
|
}
|
|
|
|
const lines: string[] = markdownContent.split("\n");
|
|
|
|
if (authorLine) {
|
|
const authorLineIndex: number = lines.indexOf(authorLine);
|
|
lines.splice(authorLineIndex, 1);
|
|
}
|
|
|
|
if (titleLine) {
|
|
const titleLineIndex: number = lines.indexOf(titleLine);
|
|
lines.splice(titleLineIndex, 1);
|
|
}
|
|
|
|
if (descriptionLine) {
|
|
const descriptionLineIndex: number = lines.indexOf(descriptionLine);
|
|
lines.splice(descriptionLineIndex, 1);
|
|
}
|
|
|
|
if (tagsLine) {
|
|
const tagsLineIndex: number = lines.indexOf(tagsLine);
|
|
lines.splice(tagsLineIndex, 1);
|
|
}
|
|
|
|
return lines.join("\n").trim();
|
|
}
|
|
|
|
public static getBlogPostFromCache(fileName: string): BlogPost | null {
|
|
const blogPost: BlogPost | null = LocalCache.getJSON(
|
|
"blog",
|
|
fileName,
|
|
) as BlogPost | null;
|
|
return blogPost;
|
|
}
|
|
|
|
public static getTitleFromFileContent(fileContent: string): string {
|
|
// title is the first line that stars with "#"
|
|
|
|
const titleLine: string =
|
|
fileContent
|
|
.split("\n")
|
|
.find((line: string) => {
|
|
return line.startsWith("#");
|
|
})
|
|
?.replace("#", "") || "OneUptime Blog";
|
|
|
|
return titleLine;
|
|
}
|
|
|
|
public static getTagsFromFileContent(fileContent: string): string[] {
|
|
// tags is the first line that starts with "Tags:"
|
|
|
|
const tagsLine: string | undefined =
|
|
fileContent
|
|
.split("\n")
|
|
.find((line: string) => {
|
|
return line.startsWith("Tags:");
|
|
})
|
|
?.replace("Tags:", "") || "";
|
|
|
|
return tagsLine.split(",").map((tag: string) => {
|
|
return tag.trim();
|
|
});
|
|
}
|
|
|
|
public static getDescriptionFromFileContent(fileContent: string): string {
|
|
// description is the first line that starts with ">"
|
|
|
|
const descriptionLine: string | undefined =
|
|
fileContent
|
|
.split("\n")
|
|
.find((line: string) => {
|
|
return line.startsWith("Description:");
|
|
})
|
|
?.replace("Description:", "") || "";
|
|
|
|
return descriptionLine;
|
|
}
|
|
|
|
public static async getAuthorFromFileContent(
|
|
fileContent: string,
|
|
): Promise<BlogPostAuthor | null> {
|
|
// author line is in this format: Author: [username](githubUrl)
|
|
|
|
const authorLine: string | undefined = fileContent
|
|
.split("\n")
|
|
.find((line: string) => {
|
|
return line.startsWith("Author:");
|
|
});
|
|
const authorUsername: string | undefined = authorLine
|
|
?.split("[")[1]
|
|
?.split("]")[0];
|
|
const authorGitHubUrl: string | undefined = authorLine
|
|
?.split("(")[1]
|
|
?.split(")")[0];
|
|
const authorProfileImageUrl: string = `https://avatars.githubusercontent.com/${authorUsername}`;
|
|
|
|
if (!authorUsername || !authorGitHubUrl) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
username: authorUsername,
|
|
githubUrl: authorGitHubUrl,
|
|
profileImageUrl: authorProfileImageUrl,
|
|
name: await this.getNameOfGitHubUser(authorUsername),
|
|
};
|
|
}
|
|
}
|