Compare commits
6 Commits
590e44345b
...
0d405f4a22
Author | SHA1 | Date |
---|---|---|
laozhoubuluo | 0d405f4a22 | |
naskya | b3d1be457b | |
Hosted Weblate | 347851d6bb | |
jolupa | abec71074b | |
Lhcfl | 272e30be0c | |
老周部落 | e79cc38002 |
|
@ -2301,3 +2301,6 @@ getQrCode: Mostrar el codi QR
|
|||
copyRemoteFollowUrl: Còpia la adreça URL del seguidor remot
|
||||
foldNotification: Agrupar les notificacions similars
|
||||
slashQuote: Cita encadenada
|
||||
i18nServerInfo: Els nous clients els trobares en {language} per defecte.
|
||||
i18nServerChange: Fes servir {language} en comptes.
|
||||
i18nServerSet: Fes servir {language} per els nous clients.
|
||||
|
|
|
@ -1083,6 +1083,12 @@ recommendedInstancesDescription: "Recommended servers separated by line breaks t
|
|||
caption: "Auto description"
|
||||
splash: "Splash Screen"
|
||||
updateAvailable: "There might be an update available!"
|
||||
updateEmailTips: "Update Email Tips"
|
||||
updateEmailTipsInfo: "To receive email tips when Firefish releases a new version. You need to
|
||||
correctly set up email sending and maintainer email for it to take effect."
|
||||
updateEmailTipsSecurityOnly: "Only receive security update tips"
|
||||
updateEmailTipsSecurityOnlyInfo: "Firefish using rolling update and new versions may be
|
||||
released frequently. This option is used to only receive email tips for security version update."
|
||||
swipeOnMobile: "Allow swiping between pages"
|
||||
swipeOnDesktop: "Allow mobile-style swiping on desktop"
|
||||
logoImageUrl: "Logo image URL"
|
||||
|
|
|
@ -1872,6 +1872,10 @@ showAds: 显示社区横幅
|
|||
enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Return 发送)
|
||||
recommendedInstances: 推荐服务器
|
||||
updateAvailable: 可能有可用更新!
|
||||
updateEmailTips: 更新提醒邮件
|
||||
updateEmailTipsInfo: 在 Firefish 发布新版本时接收更新提醒邮件。需要您正确设置发送邮件功能和管理员邮箱才会生效。
|
||||
updateEmailTipsSecurityOnly: 只接收安全版本更新提醒
|
||||
updateEmailTipsSecurityOnlyInfo: Firefish 采用滚动更新模式因此新版本可能会频繁发布。此选项用于只在安全更新发布时接收更新提醒邮件。
|
||||
swipeOnMobile: 允许在页面之间滑动
|
||||
swipeOnDesktop: 允许在桌面端以移动设备方式滑动
|
||||
logoImageUrl: Logo 图像 URL
|
||||
|
|
|
@ -50,5 +50,8 @@
|
|||
"execa": "8.0.1",
|
||||
"pnpm": "8.15.7",
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"firefishCustomFields": {
|
||||
"lastSecurityUpdate": "20240330"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,6 +128,12 @@ pub struct Model {
|
|||
pub secure_mode: Option<bool>,
|
||||
#[sea_orm(column_name = "privateMode")]
|
||||
pub private_mode: Option<bool>,
|
||||
#[sea_orm(column_name = "updateEmailTips")]
|
||||
pub update_email_tips: Option<bool>,
|
||||
#[sea_orm(column_name = "updateEmailTipsSecurityOnly")]
|
||||
pub update_email_tips_security_only: Option<bool>,
|
||||
#[sea_orm(column_name = "updateTipsVersion")]
|
||||
pub update_tips_version: Option<String>,
|
||||
#[sea_orm(column_name = "deeplAuthKey")]
|
||||
pub deepl_auth_key: Option<String>,
|
||||
#[sea_orm(column_name = "deeplIsPro")]
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class updateEmailTips1711616400000 implements MigrationInterface {
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" ADD "updateEmailTips" bool default true`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" ADD "updateEmailTipsSecurityOnly" bool default true`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" ADD "updateTipsVersion" character varying(512)`,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "updateEmailTips"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" DROP COLUMN "updateEmailTipsSecurityOnly"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" DROP COLUMN "updateTipsVersion"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -152,6 +152,22 @@ export class Meta {
|
|||
})
|
||||
public allowedHosts: string[];
|
||||
|
||||
@Column("boolean", {
|
||||
default: true,
|
||||
})
|
||||
public updateEmailTips: boolean;
|
||||
|
||||
@Column("boolean", {
|
||||
default: true,
|
||||
})
|
||||
public updateEmailTipsSecurityOnly: boolean;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
})
|
||||
public updateTipsVersion: string | null;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
array: true,
|
||||
|
|
|
@ -559,6 +559,16 @@ export default function () {
|
|||
},
|
||||
);
|
||||
|
||||
systemQueue.add(
|
||||
"updateEmailTips",
|
||||
{},
|
||||
{
|
||||
repeat: { cron: "0 0 * * 0" },
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
|
||||
processSystemQueue(systemQueue);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { checkExpiredMutings } from "./check-expired-mutings.js";
|
|||
import { clean } from "./clean.js";
|
||||
import { setLocalEmojiSizes } from "./local-emoji-size.js";
|
||||
import { verifyLinks } from "./verify-links.js";
|
||||
import { updateEmailTips } from "./update-email-tips.js";
|
||||
|
||||
const jobs = {
|
||||
cleanCharts,
|
||||
|
@ -11,6 +12,7 @@ const jobs = {
|
|||
clean,
|
||||
setLocalEmojiSizes,
|
||||
verifyLinks,
|
||||
updateEmailTips,
|
||||
} as Record<
|
||||
string,
|
||||
| Bull.ProcessCallbackFunction<Record<string, unknown>>
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import type Bull from "bull";
|
||||
|
||||
import { Meta } from "@/models/entities/meta.js";
|
||||
import fetch from "node-fetch";
|
||||
import { queueLogger } from "../../logger.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { sendEmail } from "@/services/send-email.js";
|
||||
|
||||
const logger = queueLogger.createSubLogger("update-email-tips");
|
||||
|
||||
export async function updateEmailTips(
|
||||
job: Bull.Job<Record<string, unknown>>,
|
||||
done: any,
|
||||
): Promise<void> {
|
||||
logger.info("Checking firefish Update...");
|
||||
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
if (!instance.updateEmailTips) {
|
||||
logger.info("Exit due to not enable update email tips.");
|
||||
} else if (!instance.enableEmail) {
|
||||
logger.info("Exit due to not enable email.");
|
||||
} else if (
|
||||
instance.maintainerEmail === null ||
|
||||
typeof instance.maintainerEmail !== "string"
|
||||
) {
|
||||
logger.info("Exit due to not vaild maintainer email.");
|
||||
} else {
|
||||
const url =
|
||||
"https://firefish.dev/firefish/firefish/-/raw/main/package.json";
|
||||
|
||||
const res = await fetch(url).catch((e) => {
|
||||
logger.info("Exit due to network error.");
|
||||
});
|
||||
|
||||
if (res !== null) {
|
||||
const packageData = await res.json();
|
||||
const version = instance.updateEmailTipsSecurityOnly
|
||||
? packageData.firefishCustomFields.lastSecurityUpdate
|
||||
: packageData.version;
|
||||
|
||||
if (instance.updateTipsVersion === null) {
|
||||
await db.transaction(async (transactionalEntityManager) => {
|
||||
const metas = await transactionalEntityManager.find(Meta, {
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
});
|
||||
|
||||
await transactionalEntityManager.update(Meta, metas[0].id, {
|
||||
updateTipsVersion: version,
|
||||
});
|
||||
});
|
||||
|
||||
logger.info("Exit due to first time update.");
|
||||
} else if (instance.updateTipsVersion < version) {
|
||||
if (
|
||||
packageData.firefishCustomFields.lastSecurityUpdate ===
|
||||
packageData.version
|
||||
) {
|
||||
logger.info(
|
||||
`Found security update version ${version}, last check version is ${instance.updateTipsVersion}`,
|
||||
);
|
||||
|
||||
await sendEmail(
|
||||
instance.maintainerEmail,
|
||||
"Security Update Tips",
|
||||
`Firefish has released a new security update version ${version}, please update as soon as possible to ensure the security of your site.<br>The changelog can be viewed at the following url: <a href="https://firefish.dev/firefish/firefish/-/blob/develop/docs/changelog.md">https://firefish.dev/firefish/firefish/-/blob/develop/docs/changelog.md</a>`,
|
||||
`Firefish has released a new security update version ${version}, please update as soon as possible to ensure the security of your site.\nThe changelog can be viewed at the following url: https://firefish.dev/firefish/firefish/-/blob/develop/docs/changelog.md`,
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Found version ${version}, last check version is ${instance.updateTipsVersion}`,
|
||||
);
|
||||
|
||||
await sendEmail(
|
||||
instance.maintainerEmail,
|
||||
"Update Tips",
|
||||
`Firefish has released a new version ${version}.<br>The changelog can be viewed at the following url: <a href="https://firefish.dev/firefish/firefish/-/blob/develop/docs/changelog.md">https://firefish.dev/firefish/firefish/-/blob/develop/docs/changelog.md</a>`,
|
||||
`Firefish has released a new version ${version}.\nThe changelog can be viewed at the following url: https://firefish.dev/firefish/firefish/-/blob/develop/docs/changelog.md`,
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (transactionalEntityManager) => {
|
||||
const metas = await transactionalEntityManager.find(Meta, {
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
});
|
||||
|
||||
await transactionalEntityManager.update(Meta, metas[0].id, {
|
||||
updateTipsVersion: version,
|
||||
});
|
||||
});
|
||||
|
||||
logger.info("Email send.");
|
||||
} else {
|
||||
logger.info("No new update.");
|
||||
}
|
||||
logger.succ("Checking firefish update successfully.");
|
||||
}
|
||||
}
|
||||
done();
|
||||
}
|
|
@ -288,6 +288,18 @@ export const meta = {
|
|||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
updateEmailTips: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
default: true,
|
||||
},
|
||||
updateEmailTipsSecurityOnly: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
default: true,
|
||||
},
|
||||
recaptchaSecretKey: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
|
@ -528,6 +540,8 @@ export default define(meta, paramDef, async () => {
|
|||
allowedHosts: instance.allowedHosts,
|
||||
privateMode: instance.privateMode,
|
||||
secureMode: instance.secureMode,
|
||||
updateEmailTips: instance.updateEmailTips,
|
||||
updateEmailTipsSecurityOnly: instance.updateEmailTipsSecurityOnly,
|
||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||
proxyAccountId: instance.proxyAccountId,
|
||||
|
|
|
@ -77,6 +77,8 @@ export const paramDef = {
|
|||
},
|
||||
secureMode: { type: "boolean", nullable: true },
|
||||
privateMode: { type: "boolean", nullable: true },
|
||||
updateEmailTips: { type: "boolean", nullable: true },
|
||||
updateEmailTipsSecurityOnly: { type: "boolean", nullable: true },
|
||||
themeColor: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
|
@ -280,6 +282,14 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.secureMode = ps.secureMode;
|
||||
}
|
||||
|
||||
if (typeof ps.updateEmailTips === "boolean") {
|
||||
set.updateEmailTips = ps.updateEmailTips;
|
||||
}
|
||||
|
||||
if (typeof ps.updateEmailTipsSecurityOnly === "boolean") {
|
||||
set.updateEmailTipsSecurityOnly = ps.updateEmailTipsSecurityOnly;
|
||||
}
|
||||
|
||||
if (ps.mascotImageUrl !== undefined) {
|
||||
set.mascotImageUrl = ps.mascotImageUrl;
|
||||
}
|
||||
|
|
|
@ -25,15 +25,21 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
function getDateSafe(n: Date | string | number) {
|
||||
try {
|
||||
if (n instanceof Date) {
|
||||
return n;
|
||||
}
|
||||
return new Date(n);
|
||||
} catch (err) {
|
||||
return {
|
||||
getTime: () => Number.NaN,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const _time = computed(() =>
|
||||
props.time == null
|
||||
? Number.NaN
|
||||
: typeof props.time === "number"
|
||||
? props.time
|
||||
: (props.time instanceof Date
|
||||
? props.time
|
||||
: new Date(props.time)
|
||||
).getTime(),
|
||||
props.time == null ? Number.NaN : getDateSafe(props.time).getTime(),
|
||||
);
|
||||
const invalid = computed(() => Number.isNaN(_time.value));
|
||||
const absolute = computed(() =>
|
||||
|
@ -41,45 +47,57 @@ const absolute = computed(() =>
|
|||
);
|
||||
|
||||
const now = ref(props.origin?.getTime() ?? Date.now());
|
||||
|
||||
const relative = computed<string>(() => {
|
||||
if (props.mode === "absolute") return ""; // absoluteではrelativeを使わないので計算しない
|
||||
if (invalid.value) return i18n.ts._ago.invalid;
|
||||
|
||||
const ago = (now.value - _time.value) / 1000; /* ms */
|
||||
return ago >= 31536000
|
||||
? i18n.t("_ago.yearsAgo", { n: Math.floor(ago / 31536000).toString() })
|
||||
: ago >= 2592000
|
||||
? i18n.t("_ago.monthsAgo", {
|
||||
n: Math.floor(ago / 2592000).toString(),
|
||||
})
|
||||
: ago >= 604800
|
||||
? i18n.t("_ago.weeksAgo", {
|
||||
n: Math.floor(ago / 604800).toString(),
|
||||
})
|
||||
: ago >= 86400
|
||||
? i18n.t("_ago.daysAgo", {
|
||||
n: Math.floor(ago / 86400).toString(),
|
||||
})
|
||||
: ago >= 3600
|
||||
? i18n.t("_ago.hoursAgo", {
|
||||
n: Math.floor(ago / 3600).toString(),
|
||||
})
|
||||
: ago >= 60
|
||||
? i18n.t("_ago.minutesAgo", {
|
||||
n: (~~(ago / 60)).toString(),
|
||||
})
|
||||
: ago >= 10
|
||||
? i18n.t("_ago.secondsAgo", {
|
||||
n: (~~(ago % 60)).toString(),
|
||||
})
|
||||
: ago >= -1
|
||||
? i18n.ts._ago.justNow
|
||||
: i18n.ts._ago.future;
|
||||
|
||||
if (ago >= 31536000) {
|
||||
return i18n.t("_ago.yearsAgo", {
|
||||
n: Math.floor(ago / 31536000).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 2592000) {
|
||||
return i18n.t("_ago.monthsAgo", {
|
||||
n: Math.floor(ago / 2592000).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 604800) {
|
||||
return i18n.t("_ago.weeksAgo", {
|
||||
n: Math.floor(ago / 604800).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 86400) {
|
||||
return i18n.t("_ago.daysAgo", {
|
||||
n: Math.floor(ago / 86400).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 3600) {
|
||||
return i18n.t("_ago.hoursAgo", {
|
||||
n: Math.floor(ago / 3600).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 60) {
|
||||
return i18n.t("_ago.minutesAgo", {
|
||||
n: (~~(ago / 60)).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 10) {
|
||||
return i18n.t("_ago.secondsAgo", {
|
||||
n: (~~(ago % 60)).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= -1) {
|
||||
return i18n.ts._ago.justNow;
|
||||
}
|
||||
return i18n.ts._ago.future;
|
||||
});
|
||||
|
||||
let tickId: number | undefined;
|
||||
|
||||
function tick() {
|
||||
function tick(forceUpdateTicker = false) {
|
||||
if (
|
||||
invalid.value ||
|
||||
props.origin ||
|
||||
|
@ -101,13 +119,16 @@ function tick() {
|
|||
|
||||
if (!tickId) {
|
||||
tickId = window.setInterval(tick, next);
|
||||
} else if (prev < next) {
|
||||
} else if (prev < next || forceUpdateTicker) {
|
||||
window.clearInterval(tickId);
|
||||
tickId = window.setInterval(tick, next);
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.time, tick);
|
||||
watch(
|
||||
() => props.time,
|
||||
() => tick(true),
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
tick();
|
||||
|
|
|
@ -134,6 +134,41 @@
|
|||
>
|
||||
</div>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>{{
|
||||
i18n.ts.updateEmailTips
|
||||
}}</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="updateEmailTips">
|
||||
<template #label>{{
|
||||
i18n.ts.updateEmailTips
|
||||
}}</template>
|
||||
<template #caption>{{
|
||||
i18n.ts.updateEmailTipsInfo
|
||||
}}</template>
|
||||
</FormSwitch>
|
||||
<FormSwitch
|
||||
v-if="updateEmailTips"
|
||||
v-model="updateEmailTipsSecurityOnly"
|
||||
>
|
||||
<template #label>{{
|
||||
i18n.ts.updateEmailTipsSecurityOnly
|
||||
}}</template>
|
||||
<template #caption>{{
|
||||
i18n.ts.updateEmailTipsSecurityOnlyInfo
|
||||
}}</template>
|
||||
</FormSwitch>
|
||||
<FormButton
|
||||
primary
|
||||
class="_formBlock"
|
||||
@click="saveUpdateEmailTips"
|
||||
><i :class="icon('ph-floppy-disk-back')"></i>
|
||||
{{ i18n.ts.save }}</FormButton
|
||||
>
|
||||
</div>
|
||||
</FormFolder>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
|
@ -166,6 +201,9 @@ const secureMode = ref(false);
|
|||
const privateMode = ref(false);
|
||||
const allowedHosts = ref("");
|
||||
|
||||
const updateEmailTips = ref(false);
|
||||
const updateEmailTipsSecurityOnly = ref(false);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api("admin/meta");
|
||||
summalyProxy.value = meta.summalyProxy;
|
||||
|
@ -177,6 +215,9 @@ async function init() {
|
|||
secureMode.value = meta.secureMode;
|
||||
privateMode.value = meta.privateMode;
|
||||
allowedHosts.value = meta.allowedHosts.join("\n");
|
||||
|
||||
updateEmailTips.value = meta.updateEmailTips;
|
||||
updateEmailTipsSecurityOnly.value = meta.updateEmailTipsSecurityOnly;
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
@ -199,6 +240,15 @@ function saveInstance() {
|
|||
});
|
||||
}
|
||||
|
||||
function saveUpdateEmailTips() {
|
||||
os.apiWithDialog("admin/update-meta", {
|
||||
updateEmailTips: updateEmailTips.value,
|
||||
updateEmailTipsSecurityOnly: updateEmailTipsSecurityOnly.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
|
Loading…
Reference in New Issue