写完了 PMail 插件总感觉还差了点什么,又翻了翻原仓库的 issues,发现有人提到没有多用户推送的功能,刚好设置页面放一个说明文档好像有点鸡肋,就想着改改。但是我要先搞明白这个前后端是怎么交互的,继续翻看源码,刚好有一个插件可以参考。
阅读源码
// server/hooks/spam_block/spam_block.go
//go:embed static/index.html
var index string
//go:embed static/jquery.js
var jquery string
// SettingsHtml 插件页面
func (s *SpamBlock) SettingsHtml(ctx *context.Context, url string, requestData string) string {
if strings.Contains(url, "jquery.js") {
return jquery
}
if strings.Contains(url, "index.html") {
if !ctx.IsAdmin {
return fmt.Sprintf(`
<div>
Please contact the administrator for configuration.
</div>
`)
}
return fmt.Sprintf(index, s.cfg.ApiURL, s.cfg.ApiTimeout, s.cfg.Threshold)
}
var cfg SpamBlockConfig
var tempCfg map[string]string
err := json.Unmarshal([]byte(requestData), &tempCfg)
if err != nil {
return err.Error()
}
cfg.ApiURL = tempCfg["url"]
cfg.Threshold = cast.ToFloat64(tempCfg["threshold"])
cfg.ApiTimeout = cast.ToInt(tempCfg["timeout"])
err = saveConfig(cfg)
if err != nil {
return err.Error()
}
s.cfg = cfg
return "success"
}大致是看明白了,通过 url 路径不同返回不同的内容,通过 requestData 获取前端发送的数据,那就可以做 api 接口,get 请求返回用户的配置信息,post 请求设置用户的配置信息,但是怎么绑定是哪个用户发起的请求呢?继续看源码!
// server/hooks/spam_block/spam_block.go
func (s *SpamBlock) SettingsHtml(ctx *context.Context, url string, requestData string) string {}
// server/utils/context/context.go
type Context struct {
context.Context `json:"-"`
UserID int
UserAccount string
UserName string
Values map[string]any
Lang string
IsAdmin bool
}context 刚好携带了用户 ID 信息,那就创建一个用户表,唯一键是用户ID,其它字段存储用户配置信息,收到邮件时通过用户 ID 判断应该发给哪个 ChatID,计划通!
创建表结构
// model/telegram_push.go
package model
import (
"xorm.io/xorm"
)
// 表结构
type TelegramPushSetting struct {
ID int `xorm:"id pk autoincr comment('主键')" json:"-"`
UserID int `xorm:"user_id int index('idx_uid') comment('用户id') unique('idx_uid')" json:"-"`
ChatID string `xorm:"chat_id varchar(255) index('idx_cid') index comment('聊天id')" json:"chat_id"`
ShowContent bool `xorm:"show_content bool not null default 1 comment('是否显示邮件内容')" json:"show_content"`
SpoilerContent bool `xorm:"spoiler_content bool not null default 1 comment('是否 spoiler 显示邮件内容')" json:"spoiler_content"`
SendAttachments bool `xorm:"send_attachments bool not null default 1 comment('是否发送附件')" json:"send_attachments"`
DisableLinkPreview bool `xorm:"disable_link_preview bool not null default 1 comment('是否禁用链接预览')" json:"disable_link_preview"`
}
// 表名
func (u *TelegramPushSetting) TableName() string {
return "plugin_telegram_push_setting"
}
// 获取配置
func GetSetting(db *xorm.Engine, userID int) (setting *TelegramPushSetting, err error) {
setting = &TelegramPushSetting{}
has, err := db.Where("user_id = ?", userID).Get(setting)
if err != nil {
return nil, err
}
if !has {
// 不存在时创建默认配置,ChatID 空时,不发送消息
setting = &TelegramPushSetting{
UserID: userID,
ChatID: "",
ShowContent: true,
SpoilerContent: true,
SendAttachments: true,
DisableLinkPreview: true,
}
_, err = db.Insert(setting)
if err != nil {
return nil, err
}
}
return setting, nil
}
// 创建或更新配置,哦,这里好像不应该创建了,上边创建过了,回去改改
func CreateOrUpdateSetting(db *xorm.Engine, setting *TelegramPushSetting) (err error) {
has, err := db.Exist(&TelegramPushSetting{UserID: setting.UserID})
if err != nil {
return err
}
if has {
_, err = db.Where("user_id = ?", setting.UserID).AllCols().Update(setting)
} else {
_, err = db.Insert(setting)
}
return err
}// db/db.go
package db
import (
"time"
"github.com/Jinnrry/pmail/config"
"github.com/Jinnrry/pmail/utils/errors"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
log "github.com/sirupsen/logrus"
"github.com/ydzydzydz/pmail_telegram_push/model"
_ "modernc.org/sqlite"
"xorm.io/xorm"
)
var Instance *xorm.Engine
// 复制粘贴原项目初始化 DB,稍作修改
func Init(config *config.Config) error {
dsn := config.DbDSN
var err error
switch config.DbType {
case "mysql":
Instance, err = xorm.NewEngine("mysql", dsn)
Instance.SetMaxOpenConns(100)
Instance.SetMaxIdleConns(10)
case "sqlite":
Instance, err = xorm.NewEngine("sqlite", dsn)
Instance.SetMaxOpenConns(1)
Instance.SetMaxIdleConns(1)
case "postgres":
Instance, err = xorm.NewEngine("postgres", dsn)
Instance.SetMaxOpenConns(100)
Instance.SetMaxIdleConns(10)
default:
return errors.New("Database Type Error!")
}
if err != nil {
log.Errorf("DB init Error! %s", err.Error())
return errors.Wrap(err)
}
Instance.SetConnMaxLifetime(30 * time.Minute)
Instance.ShowSQL(false)
// 创建表结构
Instance.Sync2(new(model.TelegramPushSetting))
return nil
}.header on -- 开启表头
.mode column -- 左对齐的列
.output stdout -- 发送输出到屏幕
.tables plugin_telegram_push_setting -- 列出表
.schema plugin_telegram_push_setting -- 列出建表语句
CREATE TABLE `plugin_telegram_push_setting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` INTEGER NULL, `chat_id` TEXT NULL, `show_content` INTEGER DEFAULT 1 NOT NULL, `spoiler_content` INTEGER DEFAULT 1 NOT NULL, `send_attachments` INTEGER DEFAULT 1 NOT NULL, `disable_link_preview` INTEGER DEFAULT 1 NOT NULL);
CREATE UNIQUE INDEX `UQE_plugin_telegram_push_setting_'idx_uid'` ON `plugin_telegram_push_setting` (`user_id`);
CREATE INDEX `IDX_plugin_telegram_push_setting_chat_id` ON `plugin_telegram_push_setting` (`chat_id`);
CREATE INDEX `IDX_plugin_telegram_push_setting_'idx_cid'` ON `plugin_telegram_push_setting` (`chat_id`);
SELECT * FROM sqlite_master WHERE type='table' AND name='plugin_telegram_push_setting'; -- 查看建表语句
PRAGMA table_info(plugin_telegram_push_setting); -- 获取表的详细列信息
cid name type notnull dflt_value pk
--- -------------------- ------- ------- ---------- --
0 id INTEGER 1 1
1 user_id INTEGER 0 0
2 chat_id TEXT 0 0
3 show_content INTEGER 1 1 0
4 spoiler_content INTEGER 1 1 0
5 send_attachments INTEGER 1 1 0
6 disable_link_preview INTEGER 1 1 0修改配置
// config/config.go
// 管理员用户只需要配置这些就够了,其它配置由普通用户自行配置
type PluginConfig struct {
TelegramBotToken string `json:"telegram_bot_token"`
Proxy string `json:"proxy" default:""`
Timeout int `json:"timeout" default:"30"`
Debug bool `json:"debug" default:"false"`
}// hook/hook.go
// 相应的这里也可简化了
type PmailTelegramPushHook struct {
bot *bot.Bot // 通过 bot 发送消息
mainConfig *pconfig.Config // 通过主配置文件获取网页版地址
pluginConfig *config.PluginConfig // 通过插件配置文件创建 bot
}接口设计
// hook/hook.go
func (h *PmailTelegramPushHook) SettingsHtml(ctx *context.Context, url string, requestData string) string {
switch {
// GET /api/plugin/settings/pmail_telegram_push/setting 返回 用户自定义配置
case strings.Contains(url, "setting"):
return h.getSetting(ctx.UserID)
// GET /api/plugin/settings/pmail_telegram_push/bot 返回机器人信息,用户绑定 bot
case strings.Contains(url, "bot"):
return h.getBotInfo()
// POST /api/plugin/settings/pmail_telegram_push/submit 保存用户的配置
case strings.Contains(url, "submit"):
return h.submitSetting(ctx.UserID, requestData)
default:
return SettingHtml
}
}
// 这里应该没有限定请求方法,好像也限制不了// hook/setting.go
var (
//go:embed dist/index.html
SettingHtml string
)
// 接口统一响应,返回前端处理
type Response struct {
// 0 成功、非 0 失败
Code int `json:"code"`
// 返回消息
Message string `json:"message"`
// 返回数据
Data any `json:"data"`
}
func (r *Response) Json() string {
json, err := json.Marshal(r)
if err != nil {
log.Errorf("marshal response failed, err: %v", err)
return ""
}
return string(json)
}
// 通过用户 ID 获取配置
func (h *PmailTelegramPushHook) getSetting(id int) string {
result, err := model.GetSetting(db.Instance, id)
if err != nil {
response := Response{
Code: -1,
Message: "get setting failed",
}
return response.Json()
}
response := Response{
Code: 0,
Message: "success",
Data: result,
}
return response.Json()
}
// 通过用户 ID 保存配置
func (h *PmailTelegramPushHook) submitSetting(id int, requestData string) string {
var setting model.TelegramPushSetting
if err := json.Unmarshal([]byte(requestData), &setting); err != nil {
log.Errorf("unmarshal setting request failed, err: %v", err)
response := Response{
Code: -1,
Message: "unmarshal setting request failed",
}
return response.Json()
}
setting.UserID = id
setting.ChatID = strings.TrimSpace(setting.ChatID)
if err := model.CreateOrUpdateSetting(db.Instance, &setting); err != nil {
log.Errorf("create or update setting failed, err: %v", err)
response := Response{
Code: -1,
Message: "create or update setting failed",
}
return response.Json()
}
response := Response{
Code: 0,
Message: "success",
}
return response.Json()
}
// 机器人信息,用户名,链接等等
type TelegramPushBotInfo struct {
Username string `json:"username"`
FirstName string `json:"first_name"`
BotLink string `json:"bot_link"`
}
// 获取机器人信息
func (h *PmailTelegramPushHook) getBotInfo() string {
me, err := h.bot.GetMe(context.Background())
if err != nil {
response := Response{
Code: -1,
Message: "get bot me failed",
}
return response.Json()
}
response := Response{
Code: 0,
Message: "success",
Data: TelegramPushBotInfo{
Username: me.Username,
FirstName: me.FirstName,
BotLink: fmt.Sprintf("https://t.me/%s", me.Username),
},
}
fmt.Println(response.Json())
return response.Json()
}发送通知
// hook/hook.go
func (h *PmailTelegramPushHook) ReceiveSaveAfter(ctx *context.Context, email *parsemail.Email, ue []*models.UserEmail) {
for _, u := range ue {
if u.IsRead == 0 && u.Status == 0 && email.MessageId > 0 {
// 根据用户 ID 获取用户配置,ChatID 不为空时发送通知
setting, err := model.GetSetting(db.Instance, u.UserID)
if err != nil || setting.ChatID == "" {
continue
}
// 后边逻辑保持不变
msg, err := h.sendNotification(email, setting)
if err != nil {
log.Errorf("send notification failed, err: %v", err)
continue
}
if setting.SendAttachments && len(email.Attachments) > 0 {
if _, err = h.sendAttachments(msg.ID, email, setting); err != nil {
log.Errorf("send attachments failed, err: %v", err)
} else {
log.Infof("send attachments success, message id: %d", msg.ID)
}
}
}
}
}后端部分基本搞定,编写设置页面
设置页面
pnpm install element-plus
pnpm install axios// main.js
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')<!-- App.vue -->
<template>
<div class="pmail-telegram-push-settings">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>Telegram 推送设置</span>
</div>
</template>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="180px"
label-position="left"
v-loading="loading"
>
<el-form-item label="Chat ID" prop="chat_id">
<el-input v-model="formData.chat_id" placeholder="请输入 Telegram Chat ID" />
</el-form-item>
<el-form-item label="显示邮件内容">
<el-switch v-model="formData.show_content" />
</el-form-item>
<el-form-item label="显示邮件内容时添加防剧透">
<el-switch v-model="formData.spoiler_content" />
</el-form-item>
<el-form-item label="发送附件">
<el-switch v-model="formData.send_attachments" />
</el-form-item>
<el-form-item label="禁用链接预览">
<el-switch v-model="formData.disable_link_preview" />
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="!botInfo?.bot_link" @click="contactBot">
推送机器人
</el-button>
<el-button type="primary" @click="submitForm" :loading="loading"> 保存设置 </el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import './resize.js'
const formRef = ref(null)
const loading = ref(false)
const formData = ref({
chat_id: '',
show_content: true,
spoiler_content: true,
send_attachments: true,
disable_link_preview: true,
})
// 这里好像有点问题,ChatID 没办法置空,一旦设置了,没法取消,回去再改改
// 这里 ChatID 应该为 int64 类型?前后端都应该限制一下,但好像 golang bot 库用的是 any,就不改了。
const rules = {
chat_id: [{ required: true, message: '请输入 Chat ID', trigger: 'blur', whitespace: true }],
}
const botInfo = ref({
username: '',
first_name: '',
bot_link: '',
})
// 获取设置数据
const fetchSettings = async () => {
try {
loading.value = true
const response = await axios.get('/api/plugin/settings/pmail_telegram_push/setting')
if (response.data && response.data.code === 0) {
formData.value = {
...formData.value,
...response.data.data,
}
} else {
ElMessage.error(response.data.message || '获取设置失败')
}
} catch (error) {
console.error('Failed to fetch settings:', error)
ElMessage.error('获取设置失败')
} finally {
loading.value = false
}
}
// 提交表单
const submitForm = async () => {
try {
await formRef.value.validate()
loading.value = true
const response = await axios.post(
'/api/plugin/settings/pmail_telegram_push/submit',
formData.value,
)
if (response.data && response.data.code === 0) {
ElMessage.success('设置保存成功')
} else {
ElMessage.error(response.data.message || '保存设置失败')
}
} catch (error) {
console.error('Failed to save settings:', error)
if (error.response && error.response.data.message) {
ElMessage.error(error.response.data.message)
} else {
ElMessage.error('保存设置失败')
}
} finally {
loading.value = false
}
}
// 获取机器人信息
const getBotInfo = async () => {
try {
const response = await axios.get('/api/plugin/settings/pmail_telegram_push/bot')
if (response.data && response.data.code === 0) {
botInfo.value = {
...botInfo.value,
...response.data.data,
}
} else {
ElMessage.error(response.data.message || '获取机器人信息失败')
}
} catch (error) {
console.error('Failed to get bot:', error)
ElMessage.error('获取机器人信息失败')
}
}
// 联系机器人
const contactBot = () => {
if (botInfo.value.bot_link) {
window.open(botInfo.value.bot_link, '_blank')
} else {
ElMessage.error('机器人链接不存在')
}
}
onMounted(() => {
fetchSettings()
getBotInfo()
})
</script>
<style scoped>
.pmail-telegram-push-settings {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.box-card {
margin-top: 20px;
}
.card-header {
font-size: 18px;
font-weight: bold;
}
</style>编译运行
make
make install
docker-compose down
docker-compose up -d
# 创建不同用户访问网页前端设置
# 向不同用户发送邮件测试
打包发布
git tag v0.0.2
git push --tags仓库链接
原创
PMail Telegram 推送插件(二)
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
赞赏支持
如果觉得文章对你有帮助,可以请作者喝杯咖啡 ☕
评论交流
欢迎留下你的想法