Compare commits

...

6 Commits

Author SHA1 Message Date
laozhoubuluo 0d405f4a22 Merge branch 'feat/update_email_tips' into 'develop'
feat: update email tips


See merge request firefish/firefish!10716
2024-05-08 13:55:10 +00:00
naskya b3d1be457b Merge branch 'fix/MkTime' into 'develop'
refactor MkTime: replace the awful ?: chain with if-else; fix: force update ticker when props.time changed

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10797
2024-05-08 12:11:31 +00:00
Hosted Weblate 347851d6bb
Merge branch 'origin/develop' into Weblate 2024-05-08 10:21:46 +02:00
jolupa abec71074b
locale: update translations (Catalan)
Currently translated at 100.0% (1930 of 1930 strings)

Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ca/
2024-05-08 10:21:45 +02:00
Lhcfl 272e30be0c refactor: replace the awful ?: chain with if-else; fix: force update ticker when props.time changed
related: ed6f866a4f

Co-authored-by: kakkokari-gtyih <kakkokari-gtyih@users.noreply.github.com>
2024-05-08 10:52:32 +08:00
老周部落 e79cc38002
feat: update email tips 2024-05-06 22:36:11 +08:00
14 changed files with 315 additions and 40 deletions

View File

@ -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.

View File

@ -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"

View File

@ -1872,6 +1872,10 @@ showAds: 显示社区横幅
enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Return 发送)
recommendedInstances: 推荐服务器
updateAvailable: 可能有可用更新!
updateEmailTips: 更新提醒邮件
updateEmailTipsInfo: 在 Firefish 发布新版本时接收更新提醒邮件。需要您正确设置发送邮件功能和管理员邮箱才会生效。
updateEmailTipsSecurityOnly: 只接收安全版本更新提醒
updateEmailTipsSecurityOnlyInfo: Firefish 采用滚动更新模式因此新版本可能会频繁发布。此选项用于只在安全更新发布时接收更新提醒邮件。
swipeOnMobile: 允许在页面之间滑动
swipeOnDesktop: 允许在桌面端以移动设备方式滑动
logoImageUrl: Logo 图像 URL

View File

@ -50,5 +50,8 @@
"execa": "8.0.1",
"pnpm": "8.15.7",
"typescript": "5.4.5"
},
"firefishCustomFields": {
"lastSecurityUpdate": "20240330"
}
}

View File

@ -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")]

View File

@ -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"`,
);
}
}

View File

@ -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,

View File

@ -559,6 +559,16 @@ export default function () {
},
);
systemQueue.add(
"updateEmailTips",
{},
{
repeat: { cron: "0 0 * * 0" },
removeOnComplete: true,
removeOnFail: true,
},
);
processSystemQueue(systemQueue);
}

View File

@ -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>>

View File

@ -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();
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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 ""; // absoluterelative使
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();

View File

@ -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(() => []);