mirror of
https://github.com/wgh136/nysoure.git
synced 2025-12-16 15:51:14 +00:00
Compare commits
29 Commits
4550720cbb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cd743af7f | |||
| 79bae828d8 | |||
| a42087ce5c | |||
| a9d2f05562 | |||
| 31b9fb5d45 | |||
| 116efcdf93 | |||
| 9ad8d9d7e9 | |||
| 8e2ab62297 | |||
| cb61ce99bf | |||
| 2767f8a30f | |||
| 5a5c2edfda | |||
| d860bdf06a | |||
| a3de195eca | |||
| 6cabff8f8d | |||
| 78f6130b23 | |||
| ddd856529b | |||
| 48638111ec | |||
| d255ecc503 | |||
| 00321b01c3 | |||
| 59904223b4 | |||
| b732e1be83 | |||
| fd86d6c221 | |||
| fbe8ac27bf | |||
| fb1f47c0c0 | |||
| ecfea63edd | |||
| ae547522ed | |||
| 96cdd2c41c | |||
| 566234c30c | |||
| 4a6c214709 |
@@ -32,4 +32,7 @@ BACKUP_SCHEDULE=0 2 * * *
|
|||||||
BACKUP_RETENTION_DAYS=30
|
BACKUP_RETENTION_DAYS=30
|
||||||
|
|
||||||
# Download Configuration
|
# Download Configuration
|
||||||
DOWNLOAD_SECRET_KEY=your_download_secret_key_here
|
DOWNLOAD_SECRET_KEY=your_download_secret_key_here
|
||||||
|
|
||||||
|
# Access Key for Development API
|
||||||
|
DEV_ACCESS_KEY=your_dev_access_key_here
|
||||||
@@ -19,6 +19,7 @@ import CreateCollectionPage from "./pages/create_collection_page.tsx";
|
|||||||
import CollectionPage from "./pages/collection_page.tsx";
|
import CollectionPage from "./pages/collection_page.tsx";
|
||||||
import { i18nData } from "./i18n.ts";
|
import { i18nData } from "./i18n.ts";
|
||||||
import { i18nContext } from "./utils/i18n.ts";
|
import { i18nContext } from "./utils/i18n.ts";
|
||||||
|
import NotificationPage from "./pages/notification_page.tsx";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -49,6 +50,7 @@ export default function App() {
|
|||||||
element={<CreateCollectionPage />}
|
element={<CreateCollectionPage />}
|
||||||
/>
|
/>
|
||||||
<Route path={"/collection/:id"} element={<CollectionPage />} />
|
<Route path={"/collection/:id"} element={<CollectionPage />} />
|
||||||
|
<Route path={"/notifications"} element={<NotificationPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { app } from "../app.ts";
|
|||||||
import { network } from "../network/network.ts";
|
import { network } from "../network/network.ts";
|
||||||
import { useNavigate, useOutlet } from "react-router";
|
import { useNavigate, useOutlet } from "react-router";
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
import { MdArrowUpward, MdOutlinePerson, MdSearch } from "react-icons/md";
|
import { MdArrowUpward, MdOutlinePerson, MdSearch, MdNotifications } from "react-icons/md";
|
||||||
import { useTranslation } from "../utils/i18n";
|
import { useTranslation } from "../utils/i18n";
|
||||||
import UploadingSideBar from "./uploading_side_bar.tsx";
|
import UploadingSideBar from "./uploading_side_bar.tsx";
|
||||||
import { ThemeSwitcher } from "./theme_switcher.tsx";
|
import { ThemeSwitcher } from "./theme_switcher.tsx";
|
||||||
import { IoLogoGithub } from "react-icons/io";
|
|
||||||
import { useAppContext } from "./AppContext.tsx";
|
import { useAppContext } from "./AppContext.tsx";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
@@ -234,16 +233,7 @@ export default function Navigator() {
|
|||||||
<SearchBar />
|
<SearchBar />
|
||||||
<UploadingSideBar />
|
<UploadingSideBar />
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<a
|
{app.isLoggedIn() && <NotificationButton />}
|
||||||
className={"hidden sm:inline"}
|
|
||||||
href="https://github.com/wgh136/nysoure"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<button className={"btn btn-circle btn-ghost"}>
|
|
||||||
<IoLogoGithub size={24} />
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
{app.isLoggedIn() ? (
|
{app.isLoggedIn() ? (
|
||||||
<UserButton />
|
<UserButton />
|
||||||
) : (
|
) : (
|
||||||
@@ -554,3 +544,41 @@ function FloatingToTopButton() {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NotificationButton() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCount = async () => {
|
||||||
|
if (!app.isLoggedIn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await network.getUserNotificationsCount();
|
||||||
|
if (res.success && res.data !== undefined) {
|
||||||
|
setCount(res.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCount();
|
||||||
|
const interval = setInterval(fetchCount, 60000); // 每分钟请求一次
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="indicator">
|
||||||
|
{count > 0 && <span className="bg-error text-white text-xs rounded-full px-1 indicator-item">
|
||||||
|
{count > 99 ? "99+" : count}
|
||||||
|
</span>}
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-circle"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/notifications");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdNotifications size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ export const i18nData = {
|
|||||||
"Cloudflare Turnstile Secret Key": "Cloudflare Turnstile 密钥",
|
"Cloudflare Turnstile Secret Key": "Cloudflare Turnstile 密钥",
|
||||||
"If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.":
|
"If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.":
|
||||||
"如果设置了 Cloudflare Turnstile 密钥,将在注册和下载时启用验证",
|
"如果设置了 Cloudflare Turnstile 密钥,将在注册和下载时启用验证",
|
||||||
"The first image will be used as the cover image":
|
"You can select a cover image using the radio button in the Cover column":
|
||||||
"第一张图片将用作封面图片",
|
"您可以使用封面列中的单选按钮选择封面图片",
|
||||||
"Please enter a search keyword": "请输入搜索关键词",
|
"Please enter a search keyword": "请输入搜索关键词",
|
||||||
"Searching...": "搜索中...",
|
"Searching...": "搜索中...",
|
||||||
"Create Tag": "创建标签",
|
"Create Tag": "创建标签",
|
||||||
@@ -179,6 +179,8 @@ export const i18nData = {
|
|||||||
"Views Descending": "浏览量降序",
|
"Views Descending": "浏览量降序",
|
||||||
"Downloads Ascending": "下载量升序",
|
"Downloads Ascending": "下载量升序",
|
||||||
"Downloads Descending": "下载量降序",
|
"Downloads Descending": "下载量降序",
|
||||||
|
"Release Date Ascending": "发布日期升序",
|
||||||
|
"Release Date Descending": "发布日期降序",
|
||||||
"File Url": "文件链接",
|
"File Url": "文件链接",
|
||||||
"Provide a file url for the server to download, and the file will be moved to the selected storage.":
|
"Provide a file url for the server to download, and the file will be moved to the selected storage.":
|
||||||
"提供一个文件链接供服务器下载,文件将被移动到选定的存储中。",
|
"提供一个文件链接供服务器下载,文件将被移动到选定的存储中。",
|
||||||
@@ -262,6 +264,8 @@ export const i18nData = {
|
|||||||
"Tag": "标签",
|
"Tag": "标签",
|
||||||
"Optional": "可选",
|
"Optional": "可选",
|
||||||
"Download": "下载",
|
"Download": "下载",
|
||||||
|
"Notifications": "通知",
|
||||||
|
"Release Date": "发售日期",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"zh-TW": {
|
"zh-TW": {
|
||||||
@@ -421,8 +425,8 @@ export const i18nData = {
|
|||||||
"Cloudflare Turnstile Secret Key": "Cloudflare Turnstile 密鑰",
|
"Cloudflare Turnstile Secret Key": "Cloudflare Turnstile 密鑰",
|
||||||
"If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.":
|
"If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.":
|
||||||
"如果設置了 Cloudflare Turnstile 密鑰,將在註冊和下載時啟用驗證",
|
"如果設置了 Cloudflare Turnstile 密鑰,將在註冊和下載時啟用驗證",
|
||||||
"The first image will be used as the cover image":
|
"You can select a cover image using the radio button in the Cover column":
|
||||||
"第一張圖片將用作封面圖片",
|
"您可以使用封面列中的單選按鈕選擇封面圖片",
|
||||||
"Please enter a search keyword": "請輸入搜尋關鍵字",
|
"Please enter a search keyword": "請輸入搜尋關鍵字",
|
||||||
"Searching...": "搜尋中...",
|
"Searching...": "搜尋中...",
|
||||||
"Create Tag": "創建標籤",
|
"Create Tag": "創建標籤",
|
||||||
@@ -444,6 +448,8 @@ export const i18nData = {
|
|||||||
"Views Descending": "瀏覽量降序",
|
"Views Descending": "瀏覽量降序",
|
||||||
"Downloads Ascending": "下載量升序",
|
"Downloads Ascending": "下載量升序",
|
||||||
"Downloads Descending": "下載量降序",
|
"Downloads Descending": "下載量降序",
|
||||||
|
"Release Date Ascending": "發布日期升序",
|
||||||
|
"Release Date Descending": "發布日期降序",
|
||||||
"File Url": "檔案連結",
|
"File Url": "檔案連結",
|
||||||
"Provide a file url for the server to download, and the file will be moved to the selected storage.":
|
"Provide a file url for the server to download, and the file will be moved to the selected storage.":
|
||||||
"提供一個檔案連結供伺服器下載,檔案將被移動到選定的儲存中。",
|
"提供一個檔案連結供伺服器下載,檔案將被移動到選定的儲存中。",
|
||||||
@@ -527,6 +533,8 @@ export const i18nData = {
|
|||||||
"Tag": "標籤",
|
"Tag": "標籤",
|
||||||
"Optional": "可選",
|
"Optional": "可選",
|
||||||
"Download": "下載",
|
"Download": "下載",
|
||||||
|
"Notifications": "通知",
|
||||||
|
"Release Date": "發售日期",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ export interface CreateResourceParams {
|
|||||||
title: string;
|
title: string;
|
||||||
alternative_titles: string[];
|
alternative_titles: string[];
|
||||||
links: RLink[];
|
links: RLink[];
|
||||||
|
release_date?: string;
|
||||||
tags: number[];
|
tags: number[];
|
||||||
article: string;
|
article: string;
|
||||||
images: number[];
|
images: number[];
|
||||||
|
cover_id?: number;
|
||||||
gallery: number[];
|
gallery: number[];
|
||||||
gallery_nsfw: number[];
|
gallery_nsfw: number[];
|
||||||
characters: CharacterParams[];
|
characters: CharacterParams[];
|
||||||
@@ -77,6 +79,7 @@ export interface Resource {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
release_date?: string;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
image?: Image;
|
image?: Image;
|
||||||
author: User;
|
author: User;
|
||||||
@@ -89,8 +92,10 @@ export interface ResourceDetails {
|
|||||||
links: RLink[];
|
links: RLink[];
|
||||||
article: string;
|
article: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
releaseDate?: string;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
images: Image[];
|
images: Image[];
|
||||||
|
coverId?: number;
|
||||||
files: RFile[];
|
files: RFile[];
|
||||||
author: User;
|
author: User;
|
||||||
views: number;
|
views: number;
|
||||||
@@ -194,6 +199,8 @@ export enum RSort {
|
|||||||
ViewsDesc = 3,
|
ViewsDesc = 3,
|
||||||
DownloadsAsc = 4,
|
DownloadsAsc = 4,
|
||||||
DownloadsDesc = 5,
|
DownloadsDesc = 5,
|
||||||
|
ReleaseDateAsc = 6,
|
||||||
|
ReleaseDateDesc = 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActivityType {
|
export enum ActivityType {
|
||||||
|
|||||||
@@ -730,6 +730,26 @@ class Network {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserNotifications(page: number = 1): Promise<PageResponse<Activity>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.get(`${this.apiBaseUrl}/notification`, {
|
||||||
|
params: { page },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetUserNotificationsCount(): Promise<Response<void>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.post(`${this.apiBaseUrl}/notification/reset`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserNotificationsCount(): Promise<Response<number>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.get(`${this.apiBaseUrl}/notification/count`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async createCollection(
|
async createCollection(
|
||||||
title: string,
|
title: string,
|
||||||
article: string,
|
article: string,
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ import CharacterEditer, { FetchVndbCharactersButton } from "../components/charac
|
|||||||
export default function EditResourcePage() {
|
export default function EditResourcePage() {
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
const [altTitles, setAltTitles] = useState<string[]>([]);
|
const [altTitles, setAltTitles] = useState<string[]>([]);
|
||||||
|
const [releaseDate, setReleaseDate] = useState<string | undefined>(undefined);
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
const [article, setArticle] = useState<string>("");
|
const [article, setArticle] = useState<string>("");
|
||||||
const [images, setImages] = useState<number[]>([]);
|
const [images, setImages] = useState<number[]>([]);
|
||||||
|
const [coverId, setCoverId] = useState<number | undefined>(undefined);
|
||||||
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
||||||
const [galleryImages, setGalleryImages] = useState<number[]>([]);
|
const [galleryImages, setGalleryImages] = useState<number[]>([]);
|
||||||
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
|
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
|
||||||
@@ -58,9 +60,11 @@ export default function EditResourcePage() {
|
|||||||
setTags(data.tags);
|
setTags(data.tags);
|
||||||
setArticle(data.article);
|
setArticle(data.article);
|
||||||
setImages(data.images.map((i) => i.id));
|
setImages(data.images.map((i) => i.id));
|
||||||
|
setCoverId(data.coverId);
|
||||||
setLinks(data.links ?? []);
|
setLinks(data.links ?? []);
|
||||||
setGalleryImages(data.gallery ?? []);
|
setGalleryImages(data.gallery ?? []);
|
||||||
setGalleryNsfw(data.galleryNsfw ?? []);
|
setGalleryNsfw(data.galleryNsfw ?? []);
|
||||||
|
setReleaseDate(data.releaseDate?.split("T")[0] ?? undefined);
|
||||||
setCharacters(data.characters ?? []);
|
setCharacters(data.characters ?? []);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -104,10 +108,12 @@ export default function EditResourcePage() {
|
|||||||
tags: tags.map((tag) => tag.id),
|
tags: tags.map((tag) => tag.id),
|
||||||
article: article,
|
article: article,
|
||||||
images: images,
|
images: images,
|
||||||
|
cover_id: coverId,
|
||||||
links: links,
|
links: links,
|
||||||
gallery: galleryImages,
|
gallery: galleryImages,
|
||||||
gallery_nsfw: galleryNsfw,
|
gallery_nsfw: galleryNsfw,
|
||||||
characters: characters,
|
characters: characters,
|
||||||
|
release_date: releaseDate,
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@@ -194,6 +200,14 @@ export default function EditResourcePage() {
|
|||||||
{t("Add Alternative Title")}
|
{t("Add Alternative Title")}
|
||||||
</button>
|
</button>
|
||||||
<div className={"h-2"}></div>
|
<div className={"h-2"}></div>
|
||||||
|
<p className={"my-1"}>{t("Release Date")}</p>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="input"
|
||||||
|
value={releaseDate || ""}
|
||||||
|
onChange={(e) => setReleaseDate(e.target.value || undefined)}
|
||||||
|
/>
|
||||||
|
<div className={"h-4"}></div>
|
||||||
<p className={"my-1"}>{t("Links")}</p>
|
<p className={"my-1"}>{t("Links")}</p>
|
||||||
<div className={"flex flex-col"}>
|
<div className={"flex flex-col"}>
|
||||||
{links.map((link, index) => {
|
{links.map((link, index) => {
|
||||||
@@ -317,7 +331,7 @@ export default function EditResourcePage() {
|
|||||||
"Images will not be displayed automatically, you need to reference them in the description",
|
"Images will not be displayed automatically, you need to reference them in the description",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p>{t("The first image will be used as the cover image")}</p>
|
<p>{t("You can select a cover image using the radio button in the Cover column")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -328,6 +342,7 @@ export default function EditResourcePage() {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{t("Preview")}</td>
|
<td>{t("Preview")}</td>
|
||||||
<td>{"Markdown"}</td>
|
<td>{"Markdown"}</td>
|
||||||
|
<td>{t("Cover")}</td>
|
||||||
<td>{t("Gallery")}</td>
|
<td>{t("Gallery")}</td>
|
||||||
<td>{"Nsfw"}</td>
|
<td>{"Nsfw"}</td>
|
||||||
<td>{t("Action")}</td>
|
<td>{t("Action")}</td>
|
||||||
@@ -357,6 +372,15 @@ export default function EditResourcePage() {
|
|||||||
<MdContentCopy />
|
<MdContentCopy />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cover"
|
||||||
|
className="radio radio-accent"
|
||||||
|
checked={coverId === image}
|
||||||
|
onChange={() => setCoverId(image)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -398,6 +422,9 @@ export default function EditResourcePage() {
|
|||||||
const newImages = [...images];
|
const newImages = [...images];
|
||||||
newImages.splice(index, 1);
|
newImages.splice(index, 1);
|
||||||
setImages(newImages);
|
setImages(newImages);
|
||||||
|
if (coverId === id) {
|
||||||
|
setCoverId(undefined);
|
||||||
|
}
|
||||||
network.deleteImage(id);
|
network.deleteImage(id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { app } from "../app.ts";
|
|||||||
import { Resource, RSort, Statistics } from "../network/models.ts";
|
import { Resource, RSort, Statistics } from "../network/models.ts";
|
||||||
import { useTranslation } from "../utils/i18n";
|
import { useTranslation } from "../utils/i18n";
|
||||||
import { useAppContext } from "../components/AppContext.tsx";
|
import { useAppContext } from "../components/AppContext.tsx";
|
||||||
import Select from "../components/select.tsx";
|
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { useNavigator } from "../components/navigator.tsx";
|
import { useNavigator } from "../components/navigator.tsx";
|
||||||
import {
|
import {
|
||||||
@@ -40,23 +39,32 @@ export default function HomePage() {
|
|||||||
<>
|
<>
|
||||||
<HomeHeader />
|
<HomeHeader />
|
||||||
<div className={"flex pt-4 px-4 items-center"}>
|
<div className={"flex pt-4 px-4 items-center"}>
|
||||||
<Select
|
<select
|
||||||
values={[
|
value={order}
|
||||||
|
className="select select-primary max-w-72"
|
||||||
|
onChange={(e) => {
|
||||||
|
const order = parseInt(e.target.value);
|
||||||
|
setOrder(order);
|
||||||
|
if (appContext) {
|
||||||
|
appContext.set("home_page_order", order);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[
|
||||||
t("Time Ascending"),
|
t("Time Ascending"),
|
||||||
t("Time Descending"),
|
t("Time Descending"),
|
||||||
t("Views Ascending"),
|
t("Views Ascending"),
|
||||||
t("Views Descending"),
|
t("Views Descending"),
|
||||||
t("Downloads Ascending"),
|
t("Downloads Ascending"),
|
||||||
t("Downloads Descending"),
|
t("Downloads Descending"),
|
||||||
]}
|
t("Release Date Ascending"),
|
||||||
current={order}
|
t("Release Date Descending"),
|
||||||
onSelected={(index) => {
|
].map((label, idx) => (
|
||||||
setOrder(index);
|
<option key={idx} value={idx}>
|
||||||
if (appContext) {
|
{label}
|
||||||
appContext.set("home_page_order", index);
|
</option>
|
||||||
}
|
))}
|
||||||
}}
|
</select>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ResourcesView
|
<ResourcesView
|
||||||
key={`home_page_${order}`}
|
key={`home_page_${order}`}
|
||||||
|
|||||||
212
frontend/src/pages/notification_page.tsx
Normal file
212
frontend/src/pages/notification_page.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { Activity, ActivityType } from "../network/models.ts";
|
||||||
|
import { network } from "../network/network.ts";
|
||||||
|
import showToast from "../components/toast.ts";
|
||||||
|
import { useTranslation } from "../utils/i18n";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import Loading from "../components/loading.tsx";
|
||||||
|
import { CommentContent } from "../components/comment_tile.tsx";
|
||||||
|
import { MdOutlineArchive, MdOutlinePhotoAlbum } from "react-icons/md";
|
||||||
|
import Badge from "../components/badge.tsx";
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import { ErrorAlert } from "../components/alert.tsx";
|
||||||
|
import { app } from "../app.ts";
|
||||||
|
import { useNavigator } from "../components/navigator.tsx";
|
||||||
|
|
||||||
|
export default function NotificationPage() {
|
||||||
|
const [activities, setActivities] = useState<Activity[]>([]);
|
||||||
|
const pageRef = useRef(0);
|
||||||
|
const maxPageRef = useRef(1);
|
||||||
|
const isLoadingRef = useRef(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigator = useNavigator();
|
||||||
|
|
||||||
|
const fetchNextPage = useCallback(async () => {
|
||||||
|
if (isLoadingRef.current || pageRef.current >= maxPageRef.current) return;
|
||||||
|
isLoadingRef.current = true;
|
||||||
|
const response = await network.getUserNotifications(pageRef.current + 1);
|
||||||
|
if (response.success) {
|
||||||
|
setActivities((prev) => [...prev, ...response.data!]);
|
||||||
|
pageRef.current += 1;
|
||||||
|
maxPageRef.current = response.totalPages!;
|
||||||
|
} else {
|
||||||
|
showToast({
|
||||||
|
type: "error",
|
||||||
|
message: response.message || "Failed to load activities",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isLoadingRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchNextPage();
|
||||||
|
}, [fetchNextPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
network.resetUserNotificationsCount();
|
||||||
|
navigator.refresh();
|
||||||
|
}, [navigator]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("Notifications");
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (
|
||||||
|
window.innerHeight + window.scrollY >=
|
||||||
|
document.documentElement.scrollHeight - 100 &&
|
||||||
|
!isLoadingRef.current &&
|
||||||
|
pageRef.current < maxPageRef.current
|
||||||
|
) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
|
}, [fetchNextPage]);
|
||||||
|
|
||||||
|
if (!app.user) {
|
||||||
|
return (
|
||||||
|
<ErrorAlert
|
||||||
|
className={"m-4"}
|
||||||
|
message={t("You are not logged in. Please log in to access this page.")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"pb-2"}>
|
||||||
|
{activities.map((activity) => (
|
||||||
|
<ActivityCard key={activity.id} activity={activity} />
|
||||||
|
))}
|
||||||
|
{pageRef.current < maxPageRef.current && <Loading />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileSizeToString(size: number) {
|
||||||
|
if (size < 1024) {
|
||||||
|
return size + "B";
|
||||||
|
} else if (size < 1024 * 1024) {
|
||||||
|
return (size / 1024).toFixed(2) + "KB";
|
||||||
|
} else if (size < 1024 * 1024 * 1024) {
|
||||||
|
return (size / 1024 / 1024).toFixed(2) + "MB";
|
||||||
|
} else {
|
||||||
|
return (size / 1024 / 1024 / 1024).toFixed(2) + "GB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActivityCard({ activity }: { activity: Activity }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
"Unknown activity",
|
||||||
|
t("Published a resource"),
|
||||||
|
t("Updated a resource"),
|
||||||
|
t("Posted a comment"),
|
||||||
|
t("Added a new file"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
let content = <></>;
|
||||||
|
|
||||||
|
if (
|
||||||
|
activity.type === ActivityType.ResourcePublished ||
|
||||||
|
activity.type === ActivityType.ResourceUpdated
|
||||||
|
) {
|
||||||
|
content = (
|
||||||
|
<div className={"mx-1"}>
|
||||||
|
<div className={"font-bold my-4 break-all"}>
|
||||||
|
{activity.resource?.title}
|
||||||
|
</div>
|
||||||
|
{activity.resource?.image && (
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
className={"object-contain max-h-52 mt-2 rounded-lg"}
|
||||||
|
src={network.getResampledImageUrl(activity.resource.image.id)}
|
||||||
|
alt={activity.resource.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (activity.type === ActivityType.NewComment) {
|
||||||
|
content = (
|
||||||
|
<div className="comment_tile">
|
||||||
|
<CommentContent content={activity.comment!.content} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (activity.type === ActivityType.NewFile) {
|
||||||
|
content = (
|
||||||
|
<div>
|
||||||
|
<h4 className={"font-bold py-2 break-all"}>
|
||||||
|
{activity.file!.filename}
|
||||||
|
</h4>
|
||||||
|
<div className={"text-sm my-1 comment_tile"}>
|
||||||
|
<Markdown>
|
||||||
|
{activity.file!.description.replaceAll("\n", " \n")}
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
|
<p className={"pt-1"}>
|
||||||
|
<Badge className={"badge-soft badge-secondary text-xs mr-2"}>
|
||||||
|
<MdOutlineArchive size={16} className={"inline-block"} />
|
||||||
|
{activity.file!.is_redirect
|
||||||
|
? t("Redirect")
|
||||||
|
: fileSizeToString(activity.file!.size)}
|
||||||
|
</Badge>
|
||||||
|
<Badge className={"badge-soft badge-accent text-xs mr-2"}>
|
||||||
|
<MdOutlinePhotoAlbum size={16} className={"inline-block"} />
|
||||||
|
{(() => {
|
||||||
|
let title = activity.resource!.title;
|
||||||
|
if (title.length > 20) {
|
||||||
|
title = title.slice(0, 20) + "...";
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
})()}
|
||||||
|
</Badge>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"card shadow m-4 p-4 hover:shadow-md transition-shadow cursor-pointer bg-base-100-tr82"
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
activity.type === ActivityType.ResourcePublished ||
|
||||||
|
activity.type === ActivityType.ResourceUpdated
|
||||||
|
) {
|
||||||
|
navigate(`/resources/${activity.resource?.id}`);
|
||||||
|
} else if (activity.type === ActivityType.NewComment) {
|
||||||
|
navigate(`/comments/${activity.comment?.id}`);
|
||||||
|
} else if (activity.type === ActivityType.NewFile) {
|
||||||
|
navigate(`/resources/${activity.resource?.id}#files`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={"flex items-center"}>
|
||||||
|
<div className={"avatar w-9 h-9 rounded-full"}>
|
||||||
|
<img
|
||||||
|
className={"rounded-full"}
|
||||||
|
alt={"avatar"}
|
||||||
|
src={network.getUserAvatar(activity.user!)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className={"mx-2 font-bold text-sm"}>
|
||||||
|
{activity.user?.username}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={"ml-2 badge-sm sm:badge-md badge badge-primary badge-soft"}
|
||||||
|
>
|
||||||
|
{messages[activity.type]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,9 +24,11 @@ import CharacterEditer, { FetchVndbCharactersButton } from "../components/charac
|
|||||||
export default function PublishPage() {
|
export default function PublishPage() {
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
const [altTitles, setAltTitles] = useState<string[]>([]);
|
const [altTitles, setAltTitles] = useState<string[]>([]);
|
||||||
|
const [releaseDate, setReleaseDate] = useState<string | undefined>(undefined);
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
const [article, setArticle] = useState<string>("");
|
const [article, setArticle] = useState<string>("");
|
||||||
const [images, setImages] = useState<number[]>([]);
|
const [images, setImages] = useState<number[]>([]);
|
||||||
|
const [coverId, setCoverId] = useState<number | undefined>(undefined);
|
||||||
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
||||||
const [galleryImages, setGalleryImages] = useState<number[]>([]);
|
const [galleryImages, setGalleryImages] = useState<number[]>([]);
|
||||||
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
|
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
|
||||||
@@ -43,9 +45,15 @@ export default function PublishPage() {
|
|||||||
const data = JSON.parse(oldData);
|
const data = JSON.parse(oldData);
|
||||||
setTitle(data.title || "");
|
setTitle(data.title || "");
|
||||||
setAltTitles(data.alternative_titles || []);
|
setAltTitles(data.alternative_titles || []);
|
||||||
|
setReleaseDate(data.release_date || undefined);
|
||||||
setTags(data.tags || []);
|
setTags(data.tags || []);
|
||||||
setArticle(data.article || "");
|
setArticle(data.article || "");
|
||||||
setImages(data.images || []);
|
setImages(data.images || []);
|
||||||
|
setCoverId(data.cover_id || undefined);
|
||||||
|
setLinks(data.links || []);
|
||||||
|
setGalleryImages(data.gallery || []);
|
||||||
|
setGalleryNsfw(data.gallery_nsfw || []);
|
||||||
|
setCharacters(data.characters || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse publish_data from localStorage", e);
|
console.error("Failed to parse publish_data from localStorage", e);
|
||||||
}
|
}
|
||||||
@@ -58,11 +66,17 @@ export default function PublishPage() {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
article: article,
|
article: article,
|
||||||
images: images,
|
images: images,
|
||||||
|
cover_id: coverId,
|
||||||
|
links: links,
|
||||||
|
gallery: galleryImages,
|
||||||
|
gallery_nsfw: galleryNsfw,
|
||||||
|
characters: characters,
|
||||||
|
release_date: releaseDate,
|
||||||
};
|
};
|
||||||
const dataString = JSON.stringify(data);
|
const dataString = JSON.stringify(data);
|
||||||
localStorage.setItem("publish_data", dataString);
|
localStorage.setItem("publish_data", dataString);
|
||||||
}
|
}
|
||||||
}, [altTitles, article, images, tags, title]);
|
}, [altTitles, article, images, coverId, tags, title, links, galleryImages, galleryNsfw, characters, releaseDate]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -105,9 +119,11 @@ export default function PublishPage() {
|
|||||||
const res = await network.createResource({
|
const res = await network.createResource({
|
||||||
title: title,
|
title: title,
|
||||||
alternative_titles: altTitles,
|
alternative_titles: altTitles,
|
||||||
|
release_date: releaseDate,
|
||||||
tags: tags.map((tag) => tag.id),
|
tags: tags.map((tag) => tag.id),
|
||||||
article: article,
|
article: article,
|
||||||
images: images,
|
images: images,
|
||||||
|
cover_id: coverId,
|
||||||
links: links,
|
links: links,
|
||||||
gallery: galleryImages,
|
gallery: galleryImages,
|
||||||
gallery_nsfw: galleryNsfw,
|
gallery_nsfw: galleryNsfw,
|
||||||
@@ -201,6 +217,14 @@ export default function PublishPage() {
|
|||||||
{t("Add Alternative Title")}
|
{t("Add Alternative Title")}
|
||||||
</button>
|
</button>
|
||||||
<div className={"h-2"}></div>
|
<div className={"h-2"}></div>
|
||||||
|
<p className={"my-1"}>{t("Release Date")}</p>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="input"
|
||||||
|
value={releaseDate || ""}
|
||||||
|
onChange={(e) => setReleaseDate(e.target.value || undefined)}
|
||||||
|
/>
|
||||||
|
<div className={"h-4"}></div>
|
||||||
<p className={"my-1"}>{t("Links")}</p>
|
<p className={"my-1"}>{t("Links")}</p>
|
||||||
<div className={"flex flex-col"}>
|
<div className={"flex flex-col"}>
|
||||||
{links.map((link, index) => {
|
{links.map((link, index) => {
|
||||||
@@ -324,7 +348,7 @@ export default function PublishPage() {
|
|||||||
"Images will not be displayed automatically, you need to reference them in the description",
|
"Images will not be displayed automatically, you need to reference them in the description",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p>{t("The first image will be used as the cover image")}</p>
|
<p>{t("You can select a cover image using the radio button in the Cover column")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -335,7 +359,8 @@ export default function PublishPage() {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{t("Preview")}</td>
|
<td>{t("Preview")}</td>
|
||||||
<td>{"Markdown"}</td>
|
<td>{"Markdown"}</td>
|
||||||
<td>{"Gallery"}</td>
|
<td>{t("Cover")}</td>
|
||||||
|
<td>{t("Gallery")}</td>
|
||||||
<td>{"Nsfw"}</td>
|
<td>{"Nsfw"}</td>
|
||||||
<td>{t("Action")}</td>
|
<td>{t("Action")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -364,6 +389,15 @@ export default function PublishPage() {
|
|||||||
<MdContentCopy />
|
<MdContentCopy />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cover"
|
||||||
|
className="radio radio-accent"
|
||||||
|
checked={coverId === image}
|
||||||
|
onChange={() => setCoverId(image)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -405,6 +439,9 @@ export default function PublishPage() {
|
|||||||
const newImages = [...images];
|
const newImages = [...images];
|
||||||
newImages.splice(index, 1);
|
newImages.splice(index, 1);
|
||||||
setImages(newImages);
|
setImages(newImages);
|
||||||
|
if (coverId === id) {
|
||||||
|
setCoverId(undefined);
|
||||||
|
}
|
||||||
network.deleteImage(id);
|
network.deleteImage(id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -209,6 +209,14 @@ export default function ResourcePage() {
|
|||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{
|
||||||
|
resource.releaseDate ? (
|
||||||
|
<div className={"px-4 py-1 text-sm text-gray-600 dark:text-gray-400 flex items-center"}>
|
||||||
|
<MdOutlineAccessTime size={18} className={"inline-block mr-1"} />
|
||||||
|
{t("Release Date")}: {resource.releaseDate.split("T")[0]}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(
|
navigate(
|
||||||
|
|||||||
14
go.mod
14
go.mod
@@ -14,14 +14,17 @@ require (
|
|||||||
github.com/blevesearch/bleve v1.0.14
|
github.com/blevesearch/bleve v1.0.14
|
||||||
github.com/chai2010/webp v1.4.0
|
github.com/chai2010/webp v1.4.0
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
|
github.com/jlaffaye/ftp v0.2.0
|
||||||
github.com/redis/go-redis/v9 v9.17.0
|
github.com/redis/go-redis/v9 v9.17.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/wgh136/cloudflare-error-page v0.0.1
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/RoaringBitmap/roaring v0.4.23 // indirect
|
github.com/RoaringBitmap/roaring v0.4.23 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||||
github.com/blevesearch/mmap-go v1.0.2 // indirect
|
github.com/blevesearch/mmap-go v1.0.2 // indirect
|
||||||
github.com/blevesearch/segment v0.9.0 // indirect
|
github.com/blevesearch/segment v0.9.0 // indirect
|
||||||
@@ -37,16 +40,22 @@ require (
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 // indirect
|
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/golang/protobuf v1.3.2 // indirect
|
github.com/golang/protobuf v1.5.0 // indirect
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/jlaffaye/ftp v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
github.com/steveyen/gtreap v0.1.0 // indirect
|
github.com/steveyen/gtreap v0.1.0 // indirect
|
||||||
github.com/willf/bitset v1.1.10 // indirect
|
github.com/willf/bitset v1.1.10 // indirect
|
||||||
go.etcd.io/bbolt v1.3.5 // indirect
|
go.etcd.io/bbolt v1.3.5 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,6 +84,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/tinylib/msgp v1.3.0 // indirect
|
github.com/tinylib/msgp v1.3.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.65.0 // indirect
|
github.com/valyala/fasthttp v1.65.0 // indirect
|
||||||
|
|||||||
40
go.sum
40
go.sum
@@ -6,6 +6,8 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06
|
|||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/blevesearch/bleve v1.0.14 h1:Q8r+fHTt35jtGXJUM0ULwM3Tzg+MRfyai4ZkWDy2xO4=
|
github.com/blevesearch/bleve v1.0.14 h1:Q8r+fHTt35jtGXJUM0ULwM3Tzg+MRfyai4ZkWDy2xO4=
|
||||||
github.com/blevesearch/bleve v1.0.14/go.mod h1:e/LJTr+E7EaoVdkQZTfoz7dt4KoDNvDbLb8MSKuNTLQ=
|
github.com/blevesearch/bleve v1.0.14/go.mod h1:e/LJTr+E7EaoVdkQZTfoz7dt4KoDNvDbLb8MSKuNTLQ=
|
||||||
github.com/blevesearch/blevex v1.0.0 h1:pnilj2Qi3YSEGdWgLj1Pn9Io7ukfXPoQcpAI1Bv8n/o=
|
github.com/blevesearch/blevex v1.0.0 h1:pnilj2Qi3YSEGdWgLj1Pn9Io7ukfXPoQcpAI1Bv8n/o=
|
||||||
@@ -45,6 +47,7 @@ github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37g
|
|||||||
github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw=
|
github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw=
|
||||||
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
|
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
|
||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d h1:SwD98825d6bdB+pEuTxWOXiSjBrHdOl/UVp75eI7JT8=
|
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d h1:SwD98825d6bdB+pEuTxWOXiSjBrHdOl/UVp75eI7JT8=
|
||||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
|
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
|
||||||
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||||
@@ -83,13 +86,17 @@ github.com/gofiber/utils/v2 v2.0.0-rc.1/go.mod h1:Y1g08g7gvST49bbjHJ1AVqcsmg9391
|
|||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
|
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
|
||||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
@@ -121,6 +128,12 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
|||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
|
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
@@ -139,6 +152,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
|||||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
@@ -148,10 +163,20 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
|||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||||
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
|
github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
|
||||||
github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
@@ -187,6 +212,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
|||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
||||||
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
||||||
|
github.com/wgh136/cloudflare-error-page v0.0.1 h1:OZ2JWfEF85JlwSVE71Jx0f+++HkotvZZ1Fb6YUyoFcQ=
|
||||||
|
github.com/wgh136/cloudflare-error-page v0.0.1/go.mod h1:/0dw1xavAlZLFlJla5qeLIh1/hv0irtR8oN7SBVMD8s=
|
||||||
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
|
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
|
||||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
@@ -196,6 +223,10 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
|||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
@@ -221,8 +252,13 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|||||||
7
main.go
7
main.go
@@ -6,7 +6,9 @@ import (
|
|||||||
"nysoure/server/middleware"
|
"nysoure/server/middleware"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
||||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||||
|
prom "github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -29,6 +31,10 @@ func main() {
|
|||||||
|
|
||||||
app.Use(middleware.FrontendMiddleware)
|
app.Use(middleware.FrontendMiddleware)
|
||||||
|
|
||||||
|
app.Use(middleware.StatMiddleware)
|
||||||
|
|
||||||
|
app.Get("/metrics", adaptor.HTTPHandler(prom.Handler()))
|
||||||
|
|
||||||
apiG := app.Group("/api")
|
apiG := app.Group("/api")
|
||||||
{
|
{
|
||||||
api.AddUserRoutes(apiG)
|
api.AddUserRoutes(apiG)
|
||||||
@@ -42,6 +48,7 @@ func main() {
|
|||||||
api.AddActivityRoutes(apiG)
|
api.AddActivityRoutes(apiG)
|
||||||
api.AddCollectionRoutes(apiG)
|
api.AddCollectionRoutes(apiG)
|
||||||
api.AddProxyRoutes(apiG)
|
api.AddProxyRoutes(apiG)
|
||||||
|
api.AddDevAPI(apiG)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Fatal(app.Listen(":3000"))
|
log.Fatal(app.Listen(":3000"))
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gofiber/fiber/v3"
|
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
"nysoure/server/service"
|
"nysoure/server/service"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleGetActivity(c fiber.Ctx) error {
|
func handleGetActivity(c fiber.Ctx) error {
|
||||||
@@ -28,6 +29,68 @@ func handleGetActivity(c fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleGetUserNotifications(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
pageStr := c.Query("page", "1")
|
||||||
|
page, err := strconv.Atoi(pageStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid page number")
|
||||||
|
}
|
||||||
|
notifications, totalPages, err := service.GetUserNotifications(uid, page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if notifications == nil {
|
||||||
|
notifications = []model.ActivityView{}
|
||||||
|
}
|
||||||
|
return c.JSON(model.PageResponse[model.ActivityView]{
|
||||||
|
Success: true,
|
||||||
|
Data: notifications,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
Message: "User notifications retrieved successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleResetUserNotificationsCount(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
err := service.ResetUserNotificationsCount(uid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(model.Response[any]{
|
||||||
|
Success: true,
|
||||||
|
Message: "User notifications count reset successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetUserNotificationsCount(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
count, err := service.GetUserNotificationsCount(uid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(model.Response[uint]{
|
||||||
|
Success: true,
|
||||||
|
Data: count,
|
||||||
|
Message: "User notifications count retrieved successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AddActivityRoutes(router fiber.Router) {
|
func AddActivityRoutes(router fiber.Router) {
|
||||||
router.Get("/activity", handleGetActivity)
|
router.Get("/activity", handleGetActivity)
|
||||||
|
notificationrouter := router.Group("/notification")
|
||||||
|
{
|
||||||
|
notificationrouter.Get("/", handleGetUserNotifications)
|
||||||
|
notificationrouter.Post("/reset", handleResetUserNotificationsCount)
|
||||||
|
notificationrouter.Get("/count", handleGetUserNotificationsCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
server/api/dev.go
Normal file
62
server/api/dev.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"nysoure/server/dao"
|
||||||
|
"nysoure/server/middleware"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"nysoure/server/search"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rebuildSearchIndex(c fiber.Ctx) error {
|
||||||
|
err := search.RebuildSearchIndex()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Failed to rebuild search index: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"message": "Search index rebuilt successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateResourceReleaseDate(c fiber.Ctx) error {
|
||||||
|
type Request struct {
|
||||||
|
ResourceID uint `json:"resource_id"`
|
||||||
|
ReleaseDate string `json:"release_date"`
|
||||||
|
}
|
||||||
|
var req Request
|
||||||
|
if err := c.Bind().JSON(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Invalid request body: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
date, err := time.Parse("2006-01-02", req.ReleaseDate)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Invalid date format: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = dao.UpdateResourceReleaseDate(req.ResourceID, date)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to update release date", "error", err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Failed to update release date",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"message": "Release date updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddDevAPI(router fiber.Router) {
|
||||||
|
devGroup := router.Group("/dev")
|
||||||
|
devGroup.Use(middleware.DevMiddleware())
|
||||||
|
{
|
||||||
|
devGroup.Post("/rebuild_search_index", rebuildSearchIndex)
|
||||||
|
devGroup.Post("/update_resource_release_date", updateResourceReleaseDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"nysoure/server/middleware"
|
"nysoure/server/middleware"
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
"nysoure/server/service"
|
"nysoure/server/service"
|
||||||
|
"nysoure/server/stat"
|
||||||
"nysoure/server/utils"
|
"nysoure/server/utils"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -240,6 +241,7 @@ func downloadFile(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
q.Set("token", token)
|
q.Set("token", token)
|
||||||
uri.RawQuery = q.Encode()
|
uri.RawQuery = q.Encode()
|
||||||
|
stat.RecordDownload()
|
||||||
return c.Redirect().Status(fiber.StatusFound).To(uri.String())
|
return c.Redirect().Status(fiber.StatusFound).To(uri.String())
|
||||||
}
|
}
|
||||||
data := map[string]string{
|
data := map[string]string{
|
||||||
@@ -251,6 +253,7 @@ func downloadFile(c fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return model.NewInternalServerError("Failed to generate download token")
|
return model.NewInternalServerError("Failed to generate download token")
|
||||||
}
|
}
|
||||||
|
stat.RecordDownload()
|
||||||
return c.Redirect().Status(fiber.StatusFound).To(fmt.Sprintf("%s/api/files/download/local?token=%s", c.BaseURL(), token))
|
return c.Redirect().Status(fiber.StatusFound).To(fmt.Sprintf("%s/api/files/download/local?token=%s", c.BaseURL(), token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ func handleListResources(c fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return model.NewRequestError("Invalid sort parameter")
|
return model.NewRequestError("Invalid sort parameter")
|
||||||
}
|
}
|
||||||
if sortInt < 0 || sortInt > 5 {
|
if sortInt < 0 || sortInt > 7 {
|
||||||
return model.NewRequestError("Sort parameter out of range")
|
return model.NewRequestError("Sort parameter out of range")
|
||||||
}
|
}
|
||||||
sort := model.RSort(sortInt)
|
sort := model.RSort(sortInt)
|
||||||
@@ -287,7 +287,11 @@ func handleGetCharactersFromVndb(c fiber.Ctx) error {
|
|||||||
if vnID == "" {
|
if vnID == "" {
|
||||||
return model.NewRequestError("VNDB ID is required")
|
return model.NewRequestError("VNDB ID is required")
|
||||||
}
|
}
|
||||||
characters, err := service.GetCharactersFromVndb(vnID)
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("You must be logged in to get characters from VNDB")
|
||||||
|
}
|
||||||
|
characters, err := service.GetCharactersFromVndb(vnID, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"nysoure/server/middleware"
|
"nysoure/server/middleware"
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
"nysoure/server/service"
|
"nysoure/server/service"
|
||||||
|
"nysoure/server/stat"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ func handleUserRegister(c fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
stat.RecordRegister()
|
||||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserViewWithToken]{
|
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserViewWithToken]{
|
||||||
Success: true,
|
Success: true,
|
||||||
Data: user,
|
Data: user,
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package dao
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"gorm.io/gorm"
|
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddNewResourceActivity(userID, resourceID uint) error {
|
func AddNewResourceActivity(userID, resourceID uint) error {
|
||||||
@@ -42,13 +43,20 @@ func AddUpdateResourceActivity(userID, resourceID uint) error {
|
|||||||
return db.Create(activity).Error
|
return db.Create(activity).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddNewCommentActivity(userID, commentID uint) error {
|
func AddNewCommentActivity(userID, commentID, notifyTo uint) error {
|
||||||
activity := &model.Activity{
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
UserID: userID,
|
activity := &model.Activity{
|
||||||
Type: model.ActivityTypeNewComment,
|
UserID: userID,
|
||||||
RefID: commentID,
|
Type: model.ActivityTypeNewComment,
|
||||||
}
|
RefID: commentID,
|
||||||
return db.Create(activity).Error
|
NotifyTo: notifyTo,
|
||||||
|
}
|
||||||
|
err := tx.Create(activity).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Model(&model.User{}).Where("id = ?", notifyTo).UpdateColumn("unread_notifications_count", gorm.Expr("unread_notifications_count + ?", 1)).Error
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddNewFileActivity(userID, fileID uint) error {
|
func AddNewFileActivity(userID, fileID uint) error {
|
||||||
@@ -82,3 +90,18 @@ func GetActivityList(offset, limit int) ([]model.Activity, int, error) {
|
|||||||
|
|
||||||
return activities, int(total), nil
|
return activities, int(total), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserNotifications(userID uint, offset, limit int) ([]model.Activity, int, error) {
|
||||||
|
var activities []model.Activity
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
if err := db.Model(&model.Activity{}).Where("notify_to = ?", userID).Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Where("notify_to = ?", userID).Offset(offset).Limit(limit).Order("id DESC").Find(&activities).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities, int(total), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ func GetResourceList(page, pageSize int, sort model.RSort) ([]model.Resource, in
|
|||||||
order = "downloads ASC"
|
order = "downloads ASC"
|
||||||
case model.RSortDownloadsDesc:
|
case model.RSortDownloadsDesc:
|
||||||
order = "downloads DESC"
|
order = "downloads DESC"
|
||||||
|
case model.RSortReleaseDateAsc:
|
||||||
|
order = "release_date ASC"
|
||||||
|
case model.RSortReleaseDateDesc:
|
||||||
|
order = "release_date DESC"
|
||||||
default:
|
default:
|
||||||
order = "modified_time DESC" // Default sort order
|
order = "modified_time DESC" // Default sort order
|
||||||
}
|
}
|
||||||
@@ -693,3 +697,25 @@ func UpdateResourceImage(resourceID, oldImageID, newImageID uint) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetResourceOwnerID(resourceID uint) (uint, error) {
|
||||||
|
var uid uint
|
||||||
|
if err := db.Model(&model.Resource{}).Select("user_id").Where("id = ?", resourceID).First(&uid).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, model.NewNotFoundError("Resource not found")
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateResourceReleaseDate(resourceID uint, releaseDate time.Time) error {
|
||||||
|
result := db.Model(&model.Resource{}).Where("id = ?", resourceID).Update("release_date", releaseDate)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return model.NewNotFoundError("Resource not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package dao
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"gorm.io/gorm"
|
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateUser(username string, hashedPassword []byte) (model.User, error) {
|
func CreateUser(username string, hashedPassword []byte) (model.User, error) {
|
||||||
@@ -21,9 +22,7 @@ func CreateUser(username string, hashedPassword []byte) (model.User, error) {
|
|||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
if exists {
|
if exists {
|
||||||
return user, &model.RequestError{
|
return user, model.NewRequestError("User already exists")
|
||||||
Message: "User already exists",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err := db.Create(&user).Error; err != nil {
|
if err := db.Create(&user).Error; err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
@@ -132,3 +131,15 @@ func DeleteUser(id uint) error {
|
|||||||
}
|
}
|
||||||
return db.Delete(&model.User{}, id).Error
|
return db.Delete(&model.User{}, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ResetUserNotificationsCount(userID uint) error {
|
||||||
|
return db.Model(&model.User{}).Where("id = ?", userID).Update("unread_notifications_count", 0).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserNotificationCount(userID uint) (uint, error) {
|
||||||
|
var count uint
|
||||||
|
if err := db.Model(&model.User{}).Where("id = ?", userID).Select("unread_notifications_count").Scan(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|||||||
22
server/middleware/dev_middleware.go
Normal file
22
server/middleware/dev_middleware.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nysoure/server/model"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DevMiddleware() func(c fiber.Ctx) error {
|
||||||
|
AccessKey := os.Getenv("DEV_ACCESS_KEY")
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
if AccessKey == "" {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
providedKey := c.Get("X-DEV-ACCESS-KEY")
|
||||||
|
if providedKey != AccessKey {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3/log"
|
"github.com/gofiber/fiber/v3/log"
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
@@ -13,73 +12,22 @@ import (
|
|||||||
func ErrorHandler(c fiber.Ctx) error {
|
func ErrorHandler(c fiber.Ctx) error {
|
||||||
err := c.Next()
|
err := c.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var requestErr *model.RequestError
|
|
||||||
var unauthorizedErr *model.UnAuthorizedError
|
|
||||||
var notFoundErr *model.NotFoundError
|
|
||||||
var fiberErr *fiber.Error
|
var fiberErr *fiber.Error
|
||||||
if errors.As(err, &requestErr) {
|
if errors.As(err, &fiberErr) {
|
||||||
log.Error("Request Error: ", err)
|
if fiberErr.Code != fiber.StatusInternalServerError {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(model.Response[any]{
|
return c.Status(fiberErr.Code).JSON(model.Response[any]{
|
||||||
Success: false,
|
Success: false,
|
||||||
Data: nil,
|
Data: nil,
|
||||||
Message: requestErr.Error(),
|
Message: fiberErr.Message,
|
||||||
})
|
})
|
||||||
} else if errors.As(err, &unauthorizedErr) {
|
|
||||||
log.Error("Unauthorized Error: ", err)
|
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(model.Response[any]{
|
|
||||||
Success: false,
|
|
||||||
Data: nil,
|
|
||||||
Message: unauthorizedErr.Error(),
|
|
||||||
})
|
|
||||||
} else if errors.As(err, ¬FoundErr) {
|
|
||||||
log.Error("Not Found Error: ", err)
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(model.Response[any]{
|
|
||||||
Success: false,
|
|
||||||
Data: nil,
|
|
||||||
Message: notFoundErr.Error(),
|
|
||||||
})
|
|
||||||
} else if errors.Is(err, fiber.ErrNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(model.Response[any]{
|
|
||||||
Success: false,
|
|
||||||
Data: nil,
|
|
||||||
Message: "Not found",
|
|
||||||
})
|
|
||||||
} else if errors.Is(err, fiber.ErrMethodNotAllowed) {
|
|
||||||
return c.Status(fiber.StatusMethodNotAllowed).JSON(model.Response[any]{
|
|
||||||
Success: false,
|
|
||||||
Data: nil,
|
|
||||||
Message: "Method not allowed",
|
|
||||||
})
|
|
||||||
} else if errors.As(err, &fiberErr) && fiberErr.Message != "" {
|
|
||||||
return c.Status(fiberErr.Code).JSON(model.Response[any]{
|
|
||||||
Success: false,
|
|
||||||
Data: nil,
|
|
||||||
Message: fiberErr.Message,
|
|
||||||
})
|
|
||||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(model.Response[any]{
|
|
||||||
Success: false,
|
|
||||||
Data: nil,
|
|
||||||
Message: "Not found",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
var fiberErr *fiber.Error
|
|
||||||
if errors.As(err, &fiberErr) {
|
|
||||||
if fiberErr.Code == fiber.StatusNotFound {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(model.Response[any]{
|
|
||||||
Success: false,
|
|
||||||
Data: nil,
|
|
||||||
Message: "Not found",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
log.Error("Internal Server Error: ", err)
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(model.Response[any]{
|
|
||||||
Success: false,
|
|
||||||
Data: nil,
|
|
||||||
Message: "Internal server error",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
log.Error("Internal Server Error: ", err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(model.Response[any]{
|
||||||
|
Success: false,
|
||||||
|
Data: nil,
|
||||||
|
Message: "Internal server error",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ func FrontendMiddleware(c fiber.Ctx) error {
|
|||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(c.Path(), "/metrics") {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
path := c.Path()
|
path := c.Path()
|
||||||
file := "static" + path
|
file := "static" + path
|
||||||
|
|
||||||
|
|||||||
21
server/middleware/stat_middleware.go
Normal file
21
server/middleware/stat_middleware.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nysoure/server/stat"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StatMiddleware(c fiber.Ctx) error {
|
||||||
|
err := c.Next()
|
||||||
|
status := "200"
|
||||||
|
if err != nil {
|
||||||
|
if e, ok := err.(*fiber.Error); ok {
|
||||||
|
status = string(rune(e.Code))
|
||||||
|
} else {
|
||||||
|
status = "500"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stat.RecordRequest(c.Method(), c.Route().Path, status)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|
||||||
|
errorpage "github.com/wgh136/cloudflare-error-page"
|
||||||
)
|
)
|
||||||
|
|
||||||
func UnsupportedRegionMiddleware(c fiber.Ctx) error {
|
func UnsupportedRegionMiddleware(c fiber.Ctx) error {
|
||||||
@@ -20,10 +22,39 @@ func UnsupportedRegionMiddleware(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if string(c.Request().Header.Peek("Unsupported-Region")) == "true" {
|
if string(c.Request().Header.Peek("Unsupported-Region")) == "true" {
|
||||||
// Return a 403 Forbidden response with an empty html for unsupported regions
|
h, err := generateForbiddenPage(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
c.Response().Header.Add("Content-Type", "text/html")
|
c.Response().Header.Add("Content-Type", "text/html")
|
||||||
c.Status(fiber.StatusForbidden)
|
c.Status(fiber.StatusForbidden)
|
||||||
return c.SendString("<html></html>")
|
return c.SendString(h)
|
||||||
}
|
}
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateForbiddenPage(c fiber.Ctx) (string, error) {
|
||||||
|
params := errorpage.Params{
|
||||||
|
"error_code": 403,
|
||||||
|
"title": "Forbidden",
|
||||||
|
"browser_status": map[string]interface{}{
|
||||||
|
"status": "error",
|
||||||
|
"status_text": "Error",
|
||||||
|
},
|
||||||
|
"cloudflare_status": map[string]interface{}{
|
||||||
|
"status": "ok",
|
||||||
|
"status_text": "Working",
|
||||||
|
},
|
||||||
|
"host_status": map[string]interface{}{
|
||||||
|
"status": "ok",
|
||||||
|
"location": c.Hostname(),
|
||||||
|
},
|
||||||
|
"error_source": "cloudflare",
|
||||||
|
|
||||||
|
"what_happened": "<p>The service is not available in your region.</p>",
|
||||||
|
"what_can_i_do": "<p>Please try again in a few minutes.</p>",
|
||||||
|
"client_ip": c.IP(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorpage.Render(params, nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ const (
|
|||||||
|
|
||||||
type Activity struct {
|
type Activity struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
UserID uint `gorm:"not null"`
|
UserID uint `gorm:"not null"`
|
||||||
Type ActivityType `gorm:"not null;index:idx_type_refid"`
|
Type ActivityType `gorm:"not null;index:idx_type_refid"`
|
||||||
RefID uint `gorm:"not null;index:idx_type_refid"`
|
RefID uint `gorm:"not null;index:idx_type_refid"`
|
||||||
|
NotifyTo uint `gorm:"default:null;index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActivityView struct {
|
type ActivityView struct {
|
||||||
|
|||||||
@@ -2,78 +2,31 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestError struct {
|
func NewRequestError(message string) error {
|
||||||
Message string `json:"message"`
|
return fiber.NewError(400, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *RequestError) Error() string {
|
func NewUnAuthorizedError(message string) error {
|
||||||
return e.Message
|
return fiber.NewError(403, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRequestError(message string) *RequestError {
|
func NewNotFoundError(message string) error {
|
||||||
return &RequestError{
|
return fiber.NewError(404, message)
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsRequestError(err error) bool {
|
|
||||||
var requestError *RequestError
|
|
||||||
ok := errors.As(err, &requestError)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnAuthorizedError struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UnAuthorizedError) Error() string {
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUnAuthorizedError(message string) *UnAuthorizedError {
|
|
||||||
return &UnAuthorizedError{
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsUnAuthorizedError(err error) bool {
|
|
||||||
var unAuthorizedError *UnAuthorizedError
|
|
||||||
ok := errors.As(err, &unAuthorizedError)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotFoundError struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *NotFoundError) Error() string {
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNotFoundError(message string) *NotFoundError {
|
|
||||||
return &NotFoundError{
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsNotFoundError(err error) bool {
|
func IsNotFoundError(err error) bool {
|
||||||
var notFoundError *NotFoundError
|
var fiberError *fiber.Error
|
||||||
ok := errors.As(err, ¬FoundError)
|
ok := errors.As(err, &fiberError)
|
||||||
return ok
|
if !ok {
|
||||||
}
|
return false
|
||||||
|
|
||||||
type InternalServerError struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *InternalServerError) Error() string {
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInternalServerError(message string) *InternalServerError {
|
|
||||||
return &InternalServerError{
|
|
||||||
Message: message,
|
|
||||||
}
|
}
|
||||||
|
return fiberError.Code == 404
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInternalServerError(message string) error {
|
||||||
|
return fiber.NewError(500, message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ type Resource struct {
|
|||||||
Title string
|
Title string
|
||||||
AlternativeTitles []string `gorm:"serializer:json"`
|
AlternativeTitles []string `gorm:"serializer:json"`
|
||||||
Links []Link `gorm:"serializer:json"`
|
Links []Link `gorm:"serializer:json"`
|
||||||
|
ReleaseDate *time.Time
|
||||||
Article string
|
Article string
|
||||||
Images []Image `gorm:"many2many:resource_images;"`
|
Images []Image `gorm:"many2many:resource_images;"`
|
||||||
Tags []Tag `gorm:"many2many:resource_tags;"`
|
CoverID *uint
|
||||||
Files []File `gorm:"foreignKey:ResourceID"`
|
Tags []Tag `gorm:"many2many:resource_tags;"`
|
||||||
|
Files []File `gorm:"foreignKey:ResourceID"`
|
||||||
UserID uint
|
UserID uint
|
||||||
User User
|
User User
|
||||||
Views uint
|
Views uint
|
||||||
@@ -32,12 +34,13 @@ type Link struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ResourceView struct {
|
type ResourceView struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Tags []TagView `json:"tags"`
|
ReleaseDate *time.Time `json:"release_date,omitempty"`
|
||||||
Image *ImageView `json:"image"`
|
Tags []TagView `json:"tags"`
|
||||||
Author UserView `json:"author"`
|
Image *ImageView `json:"image"`
|
||||||
|
Author UserView `json:"author"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceDetailView struct {
|
type ResourceDetailView struct {
|
||||||
@@ -47,8 +50,10 @@ type ResourceDetailView struct {
|
|||||||
Links []Link `json:"links"`
|
Links []Link `json:"links"`
|
||||||
Article string `json:"article"`
|
Article string `json:"article"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
ReleaseDate *time.Time `json:"releaseDate,omitempty"`
|
||||||
Tags []TagView `json:"tags"`
|
Tags []TagView `json:"tags"`
|
||||||
Images []ImageView `json:"images"`
|
Images []ImageView `json:"images"`
|
||||||
|
CoverID *uint `json:"coverId,omitempty"`
|
||||||
Files []FileView `json:"files"`
|
Files []FileView `json:"files"`
|
||||||
Author UserView `json:"author"`
|
Author UserView `json:"author"`
|
||||||
Views uint `json:"views"`
|
Views uint `json:"views"`
|
||||||
@@ -75,18 +80,30 @@ func (r *Resource) ToView() ResourceView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var image *ImageView
|
var image *ImageView
|
||||||
if len(r.Images) > 0 {
|
if r.CoverID != nil {
|
||||||
|
// Use the cover image if specified
|
||||||
|
for _, img := range r.Images {
|
||||||
|
if img.ID == *r.CoverID {
|
||||||
|
v := img.ToView()
|
||||||
|
image = &v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no cover is set or cover image not found, use the first image
|
||||||
|
if image == nil && len(r.Images) > 0 {
|
||||||
v := r.Images[0].ToView()
|
v := r.Images[0].ToView()
|
||||||
image = &v
|
image = &v
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResourceView{
|
return ResourceView{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
Tags: tags,
|
ReleaseDate: r.ReleaseDate,
|
||||||
Image: image,
|
Tags: tags,
|
||||||
Author: r.User.ToView(),
|
Image: image,
|
||||||
|
Author: r.User.ToView(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +132,10 @@ func (r *Resource) ToDetailView() ResourceDetailView {
|
|||||||
Links: r.Links,
|
Links: r.Links,
|
||||||
Article: r.Article,
|
Article: r.Article,
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
|
ReleaseDate: r.ReleaseDate,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
Images: images,
|
Images: images,
|
||||||
|
CoverID: r.CoverID,
|
||||||
Files: files,
|
Files: files,
|
||||||
Author: r.User.ToView(),
|
Author: r.User.ToView(),
|
||||||
Views: r.Views,
|
Views: r.Views,
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ const (
|
|||||||
RSortViewsDesc
|
RSortViewsDesc
|
||||||
RSortDownloadsAsc
|
RSortDownloadsAsc
|
||||||
RSortDownloadsDesc
|
RSortDownloadsDesc
|
||||||
|
RSortReleaseDateAsc
|
||||||
|
RSortReleaseDateDesc
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,16 +9,17 @@ import (
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Username string `gorm:"uniqueIndex;not null"`
|
Username string `gorm:"uniqueIndex;not null"`
|
||||||
PasswordHash []byte
|
PasswordHash []byte
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
CanUpload bool
|
CanUpload bool
|
||||||
AvatarVersion int
|
AvatarVersion int
|
||||||
ResourcesCount int
|
ResourcesCount int
|
||||||
FilesCount int
|
FilesCount int
|
||||||
CommentsCount int
|
CommentsCount int
|
||||||
Resources []Resource `gorm:"foreignKey:UserID"`
|
Resources []Resource `gorm:"foreignKey:UserID"`
|
||||||
Bio string
|
Bio string
|
||||||
|
UnreadNotificationsCount uint `gorm:"not null;default:0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserView struct {
|
type UserView struct {
|
||||||
|
|||||||
@@ -3,30 +3,54 @@ package search
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"nysoure/server/dao"
|
"nysoure/server/dao"
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
"nysoure/server/utils"
|
"nysoure/server/utils"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/blevesearch/bleve"
|
"github.com/blevesearch/bleve"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
index bleve.Index
|
||||||
|
mu = sync.RWMutex{}
|
||||||
|
)
|
||||||
|
|
||||||
type ResourceParams struct {
|
type ResourceParams struct {
|
||||||
Id uint
|
Id uint
|
||||||
Title string
|
Title string
|
||||||
Subtitles []string
|
Subtitles []string
|
||||||
Time time.Time
|
Time time.Time
|
||||||
|
Characters []ResourceCharacter
|
||||||
}
|
}
|
||||||
|
|
||||||
var index bleve.Index
|
type ResourceCharacter struct {
|
||||||
|
Name string
|
||||||
|
Alias []string
|
||||||
|
CV string
|
||||||
|
}
|
||||||
|
|
||||||
func AddResourceToIndex(r model.Resource) error {
|
func AddResourceToIndex(r model.Resource) error {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
cs := make([]ResourceCharacter, 0, len(r.Characters))
|
||||||
|
for _, c := range r.Characters {
|
||||||
|
cs = append(cs, ResourceCharacter{
|
||||||
|
Name: c.Name,
|
||||||
|
Alias: c.Alias,
|
||||||
|
CV: c.CV,
|
||||||
|
})
|
||||||
|
}
|
||||||
return index.Index(fmt.Sprintf("%d", r.ID), ResourceParams{
|
return index.Index(fmt.Sprintf("%d", r.ID), ResourceParams{
|
||||||
Id: r.ID,
|
Id: r.ID,
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
Subtitles: r.AlternativeTitles,
|
Subtitles: r.AlternativeTitles,
|
||||||
Time: r.CreatedAt,
|
Time: r.CreatedAt,
|
||||||
|
Characters: cs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,16 +64,25 @@ func createIndex() error {
|
|||||||
}
|
}
|
||||||
page := 1
|
page := 1
|
||||||
total := 1
|
total := 1
|
||||||
|
current := 0
|
||||||
for page <= total {
|
for page <= total {
|
||||||
res, totalPages, err := dao.GetResourceList(page, 100, model.RSortTimeAsc)
|
res, totalPages, err := dao.GetResourceList(page, 100, model.RSortTimeAsc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, r := range res {
|
for _, r := range res {
|
||||||
err := AddResourceToIndex(r)
|
r, err := dao.GetResourceByID(r.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = AddResourceToIndex(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current++
|
||||||
|
if current%20 == 0 {
|
||||||
|
slog.Info("Rebuilding search index", "current", current, "total", totalPages*100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
page++
|
page++
|
||||||
total = totalPages
|
total = totalPages
|
||||||
@@ -80,6 +113,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SearchResource(keyword string) ([]uint, error) {
|
func SearchResource(keyword string) ([]uint, error) {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
query := bleve.NewMatchQuery(keyword)
|
query := bleve.NewMatchQuery(keyword)
|
||||||
searchRequest := bleve.NewSearchRequest(query)
|
searchRequest := bleve.NewSearchRequest(query)
|
||||||
searchResults, err := index.Search(searchRequest)
|
searchResults, err := index.Search(searchRequest)
|
||||||
@@ -112,3 +147,24 @@ func IsStopWord(word string) bool {
|
|||||||
tokens := analyzer.Analyze([]byte(word))
|
tokens := analyzer.Analyze([]byte(word))
|
||||||
return len(tokens) == 0
|
return len(tokens) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RebuildSearchIndex() error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
err := index.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close search index: %w", err)
|
||||||
|
}
|
||||||
|
indexPath := utils.GetStoragePath() + "/resource_index.bleve"
|
||||||
|
err = os.RemoveAll(indexPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove search index: %w", err)
|
||||||
|
}
|
||||||
|
mapping := bleve.NewIndexMapping()
|
||||||
|
index, err = bleve.New(indexPath, mapping)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create search index: %w", err)
|
||||||
|
}
|
||||||
|
go createIndex()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,3 +68,67 @@ func GetActivityList(page int) ([]model.ActivityView, int, error) {
|
|||||||
|
|
||||||
return views, totalPages, nil
|
return views, totalPages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserNotifications(userID uint, page int) ([]model.ActivityView, int, error) {
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
limit := pageSize
|
||||||
|
|
||||||
|
activities, total, err := dao.GetUserNotifications(userID, offset, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var views []model.ActivityView
|
||||||
|
for _, activity := range activities {
|
||||||
|
user, err := dao.GetUserByID(activity.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
var comment *model.CommentView
|
||||||
|
var resource *model.ResourceView
|
||||||
|
var file *model.FileView
|
||||||
|
switch activity.Type {
|
||||||
|
case model.ActivityTypeNewComment:
|
||||||
|
c, err := dao.GetCommentByID(activity.RefID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
comment = c.ToView()
|
||||||
|
comment.Content, comment.ContentTruncated = restrictCommentLength(c.Content)
|
||||||
|
case model.ActivityTypeNewResource, model.ActivityTypeUpdateResource:
|
||||||
|
r, err := dao.GetResourceByID(activity.RefID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
rv := r.ToView()
|
||||||
|
resource = &rv
|
||||||
|
case model.ActivityTypeNewFile:
|
||||||
|
f, err := dao.GetFileByID(activity.RefID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
fv := f.ToView()
|
||||||
|
file = fv
|
||||||
|
r, err := dao.GetResourceByID(f.ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
rv := r.ToView()
|
||||||
|
resource = &rv
|
||||||
|
}
|
||||||
|
view := model.ActivityView{
|
||||||
|
ID: activity.ID,
|
||||||
|
User: user.ToView(),
|
||||||
|
Type: activity.Type,
|
||||||
|
Time: activity.CreatedAt,
|
||||||
|
Comment: comment,
|
||||||
|
Resource: resource,
|
||||||
|
File: file,
|
||||||
|
}
|
||||||
|
views = append(views, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (total + pageSize - 1) / pageSize
|
||||||
|
|
||||||
|
return views, totalPages, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ func CreateComment(req CommentRequest, userID uint, refID uint, ip string, cType
|
|||||||
return nil, model.NewRequestError("Comment content exceeds maximum length of 1024 characters")
|
return nil, model.NewRequestError("Comment content exceeds maximum length of 1024 characters")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var notifyTo uint
|
||||||
|
|
||||||
switch cType {
|
switch cType {
|
||||||
case model.CommentTypeResource:
|
case model.CommentTypeResource:
|
||||||
resourceExists, err := dao.ExistsResource(refID)
|
resourceExists, err := dao.ExistsResource(refID)
|
||||||
@@ -39,12 +41,18 @@ func CreateComment(req CommentRequest, userID uint, refID uint, ip string, cType
|
|||||||
if !resourceExists {
|
if !resourceExists {
|
||||||
return nil, model.NewNotFoundError("Resource not found")
|
return nil, model.NewNotFoundError("Resource not found")
|
||||||
}
|
}
|
||||||
|
notifyTo, err = dao.GetResourceOwnerID(refID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error getting resource owner ID:", err)
|
||||||
|
return nil, model.NewInternalServerError("Error getting resource owner ID")
|
||||||
|
}
|
||||||
case model.CommentTypeReply:
|
case model.CommentTypeReply:
|
||||||
_, err := dao.GetCommentByID(refID)
|
comment, err := dao.GetCommentByID(refID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error getting reply comment:", err)
|
log.Error("Error getting reply comment:", err)
|
||||||
return nil, model.NewNotFoundError("Reply comment not found")
|
return nil, model.NewNotFoundError("Reply comment not found")
|
||||||
}
|
}
|
||||||
|
notifyTo = comment.UserID
|
||||||
}
|
}
|
||||||
|
|
||||||
userExists, err := dao.ExistsUserByID(userID)
|
userExists, err := dao.ExistsUserByID(userID)
|
||||||
@@ -63,7 +71,7 @@ func CreateComment(req CommentRequest, userID uint, refID uint, ip string, cType
|
|||||||
log.Error("Error creating comment:", err)
|
log.Error("Error creating comment:", err)
|
||||||
return nil, model.NewInternalServerError("Error creating comment")
|
return nil, model.NewInternalServerError("Error creating comment")
|
||||||
}
|
}
|
||||||
err = dao.AddNewCommentActivity(userID, c.ID)
|
err = dao.AddNewCommentActivity(userID, c.ID, notifyTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error creating comment activity:", err)
|
log.Error("Error creating comment activity:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ type ResourceParams struct {
|
|||||||
Title string `json:"title" binding:"required"`
|
Title string `json:"title" binding:"required"`
|
||||||
AlternativeTitles []string `json:"alternative_titles"`
|
AlternativeTitles []string `json:"alternative_titles"`
|
||||||
Links []model.Link `json:"links"`
|
Links []model.Link `json:"links"`
|
||||||
|
ReleaseDate string `json:"release_date"`
|
||||||
Tags []uint `json:"tags"`
|
Tags []uint `json:"tags"`
|
||||||
Article string `json:"article"`
|
Article string `json:"article"`
|
||||||
Images []uint `json:"images"`
|
Images []uint `json:"images"`
|
||||||
|
CoverID *uint `json:"cover_id"`
|
||||||
Gallery []uint `json:"gallery"`
|
Gallery []uint `json:"gallery"`
|
||||||
GalleryNsfw []uint `json:"gallery_nsfw"`
|
GalleryNsfw []uint `json:"gallery_nsfw"`
|
||||||
Characters []CharacterParams `json:"characters"`
|
Characters []CharacterParams `json:"characters"`
|
||||||
@@ -101,12 +103,30 @@ func CreateResource(uid uint, params *ResourceParams) (uint, error) {
|
|||||||
ImageID: imageID,
|
ImageID: imageID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var date *time.Time
|
||||||
|
if params.ReleaseDate != "" {
|
||||||
|
parsedDate, err := time.Parse("2006-01-02", params.ReleaseDate)
|
||||||
|
if err != nil {
|
||||||
|
return 0, model.NewRequestError("Invalid release date format, expected YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
date = &parsedDate
|
||||||
|
}
|
||||||
|
// Validate CoverID if provided
|
||||||
|
var coverID *uint
|
||||||
|
if params.CoverID != nil && *params.CoverID != 0 {
|
||||||
|
if !slices.Contains(params.Images, *params.CoverID) {
|
||||||
|
return 0, model.NewRequestError("Cover ID must be one of the resource images")
|
||||||
|
}
|
||||||
|
coverID = params.CoverID
|
||||||
|
}
|
||||||
r := model.Resource{
|
r := model.Resource{
|
||||||
Title: params.Title,
|
Title: params.Title,
|
||||||
AlternativeTitles: params.AlternativeTitles,
|
AlternativeTitles: params.AlternativeTitles,
|
||||||
Article: params.Article,
|
Article: params.Article,
|
||||||
Links: params.Links,
|
Links: params.Links,
|
||||||
|
ReleaseDate: date,
|
||||||
Images: images,
|
Images: images,
|
||||||
|
CoverID: coverID,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
UserID: uid,
|
UserID: uid,
|
||||||
Gallery: gallery,
|
Gallery: gallery,
|
||||||
@@ -529,10 +549,30 @@ func UpdateResource(uid, rid uint, params *ResourceParams) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var date *time.Time
|
||||||
|
if params.ReleaseDate != "" {
|
||||||
|
parsedDate, err := time.Parse("2006-01-02", params.ReleaseDate)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid release date format, expected YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
date = &parsedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CoverID if provided
|
||||||
|
var coverID *uint
|
||||||
|
if params.CoverID != nil && *params.CoverID != 0 {
|
||||||
|
if !slices.Contains(params.Images, *params.CoverID) {
|
||||||
|
return model.NewRequestError("Cover ID must be one of the resource images")
|
||||||
|
}
|
||||||
|
coverID = params.CoverID
|
||||||
|
}
|
||||||
|
|
||||||
r.Title = params.Title
|
r.Title = params.Title
|
||||||
r.AlternativeTitles = params.AlternativeTitles
|
r.AlternativeTitles = params.AlternativeTitles
|
||||||
r.Article = params.Article
|
r.Article = params.Article
|
||||||
r.Links = params.Links
|
r.Links = params.Links
|
||||||
|
r.ReleaseDate = date
|
||||||
|
r.CoverID = coverID
|
||||||
r.Gallery = gallery
|
r.Gallery = gallery
|
||||||
r.GalleryNsfw = nsfw
|
r.GalleryNsfw = nsfw
|
||||||
r.Characters = characters
|
r.Characters = characters
|
||||||
@@ -615,7 +655,15 @@ func GetPinnedResources() ([]model.ResourceView, error) {
|
|||||||
return views, nil
|
return views, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCharactersFromVndb(vnID string) ([]CharacterParams, error) {
|
func GetCharactersFromVndb(vnID string, uid uint) ([]CharacterParams, error) {
|
||||||
|
canUpload, err := checkUserCanUpload(uid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !canUpload {
|
||||||
|
return nil, model.NewUnAuthorizedError("You have not permission to fetch characters from VNDB")
|
||||||
|
}
|
||||||
|
|
||||||
client := http.Client{}
|
client := http.Client{}
|
||||||
jsonStr := fmt.Sprintf(`
|
jsonStr := fmt.Sprintf(`
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -390,3 +390,11 @@ func validateUsername(username string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ResetUserNotificationsCount(userID uint) error {
|
||||||
|
return dao.ResetUserNotificationsCount(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserNotificationsCount(userID uint) (uint, error) {
|
||||||
|
return dao.GetUserNotificationCount(userID)
|
||||||
|
}
|
||||||
|
|||||||
52
server/stat/stat.go
Normal file
52
server/stat/stat.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package stat
|
||||||
|
|
||||||
|
import (
|
||||||
|
prom "github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
RequestCount = prom.NewCounterVec(
|
||||||
|
prom.CounterOpts{
|
||||||
|
Name: "http_requests_total",
|
||||||
|
Help: "Total number of HTTP requests",
|
||||||
|
},
|
||||||
|
[]string{"path", "status"},
|
||||||
|
)
|
||||||
|
RegisterCount = prom.NewCounterVec(
|
||||||
|
prom.CounterOpts{
|
||||||
|
Name: "register_requests_total",
|
||||||
|
Help: "Total number of registration requests",
|
||||||
|
},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
DownloadCount = prom.NewCounterVec(
|
||||||
|
prom.CounterOpts{
|
||||||
|
Name: "download_requests_total",
|
||||||
|
Help: "Total number of download requests",
|
||||||
|
},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
prom.MustRegister(RequestCount)
|
||||||
|
prom.MustRegister(RegisterCount)
|
||||||
|
prom.MustRegister(DownloadCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordRequest(method, path string, status string) {
|
||||||
|
if status == "404" {
|
||||||
|
// Aggregate all 404s under a single label
|
||||||
|
path = "NOT_FOUND"
|
||||||
|
}
|
||||||
|
path = method + " " + path
|
||||||
|
RequestCount.WithLabelValues(path, status).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordRegister() {
|
||||||
|
RegisterCount.WithLabelValues().Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordDownload() {
|
||||||
|
DownloadCount.WithLabelValues().Inc()
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ type FTPStorage struct {
|
|||||||
|
|
||||||
func (f *FTPStorage) Upload(filePath string, fileName string) (string, error) {
|
func (f *FTPStorage) Upload(filePath string, fileName string) (string, error) {
|
||||||
// 连接到FTP服务器
|
// 连接到FTP服务器
|
||||||
conn, err := ftp.Dial(f.Host, ftp.DialWithTimeout(10*time.Second))
|
conn, err := ftp.Dial(f.Host, ftp.DialWithTimeout(10*time.Second), ftp.DialWithExplicitTLS(nil))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to connect to FTP server: ", err)
|
log.Error("Failed to connect to FTP server: ", err)
|
||||||
return "", errors.New("failed to connect to FTP server")
|
return "", errors.New("failed to connect to FTP server")
|
||||||
@@ -76,7 +76,7 @@ func (f *FTPStorage) Download(storageKey string, fileName string) (string, error
|
|||||||
|
|
||||||
func (f *FTPStorage) Delete(storageKey string) error {
|
func (f *FTPStorage) Delete(storageKey string) error {
|
||||||
// 连接到FTP服务器
|
// 连接到FTP服务器
|
||||||
conn, err := ftp.Dial(f.Host, ftp.DialWithTimeout(10*time.Second))
|
conn, err := ftp.Dial(f.Host, ftp.DialWithTimeout(10*time.Second), ftp.DialWithExplicitTLS(nil))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to connect to FTP server: ", err)
|
log.Error("Failed to connect to FTP server: ", err)
|
||||||
return errors.New("failed to connect to FTP server")
|
return errors.New("failed to connect to FTP server")
|
||||||
|
|||||||
Reference in New Issue
Block a user