init: reinitialize from gin-template
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
const WebSocket = require('ws');
|
||||
const { tokenStore } = require('./token');
|
||||
|
||||
async function pushClientMessage(userPrefix, message) {
|
||||
let user = tokenStore.get(userPrefix);
|
||||
if (!user || !user.ws || user.ws.readyState !== WebSocket.OPEN) {
|
||||
return {
|
||||
success: false,
|
||||
message: `客户端未连接`,
|
||||
};
|
||||
}
|
||||
let data = {
|
||||
title: message.title,
|
||||
description: message.description,
|
||||
};
|
||||
user.ws.send(JSON.stringify(data));
|
||||
return {
|
||||
success: true,
|
||||
message: '消息已发送至客户端',
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pushClientMessage,
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var StartTime = time.Now().Unix() // unit: second
|
||||
var Version = "v0.0.0"
|
||||
var SystemName = "消息推送服务"
|
||||
var ServerAddress = "http://localhost:3000"
|
||||
var Footer = ""
|
||||
|
||||
// Any options with "Secret", "Token" in its key won't be return by GetOptions
|
||||
|
||||
var SessionSecret = uuid.New().String()
|
||||
var SQLitePath = ".message-pusher.db"
|
||||
|
||||
var OptionMap map[string]string
|
||||
var OptionMapRWMutex sync.RWMutex
|
||||
|
||||
var ItemsPerPage = 10
|
||||
|
||||
var PasswordLoginEnabled = true
|
||||
var PasswordRegisterEnabled = true
|
||||
var EmailVerificationEnabled = false
|
||||
var GitHubOAuthEnabled = false
|
||||
var WeChatAuthEnabled = false
|
||||
|
||||
var SMTPServer = ""
|
||||
var SMTPAccount = ""
|
||||
var SMTPToken = ""
|
||||
|
||||
var GitHubClientId = ""
|
||||
var GitHubClientSecret = ""
|
||||
|
||||
var WeChatServerAddress = ""
|
||||
var WeChatServerToken = ""
|
||||
var WeChatAccountQRCodeImageURL = ""
|
||||
|
||||
const (
|
||||
RoleGuestUser = 0
|
||||
RoleCommonUser = 1
|
||||
RoleAdminUser = 10
|
||||
RoleRootUser = 100
|
||||
)
|
||||
|
||||
var (
|
||||
FileUploadPermission = RoleGuestUser
|
||||
FileDownloadPermission = RoleGuestUser
|
||||
ImageUploadPermission = RoleGuestUser
|
||||
ImageDownloadPermission = RoleGuestUser
|
||||
)
|
||||
|
||||
// All duration's unit is seconds
|
||||
// Shouldn't larger then RateLimitKeyExpirationDuration
|
||||
var (
|
||||
GlobalApiRateLimitNum = 60
|
||||
GlobalApiRateLimitDuration int64 = 3 * 60
|
||||
|
||||
GlobalWebRateLimitNum = 60
|
||||
GlobalWebRateLimitDuration int64 = 3 * 60
|
||||
|
||||
UploadRateLimitNum = 10
|
||||
UploadRateLimitDuration int64 = 60
|
||||
|
||||
DownloadRateLimitNum = 10
|
||||
DownloadRateLimitDuration int64 = 60
|
||||
|
||||
CriticalRateLimitNum = 20
|
||||
CriticalRateLimitDuration int64 = 20 * 60
|
||||
)
|
||||
|
||||
var RateLimitKeyExpirationDuration = 20 * time.Minute
|
||||
|
||||
const (
|
||||
UserStatusEnabled = 1 // don't use 0, 0 is the default value!
|
||||
UserStatusDisabled = 2 // also don't use 0
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package common
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
func Password2Hash(password string) (string, error) {
|
||||
passwordBytes := []byte(password)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
|
||||
return string(hashedPassword), err
|
||||
}
|
||||
|
||||
func ValidatePasswordAndHash(password string, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: 'data.db',
|
||||
logging: false,
|
||||
});
|
||||
|
||||
module.exports = sequelize;
|
||||
@@ -0,0 +1,14 @@
|
||||
package common
|
||||
|
||||
import "gopkg.in/gomail.v2"
|
||||
|
||||
func SendEmail(subject string, receiver string, content string) error {
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", SMTPAccount)
|
||||
m.SetHeader("To", receiver)
|
||||
m.SetHeader("Subject", subject)
|
||||
m.SetBody("text/html", content)
|
||||
d := gomail.NewDialer(SMTPServer, 587, SMTPAccount, SMTPToken)
|
||||
err := d.DialAndSend(m)
|
||||
return err
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const { tokenStore } = require('./token');
|
||||
const config = require('../config');
|
||||
const { md2html } = require('./utils');
|
||||
|
||||
async function pushEmailMessage(userPrefix, message) {
|
||||
let user = tokenStore.get(userPrefix);
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
message: `不存在的前缀:${userPrefix},请注意大小写`,
|
||||
};
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport({
|
||||
host: user.smtpServer,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: user.smtpUser,
|
||||
pass: user.smtpPass,
|
||||
},
|
||||
});
|
||||
|
||||
let targetEmail = user.email;
|
||||
if (message.email) {
|
||||
targetEmail = message.email;
|
||||
}
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: `"消息推送服务" <${user.smtpUser}>`,
|
||||
to: targetEmail,
|
||||
subject: message.description,
|
||||
text: message.content,
|
||||
html: md2html(message.content),
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: 'ok',
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pushEmailMessage,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/gin-gonic/contrib/static"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Credit: https://github.com/gin-contrib/static/issues/19
|
||||
|
||||
type embedFileSystem struct {
|
||||
http.FileSystem
|
||||
}
|
||||
|
||||
func (e embedFileSystem) Exists(prefix string, path string) bool {
|
||||
_, err := e.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
|
||||
efs, err := fs.Sub(fsEmbed, targetPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return embedFileSystem{
|
||||
FileSystem: http.FS(efs),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
Port = flag.Int("port", 3000, "the listening port")
|
||||
PrintVersion = flag.Bool("version", false, "print version and exit")
|
||||
LogDir = flag.String("log-dir", "", "specify the log directory")
|
||||
//Host = flag.String("host", "localhost", "the server's ip address or domain")
|
||||
//Path = flag.String("path", "", "specify a local path to public")
|
||||
//VideoPath = flag.String("video", "", "specify a video folder to public")
|
||||
//NoBrowser = flag.Bool("no-browser", false, "open browser or not")
|
||||
)
|
||||
|
||||
// UploadPath Maybe override by ENV_VAR
|
||||
var UploadPath = "upload"
|
||||
|
||||
//var ExplorerRootPath = UploadPath
|
||||
//var ImageUploadPath = "upload/images"
|
||||
//var VideoServePath = "upload"
|
||||
|
||||
func init() {
|
||||
flag.Parse()
|
||||
|
||||
if *PrintVersion {
|
||||
fmt.Println(Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if os.Getenv("SESSION_SECRET") != "" {
|
||||
SessionSecret = os.Getenv("SESSION_SECRET")
|
||||
}
|
||||
if os.Getenv("SQLITE_PATH") != "" {
|
||||
SQLitePath = os.Getenv("SQLITE_PATH")
|
||||
}
|
||||
if os.Getenv("UPLOAD_PATH") != "" {
|
||||
UploadPath = os.Getenv("UPLOAD_PATH")
|
||||
//ExplorerRootPath = UploadPath
|
||||
//ImageUploadPath = path.Join(UploadPath, "images")
|
||||
//VideoServePath = UploadPath
|
||||
}
|
||||
if *LogDir != "" {
|
||||
var err error
|
||||
*LogDir, err = filepath.Abs(*LogDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
|
||||
err = os.Mkdir(*LogDir, 0777)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
//if *Path != "" {
|
||||
// ExplorerRootPath = *Path
|
||||
//}
|
||||
//if *VideoPath != "" {
|
||||
// VideoServePath = *VideoPath
|
||||
//}
|
||||
//
|
||||
//ExplorerRootPath, _ = filepath.Abs(ExplorerRootPath)
|
||||
//VideoServePath, _ = filepath.Abs(VideoServePath)
|
||||
//ImageUploadPath, _ = filepath.Abs(ImageUploadPath)
|
||||
//
|
||||
if _, err := os.Stat(UploadPath); os.IsNotExist(err) {
|
||||
_ = os.Mkdir(UploadPath, 0777)
|
||||
}
|
||||
//if _, err := os.Stat(ImageUploadPath); os.IsNotExist(err) {
|
||||
// _ = os.Mkdir(ImageUploadPath, 0777)
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SetupGinLog() {
|
||||
if *LogDir != "" {
|
||||
commonLogPath := filepath.Join(*LogDir, "common.log")
|
||||
errorLogPath := filepath.Join(*LogDir, "error.log")
|
||||
commonFd, err := os.OpenFile(commonLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Fatal("failed to open log file")
|
||||
}
|
||||
errorFd, err := os.OpenFile(errorLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Fatal("failed to open log file")
|
||||
}
|
||||
gin.DefaultWriter = io.MultiWriter(os.Stdout, commonFd)
|
||||
gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, errorFd)
|
||||
}
|
||||
}
|
||||
|
||||
func SysLog(s string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
}
|
||||
|
||||
func FatalLog(v ...any) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
const {
|
||||
getUserDefaultMethod,
|
||||
checkAccessToken,
|
||||
checkPrefix,
|
||||
} = require('./token');
|
||||
const { pushWeChatMessage } = require('./wechat');
|
||||
const { pushWeChatCorpMessage } = require('./wechat-corp');
|
||||
const { pushEmailMessage } = require('./email');
|
||||
const { pushClientMessage } = require('./client');
|
||||
const { Message } = require('../models');
|
||||
|
||||
async function processMessage(userPrefix, message) {
|
||||
if (!checkPrefix(userPrefix)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `不存在的用户前缀:${userPrefix}`,
|
||||
};
|
||||
}
|
||||
if (!checkAccessToken(userPrefix, message.token)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `无效的访问凭证,请检查 token 参数是否正确`,
|
||||
};
|
||||
}
|
||||
if (message.email) {
|
||||
// If message has the attribute "email", override its type.
|
||||
message.type = 'email';
|
||||
}
|
||||
if (!message.type) {
|
||||
message.type = getUserDefaultMethod(userPrefix);
|
||||
}
|
||||
if (message.content && message.type !== 'email') {
|
||||
// If message is not email type, we should save it because we have to serve the page.
|
||||
message = await Message.create(message, { raw: true });
|
||||
}
|
||||
let result;
|
||||
switch (message.type) {
|
||||
case 'test': // WeChat message
|
||||
result = await pushWeChatMessage(userPrefix, message);
|
||||
break;
|
||||
case 'email': // Email message
|
||||
result = await pushEmailMessage(userPrefix, message);
|
||||
break;
|
||||
case 'corp': // WeChat corp message
|
||||
result = await pushWeChatCorpMessage(userPrefix, message);
|
||||
break;
|
||||
case 'client': // Client message
|
||||
result = await pushClientMessage(userPrefix, message);
|
||||
break;
|
||||
default:
|
||||
result = {
|
||||
success: false,
|
||||
message: `不支持的消息类型:${message.type}`,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processMessage,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type InMemoryRateLimiter struct {
|
||||
store map[string]*[]int64
|
||||
mutex sync.Mutex
|
||||
expirationDuration time.Duration
|
||||
}
|
||||
|
||||
func (l *InMemoryRateLimiter) Init(expirationDuration time.Duration) {
|
||||
if l.store == nil {
|
||||
l.mutex.Lock()
|
||||
if l.store == nil {
|
||||
l.store = make(map[string]*[]int64)
|
||||
l.expirationDuration = expirationDuration
|
||||
if expirationDuration > 0 {
|
||||
go l.clearExpiredItems()
|
||||
}
|
||||
}
|
||||
l.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *InMemoryRateLimiter) clearExpiredItems() {
|
||||
for {
|
||||
time.Sleep(l.expirationDuration)
|
||||
l.mutex.Lock()
|
||||
now := time.Now().Unix()
|
||||
for key := range l.store {
|
||||
queue := l.store[key]
|
||||
size := len(*queue)
|
||||
if size == 0 || now-(*queue)[size-1] > int64(l.expirationDuration.Seconds()) {
|
||||
delete(l.store, key)
|
||||
}
|
||||
}
|
||||
l.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Request parameter duration's unit is seconds
|
||||
func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration int64) bool {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
// [old <-- new]
|
||||
queue, ok := l.store[key]
|
||||
now := time.Now().Unix()
|
||||
if ok {
|
||||
if len(*queue) < maxRequestNum {
|
||||
*queue = append(*queue, now)
|
||||
return true
|
||||
} else {
|
||||
if now-(*queue)[0] >= duration {
|
||||
*queue = (*queue)[1:]
|
||||
*queue = append(*queue, now)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s := make([]int64, 0, maxRequestNum)
|
||||
l.store[key] = &s
|
||||
*(l.store[key]) = append(*(l.store[key]), now)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var RDB *redis.Client
|
||||
var RedisEnabled = true
|
||||
|
||||
// InitRedisClient This function is called after init()
|
||||
func InitRedisClient() (err error) {
|
||||
if os.Getenv("REDIS_CONN_STRING") == "" {
|
||||
RedisEnabled = false
|
||||
SysLog("REDIS_CONN_STRING not set, Redis is not enabled")
|
||||
return nil
|
||||
}
|
||||
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RDB = redis.NewClient(opt)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = RDB.Ping(ctx).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func ParseRedisOption() *redis.Options {
|
||||
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return opt
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
const { User } = require('../models');
|
||||
|
||||
const tokenStore = new Map();
|
||||
|
||||
async function initializeTokenStore() {
|
||||
let users = [];
|
||||
if (process.env.MODE === '1') {
|
||||
console.log('Current mode is Heroku mode.');
|
||||
let user = {
|
||||
// Common
|
||||
prefix: process.env.PREFIX,
|
||||
accessToken: process.env.ACCESS_TOKEN,
|
||||
defaultMethod: process.env.DEFAULT_METHOD,
|
||||
// WeChat public account
|
||||
wechatAppId: process.env.WECHAT_APP_ID,
|
||||
wechatAppSecret: process.env.WECHAT_APP_SECRET,
|
||||
wechatTemplateId: process.env.WECHAT_TEMPLATE_ID,
|
||||
wechatOpenId: process.env.WECHAT_OPEN_ID,
|
||||
wechatVerifyToken: process.env.WECHAT_VERIFY_TOKEN,
|
||||
// Email
|
||||
email: process.env.EMAIL,
|
||||
smtpServer: process.env.SMTP_SERVER,
|
||||
smtpUser: process.env.SMTP_USER,
|
||||
smtpPass: process.env.SMTP_PASS,
|
||||
// WeChat corporation account
|
||||
corpId: process.env.CORP_ID,
|
||||
corpAgentId: process.env.CORP_AGENT_ID,
|
||||
corpAppSecret: process.env.CORP_APP_SECRET,
|
||||
corpUserId: process.env.CORP_USER_ID,
|
||||
};
|
||||
users.push(user);
|
||||
} else {
|
||||
users = await User.findAll({
|
||||
raw: true,
|
||||
});
|
||||
}
|
||||
users.forEach((user) => {
|
||||
if (user.prefix) {
|
||||
tokenStore.set(user.prefix, {
|
||||
// Common
|
||||
accessToken: user.accessToken,
|
||||
defaultMethod: user.defaultMethod,
|
||||
// WeChat test account
|
||||
wechatAppId: user.wechatAppId,
|
||||
wechatAppSecret: user.wechatAppSecret,
|
||||
wechatTemplateId: user.wechatTemplateId,
|
||||
wechatOpenId: user.wechatOpenId,
|
||||
wechatVerifyToken: user.wechatVerifyToken,
|
||||
wechatToken: '',
|
||||
// Email
|
||||
email: user.email,
|
||||
smtpServer: user.smtpServer,
|
||||
smtpUser: user.smtpUser,
|
||||
smtpPass: user.smtpPass,
|
||||
// WeChat corporation account
|
||||
corpId: user.corpId,
|
||||
corpAgentId: user.corpAgentId,
|
||||
corpAppSecret: user.corpAppSecret,
|
||||
corpUserId: user.corpUserId,
|
||||
corpToken: '',
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log('Token store initialized.');
|
||||
}
|
||||
|
||||
function updateTokenStore(prefix, key, value) {
|
||||
let user = tokenStore.get(prefix);
|
||||
user[key] = value;
|
||||
tokenStore.set(prefix, user);
|
||||
}
|
||||
|
||||
function getUserDefaultMethod(prefix) {
|
||||
let user = tokenStore.get(prefix);
|
||||
return user.defaultMethod;
|
||||
}
|
||||
|
||||
function checkAccessToken(prefix, token) {
|
||||
let user = tokenStore.get(prefix);
|
||||
if (user.accessToken === '') {
|
||||
return true;
|
||||
} else {
|
||||
return user.accessToken === token;
|
||||
}
|
||||
}
|
||||
|
||||
function checkPrefix(prefix) {
|
||||
let user = tokenStore.get(prefix);
|
||||
return user !== undefined;
|
||||
}
|
||||
|
||||
function registerWebSocket(prefix, token, ws) {
|
||||
let user = tokenStore.get(prefix);
|
||||
if (user && user.accessToken === token) {
|
||||
updateTokenStore(prefix, 'ws', ws);
|
||||
} else {
|
||||
ws.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeTokenStore,
|
||||
updateTokenStore,
|
||||
getUserDefaultMethod,
|
||||
tokenStore,
|
||||
checkAccessToken,
|
||||
checkPrefix,
|
||||
registerWebSocket,
|
||||
};
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func OpenBrowser(url string) {
|
||||
var err error
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
}
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetIp() (ip string) {
|
||||
ips, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return ip
|
||||
}
|
||||
|
||||
for _, a := range ips {
|
||||
if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||
if ipNet.IP.To4() != nil {
|
||||
ip = ipNet.IP.String()
|
||||
if strings.HasPrefix(ip, "10") {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(ip, "172") {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(ip, "192.168") {
|
||||
return
|
||||
}
|
||||
ip = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var sizeKB = 1024
|
||||
var sizeMB = sizeKB * 1024
|
||||
var sizeGB = sizeMB * 1024
|
||||
|
||||
func Bytes2Size(num int64) string {
|
||||
numStr := ""
|
||||
unit := "B"
|
||||
if num/int64(sizeGB) > 1 {
|
||||
numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
|
||||
unit = "GB"
|
||||
} else if num/int64(sizeMB) > 1 {
|
||||
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
|
||||
unit = "MB"
|
||||
} else if num/int64(sizeKB) > 1 {
|
||||
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
|
||||
unit = "KB"
|
||||
} else {
|
||||
numStr = fmt.Sprintf("%d", num)
|
||||
}
|
||||
return numStr + " " + unit
|
||||
}
|
||||
|
||||
func Seconds2Time(num int) (time string) {
|
||||
if num/31104000 > 0 {
|
||||
time += strconv.Itoa(num/31104000) + " 年 "
|
||||
num %= 31104000
|
||||
}
|
||||
if num/2592000 > 0 {
|
||||
time += strconv.Itoa(num/2592000) + " 个月 "
|
||||
num %= 2592000
|
||||
}
|
||||
if num/86400 > 0 {
|
||||
time += strconv.Itoa(num/86400) + " 天 "
|
||||
num %= 86400
|
||||
}
|
||||
if num/3600 > 0 {
|
||||
time += strconv.Itoa(num/3600) + " 小时 "
|
||||
num %= 3600
|
||||
}
|
||||
if num/60 > 0 {
|
||||
time += strconv.Itoa(num/60) + " 分钟 "
|
||||
num %= 60
|
||||
}
|
||||
time += strconv.Itoa(num) + " 秒"
|
||||
return
|
||||
}
|
||||
|
||||
func Interface2String(inter interface{}) string {
|
||||
switch inter.(type) {
|
||||
case string:
|
||||
return inter.(string)
|
||||
case int:
|
||||
return fmt.Sprintf("%d", inter.(int))
|
||||
case float64:
|
||||
return fmt.Sprintf("%f", inter.(float64))
|
||||
}
|
||||
return "Not Implemented"
|
||||
}
|
||||
|
||||
func UnescapeHTML(x string) interface{} {
|
||||
return template.HTML(x)
|
||||
}
|
||||
|
||||
func IntMax(a int, b int) int {
|
||||
if a >= b {
|
||||
return a
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
func GetUUID() string {
|
||||
code := uuid.New().String()
|
||||
code = strings.Replace(code, "-", "", -1)
|
||||
return code
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
const lexer = require('marked').lexer;
|
||||
const parser = require('marked').parser;
|
||||
|
||||
function md2html(markdown) {
|
||||
if (markdown) {
|
||||
return parser(lexer(markdown));
|
||||
}
|
||||
return markdown;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
md2html,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
package common
|
||||
|
||||
import "github.com/go-playground/validator/v10"
|
||||
|
||||
var Validate *validator.Validate
|
||||
|
||||
func init() {
|
||||
Validate = validator.New()
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type verificationValue struct {
|
||||
code string
|
||||
time time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
EmailVerificationPurpose = "v"
|
||||
PasswordResetPurpose = "r"
|
||||
)
|
||||
|
||||
var verificationMutex sync.Mutex
|
||||
var verificationMap map[string]verificationValue
|
||||
var verificationMapMaxSize = 10
|
||||
var VerificationValidMinutes = 10
|
||||
|
||||
func GenerateVerificationCode(length int) string {
|
||||
code := uuid.New().String()
|
||||
code = strings.Replace(code, "-", "", -1)
|
||||
if length == 0 {
|
||||
return code
|
||||
}
|
||||
return code[:length]
|
||||
}
|
||||
|
||||
func RegisterVerificationCodeWithKey(key string, code string, purpose string) {
|
||||
verificationMutex.Lock()
|
||||
defer verificationMutex.Unlock()
|
||||
verificationMap[purpose+key] = verificationValue{
|
||||
code: code,
|
||||
time: time.Now(),
|
||||
}
|
||||
if len(verificationMap) > verificationMapMaxSize {
|
||||
removeExpiredPairs()
|
||||
}
|
||||
}
|
||||
|
||||
func VerifyCodeWithKey(key string, code string, purpose string) bool {
|
||||
verificationMutex.Lock()
|
||||
defer verificationMutex.Unlock()
|
||||
value, okay := verificationMap[purpose+key]
|
||||
now := time.Now()
|
||||
if !okay || int(now.Sub(value.time).Seconds()) >= VerificationValidMinutes*60 {
|
||||
return false
|
||||
}
|
||||
return code == value.code
|
||||
}
|
||||
|
||||
func DeleteKey(key string, purpose string) {
|
||||
verificationMutex.Lock()
|
||||
defer verificationMutex.Unlock()
|
||||
delete(verificationMap, purpose+key)
|
||||
}
|
||||
|
||||
// no lock inside!
|
||||
func removeExpiredPairs() {
|
||||
now := time.Now()
|
||||
for key := range verificationMap {
|
||||
if int(now.Sub(verificationMap[key].time).Seconds()) >= VerificationValidMinutes*60 {
|
||||
delete(verificationMap, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
verificationMutex.Lock()
|
||||
defer verificationMutex.Unlock()
|
||||
verificationMap = make(map[string]verificationValue)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const { tokenStore, updateTokenStore } = require('./token');
|
||||
const config = require('../config');
|
||||
|
||||
async function refreshToken() {
|
||||
for (let [key, value] of tokenStore) {
|
||||
if (value.corpId) {
|
||||
value.corpToken = await requestToken(value.corpId, value.corpAppSecret);
|
||||
tokenStore.set(key, value);
|
||||
}
|
||||
}
|
||||
console.log('Token refreshed.');
|
||||
}
|
||||
|
||||
async function requestToken(corpId, corpAppSecret) {
|
||||
// Reference: https://work.weixin.qq.com/api/doc/90000/90135/91039
|
||||
|
||||
let token = '';
|
||||
try {
|
||||
let res = await axios.get(
|
||||
`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpId}&corpsecret=${corpAppSecret}`
|
||||
);
|
||||
// console.debug(res);
|
||||
if (res && res.data) {
|
||||
if (res.data.access_token) {
|
||||
token = res.data.access_token;
|
||||
} else {
|
||||
console.error(res.data);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async function pushWeChatCorpMessage(userPrefix, message) {
|
||||
// Reference: https://work.weixin.qq.com/api/doc/90000/90135/90236
|
||||
|
||||
let user = tokenStore.get(userPrefix);
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
message: `不存在的前缀:${userPrefix},请注意大小写`,
|
||||
};
|
||||
}
|
||||
let access_token = user.corpToken;
|
||||
let request_data = {
|
||||
msgtype: 'textcard',
|
||||
touser: user.corpUserId,
|
||||
agentid: user.corpAgentId,
|
||||
textcard: {
|
||||
title: message.title,
|
||||
description: message.description,
|
||||
},
|
||||
};
|
||||
if (message.content) {
|
||||
request_data.textcard.url = `${config.href}message/${message.id}`;
|
||||
} else {
|
||||
request_data.textcard.url = `${config.href}`;
|
||||
}
|
||||
let requestUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`;
|
||||
try {
|
||||
let response = await axios.post(requestUrl, request_data);
|
||||
if (response && response.data && response.data.errcode !== 0) {
|
||||
// Failed to push message, get a new token and try again.
|
||||
access_token = await requestToken(user.corpId, user.corpAppSecret);
|
||||
updateTokenStore(userPrefix, 'corpToken', access_token);
|
||||
requestUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`;
|
||||
response = await axios.post(requestUrl, request_data);
|
||||
}
|
||||
if (response.data.errcode === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'ok',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: response.data.errmsg,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let msg = e.message;
|
||||
if (msg.startsWith('access_token missing')) {
|
||||
msg = '请求微信服务器失败,请检查配置是否正确或重试!';
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: msg,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
refreshToken,
|
||||
requestToken,
|
||||
pushWeChatCorpMessage,
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const { tokenStore, updateTokenStore } = require('./token');
|
||||
const config = require('../config');
|
||||
|
||||
async function refreshToken() {
|
||||
for (let [key, value] of tokenStore) {
|
||||
if (value.wechatAppId) {
|
||||
value.wechatToken = await requestToken(
|
||||
value.wechatAppId,
|
||||
value.wechatAppSecret
|
||||
);
|
||||
tokenStore.set(key, value);
|
||||
}
|
||||
}
|
||||
console.log('Token refreshed.');
|
||||
}
|
||||
|
||||
async function requestToken(appId, appSecret) {
|
||||
let token = '';
|
||||
try {
|
||||
let res = await axios.get(
|
||||
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
|
||||
);
|
||||
// console.debug(res);
|
||||
if (res && res.data) {
|
||||
if (res.data.access_token) {
|
||||
token = res.data.access_token;
|
||||
} else {
|
||||
console.error(res.data);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async function pushWeChatMessage(userPrefix, message) {
|
||||
// Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl
|
||||
let user = tokenStore.get(userPrefix);
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
message: `不存在的前缀:${userPrefix},请注意大小写`,
|
||||
};
|
||||
}
|
||||
let access_token = user.wechatToken;
|
||||
let request_data = {
|
||||
touser: user.wechatOpenId,
|
||||
template_id: user.wechatTemplateId,
|
||||
};
|
||||
if (message.content) {
|
||||
request_data.url = `${config.href}message/${message.id}`;
|
||||
}
|
||||
request_data.data = { text: { value: message.description } };
|
||||
let requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`;
|
||||
try {
|
||||
let response = await axios.post(requestUrl, request_data);
|
||||
if (response && response.data && response.data.errcode !== 0) {
|
||||
// Failed to push message, get a new token and try again.
|
||||
let token = await requestToken(user.wechatAppId, user.wechatAppSecret);
|
||||
updateTokenStore(userPrefix, 'wechatToken', token);
|
||||
requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`;
|
||||
response = await axios.post(requestUrl, request_data);
|
||||
}
|
||||
if (response.data.errcode === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'ok',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: response.data.errmsg,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
refreshToken,
|
||||
requestToken,
|
||||
pushWeChatMessage,
|
||||
};
|
||||
Reference in New Issue
Block a user