Skip to content

Commit

Permalink
feat: telegram bot global push
Browse files Browse the repository at this point in the history
  • Loading branch information
dreamhunter2333 committed May 25, 2024
1 parent 9414f7a commit 44830df
Show file tree
Hide file tree
Showing 24 changed files with 232 additions and 147 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
# CHANGE LOG

## main branch

- UI lazy load
- telegram bot 添加用户全局推送功能
- 增加对 cloudflare verified 用户发送邮件

## v0.4.4

- 增加 telegram mini app
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/views/admin/AccountSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,29 @@ const { t } = useI18n({
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
address_block_list_placeholder: 'Please enter the keywords you want to block',
send_address_block_list: 'Address Block Keywords for send email',
verified_address_list: 'Verified Address List(Can send email by cf internal api)',
},
zh: {
save: '保存',
successTip: '保存成功',
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
send_address_block_list: '发送邮件地址屏蔽关键词',
verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
}
}
});
const addressBlockList = ref([])
const sendAddressBlockList = ref([])
const verifiedAddressList = ref([])
const fetchData = async () => {
try {
const res = await api.fetch(`/admin/account_settings`)
addressBlockList.value = res.blockList || []
sendAddressBlockList.value = res.sendBlockList || []
verifiedAddressList.value = res.verifiedAddressList || []
} catch (error) {
message.error(error.message || "error");
}
Expand All @@ -47,7 +51,8 @@ const save = async () => {
method: 'POST',
body: JSON.stringify({
blockList: addressBlockList.value || [],
sendBlockList: sendAddressBlockList.value || []
sendBlockList: sendAddressBlockList.value || [],
verifiedAddressList: verifiedAddressList.value || []
})
})
message.success(t('successTip'))
Expand All @@ -73,6 +78,10 @@ onMounted(async () => {
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
:placeholder="t('address_block_list_placeholder')" />
</n-form-item-row>
<n-form-item-row :label="t('verified_address_list')">
<n-select v-model:value="verifiedAddressList" filterable multiple tag
:placeholder="t('verified_address_list')" />
</n-form-item-row>
<n-button @click="save" type="primary" block :loading="loading">
{{ t('save') }}
</n-button>
Expand Down
24 changes: 22 additions & 2 deletions frontend/src/views/admin/Telegram.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const { t } = useI18n({
telegramAllowList: 'Telegram Allow List',
save: 'Save',
miniAppUrl: 'Telegram Mini App URL',
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
globalMailPushList: 'Global Mail Push List',
},
zh: {
init: '初始化',
Expand All @@ -33,6 +35,8 @@ const { t } = useI18n({
telegramAllowList: 'Telegram 白名单',
save: '保存',
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
enableGlobalMailPush: '启用全局邮件推送(手动输入 telegram 用户 ID)',
globalMailPushList: '全局邮件推送用户列表',
}
}
});
Expand Down Expand Up @@ -66,15 +70,22 @@ class TelegramSettings {
enableAllowList: boolean;
allowList: string[];
miniAppUrl: string;
enableGlobalMailPush: boolean;
globalMailPushList: string[];
constructor(enableAllowList: boolean, allowList: string[], miniAppUrl: string) {
constructor(
enableAllowList: boolean, allowList: string[], miniAppUrl: string,
enableGlobalMailPush: boolean, globalMailPushList: string[]
) {
this.enableAllowList = enableAllowList;
this.allowList = allowList;
this.miniAppUrl = miniAppUrl;
this.enableGlobalMailPush = enableGlobalMailPush;
this.globalMailPushList = globalMailPushList;
}
}
const settings = ref(new TelegramSettings(false, [], ''))
const settings = ref(new TelegramSettings(false, [], '', false, []))
const getSettings = async () => {
try {
Expand Down Expand Up @@ -115,6 +126,15 @@ onMounted(async () => {
:placeholder="t('telegramAllowList')" />
</n-input-group>
</n-form-item-row>
<n-form-item-row :label="t('enableGlobalMailPush')">
<n-input-group>
<n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;">
{{ t('enable') }}
</n-checkbox>
<n-select v-model:value="settings.globalMailPushList" filterable multiple tag
style="width: 80%;" :placeholder="t('globalMailPushList')" />
</n-input-group>
</n-form-item-row>
<n-form-item-row :label="t('miniAppUrl')">
<n-input v-model:value="settings.miniAppUrl"></n-input>
</n-form-item-row>
Expand Down
5 changes: 5 additions & 0 deletions vitepress-docs/docs/en/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ node_compat = true
# [triggers]
# crons = [ "0 0 * * *" ]

# send mail by cf mail
# send_email = [
# { name = "SEND_MAIL" },
# ]

[vars]
PREFIX = "tmp" # The mailbox name prefix to be processed
# If you want your site to be private, uncomment below and change your password
Expand Down
5 changes: 5 additions & 0 deletions vitepress-docs/docs/zh/guide/cli/worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ node_compat = true
# [triggers]
# crons = [ "0 0 * * *" ]

# 通过 Cloudflare 发送邮件
# send_email = [
# { name = "SEND_MAIL" },
# ]

[vars]
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
# 如果你想要你的网站私有,取消下面的注释,并修改密码
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { Context } from 'hono';

import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils';
import { UserSettings, GeoData, UserInfo } from "../models";
import { handleListQuery } from '../common'
import { HonoCustomType } from '../types';

export default {
getSetting: async (c) => {
getSetting: async (c: Context<HonoCustomType>) => {
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value);
return c.json(settings)
},
saveSetting: async (c) => {
saveSetting: async (c: Context<HonoCustomType>) => {
const value = await c.req.json();
const settings = new UserSettings(value);
if (settings.enableMailVerify && !c.env.KV) {
return c.text("Please enable KV first if you want to enable mail verify", 403)
}
if (settings.enableMailVerify) {
if (settings.enableMailVerify && !settings.verifyMailSender) {
return c.text("Please provide verifyMailSender", 400)
}
if (settings.enableMailVerify && settings.verifyMailSender) {
const mailDomain = settings.verifyMailSender.split("@")[1];
const domains = getDomains(c);
if (!domains.includes(mailDomain)) {
Expand All @@ -28,7 +34,7 @@ export default {
await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings));
return c.json({ success: true })
},
getUsers: async (c) => {
getUsers: async (c: Context<HonoCustomType>) => {
const { limit, offset, query } = c.req.query();
if (query) {
return await handleListQuery(c,
Expand All @@ -48,15 +54,15 @@ export default {
[], limit, offset
);
},
createUser: async (c) => {
createUser: async (c: Context<HonoCustomType>) => {
const { email, password } = await c.req.json();
if (!email || !password) {
return c.text("Invalid email or password", 400)
}
// geo data
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
const geoData = new GeoData(reqIp, c.req.raw.cf);
const userInfo = new UserInfo(geoData);
const geoData = new GeoData(reqIp, c.req.raw.cf as any);
const userInfo = new UserInfo(geoData, email);
try {
checkUserPassword(password);
const { success } = await c.env.DB.prepare(
Expand All @@ -69,14 +75,15 @@ export default {
return c.text("Failed to register", 500)
}
} catch (e) {
if (e.message && e.message.includes("UNIQUE")) {
const errorMsg = (e as Error).message;
if (errorMsg && errorMsg.includes("UNIQUE")) {
return c.text("User already exists", 400)
}
return c.text(`Failed to register: ${e.message}`, 500)
return c.text(`Failed to register: ${errorMsg}`, 500)
}
return c.json({ success: true })
},
deleteUser: async (c) => {
deleteUser: async (c: Context<HonoCustomType>) => {
const { user_id } = c.req.param();
if (!user_id) return c.text("Invalid user_id", 400);
const { success } = await c.env.DB.prepare(
Expand All @@ -90,7 +97,7 @@ export default {
}
return c.json({ success: true })
},
resetPassword: async (c) => {
resetPassword: async (c: Context<HonoCustomType>) => {
const { user_id } = c.req.param();
const { password } = await c.req.json();
if (!user_id) return c.text("Invalid user_id", 400);
Expand All @@ -103,7 +110,7 @@ export default {
return c.text("Failed to reset password", 500)
}
} catch (e) {
return c.text(`Failed to reset password: ${e.message}`, 500)
return c.text(`Failed to reset password: ${(e as Error).message}`, 500)
}
return c.json({ success: true });
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { Context } from 'hono';

import { cleanup } from '../common';
import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting } from '../utils';
import { CleanupSettings } from '../models';
import { HonoCustomType } from '../types';

export default {
cleanup: async (c) => {
cleanup: async (c: Context<HonoCustomType>) => {
const { cleanType, cleanDays } = await c.req.json();
try {
await cleanup(c, cleanType, cleanDays);
} catch (error) {
console.error(error);
return c.text(`Failed to cleanup ${error.message}`, 500)
return c.text(`Failed to cleanup ${(error as Error).message}`, 500)
}
return c.json({ success: true })
},
getCleanup: async (c) => {
getCleanup: async (c: Context<HonoCustomType>) => {
const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY);
const cleanupSetting = new CleanupSettings(value);
return c.json(cleanupSetting)
},
saveCleanup: async (c) => {
saveCleanup: async (c: Context<HonoCustomType>) => {
const value = await c.req.json();
const cleanupSetting = new CleanupSettings(value);
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
Expand Down
33 changes: 20 additions & 13 deletions worker/src/admin_api/index.js → worker/src/admin_api/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt'

import { HonoCustomType } from '../types'
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
import { newAddress, handleListQuery } from '../common'
import { CONSTANTS } from '../constants'
import cleanup_api from './cleanup_api'
import admin_user_api from './admin_user_api'
import webhook_settings from './webhook_settings'

const api = new Hono()
export const api = new Hono<HonoCustomType>()

api.get('/admin/address', async (c) => {
const { limit, offset, query } = c.req.query();
Expand Down Expand Up @@ -41,7 +43,7 @@ api.post('/admin/new_address', async (c) => {
const res = await newAddress(c, name, domain, enablePrefix);
return c.json(res);
} catch (e) {
return c.text(`Failed create address: ${e.message}`, 400)
return c.text(`Failed create address: ${(e as Error).message}`, 400)
}
})

Expand Down Expand Up @@ -181,16 +183,16 @@ api.get('/admin/sendbox', async (c) => {
api.get('/admin/statistics', async (c) => {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM raw_mails`
).first();
).first<{ count: number }>() || {};
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address`
).first();
).first<{ count: number }>() || {};
const { count: activeUserCount7days } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
).first();
).first<{ count: number }>() || {};
const { count: sendMailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM sendbox`
).first();
).first<{ count: number }>() || {};
return c.json({
mailCount: mailCount,
userCount: addressCount,
Expand All @@ -201,13 +203,13 @@ api.get('/admin/statistics', async (c) => {

api.get('/admin/account_settings', async (c) => {
try {
/** @type {Array<string>|undefined|null} */
const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
/** @type {Array<string>|undefined|null} */
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY);
return c.json({
blockList: blockList || [],
sendBlockList: sendBlockList || []
sendBlockList: sendBlockList || [],
verifiedAddressList: verifiedAddressList || []
})
} catch (error) {
console.error(error);
Expand All @@ -217,10 +219,13 @@ api.get('/admin/account_settings', async (c) => {

api.post('/admin/account_settings', async (c) => {
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
const { blockList, sendBlockList } = await c.req.json();
if (!blockList || !sendBlockList) {
const { blockList, sendBlockList, verifiedAddressList } = await c.req.json();
if (!blockList || !sendBlockList || !verifiedAddressList) {
return c.text("Invalid blockList or sendBlockList", 400)
}
if (!c.env.SEND_MAIL && verifiedAddressList.length > 0) {
return c.text("Please enable SEND_MAIL to use verifiedAddressList", 400)
}
await saveSetting(
c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY,
JSON.stringify(blockList)
Expand All @@ -229,6 +234,10 @@ api.post('/admin/account_settings', async (c) => {
c, CONSTANTS.SEND_BLOCK_LIST_KEY,
JSON.stringify(sendBlockList)
);
await saveSetting(
c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY,
JSON.stringify(verifiedAddressList)
)
return c.json({
success: true
})
Expand All @@ -245,5 +254,3 @@ api.post('/admin/users', admin_user_api.createUser)
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings);
api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings);

export { api }
2 changes: 1 addition & 1 deletion worker/src/admin_api/webhook_settings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context } from "hono";
import { HonoCustomType } from "../types";
import { CONSTANTS } from "../constants";
import { AdminWebhookSettings } from "../models/models";
import { AdminWebhookSettings } from "../models";

async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
const settings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
Expand Down
1 change: 1 addition & 0 deletions worker/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const CONSTANTS = {
SEND_BLOCK_LIST_KEY: 'send_block_list',
AUTO_CLEANUP_KEY: 'auto_cleanup',
USER_SETTINGS_KEY: 'user_settings',
VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list',

// KV
TG_KV_PREFIX: "temp-mail-telegram",
Expand Down
Loading

0 comments on commit 44830df

Please sign in to comment.