This commit is contained in:
Dot-Rar 2020-04-25 17:42:25 +01:00
parent 64c19c4dcf
commit 6e457bc1a4
15 changed files with 470 additions and 59 deletions

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
@ -56,9 +57,8 @@ func GetTicket(ctx *gin.Context) {
continue
}
ch := make(chan string)
go table.GetUsername(mentionedId, ch)
content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), fmt.Sprintf("@%s", <-ch))
user, _ := cache.Instance.GetUser(mentionedId)
content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), fmt.Sprintf("@%s", user.Username))
}
}

View File

@ -0,0 +1,81 @@
package api
import (
"context"
"fmt"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/gin-gonic/gin"
"regexp"
"strconv"
)
var modmailPathRegex = regexp.MustCompile(`(\d+)\/modmail\/(?:free-)?([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})`)
type wrappedModLog struct {
Uuid string `json:"uuid"`
GuildId uint64 `json:"guild_id,string"`
UserId uint64 `json:"user_id,string"`
}
func GetModmailLogs(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
page, err := strconv.Atoi(ctx.Param("page"))
if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
}
// filter
var userId uint64
if userIdRaw, filterByUserId := ctx.GetQuery("userid"); filterByUserId {
userId, err = strconv.ParseUint(userIdRaw, 10, 64)
if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Invalid user ID",
})
return
}
} else if username, filterByUsername := ctx.GetQuery("username"); filterByUsername {
if err := cache.Instance.QueryRow(context.Background(), `select users.user_id from users where LOWER("data"->>'Username') LIKE LOWER($1) and exists(SELECT FROM members where members.guild_id=$2);`, fmt.Sprintf("%%%s%%", username), guildId).Scan(&userId); err != nil {
ctx.AbortWithStatusJSON(404, gin.H{
"success": false,
"error": "User not found",
})
return
}
}
shouldFilter := userId > 0
start := pageLimit * (page - 1)
end := start + pageLimit - 1
wrapped := make([]wrappedModLog, 0)
var archives []table.ModMailArchive
if shouldFilter {
archivesCh := make(chan []table.ModMailArchive)
go table.GetModmailArchivesByUser(userId, guildId, archivesCh)
archives = <-archivesCh
} else {
archivesCh := make(chan []table.ModMailArchive)
go table.GetModmailArchivesByGuild(guildId, archivesCh)
archives = <-archivesCh
}
for i := start; i < end && i < len(archives); i++ {
wrapped = append(wrapped, wrappedModLog{
Uuid: archives[i].Uuid,
GuildId: archives[i].Guild,
UserId: archives[i].User,
})
}
ctx.JSON(200, wrapped)
}

View File

@ -0,0 +1,36 @@
package api
import (
"context"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/gin-gonic/gin"
"strconv"
)
func UserHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
userId, err := strconv.ParseUint(ctx.Param("user"), 10, 64)
if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Invalid user ID",
})
return
}
var username string
if err := cache.Instance.QueryRow(context.Background(), `SELECT "data"->>'Username' FROM users WHERE users.user_id=$1 AND EXISTS(SELECT FROM members WHERE members.guild_id=$2);`, userId, guildId).Scan(&username); err != nil {
ctx.JSON(404, gin.H{
"success": false,
"error": "Not found",
})
return
}
ctx.JSON(200, gin.H{
"user_id": userId,
"guild_id": guildId,
"username": username,
})
}

View File

@ -67,14 +67,14 @@ func LogViewHandler(ctx *gin.Context) {
return
}
ctx.String(500, fmt.Sprintf("Failed to archives - please contact the developers: %s", err.Error()))
ctx.String(500, fmt.Sprintf("Failed to retrieve archive - please contact the developers: %s", err.Error()))
return
}
// format to html
html, err := Archiver.Encode(messages, ticketId)
html, err := Archiver.Encode(messages, fmt.Sprintf("ticket-%d", ticketId))
if err != nil {
ctx.String(500, fmt.Sprintf("Failed to archives - please contact the developers: %s", err.Error()))
ctx.String(500, fmt.Sprintf("Failed to retrieve archive - please contact the developers: %s", err.Error()))
return
}

View File

@ -0,0 +1,19 @@
package manage
import (
"github.com/TicketsBot/GoPanel/config"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
)
func ModmailLogsHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
guildId := ctx.Keys["guildid"].(uint64)
ctx.HTML(200, "manage/modmaillogs", gin.H{
"name": store.Get("name").(string),
"guildId": guildId,
"avatar": store.Get("avatar").(string),
"baseUrl": config.Conf.Server.BaseUrl,
})
}

View File

@ -0,0 +1,86 @@
package manage
import (
"errors"
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/archiverclient"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"strconv"
)
func ModmailLogViewHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildId, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// get ticket UUID
uuid := ctx.Param("uuid")
// get ticket object
archiveCh := make(chan table.ModMailArchive)
go table.GetModmailArchive(uuid, archiveCh)
archive := <-archiveCh
// Verify this is a valid ticket and it is closed
if archive.Uuid == "" {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs/modmail", guild.Id))
return
}
// Verify this modmail ticket was for this guild
if archive.Guild != guildId {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs/modmail", guild.Id))
return
}
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin && archive.User != userId {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// retrieve ticket messages from bucket
messages, err := Archiver.GetModmail(guildId, uuid)
if err != nil {
if errors.Is(err, archiverclient.ErrExpired) {
ctx.String(200, "Archives expired: Purchase premium for permanent log storage") // TODO: Actual error page
return
}
ctx.String(500, fmt.Sprintf("Failed to retrieve archive - please contact the developers: %s", err.Error()))
return
}
// format to html
html, err := Archiver.Encode(messages, fmt.Sprintf("modmail-%s", uuid))
if err != nil {
ctx.String(500, fmt.Sprintf("Failed to retrieve archive - please contact the developers: %s", err.Error()))
return
}
ctx.Data(200, gin.MIMEHTML, html)
} else {
ctx.Redirect(302, fmt.Sprintf("/login?noguilds&state=viewlog.%s.%s", ctx.Param("id"), ctx.Param("ticket")))
}
}

View File

@ -11,7 +11,11 @@ import (
"github.com/gin-contrib/static"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/ulule/limiter/v3"
mgin "github.com/ulule/limiter/v3/drivers/middleware/gin"
"github.com/ulule/limiter/v3/drivers/store/memory"
"log"
"time"
)
func StartServer() {
@ -34,6 +38,7 @@ func StartServer() {
router.Use(static.Serve("/assets/", static.LocalFile("./public/static", false)))
router.Use(gin.Recovery())
router.Use(createLimiter())
// Register templates
router.HTMLRender = createRenderer()
@ -42,6 +47,7 @@ func StartServer() {
router.GET("/callback", root.CallbackHandler)
router.GET("/manage/:id/logs/view/:ticket", manage.LogViewHandler) // we check in the actual handler bc of a custom redirect
router.GET("/manage/:id/logs/modmail/view/:uuid", manage.ModmailLogViewHandler) // we check in the actual handler bc of a custom redirect
authorized := router.Group("/", middleware.AuthenticateCookie)
{
@ -54,6 +60,7 @@ func StartServer() {
authenticateGuild.GET("/manage/:id/settings", manage.SettingsHandler)
authenticateGuild.GET("/manage/:id/logs", manage.LogsHandler)
authenticateGuild.GET("/manage/:id/logs/modmail", manage.ModmailLogsHandler)
authenticateGuild.GET("/manage/:id/blacklist", manage.BlacklistHandler)
authenticateGuild.GET("/manage/:id/panels", manage.PanelHandler)
@ -69,6 +76,7 @@ func StartServer() {
{
guildAuthApi.GET("/:id/channels", api.ChannelsHandler)
guildAuthApi.GET("/:id/premium", api.PremiumHandler)
guildAuthApi.GET("/:id/user/:user", api.UserHandler)
guildAuthApi.GET("/:id/settings", api.GetSettingsHandler)
guildAuthApi.POST("/:id/settings", api.UpdateSettingsHandler)
@ -82,6 +90,7 @@ func StartServer() {
guildAuthApi.DELETE("/:id/panels/:message", api.DeletePanel)
guildAuthApi.GET("/:id/logs/:page", api.GetLogs)
guildAuthApi.GET("/:id/modmail/logs/:page", api.GetModmailLogs)
guildAuthApi.GET("/:id/tickets", api.GetTickets)
guildAuthApi.GET("/:id/tickets/:uuid", api.GetTicket)
@ -106,6 +115,7 @@ func createRenderer() multitemplate.Renderer {
r = addManageTemplate(r, "blacklist")
r = addManageTemplate(r, "logs")
r = addManageTemplate(r, "modmaillogs")
r = addManageTemplate(r, "settings")
r = addManageTemplate(r, "ticketlist")
r = addManageTemplate(r, "ticketview")
@ -135,3 +145,12 @@ func addManageTemplate(renderer multitemplate.Renderer, name string) multitempla
return renderer
}
func createLimiter() func(*gin.Context) {
store := memory.NewStore()
rate := limiter.Rate{
Period: time.Minute * 10,
Limit: 600,
}
return mgin.NewMiddleware(limiter.New(store, rate))
}

View File

@ -33,7 +33,7 @@ func main() {
database.ConnectToDatabase()
cache.Instance = cache.NewCache()
manage.Archiver = archiverclient.NewArchiverClient(config.Conf.Bot.ObjectStore)
manage.Archiver = archiverclient.NewArchiverClientWithTimeout(config.Conf.Bot.ObjectStore, time.Second * 15)
utils.LoadEmoji()

View File

@ -0,0 +1,39 @@
package table
import (
"github.com/TicketsBot/GoPanel/database"
"time"
)
type ModMailArchive struct {
Uuid string `gorm:"column:UUID;type:varchar(36);unique;primary_key"`
Guild uint64 `gorm:"column:GUILDID"`
User uint64 `gorm:"column:USERID"`
CloseTime time.Time `gorm:"column:CLOSETIME"`
}
func (ModMailArchive) TableName() string {
return "modmail_archive"
}
func (m *ModMailArchive) Store() {
database.Database.Create(m)
}
func GetModmailArchive(uuid string, ch chan ModMailArchive) {
var row ModMailArchive
database.Database.Where(ModMailArchive{Uuid: uuid}).Take(&row)
ch <- row
}
func GetModmailArchivesByUser(userId, guildId uint64, ch chan []ModMailArchive) {
var rows []ModMailArchive
database.Database.Where(ModMailArchive{User: userId, Guild: guildId}).Order("CLOSETIME desc").Find(&rows)
ch <- rows
}
func GetModmailArchivesByGuild(guildId uint64, ch chan []ModMailArchive) {
var rows []ModMailArchive
database.Database.Where(ModMailArchive{Guild: guildId}).Order("CLOSETIME desc").Find(&rows)
ch <- rows
}

View File

@ -1,34 +0,0 @@
package table
import (
"github.com/TicketsBot/GoPanel/database"
)
type UsernameNode struct {
Id uint64 `gorm:"column:USERID;primary_key"`
Name string `gorm:"column:USERNAME;type:text"` // Base 64 encoded
Discriminator string `gorm:"column:DISCRIM;type:varchar(4)"`
Avatar string `gorm:"column:AVATARHASH;type:varchar(100)"`
}
func (UsernameNode) TableName() string {
return "usernames"
}
func GetUsername(id uint64, ch chan string) {
node := UsernameNode{Name: "Unknown"}
database.Database.Where(&UsernameNode{Id: id}).First(&node)
ch <- node.Name
}
func GetUserNodes(ids []uint64) []UsernameNode {
var nodes []UsernameNode
database.Database.Where(ids).Find(&nodes)
return nodes
}
func GetUserId(name, discrim string) uint64 {
var node UsernameNode
database.Database.Where(&UsernameNode{Name: name, Discriminator: discrim}).First(&node)
return node.Id
}

5
go.mod
View File

@ -4,7 +4,7 @@ go 1.14
require (
github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20200420161043-3532ff9ea943
github.com/TicketsBot/archiverclient v0.0.0-20200425115930-0ca198cc8306
github.com/apex/log v1.1.2
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
@ -21,6 +21,7 @@ require (
github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c
github.com/pkg/errors v0.9.1
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62
github.com/rxdn/gdl v0.0.0-20200417164852-76b2d3c847c1
github.com/rxdn/gdl v0.0.0-20200421193445-f200b9f466d7
github.com/ulule/limiter/v3 v3.5.0
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
)

View File

@ -7,6 +7,9 @@
<li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/logs"><i class="fas fa-copy icon"></i>Logs</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/logs/modmail"><i class="fas fa-copy icon"></i>Modmail Logs</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/blacklist"><i class="fas fa-ban icon"></i>Blacklist</a>
</li>

View File

@ -65,14 +65,6 @@
</tr>
</thead>
<tbody id="log-container">
{{range .logs}}
<tr>
<td>{{.ticketid}}</td>
<td>{{.username}}</td>
<td>{{.userid}}</td>
<td><a href="/manage/{{$.guildId}}/logs/view/{{.ticketid}}">View</a></td>
</tr>
{{end}}
</tbody>
</table>
@ -80,7 +72,7 @@
<div class="col-md-12">
<ul class="pagination justify-content-center">
<li class="waves-effect"><a href="#" onclick="previous()"><i class="fas fa-chevron-left"></i></a></li>
<p class="center-align white" style="padding-left: 10px; padding-right: 10px;">Page {{.page}}</p>
<p class="center-align white" style="padding-left: 10px; padding-right: 10px;">Page <span id="page-number">1</span></p>
<li class="waves-effect"><a href="#" onclick="next()"><i class="fas fa-chevron-right"></i></a></li>
</ul>
</div>
@ -110,10 +102,6 @@
appendTd(tr, log.userid);
appendButton(tr, 'View', () => { location.href = '/manage/{{.guildId}}/logs/view/' + log.ticketid });
// create view button
const viewTd = document.createElement('td');
tr.appendChild(viewTd);
container.appendChild(tr);
}
@ -135,6 +123,8 @@
for (log of res.data) {
appendLog(log);
}
document.getElementById('page-number').innerText = page;
}
async function loadData() {

View File

@ -0,0 +1,166 @@
{{define "content"}}
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div id="accordion">
<div class="card">
<div class="card-header collapsed filterCard" id="filterHeader" data-toggle="collapse" data-target="#filterLogs" aria-expanded="false" aria-controls="filterLogs">
<span class="align-middle white" data-toggle="collapse" data-target="#filterLogs" aria-expanded="false" aria-controls="filterLogs">
<i class="fas fa-search"></i> Filter Logs
</span>
</div>
<div id="filterLogs" class="collapse" aria-labelledby="filterHeader" data-parent="#accordion">
<div class="card-body">
<form onsubmit="filterLogs(); return false;">
<div class="row">
<div class="col-md-4 px-1">
<div class="form-group">
<label>Username</label>
<input name="username" type="text" class="form-control" placeholder="Username" id="username">
</div>
</div>
<div class="col-md-4 px-1">
<div class="form-group">
<label>User ID</label>
<input name="userid" type="text" class="form-control" placeholder="User ID" id="userid">
</div>
</div>
</div>
<div class="row">
<div class="col-md-2">
<div class="form-group">
<button type="submit" class="btn btn-primary mx-auto btn-fill"><i class="fas fa-paper-plane"></i> Filter</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Logs</h4>
</div>
<div class="card-body">
<div class="card-body table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Username</th>
<th>User ID</th>
<th>Archive</th>
</tr>
</thead>
<tbody id="log-container">
</tbody>
</table>
<div class="row">
<div class="col-md-12">
<ul class="pagination justify-content-center">
<li class="waves-effect"><a href="#" onclick="previous()"><i class="fas fa-chevron-left"></i></a></li>
<p class="center-align white" style="padding-left: 10px; padding-right: 10px;">Page <span id="page-number">1</span></p>
<li class="waves-effect"><a href="#" onclick="next()"><i class="fas fa-chevron-right"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div aria-live="polite" aria-atomic="true" style="position: relative">
<div style="position: absolute; right: 10px" id="toast-container">
</div>
</div>
<script>
let currentPage = 1;
async function getUsername(userId) {
const res = await axios.get('/api/{{.guildId}}/user/' + userId);
return res.data.username;
}
async function appendLog(log) {
const username = await getUsername(log.user_id);
const container = document.getElementById('log-container');
const tr = document.createElement('tr');
appendTd(tr, username);
appendTd(tr, log.user_id);
appendButton(tr, 'View', () => { location.href = '/manage/{{.guildId}}/logs/modmail/view/' + log.uuid });
container.appendChild(tr);
}
async function loadPage(page, ticketId, username, userId) {
const container = document.getElementById('log-container');
container.innerHTML = '';
let url = '/api/{{.guildId}}/modmail/logs/' + page;
if (username !== undefined) {
url += `?username=${username}`;
} else if (userId !== undefined) {
url += `?userid=${userId}`;
}
const res = await axios.get(url);
if (res.status === 200) {
for (log of res.data) {
await appendLog(log);
}
} else {
showToast('Error', res.data.error);
}
document.getElementById('page-number').innerText = page;
}
async function loadData() {
await loadPage(currentPage);
}
loadData();
</script>
<script>
async function next() {
currentPage += 1;
await loadPage(currentPage);
}
async function previous() {
if (currentPage <= 1) {
return
}
currentPage -= 1;
await loadPage(currentPage);
}
async function filterLogs() {
const username = document.getElementById('username').value;
const userId = document.getElementById('userid').value;
if (username !== "") {
await loadPage(1, undefined, username);
} else if (userId !== "") {
await loadPage(1, undefined, undefined, userId);
} else {
await loadPage(1);
}
}
</script>
</div>
{{end}}

View File

@ -111,7 +111,12 @@
}
function getChannelName(channels, channelId) {
return channels.find(ch => ch.id === channelId);
const ch = channels.find(ch => ch.id === channelId);
if (ch === undefined) {
return "Unknown";
} else {
return ch.name;
}
}
async function deletePanel(messageId) {
@ -200,10 +205,10 @@
const tr = document.createElement('tr');
tr.id = panel.message_id; // TODO: When we call this after creating a panel, we don't know the message ID yet
appendTd(tr, `#${getChannelName(channels, panel.channel_id).name}`);
appendTd(tr, `#${getChannelName(channels, panel.channel_id)}`);
appendTd(tr, panel.title);
appendTd(tr, panel.content);
appendTd(tr, getChannelName(channels, panel.category_id).name);
appendTd(tr, getChannelName(channels, panel.category_id));
// build remove button
const deleteTd = document.createElement('td');