Compare commits

...

10 Commits

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


See merge request firefish/firefish!10716
2024-05-08 02:08:46 +00:00
naskya 971f196627
ci: yet another fix 2024-05-08 08:27:54 +09:00
naskya 8cc0e40d35
ci: remove more unneeded paths 2024-05-08 07:16:32 +09:00
naskya beeea86253
ci: remove unneeded steps from clippy check 2024-05-08 06:54:43 +09:00
naskya 084a4bc63a
ci: add pull_policy 2024-05-08 06:46:41 +09:00
naskya cda31d3dc7
Revert "refactor (backend): port publishNotesStream to backend-rs"
This reverts commit 5382dc5da8.

It turns out this sends an inccorect time info to the stream
since JavaScript's Date object doesn't have timezone info

I'll revisit this in the future
2024-05-08 06:08:26 +09:00
naskya 907578e8f8
ci: fix config error 2024-05-08 05:28:41 +09:00
naskya 2923ea86de
ci: update workflow rules 2024-05-08 05:26:59 +09:00
naskya 226c990385
ci: use buildah caches 2024-05-08 05:26:36 +09:00
老周部落 e79cc38002
feat: update email tips 2024-05-06 22:36:11 +08:00
19 changed files with 298 additions and 28 deletions

View File

@ -3,8 +3,10 @@ image: docker.io/rust:slim-bookworm
services:
- name: docker.io/groonga/pgroonga:latest-alpine-12-slim
alias: postgres
pull_policy: if-not-present
- name: docker.io/redis:7-alpine
alias: redis
pull_policy: if-not-present
workflow:
rules:
@ -12,6 +14,11 @@ workflow:
when: always
- if: $CI_MERGE_REQUEST_PROJECT_PATH == 'firefish/firefish'
when: always
- if: $CI_PROJECT_PATH != 'firefish/firefish'
changes:
paths:
- .gitlab-ci.yml
when: never
- when: never
cache:
@ -79,10 +86,7 @@ client_build_test:
- packages/client/*
- packages/firefish-js/*
- packages/sw/*
- scripts/**/*
- locales/**/*
- package.json
- pnpm-lock.yaml
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
@ -93,6 +97,15 @@ client_build_test:
- Cargo.toml
- Cargo.lock
when: never
services: []
before_script:
- apt-get update && apt-get -y upgrade
- apt-get -y --no-install-recommends install curl
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
- apt-get install -y --no-install-recommends build-essential python3 perl nodejs
- corepack enable
- corepack prepare pnpm@latest --activate
- cp .config/ci.yml .config/default.yml
script:
- pnpm install --frozen-lockfile
- pnpm --filter 'firefish-js' --filter 'client' --filter 'sw' run build:debug
@ -119,8 +132,21 @@ container_image_build:
- apt-get install -y --no-install-recommends buildah ca-certificates fuse-overlayfs
- buildah login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
- export IMAGE_CACHE="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop/cache"
script:
- buildah build --isolation chroot --device /dev/fuse:rw --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add all --tag "${IMAGE_TAG}" --platform linux/amd64 .
- |-
buildah build \
--isolation chroot \
--device /dev/fuse:rw \
--security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
--cap-add all \
--platform linux/amd64 \
--layers \
--cache-to "${IMAGE_CACHE}" \
--cache-from "${IMAGE_CACHE}" \
--tag "${IMAGE_TAG}" \
.
- buildah inspect "${IMAGE_TAG}"
- buildah push "${IMAGE_TAG}"
@ -157,8 +183,12 @@ cargo_clippy:
- Cargo.lock
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
script:
services: []
before_script:
- apt-get install -y --no-install-recommends build-essential clang mold perl
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
- rustup component add clippy
script:
- cargo clippy -- -D warnings
renovate:

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

@ -1292,7 +1292,6 @@ export interface AbuseUserReportLike {
comment: string
}
export function publishToModerationStream(moderatorId: string, report: AbuseUserReportLike): void
export function publishToNotesStream(note: Note): void
export function getTimestamp(id: string): number
/**
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.

View File

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, publishToNotesStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE
@ -381,7 +381,6 @@ module.exports.publishToChatIndexStream = publishToChatIndexStream
module.exports.publishToBroadcastStream = publishToBroadcastStream
module.exports.publishToGroupChatStream = publishToGroupChatStream
module.exports.publishToModerationStream = publishToModerationStream
module.exports.publishToNotesStream = publishToNotesStream
module.exports.getTimestamp = getTimestamp
module.exports.genId = genId
module.exports.genIdAt = genIdAt

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

@ -5,7 +5,6 @@ pub mod chat_index;
pub mod custom_emoji;
pub mod group_chat;
pub mod moderation;
pub mod new_note;
use crate::config::CONFIG;
use crate::database::redis_conn;
@ -26,7 +25,7 @@ pub enum Stream {
#[strum(to_string = "noteStream:{note_id}")]
Note { note_id: String },
#[strum(serialize = "notesStream")]
NewNote,
Notes,
#[strum(to_string = "userListStream:{list_id}")]
UserList { list_id: String },
#[strum(to_string = "mainStream:{user_id}")]

View File

@ -1,10 +0,0 @@
use crate::model::entity::note;
use crate::service::stream::{publish_to_stream, Error, Stream};
// for napi export (https://github.com/napi-rs/napi-rs/issues/2060)
type Note = note::Model;
#[crate::export(js_name = "publishToNotesStream")]
pub fn publish(note: &Note) -> Result<(), Error> {
publish_to_stream(&Stream::NewNote, None, Some(serde_json::to_string(note)?))
}

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

@ -1,5 +1,9 @@
import * as mfm from "mfm-js";
import { publishMainStream, publishNoteStream } from "@/services/stream.js";
import {
publishMainStream,
publishNotesStream,
publishNoteStream,
} from "@/services/stream.js";
import DeliverManager from "@/remote/activitypub/deliver-manager.js";
import renderNote from "@/remote/activitypub/renderer/note.js";
import renderCreate from "@/remote/activitypub/renderer/create.js";
@ -45,7 +49,6 @@ import {
genId,
genIdAt,
isSilencedServer,
publishToNotesStream,
} from "backend-rs";
import { countSameRenotes } from "@/misc/count-same-renotes.js";
import { deliverToRelays, getCachedRelays } from "../relay.js";
@ -508,7 +511,7 @@ export default async (
30,
);
}
publishToNotesStream(toRustObject(noteToPublish));
publishNotesStream(noteToPublish);
}
} finally {
await lock.release();

View File

@ -193,10 +193,9 @@ class Publisher {
// );
// };
/* ported to backend-rs */
// public publishNotesStream = (note: Note): void => {
// this.publish("notesStream", null, note);
// };
public publishNotesStream = (note: Note): void => {
this.publish("notesStream", null, note);
};
/* ported to backend-rs */
// public publishAdminStream = <K extends keyof AdminStreamTypes>(
@ -222,7 +221,7 @@ export const publishUserEvent = publisher.publishUserEvent;
export const publishMainStream = publisher.publishMainStream;
export const publishDriveStream = publisher.publishDriveStream;
export const publishNoteStream = publisher.publishNoteStream;
// export const publishNotesStream = publisher.publishNotesStream;
export const publishNotesStream = publisher.publishNotesStream;
// export const publishChannelStream = publisher.publishChannelStream;
export const publishUserListStream = publisher.publishUserListStream;
// export const publishAntennaStream = publisher.publishAntennaStream;

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