├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── github-pages.yml │ ├── macos-release.yml │ ├── windows-release.yml │ ├── docker-image-amd64.yml │ ├── linux-release.yml │ └── docker-image-arm64.yml ├── .gitignore ├── web ├── vercel.json ├── public │ ├── logo.png │ ├── robots.txt │ ├── favicon.ico │ └── index.html ├── src │ ├── helpers │ │ ├── history.js │ │ ├── index.js │ │ ├── auth-header.js │ │ ├── api.js │ │ ├── render.js │ │ └── loader.js │ ├── constants │ │ ├── common.constant.js │ │ ├── index.js │ │ ├── toast.constants.js │ │ ├── user.constants.js │ │ └── channel.constants.js │ ├── pages │ │ ├── User │ │ │ ├── index.js │ │ │ ├── AddUser.js │ │ │ └── EditUser.js │ │ ├── Channel │ │ │ └── index.js │ │ ├── Message │ │ │ ├── index.js │ │ │ └── EditMessage.js │ │ ├── Webhook │ │ │ ├── index.js │ │ │ └── EditWebhook.js │ │ ├── NotFound │ │ │ └── index.js │ │ ├── Setting │ │ │ └── index.js │ │ ├── About │ │ │ └── index.js │ │ └── Home │ │ │ └── index.js │ ├── components │ │ ├── PrivateRoute.js │ │ ├── Loading.js │ │ ├── Footer.js │ │ ├── GitHubOAuth.js │ │ ├── PasswordResetConfirm.js │ │ ├── PasswordResetForm.js │ │ ├── PushSetting.js │ │ ├── OtherSetting.js │ │ └── Header.js │ ├── context │ │ ├── User │ │ │ ├── reducer.js │ │ │ └── index.js │ │ └── Status │ │ │ ├── reducer.js │ │ │ └── index.js │ ├── index.css │ └── index.js ├── .gitignore ├── README.md └── package.json ├── common ├── validate.go ├── template.go ├── crypto.go ├── embed-file-system.go ├── redis.go ├── public │ └── message.html ├── logger.go ├── init.go ├── rate-limit.go ├── email.go ├── verification.go ├── constants.go └── utils.go ├── router ├── main.go ├── web-router.go └── api-router.go ├── middleware ├── cache.go ├── sse.go ├── auth.go ├── turnstile-check.go └── rate-limit.go ├── docker-compose.yml ├── Dockerfile ├── channel ├── email.go ├── custom.go ├── bark.go ├── group.go ├── main.go ├── discord.go ├── corp.go ├── message-queue.go ├── one-bot.go ├── tencent-alarm.go ├── ding.go ├── telegram.go ├── lark.go ├── wechat-test-account.go ├── client.go ├── lark-app.go └── wechat-corp-account.go ├── LICENSE ├── controller ├── message-sse.go ├── websocket.go ├── option.go ├── wechat.go ├── misc.go ├── github.go └── channel.go ├── docs └── API.md ├── main.go ├── model ├── main.go ├── webhook.go ├── message.go ├── channel.go └── option.go ├── go.mod └── bin ├── migrate_v3_to_v4.sql └── migrate_v3_to_v4.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://iamazing.cn/page/reward'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | upload 4 | *.exe 5 | *.db 6 | build -------------------------------------------------------------------------------- /web/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/songquanpeng/message-pusher/HEAD/web/public/logo.png -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/songquanpeng/message-pusher/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/src/helpers/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | export const history = createBrowserHistory(); -------------------------------------------------------------------------------- /web/src/constants/common.constant.js: -------------------------------------------------------------------------------- 1 | export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend! 2 | -------------------------------------------------------------------------------- /web/src/helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from './history'; 2 | export * from './auth-header'; 3 | export * from './utils'; 4 | export * from './api'; -------------------------------------------------------------------------------- /web/src/constants/index.js: -------------------------------------------------------------------------------- 1 | export * from './toast.constants'; 2 | export * from './user.constants'; 3 | export * from './common.constant'; 4 | export * from './channel.constants'; -------------------------------------------------------------------------------- /common/validate.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/go-playground/validator/v10" 4 | 5 | var Validate *validator.Validate 6 | 7 | func init() { 8 | Validate = validator.New() 9 | } 10 | -------------------------------------------------------------------------------- /web/src/constants/toast.constants.js: -------------------------------------------------------------------------------- 1 | export const toastConstants = { 2 | SUCCESS_TIMEOUT: 500, 3 | INFO_TIMEOUT: 3000, 4 | ERROR_TIMEOUT: 5000, 5 | WARNING_TIMEOUT: 10000, 6 | NOTICE_TIMEOUT: 20000 7 | }; 8 | -------------------------------------------------------------------------------- /router/main.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "embed" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { 9 | SetApiRouter(router) 10 | setWebRouter(router, buildFS, indexPage) 11 | } 12 | -------------------------------------------------------------------------------- /middleware/cache.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | func Cache() func(c *gin.Context) { 8 | return func(c *gin.Context) { 9 | c.Header("Cache-Control", "max-age=604800") // one week 10 | c.Next() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | message-pusher: 5 | image: justsong/message-pusher 6 | restart: unless-stopped 7 | ports: 8 | - 3000:3000 9 | environment: 10 | - TZ=Asia/Shanghai 11 | volumes: 12 | - ./data:/data 13 | - /etc/localtime:/etc/localtime:ro -------------------------------------------------------------------------------- /web/src/helpers/auth-header.js: -------------------------------------------------------------------------------- 1 | export function authHeader() { 2 | // return authorization header with jwt token 3 | let user = JSON.parse(localStorage.getItem('user')); 4 | 5 | if (user && user.token) { 6 | return { 'Authorization': 'Bearer ' + user.token }; 7 | } else { 8 | return {}; 9 | } 10 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 项目群聊 4 | url: https://msgpusher.com/ 5 | about: 演示站首页有官方群聊信息 6 | - name: 赞赏支持 7 | url: https://iamazing.cn/page/reward 8 | about: 请作者喝杯咖啡,以激励作者持续开发 9 | - name: 付费部署或定制功能 10 | url: https://msgpusher.com/ 11 | about: 加群后联系群主 12 | -------------------------------------------------------------------------------- /web/src/helpers/api.js: -------------------------------------------------------------------------------- 1 | import { showError } from './utils'; 2 | import axios from 'axios'; 3 | 4 | export const API = axios.create({ 5 | baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '', 6 | }); 7 | 8 | API.interceptors.response.use( 9 | (response) => response, 10 | (error) => { 11 | showError(error); 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /web/src/pages/User/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Segment, Header } from 'semantic-ui-react'; 3 | import UsersTable from '../../components/UsersTable'; 4 | 5 | const User = () => ( 6 | <> 7 | 8 |
管理用户
9 | 10 |
11 | 12 | ); 13 | 14 | export default User; 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能请求 3 | about: 使用简练详细的语言描述希望加入的新功能 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **例行检查** 11 | + [ ] 我已确认目前没有类似 issue 12 | + [ ] 我已确认我已升级到最新版本 13 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 14 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭 15 | 16 | **功能描述** 17 | 18 | **应用场景** 19 | -------------------------------------------------------------------------------- /web/src/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router-dom'; 2 | 3 | import { history } from '../helpers'; 4 | 5 | 6 | function PrivateRoute({ children }) { 7 | if (!localStorage.getItem('user')) { 8 | return ; 9 | } 10 | return children; 11 | } 12 | 13 | export { PrivateRoute }; -------------------------------------------------------------------------------- /common/template.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "embed" 5 | "html/template" 6 | ) 7 | 8 | //go:embed public 9 | var FS embed.FS 10 | 11 | func LoadTemplate() *template.Template { 12 | var funcMap = template.FuncMap{ 13 | "unescape": UnescapeHTML, 14 | } 15 | t := template.Must(template.New("").Funcs(funcMap).ParseFS(FS, "public/*.html")) 16 | return t 17 | } 18 | -------------------------------------------------------------------------------- /web/src/pages/Channel/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header, Segment } from 'semantic-ui-react'; 3 | import ChannelsTable from '../../components/ChannelsTable'; 4 | 5 | const Channel = () => ( 6 | <> 7 | 8 |
我的通道
9 | 10 |
11 | 12 | ); 13 | 14 | export default Channel; 15 | -------------------------------------------------------------------------------- /web/src/pages/Message/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header, Segment } from 'semantic-ui-react'; 3 | import MessagesTable from '../../components/MessagesTable'; 4 | 5 | const Message = () => ( 6 | <> 7 | 8 |
我的消息
9 | 10 |
11 | 12 | ); 13 | 14 | export default Message; 15 | -------------------------------------------------------------------------------- /web/src/pages/Webhook/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header, Segment } from 'semantic-ui-react'; 3 | import WebhooksTable from '../../components/WebhooksTable'; 4 | 5 | const Webhook = () => ( 6 | <> 7 | 8 |
我的接口
9 | 10 |
11 | 12 | ); 13 | 14 | export default Webhook; 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告问题 3 | about: 使用简练详细的语言描述你遇到的问题 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **例行检查** 11 | + [ ] 我已确认目前没有类似 issue 12 | + [ ] 我已确认我已升级到最新版本 13 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 14 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭 15 | 16 | **问题描述** 17 | 18 | **复现步骤** 19 | 20 | **预期结果** 21 | 22 | **相关截图** 23 | 如果没有的话,请删除此节。 -------------------------------------------------------------------------------- /web/src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dimmer, Loader, Segment } from 'semantic-ui-react'; 3 | 4 | const Loading = ({ prompt: name = 'page' }) => { 5 | return ( 6 | 7 | 8 | 加载 {name} 中... 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default Loading; 15 | -------------------------------------------------------------------------------- /web/src/context/User/reducer.js: -------------------------------------------------------------------------------- 1 | export const reducer = (state, action) => { 2 | switch (action.type) { 3 | case 'login': 4 | return { 5 | ...state, 6 | user: action.payload 7 | }; 8 | case 'logout': 9 | return { 10 | ...state, 11 | user: undefined 12 | }; 13 | 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export const initialState = { 20 | user: undefined 21 | }; -------------------------------------------------------------------------------- /web/src/context/Status/reducer.js: -------------------------------------------------------------------------------- 1 | export const reducer = (state, action) => { 2 | switch (action.type) { 3 | case 'set': 4 | return { 5 | ...state, 6 | status: action.payload, 7 | }; 8 | case 'unset': 9 | return { 10 | ...state, 11 | status: undefined, 12 | }; 13 | default: 14 | return state; 15 | } 16 | }; 17 | 18 | export const initialState = { 19 | status: undefined, 20 | }; 21 | -------------------------------------------------------------------------------- /web/src/pages/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Segment, Header } from 'semantic-ui-react'; 3 | 4 | const NotFound = () => ( 5 | <> 6 |
14 | 15 | 未找到所请求的页面 16 | 17 | 18 | ); 19 | 20 | export default NotFound; 21 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .idea 25 | package-lock.json 26 | yarn.lock -------------------------------------------------------------------------------- /middleware/sse.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func SetSSEHeaders() func(c *gin.Context) { 6 | return func(c *gin.Context) { 7 | c.Writer.Header().Set("Content-Type", "text/event-stream") 8 | c.Writer.Header().Set("Cache-Control", "no-cache") 9 | c.Writer.Header().Set("Connection", "keep-alive") 10 | c.Writer.Header().Set("Transfer-Encoding", "chunked") 11 | c.Writer.Header().Set("X-Accel-Buffering", "no") 12 | c.Next() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /common/crypto.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | func Password2Hash(password string) (string, error) { 6 | passwordBytes := []byte(password) 7 | hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) 8 | return string(hashedPassword), err 9 | } 10 | 11 | func ValidatePasswordAndHash(password string, hash string) bool { 12 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 13 | return err == nil 14 | } 15 | -------------------------------------------------------------------------------- /web/src/context/User/index.js: -------------------------------------------------------------------------------- 1 | // contexts/User/index.jsx 2 | 3 | import React from "react" 4 | import { reducer, initialState } from "./reducer" 5 | 6 | export const UserContext = React.createContext({ 7 | state: initialState, 8 | dispatch: () => null 9 | }) 10 | 11 | export const UserProvider = ({ children }) => { 12 | const [state, dispatch] = React.useReducer(reducer, initialState) 13 | 14 | return ( 15 | 16 | { children } 17 | 18 | ) 19 | } -------------------------------------------------------------------------------- /web/src/context/Status/index.js: -------------------------------------------------------------------------------- 1 | // contexts/User/index.jsx 2 | 3 | import React from 'react'; 4 | import { initialState, reducer } from './reducer'; 5 | 6 | export const StatusContext = React.createContext({ 7 | state: initialState, 8 | dispatch: () => null, 9 | }); 10 | 11 | export const StatusProvider = ({ children }) => { 12 | const [state, dispatch] = React.useReducer(reducer, initialState); 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 消息推送服务 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /web/src/constants/user.constants.js: -------------------------------------------------------------------------------- 1 | export const userConstants = { 2 | REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', 3 | REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', 4 | REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', 5 | 6 | LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', 7 | LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', 8 | LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', 9 | 10 | LOGOUT: 'USERS_LOGOUT', 11 | 12 | GETALL_REQUEST: 'USERS_GETALL_REQUEST', 13 | GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', 14 | GETALL_FAILURE: 'USERS_GETALL_FAILURE', 15 | 16 | DELETE_REQUEST: 'USERS_DELETE_REQUEST', 17 | DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', 18 | DELETE_FAILURE: 'USERS_DELETE_FAILURE' 19 | }; 20 | -------------------------------------------------------------------------------- /common/embed-file-system.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "embed" 5 | "github.com/gin-contrib/static" 6 | "io/fs" 7 | "net/http" 8 | ) 9 | 10 | // Credit: https://github.com/gin-contrib/static/issues/19 11 | 12 | type embedFileSystem struct { 13 | http.FileSystem 14 | } 15 | 16 | func (e embedFileSystem) Exists(prefix string, path string) bool { 17 | _, err := e.Open(path) 18 | if err != nil { 19 | return false 20 | } 21 | return true 22 | } 23 | 24 | func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem { 25 | efs, err := fs.Sub(fsEmbed, targetPath) 26 | if err != nil { 27 | panic(err) 28 | } 29 | return embedFileSystem{ 30 | FileSystem: http.FS(efs), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # React Template 2 | 3 | ## Basic Usages 4 | 5 | ```shell 6 | # Install dependencies 7 | npm install 8 | 9 | # Runs the app in the development mode 10 | npm start 11 | 12 | # Builds the app for production to the `build` folder 13 | npm run build 14 | ``` 15 | 16 | If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build, 17 | for example: `REACT_APP_SERVER=http://your.domain.com`. 18 | 19 | Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled. 20 | 21 | ## Reference 22 | 23 | 1. https://github.com/OIerDb-ng/OIerDb 24 | 2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example 25 | -------------------------------------------------------------------------------- /router/web-router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "embed" 5 | "github.com/gin-contrib/static" 6 | "github.com/gin-gonic/gin" 7 | "message-pusher/common" 8 | "message-pusher/controller" 9 | "message-pusher/middleware" 10 | "net/http" 11 | ) 12 | 13 | func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { 14 | router.Use(middleware.GlobalWebRateLimit()) 15 | router.Use(middleware.Cache()) 16 | router.GET("/public/static/:file", controller.GetStaticFile) 17 | router.GET("/message/:link", controller.RenderMessage) 18 | router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) 19 | router.NoRoute(func(c *gin.Context) { 20 | c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 as builder 2 | 3 | WORKDIR /build 4 | COPY ./web . 5 | COPY ./VERSION . 6 | RUN yarn install 7 | RUN REACT_APP_VERSION=$(cat VERSION) yarn build 8 | 9 | FROM golang AS builder2 10 | 11 | ENV GO111MODULE=on \ 12 | CGO_ENABLED=1 \ 13 | GOOS=linux 14 | 15 | WORKDIR /build 16 | COPY . . 17 | COPY --from=builder /build/build ./web/build 18 | RUN go mod download 19 | RUN go build -ldflags "-s -w -X 'message-pusher/common.Version=$(cat VERSION)' -extldflags '-static'" -o message-pusher 20 | 21 | FROM alpine 22 | 23 | ENV PORT=3000 24 | RUN apk update \ 25 | && apk upgrade \ 26 | && apk add --no-cache ca-certificates tzdata \ 27 | && update-ca-certificates 2>/dev/null || true 28 | COPY --from=builder2 /build/message-pusher / 29 | EXPOSE 3000 30 | WORKDIR /data 31 | ENTRYPOINT ["/message-pusher"] 32 | -------------------------------------------------------------------------------- /web/src/helpers/render.js: -------------------------------------------------------------------------------- 1 | import { Label } from 'semantic-ui-react'; 2 | import { timestamp2string } from './utils'; 3 | import React from 'react'; 4 | import { CHANNEL_OPTIONS } from '../constants'; 5 | 6 | let channelMap = undefined; 7 | 8 | export function renderChannel(key) { 9 | if (channelMap === undefined) { 10 | channelMap = new Map(); 11 | CHANNEL_OPTIONS.forEach((option) => { 12 | channelMap[option.key] = option; 13 | }); 14 | } 15 | let channel = channelMap[key]; 16 | if (channel) { 17 | return ( 18 | 21 | ); 22 | } 23 | return ( 24 | 27 | ); 28 | } 29 | 30 | export function renderTimestamp(timestamp) { 31 | return <>{timestamp2string(timestamp)}; 32 | } 33 | -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding-top: 55px; 4 | overflow-y: scroll; 5 | font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | scrollbar-width: none; 9 | } 10 | 11 | body::-webkit-scrollbar { 12 | display: none; 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 17 | } 18 | 19 | .main-content { 20 | padding: 4px; 21 | } 22 | 23 | .small-icon .icon { 24 | font-size: 1em !important; 25 | } 26 | 27 | .custom-footer { 28 | font-size: 1.1em; 29 | } 30 | 31 | @media only screen and (max-width: 600px) { 32 | .hide-on-mobile { 33 | display: none !important; 34 | } 35 | } 36 | 37 | .quote { 38 | margin-left: 0; 39 | padding: 0 1em; 40 | border-left: 0.25em solid #ddd; 41 | } -------------------------------------------------------------------------------- /common/redis.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "os" 7 | "time" 8 | ) 9 | 10 | var RDB *redis.Client 11 | var RedisEnabled = true 12 | 13 | // InitRedisClient This function is called after init() 14 | func InitRedisClient() (err error) { 15 | if os.Getenv("REDIS_CONN_STRING") == "" { 16 | RedisEnabled = false 17 | SysLog("REDIS_CONN_STRING not set, Redis is not enabled") 18 | return nil 19 | } 20 | opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING")) 21 | if err != nil { 22 | panic(err) 23 | } 24 | RDB = redis.NewClient(opt) 25 | 26 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 27 | defer cancel() 28 | 29 | _, err = RDB.Ping(ctx).Result() 30 | return err 31 | } 32 | 33 | func ParseRedisOption() *redis.Options { 34 | opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING")) 35 | if err != nil { 36 | panic(err) 37 | } 38 | return opt 39 | } 40 | -------------------------------------------------------------------------------- /web/src/helpers/loader.js: -------------------------------------------------------------------------------- 1 | import { API } from './api'; 2 | import { showError } from './utils'; 3 | 4 | export const loadUser = async () => { 5 | let res = await API.get(`/api/user/self`); 6 | const { success, message, data } = res.data; 7 | if (success) { 8 | if (data.channel === '') { 9 | data.channel = 'email'; 10 | } 11 | if (data.token === ' ') { 12 | data.token = ''; 13 | } 14 | return data; 15 | } else { 16 | showError(message); 17 | return null; 18 | } 19 | }; 20 | 21 | export const loadUserChannels = async () => { 22 | let res = await API.get(`/api/channel?brief=true`); 23 | const { success, message, data } = res.data; 24 | if (success) { 25 | data.forEach((channel) => { 26 | channel.key = channel.name; 27 | channel.text = channel.name; 28 | channel.value = channel.name; 29 | if (channel.description === '') { 30 | channel.description = '无备注信息'; 31 | } 32 | }); 33 | return data; 34 | } else { 35 | showError(message); 36 | return null; 37 | } 38 | }; -------------------------------------------------------------------------------- /channel/email.go: -------------------------------------------------------------------------------- 1 | package channel 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "message-pusher/common" 7 | "message-pusher/model" 8 | "strings" 9 | ) 10 | 11 | func SendEmailMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { 12 | if message.To != "" { 13 | if user.SendEmailToOthers != common.SendEmailToOthersAllowed && user.Role < common.RoleAdminUser { 14 | return errors.New("没有权限发送邮件给其他人,请联系管理员为你添加该权限") 15 | } 16 | user.Email = message.To 17 | } 18 | if user.Email == "" { 19 | return errors.New("未配置邮箱地址") 20 | } 21 | subject := message.Title 22 | content := message.Content 23 | if subject == common.SystemName || subject == "" { 24 | subject = message.Description 25 | } else { 26 | content = fmt.Sprintf("%s\n\n%s", message.Description, message.Content) 27 | } 28 | var err error 29 | message.HTMLContent, err = common.Markdown2HTML(content) 30 | if err != nil { 31 | common.SysLog(err.Error()) 32 | } 33 | user.Email = strings.ReplaceAll(user.Email, "|", ";") 34 | return common.SendEmail(subject, user.Email, message.HTMLContent) 35 | } 36 | -------------------------------------------------------------------------------- /web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { Container } from 'semantic-ui-react'; 5 | import App from './App'; 6 | import Header from './components/Header'; 7 | import Footer from './components/Footer'; 8 | import 'semantic-ui-css/semantic.min.css'; 9 | import './index.css'; 10 | import { UserProvider } from './context/User'; 11 | import { ToastContainer } from 'react-toastify'; 12 | import 'react-toastify/dist/ReactToastify.css'; 13 | import { StatusProvider } from './context/Status'; 14 | 15 | const root = ReactDOM.createRoot(document.getElementById('root')); 16 | root.render( 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 |