浏览代码

feat: save messages to database (close #37)

JustSong 2 年之前
父节点
当前提交
d26e578762

+ 1 - 1
channel/bark.go

@@ -13,7 +13,7 @@ type barkMessageResponse struct {
 	Message string `json:"message"`
 }
 
-func SendBarkMessage(message *Message, user *model.User) error {
+func SendBarkMessage(message *model.Message, user *model.User) error {
 	if user.BarkServer == "" || user.BarkSecret == "" {
 		return errors.New("未配置 Bark 消息推送方式")
 	}

+ 6 - 6
channel/client.go

@@ -19,7 +19,7 @@ const (
 type webSocketClient struct {
 	userId    int
 	conn      *websocket.Conn
-	message   chan *Message
+	message   chan *model.Message
 	pong      chan bool
 	stop      chan bool
 	timestamp int64
@@ -98,7 +98,7 @@ func (c *webSocketClient) handleDataWriting() {
 	}
 }
 
-func (c *webSocketClient) sendMessage(message *Message) {
+func (c *webSocketClient) sendMessage(message *model.Message) {
 	c.message <- message
 }
 
@@ -122,21 +122,21 @@ func RegisterClient(userId int, conn *websocket.Conn) {
 	oldClient, existed := clientMap[userId]
 	clientConnMapMutex.Unlock()
 	if existed {
-		byeMessage := &Message{
+		byeMessage := &model.Message{
 			Title:       common.SystemName,
 			Description: "其他客户端已连接服务器,本客户端已被挤下线!",
 		}
 		oldClient.sendMessage(byeMessage)
 		oldClient.close()
 	}
-	helloMessage := &Message{
+	helloMessage := &model.Message{
 		Title:       common.SystemName,
 		Description: "客户端连接成功!",
 	}
 	newClient := &webSocketClient{
 		userId:    userId,
 		conn:      conn,
-		message:   make(chan *Message),
+		message:   make(chan *model.Message),
 		pong:      make(chan bool),
 		stop:      make(chan bool),
 		timestamp: time.Now().UnixMilli(),
@@ -149,7 +149,7 @@ func RegisterClient(userId int, conn *websocket.Conn) {
 	clientConnMapMutex.Unlock()
 }
 
-func SendClientMessage(message *Message, user *model.User) error {
+func SendClientMessage(message *model.Message, user *model.User) error {
 	if user.ClientSecret == "" {
 		return errors.New("未配置 WebSocket 客户端消息推送方式")
 	}

+ 1 - 1
channel/corp.go

@@ -24,7 +24,7 @@ type corpMessageResponse struct {
 	Message string `json:"errmsg"`
 }
 
-func SendCorpMessage(message *Message, user *model.User) error {
+func SendCorpMessage(message *model.Message, user *model.User) error {
 	if user.CorpWebhookURL == "" {
 		return errors.New("未配置企业微信群机器人消息推送方式")
 	}

+ 1 - 1
channel/ding.go

@@ -30,7 +30,7 @@ type dingMessageResponse struct {
 	Message string `json:"errmsg"`
 }
 
-func SendDingMessage(message *Message, user *model.User) error {
+func SendDingMessage(message *model.Message, user *model.User) error {
 	if user.DingWebhookURL == "" {
 		return errors.New("未配置钉钉群机器人消息推送方式")
 	}

+ 1 - 1
channel/email.go

@@ -8,7 +8,7 @@ import (
 	"message-pusher/model"
 )
 
-func SendEmailMessage(message *Message, user *model.User) error {
+func SendEmailMessage(message *model.Message, user *model.User) error {
 	if user.Email == "" {
 		return errors.New("未配置邮箱地址")
 	}

+ 1 - 1
channel/lark.go

@@ -45,7 +45,7 @@ type larkMessageResponse struct {
 	Message string `json:"msg"`
 }
 
-func SendLarkMessage(message *Message, user *model.User) error {
+func SendLarkMessage(message *model.Message, user *model.User) error {
 	if user.LarkWebhookURL == "" {
 		return errors.New("未配置飞书群机器人消息推送方式")
 	}

+ 4 - 12
channel/main.go

@@ -15,20 +15,10 @@ const (
 	TypeTelegram          = "telegram"
 	TypeBark              = "bark"
 	TypeClient            = "client"
+	TypeNone              = "none"
 )
 
-type Message struct {
-	Title       string `json:"title"`
-	Description string `json:"description"`
-	Desp        string `json:"desp"` // alias for description
-	Content     string `json:"content"`
-	URL         string `json:"url"`
-	Channel     string `json:"channel"`
-	Token       string `json:"token"`
-	HTMLContent string `json:"html_content"`
-}
-
-func (message *Message) Send(user *model.User) error {
+func SendMessage(message *model.Message, user *model.User) error {
 	switch message.Channel {
 	case TypeEmail:
 		return SendEmailMessage(message, user)
@@ -48,6 +38,8 @@ func (message *Message) Send(user *model.User) error {
 		return SendClientMessage(message, user)
 	case TypeTelegram:
 		return SendTelegramMessage(message, user)
+	case TypeNone:
+		return nil
 	default:
 		return errors.New("不支持的消息通道:" + message.Channel)
 	}

+ 1 - 1
channel/telegram.go

@@ -20,7 +20,7 @@ type telegramMessageResponse struct {
 	Description string `json:"description"`
 }
 
-func SendTelegramMessage(message *Message, user *model.User) error {
+func SendTelegramMessage(message *model.Message, user *model.User) error {
 	if user.TelegramBotToken == "" || user.TelegramChatId == "" {
 		return errors.New("未配置 Telegram 机器人消息推送方式")
 	}

+ 2 - 3
channel/wechat-corp-account.go

@@ -95,7 +95,7 @@ type wechatCorpMessageResponse struct {
 	ErrorMessage string `json:"errmsg"`
 }
 
-func SendWeChatCorpMessage(message *Message, user *model.User) error {
+func SendWeChatCorpMessage(message *model.Message, user *model.User) error {
 	if user.WeChatCorpAccountId == "" {
 		return errors.New("未配置微信企业号消息推送方式")
 	}
@@ -119,8 +119,7 @@ func SendWeChatCorpMessage(message *Message, user *model.User) error {
 			messageRequest.MessageType = "textcard"
 			messageRequest.TextCard.Title = message.Title
 			messageRequest.TextCard.Description = message.Description
-			// TODO: render content and set URL
-			messageRequest.TextCard.URL = common.ServerAddress
+			messageRequest.TextCard.URL = message.URL
 		} else {
 			messageRequest.MessageType = "markdown"
 			messageRequest.Markdown.Content = message.Content

+ 2 - 2
channel/wechat-test-account.go

@@ -88,7 +88,7 @@ type wechatTestMessageResponse struct {
 	ErrorMessage string `json:"errmsg"`
 }
 
-func SendWeChatTestMessage(message *Message, user *model.User) error {
+func SendWeChatTestMessage(message *model.Message, user *model.User) error {
 	if user.WeChatTestAccountId == "" {
 		return errors.New("未配置微信测试号消息推送方式")
 	}
@@ -97,8 +97,8 @@ func SendWeChatTestMessage(message *Message, user *model.User) error {
 		TemplateId: user.WeChatTestAccountTemplateId,
 		URL:        "",
 	}
-	// TODO: render content and set URL
 	values.Data.Text.Value = message.Description
+	values.URL = message.URL
 	jsonData, err := json.Marshal(values)
 	if err != nil {
 		return err

+ 31 - 0
common/public/message.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet" href="/public/static/app.css">
+    <title>{{.title}}</title>
+    <meta name="description" content="{{.description}}">
+</head>
+<body>
+
+<div>
+    <div class="article-container" style="max-width: 960px">
+        <div class="columns is-desktop">
+            <div class="column">
+                <article id="article">
+                    <h1 class="title is-3 is-4-mobile">{{.title}}</h1>
+                    <div class="info">
+                        <span class="line">发布于:<span class="tag is-light">{{.time}}</span></span>
+                    </div>
+                    <blockquote>
+                        <p>{{.description}}</p>
+                    </blockquote>
+                    {{.content | unescape}}
+                </article>
+            </div>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 399 - 0
common/public/static/app.css

@@ -0,0 +1,399 @@
+body {
+    font-family: Verdana, Candara, Arial, Helvetica, Microsoft YaHei, sans-serif;
+    line-height: 1.6;
+    margin: 0;
+}
+
+nav {
+    margin-bottom: 16px;
+}
+
+a {
+    text-decoration: none;
+    color: #007bff;
+}
+
+a:hover {
+    text-decoration: none !important;
+    color: #007bff;
+}
+
+.page-card-title a {
+    color: #368CCB;
+    text-decoration: none;
+}
+
+.page-card-title a:hover {
+    color: #368CCB;
+    text-decoration: none;
+}
+
+.wrapper {
+    max-width: 960px;
+    margin: 0 auto;
+}
+
+#page-container {
+    position: relative;
+    min-height: 97vh;
+}
+
+#content-wrap {
+    padding-bottom: 4rem;
+}
+
+#footer {
+    height: 4rem;
+}
+
+#footer a {
+    /*color: black;*/
+}
+
+code {
+    font-family: Consolas, 'Courier New', monospace;
+}
+
+.page-card-list {
+    margin: 8px 8px;
+}
+
+.page-card-title {
+    font-size: x-large;
+    font-weight: 500;
+    color: #000000;
+    text-decoration: none;
+    margin-bottom: 4px;
+}
+
+.page-card-text {
+    margin-top: 16px;
+}
+
+.pagination {
+    margin: 16px 4px;
+}
+
+.pagination a {
+    border: none;
+    overflow: hidden;
+}
+
+.shadow {
+    box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .02);
+}
+
+.nav-shadow {
+    box-shadow: 0 2px 3px rgba(26, 26, 26, .1);
+}
+
+.paginator div {
+    border: 2px solid #000;
+    cursor: pointer;
+    display: inline-block;
+    min-width: 100px;
+    text-align: center;
+    font-weight: bold;
+    padding: 10px;
+}
+
+.box article {
+    overflow-wrap: break-word;
+    /*font-size: larger;*/
+    word-break: break-word;
+    line-height: 1.6;
+    padding: 16px;
+    /*margin-bottom: 16px;*/
+    background-color: #ffffff;
+}
+
+.toc-level-1 {
+    list-style-type: none;
+}
+
+.toc-level-2 {
+    list-style-type: none;
+}
+
+.toc-level-3 {
+    list-style-type: disc;
+}
+
+.toc-level-4 {
+    list-style-type: circle;
+}
+
+.toc-level-5 {
+    list-style-type: square;
+}
+
+.toc-level-6 {
+    list-style-type: square;
+}
+
+img {
+    max-width: 100%;
+    max-height: 100%;
+}
+
+.article-container {
+    margin: auto;
+    max-width: 960px;
+    padding: 16px 16px;
+    overflow-wrap: break-word;
+    word-break: break-word;
+    line-height: 1.6;
+    /*font-size: larger;*/
+}
+
+article {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+    font-size: 16px;
+    line-height: 1.5;
+    word-wrap: break-word;
+    color: #24292f;
+}
+
+article p, article blockquote, article ul, article ol, article dl, article table, article pre, article details {
+    margin-top: 0;
+    margin-bottom: 16px;
+}
+
+article ul, article ol {
+    padding-left: 2em;
+}
+
+article ul ul, article ul ol, article ol ol, article ol ul {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+article .tag {
+    font-family: Verdana, Candara, Arial, Helvetica, Microsoft YaHei, sans-serif;
+}
+
+article a {
+    color: #007bff;
+    text-decoration: none;
+}
+
+article a:hover {
+    color: #007bff;
+    text-decoration: none;
+}
+
+article h2,
+article h3,
+article h4,
+article h5,
+article h6 {
+    margin-top: 24px;
+    margin-bottom: 16px;
+    font-weight: 600;
+    line-height: 1.5;
+    margin-block-start: 1em;
+    margin-block-end: 0.2em;
+}
+
+article h1 {
+    font-size: 2em
+}
+
+article h2 {
+    padding-bottom: 0.3em;
+    font-size: 1.5em;
+}
+
+article h3 {
+    font-size: 1.25em
+}
+
+article h4 {
+    font-size: 1.25em;
+}
+
+article h5 {
+    font-size: 1.1em;
+}
+
+article h6 {
+    font-size: 1em;
+    font-weight: bold
+}
+
+@media screen and (max-width: 960px) {
+    article h1 {
+        font-size: 1.5em
+    }
+
+    article h2 {
+        font-size: 1.35em
+    }
+
+    article h3 {
+        font-size: 1.3em
+    }
+
+    article h4 {
+        font-size: 1.2em;
+    }
+}
+
+article p {
+    margin-top: 0;
+    margin-bottom: 16px;
+}
+
+article table {
+    margin: auto;
+    border-collapse: collapse;
+    border-spacing: 0;
+    vertical-align: middle;
+    text-align: left;
+    min-width: 66%;
+}
+
+article table td,
+article table th {
+    padding: 5px 8px;
+    border: 1px solid #bbb;
+}
+
+article blockquote {
+    margin-left: 0;
+    padding: 0 1em;
+    border-left: 0.25em solid #ddd;
+}
+
+article ol ul {
+    list-style-type: circle;
+}
+
+article pre {
+    max-width: 960px;
+    display: block;
+    overflow: auto;
+    padding: 0;
+    margin-top: 12px;
+    margin-bottom: 12px;
+    border-radius: 6px;
+}
+
+article pre code {
+    font-size: 14px;
+}
+
+article ol {
+    text-decoration: none;
+    padding-inline-start: 40px;
+    margin-bottom: 1.25rem;
+    padding-left: 2em;
+}
+
+article ul {
+    padding-left: 2em;
+}
+
+article li + li {
+    margin-top: 0.25em;
+}
+
+code {
+    font-family: "JetBrains Mono", "Cascadia Code", Consolas, Microsoft YaHei, monospace;
+}
+
+article code {
+    color: #24292f;
+    background-color: rgb(175 184 193 / 20%);
+    padding: .065em .4em;
+    border-radius: 6px;
+    font-family: "JetBrains Mono", "Cascadia Code", Consolas, Microsoft YaHei, monospace;
+}
+
+article .copyright {
+    display: none;
+}
+
+.info {
+    font-size: 14px;
+    line-height: 28px;
+    text-align: left;
+    color: #738292;
+    margin-bottom: 24px;
+}
+
+.info a {
+    text-decoration: none;
+    color: inherit;
+}
+
+/* Code Page Style*/
+.code-page {
+    margin-top: 32px;
+    padding-left: 16px;
+    padding-right: 16px;
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+}
+
+.code-page code {
+    font-size: 16px;
+    width: 100%;
+    height: 100%;
+}
+
+.code-page pre {
+    margin-top: 2px;
+    overflow-x: auto;
+    padding: 0;
+    font-size: 16px;
+    background-color: rgba(0, 0, 0, 0);
+}
+
+.code-page .control-panel {
+    width: 100%;
+}
+
+#code-display {
+    padding: 16px 24px;
+}
+
+.discuss h1 {
+    font-size: 24px;
+    line-height: 36px;
+    text-align: left;
+}
+
+.discuss .time {
+    font-size: 12px;
+    line-height: 18px;
+    text-align: left;
+    color: #738292;
+}
+
+.discuss .content {
+    font-size: 16px;
+    line-height: 24px;
+    text-align: left;
+}
+
+.raw {
+    padding: 16px;
+}
+
+.raw .raw-content {
+    overflow-y: hidden;
+    overflow-x: scroll;
+}
+
+.links {
+    margin: 16px;
+}
+
+span.line {
+    display: inline-block;
+}
+
+.toc {
+    position: sticky;
+    top: 24px;
+}

+ 17 - 0
common/template.go

@@ -0,0 +1,17 @@
+package common
+
+import (
+	"embed"
+	"html/template"
+)
+
+//go:embed public
+var FS embed.FS
+
+func LoadTemplate() *template.Template {
+	var funcMap = template.FuncMap{
+		"unescape": UnescapeHTML,
+	}
+	t := template.Must(template.New("").Funcs(funcMap).ParseFS(FS, "public/*.html"))
+	return t
+}

+ 124 - 5
controller/message.go

@@ -1,16 +1,21 @@
 package controller
 
 import (
+	"bytes"
 	"encoding/json"
+	"fmt"
 	"github.com/gin-gonic/gin"
+	"github.com/yuin/goldmark"
 	"message-pusher/channel"
 	"message-pusher/common"
 	"message-pusher/model"
 	"net/http"
+	"strconv"
+	"time"
 )
 
 func GetPushMessage(c *gin.Context) {
-	message := channel.Message{
+	message := model.Message{
 		Title:       c.Query("title"),
 		Description: c.Query("description"),
 		Content:     c.Query("content"),
@@ -30,7 +35,7 @@ func GetPushMessage(c *gin.Context) {
 }
 
 func PostPushMessage(c *gin.Context) {
-	message := channel.Message{
+	message := model.Message{
 		Title:       c.PostForm("title"),
 		Description: c.PostForm("description"),
 		Content:     c.PostForm("content"),
@@ -39,7 +44,7 @@ func PostPushMessage(c *gin.Context) {
 		Token:       c.PostForm("token"),
 		Desp:        c.PostForm("desp"),
 	}
-	if message == (channel.Message{}) {
+	if message == (model.Message{}) {
 		// Looks like the user is using JSON
 		err := json.NewDecoder(c.Request.Body).Decode(&message)
 		if err != nil {
@@ -56,7 +61,7 @@ func PostPushMessage(c *gin.Context) {
 	pushMessageHelper(c, &message)
 }
 
-func pushMessageHelper(c *gin.Context, message *channel.Message) {
+func pushMessageHelper(c *gin.Context, message *model.Message) {
 	user := model.User{Username: c.Param("username")}
 	err := user.FillUserByUsername()
 	if err != nil {
@@ -108,7 +113,16 @@ func pushMessageHelper(c *gin.Context, message *channel.Message) {
 			message.Channel = channel.TypeEmail
 		}
 	}
-	err = message.Send(&user)
+	err = message.UpdateAndInsert(user.Id)
+	message.URL = fmt.Sprintf("%s/message/%s", common.ServerAddress, message.Link)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	err = channel.SendMessage(message, &user)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
@@ -122,3 +136,108 @@ func pushMessageHelper(c *gin.Context, message *channel.Message) {
 	})
 	return
 }
+
+func GetStaticFile(c *gin.Context) {
+	path := c.Param("file")
+	c.FileFromFS("public/static/"+path, http.FS(common.FS))
+}
+
+func RenderMessage(c *gin.Context) {
+	link := c.Param("link")
+	message, err := model.GetMessageByLink(link)
+	if err != nil {
+		c.Status(http.StatusNotFound)
+		return
+	}
+	if message.Content != "" {
+		var buf bytes.Buffer
+		err := goldmark.Convert([]byte(message.Content), &buf)
+		if err != nil {
+			common.SysLog(err.Error())
+		} else {
+			message.HTMLContent = buf.String()
+		}
+	}
+	c.HTML(http.StatusOK, "message.html", gin.H{
+		"title":       message.Title,
+		"time":        time.Unix(message.Timestamp, 0).Format("2006-01-02 15:04:05"),
+		"description": message.Description,
+		"content":     message.HTMLContent,
+	})
+	return
+}
+
+func GetUserMessages(c *gin.Context) {
+	userId := c.GetInt("id")
+	p, _ := strconv.Atoi(c.Query("p"))
+	if p < 0 {
+		p = 0
+	}
+	messages, err := model.GetMessagesByUserId(userId, p*common.ItemsPerPage, common.ItemsPerPage)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    messages,
+	})
+	return
+}
+
+func GetMessage(c *gin.Context) {
+	messageId, _ := strconv.Atoi(c.Param("id"))
+	userId := c.GetInt("id")
+	message, err := model.GetMessageById(messageId, userId)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    message,
+	})
+	return
+}
+
+func DeleteMessage(c *gin.Context) {
+	messageId, _ := strconv.Atoi(c.Param("id"))
+	userId := c.GetInt("id")
+	err := model.DeleteMessageById(messageId, userId)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+	})
+	return
+}
+
+func DeleteAllMessages(c *gin.Context) {
+	err := model.DeleteAllMessages()
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+	})
+	return
+}

+ 1 - 0
main.go

@@ -54,6 +54,7 @@ func main() {
 
 	// Initialize HTTP server
 	server := gin.Default()
+	server.SetHTMLTemplate(common.LoadTemplate())
 	server.Use(gzip.Gzip(gzip.DefaultCompression))
 
 	// Initialize session store

+ 4 - 0
model/main.go

@@ -60,6 +60,10 @@ func InitDB() (err error) {
 		if err != nil {
 			return err
 		}
+		err = db.AutoMigrate(&Message{})
+		if err != nil {
+			return err
+		}
 		err = createRootAccountIfNeed()
 		return err
 	} else {

+ 76 - 0
model/message.go

@@ -0,0 +1,76 @@
+package model
+
+import (
+	"errors"
+	"message-pusher/common"
+	"time"
+)
+
+type Message struct {
+	Id          int    `json:"id"`
+	UserId      int    `json:"user_id" gorm:"index"`
+	Title       string `json:"title"`
+	Description string `json:"description"`
+	Desp        string `json:"desp" gorm:"-:all"` // alias for description
+	Content     string `json:"content"`
+	URL         string `json:"url" gorm:"-:all"`
+	Channel     string `json:"channel"`
+	Token       string `json:"token" gorm:"-:all"`
+	HTMLContent string `json:"html_content"  gorm:"-:all"`
+	Timestamp   int64  `json:"timestamp" gorm:"type:int64"`
+	Link        string `json:"link" gorm:"unique;index"`
+}
+
+func GetMessageById(id int, userId int) (*Message, error) {
+	if id == 0 || userId == 0 {
+		return nil, errors.New("id 或 userId 为空!")
+	}
+	message := Message{Id: id, UserId: userId}
+	err := DB.Where(message).First(&message).Error
+	return &message, err
+}
+
+func GetMessageByLink(link string) (*Message, error) {
+	if link == "" {
+		return nil, errors.New("link 为空!")
+	}
+	message := Message{Link: link}
+	err := DB.Where(message).First(&message).Error
+	return &message, err
+}
+
+func GetMessagesByUserId(userId int, startIdx int, num int) (messages []*Message, err error) {
+	err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&messages).Error
+	return messages, err
+}
+
+func DeleteMessageById(id int, userId int) (err error) {
+	// Why we need userId here? In case user want to delete other's message.
+	if id == 0 || userId == 0 {
+		return errors.New("id 或 userId 为空!")
+	}
+	message := Message{Id: id, UserId: userId}
+	err = DB.Where(message).First(&message).Error
+	if err != nil {
+		return err
+	}
+	return message.Delete()
+}
+
+func DeleteAllMessages() error {
+	return DB.Exec("DELETE FROM messages").Error
+}
+
+func (message *Message) UpdateAndInsert(userId int) error {
+	message.Link = common.GetUUID()
+	message.Timestamp = time.Now().Unix()
+	message.UserId = userId
+	var err error
+	err = DB.Create(message).Error
+	return err
+}
+
+func (message *Message) Delete() error {
+	err := DB.Delete(message).Error
+	return err
+}

+ 7 - 0
router/api-router.go

@@ -55,6 +55,13 @@ func SetApiRouter(router *gin.Engine) {
 			optionRoute.GET("/", controller.GetOptions)
 			optionRoute.PUT("/", controller.UpdateOption)
 		}
+		messageRoute := apiRouter.Group("/message")
+		{
+			messageRoute.GET("/all", middleware.UserAuth(), controller.GetUserMessages)
+			messageRoute.GET("/:id", middleware.UserAuth(), controller.GetMessage)
+			messageRoute.DELETE("/all", middleware.RootAuth(), controller.DeleteAllMessages)
+			messageRoute.DELETE("/:id", middleware.UserAuth(), controller.DeleteMessage)
+		}
 	}
 	pushRouter := router.Group("/push")
 	pushRouter.Use(middleware.GlobalAPIRateLimit())

+ 3 - 0
router/web-router.go

@@ -5,6 +5,7 @@ import (
 	"github.com/gin-contrib/static"
 	"github.com/gin-gonic/gin"
 	"message-pusher/common"
+	"message-pusher/controller"
 	"message-pusher/middleware"
 	"net/http"
 )
@@ -12,6 +13,8 @@ import (
 func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
 	router.Use(middleware.GlobalWebRateLimit())
 	router.Use(middleware.Cache())
+	router.GET("/public/static/:file", controller.GetStaticFile)
+	router.GET("/message/:link", controller.RenderMessage)
 	router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
 	router.NoRoute(func(c *gin.Context) {
 		c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage)