Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions discord/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func InitializeRoutes(router *gin.Engine) {
router.POST("/discord/onboarding-tokens/:id/consume", ConsumeOnboardingToken)
router.GET("/discord/roles", GetRoles)
router.GET("/discord/channels", GetChannels)
router.GET("/discord/archived-channels", GetArchivedChannels)
router.GET("/discord/role-bindings", ListRoleBindings)
router.POST("/discord/role-bindings", CreateRoleBinding)
router.DELETE("/discord/role-bindings/:bindingID", DeleteRoleBinding)
Expand Down
21 changes: 21 additions & 0 deletions discord/api/channel_archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package api

import (
"net/http"

"github.com/gaucho-racing/sentinel/discord/pkg/logger"
"github.com/gaucho-racing/sentinel/discord/service"
"github.com/gin-gonic/gin"
)

func GetArchivedChannels(c *gin.Context) {
Require(c, RequestTokenHasScope(c, "sentinel:all"))

records, err := service.GetAllArchivedChannels()
if err != nil {
logger.SugarLogger.Errorf("Failed to fetch archived channels: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch archived channels"})
return
}
c.JSON(http.StatusOK, records)
}
24 changes: 24 additions & 0 deletions discord/commands/archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package commands

import (
"fmt"

"github.com/bwmarrin/discordgo"
"github.com/gaucho-racing/sentinel/discord/pkg/logger"
"github.com/gaucho-racing/sentinel/discord/service"
)

func Archive(args []string, s *discordgo.Session, m *discordgo.MessageCreate) {
allowedGroups := []string{"Admins", "Leads", "Officers"}
if !requireGroupMembership(m, "archive", allowedGroups) {
return
}
if _, err := service.GetArchivedChannel(m.ChannelID); err == nil {
service.SendDisappearingMessage(m.ChannelID, fmt.Sprintf("<@%s> this channel is already archived.", m.Author.ID), commandReplyTTL)
return
}
if err := service.ArchiveChannel(m.ChannelID, m.Author.ID); err != nil {
logger.SugarLogger.Errorf("archive: failed for channel %s: %v", m.ChannelID, err)
service.SendDisappearingMessage(m.ChannelID, fmt.Sprintf("<@%s> archiving failed — check the logs.", m.Author.ID), commandReplyTTL)
}
}
29 changes: 29 additions & 0 deletions discord/commands/handler.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package commands

import (
"fmt"
"strings"
"sync"
"time"

"github.com/bwmarrin/discordgo"
"github.com/gaucho-racing/sentinel/discord/config"
Expand All @@ -11,6 +13,29 @@ import (
"github.com/gaucho-racing/sentinel/discord/service"
)

const commandReplyTTL = 10 * time.Second

// requireGroupMembership gates a command to members of the given Sentinel
// groups (matched by name, case-insensitive), replying with a disappearing
// message when the check fails. Fails closed: a missing entity link or a
// core lookup failure both deny access.
func requireGroupMembership(m *discordgo.MessageCreate, command string, allowedGroups []string) bool {
groupNames, err := service.GetGroupNamesForDiscordUser(m.Author.ID)
if err != nil {
logger.SugarLogger.Errorf("%s: failed to fetch sentinel groups for %s: %v", command, m.Author.ID, err)
} else {
for _, name := range groupNames {
for _, allowed := range allowedGroups {
if strings.EqualFold(name, allowed) {
return true
}
}
}
}
service.SendDisappearingMessage(m.ChannelID, fmt.Sprintf("<@%s> you don't have permission to use the `%s%s` command.", m.Author.ID, config.DiscordPrefix, command), commandReplyTTL)
return false
}

// readyOnce guards the startup sweep so a gateway reconnect (which also
// fires Ready) doesn't repeatedly kick the sweep. Subsequent reconnects
// are covered by the periodic cron + per-user event reconciles anyway.
Expand Down Expand Up @@ -72,6 +97,10 @@ func OnDiscordMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
Ping(args, s, m)
case "verify":
Verify(args, s, m)
case "archive":
Archive(args, s, m)
case "unarchive":
Unarchive(args, s, m)
default:
logger.SugarLogger.Infof("Unknown command: %s", command)
}
Expand Down
31 changes: 31 additions & 0 deletions discord/commands/unarchive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package commands

import (
"fmt"

"github.com/bwmarrin/discordgo"
"github.com/gaucho-racing/sentinel/discord/pkg/logger"
"github.com/gaucho-racing/sentinel/discord/service"
)

func Unarchive(args []string, s *discordgo.Session, m *discordgo.MessageCreate) {
allowedGroups := []string{"Admins", "Leads", "Officers"}
if !requireGroupMembership(m, "unarchive", allowedGroups) {
return
}

record, err := service.UnarchiveChannel(m.ChannelID)
if err != nil {
logger.SugarLogger.Errorf("unarchive: failed for channel %s: %v", m.ChannelID, err)
service.SendDisappearingMessage(m.ChannelID, fmt.Sprintf("<@%s> this channel isn't archived, or restoring it failed — check the logs.", m.Author.ID), commandReplyTTL)
return
}

content := "This channel has been unarchived and its permissions restored."
if record.PreviousParentID == "" {
content += " It wasn't in a category before it was archived, so it'll need to be moved out manually."
}
if _, err := s.ChannelMessageSend(m.ChannelID, content); err != nil {
logger.SugarLogger.Errorf("unarchive: failed to send confirmation in %s: %v", m.ChannelID, err)
}
}
7 changes: 7 additions & 0 deletions discord/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,16 @@ func IsProduction() bool {
return Env == "PROD"
}

// DiscordArchiveCategoryName is the channel category (matched by name,
// case-insensitive) that channels get moved into to archive them.
const DiscordArchiveCategoryName = "ARCHIVE"

var MembersDiscordRoleID = "820467859477889034"
var AlumniDiscordRoleID = "817577502968512552"
var GuestDiscordRoleID = "1511273081824477245"
var RobotDiscordRoleID = "1229611357259694132"
var SpecialAdvisorDiscordRoleID = "1386909324596609034"
var DevOpsDiscordRoleID = "1527194309915443271"

var AeroSubteamDiscordRoleID = "761114473565519882"
var BusinessSubteamDiscordRoleID = "761331962563919874"
Expand Down
1 change: 1 addition & 0 deletions discord/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func Init() {
&model.DiscordReaction{},
&model.OnboardingToken{},
&model.GroupDiscordRoleBinding{},
&model.ArchivedChannel{},
)
logger.SugarLogger.Infoln("AutoMigration complete")
DB = db
Expand Down
20 changes: 20 additions & 0 deletions discord/model/archived_channel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package model

import "time"

// ArchivedChannel snapshots a channel's pre-archive state so it can be
// restored by the unarchive command. PreviousOverwrites holds the channel's
// own permission overwrites as JSON ([]*discordgo.PermissionOverwrite).
type ArchivedChannel struct {
ChannelID string `json:"channel_id" gorm:"primaryKey"`
ChannelName string `json:"channel_name"`
PreviousParentID string `json:"previous_parent_id"`
PreviousOverwrites string `json:"previous_overwrites"`
ArchivedByEntityID string `json:"archived_by_entity_id"`
ArchivedByDiscordID string `json:"archived_by_discord_id"`
ArchivedAt time.Time `json:"archived_at" gorm:"autoCreateTime"`
}

func (ArchivedChannel) TableName() string {
return "archived_channel"
}
Loading
Loading