Skip to content

Commit

Permalink
Implemented server list and linking to dashboard page. Server list sy…
Browse files Browse the repository at this point in the history
…ncs to database
  • Loading branch information
Scott Bragg committed Dec 28, 2024
1 parent 461cbec commit f8e1af2
Show file tree
Hide file tree
Showing 13 changed files with 441 additions and 86 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ docker compose logs -f
- models -- database models
- static -- web UI static files
- templates -- web UI templates
- discordtypes -- structs used by db and discordapi to populate data
- discordapi -- functions that do useful things with the discord REST API

59 changes: 21 additions & 38 deletions cmd/web/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,56 @@
package main

import (
"context"
"encoding/json"
"log"
"net/http"

"github.com/gin-gonic/gin"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"

"github.com/faulteh/nap-and-go/config"
"github.com/faulteh/nap-and-go/discordapi"
)

// Guild represents a Discord guild/server
type Guild struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
Owner bool `json:"owner"`
Permissions int64 `json:"permissions"`
}

// serversHandler handles the server view
func serversHandler(c *gin.Context) {
// Get the user session
session := sessions.Default(c)
token := session.Get("token").(*oauth2.Token)

// Retrieve list of servers for user from discord
oauth2Config := config.LoadDiscordConfig().OAuth2Config()
client := oauth2Config.Client(context.Background(), token)
resp, err := client.Get("https://discord.com/api/users/@me/guilds")
// Retrieve list of servers for user and bot from discord
userGuilds, err := discordapi.UserAdminGuildList(token)
if err != nil {
log.Printf("Failed to get user info: %v\n", err)
// For now just return an error
log.Printf("Failed to get user guilds: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get user info",
"error": "Failed to get user guilds",
"msg": err.Error(),
})
return
}
defer resp.Body.Close() //nolint:errcheck

if resp.StatusCode != http.StatusOK {
log.Printf("Discord API responded with status %d\n", resp.StatusCode)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch servers"})
return
}

var guilds []Guild
if err := json.NewDecoder(resp.Body).Decode(&guilds); err != nil {
log.Printf("Error decoding guilds response: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode servers"})
botGuilds, err := discordapi.BotGuildList()
if err != nil {
log.Printf("Failed to get bot guilds: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get bot guilds",
"msg": err.Error(),
})
return
}

// Filter guilds where the user has admin permissions
var adminGuilds []Guild
const ADMINISTRATOR = 0x00000008
for _, guild := range guilds {
if guild.Permissions&ADMINISTRATOR != 0 {
adminGuilds = append(adminGuilds, guild)
// Mark the servers where the bot is present
for i := range userGuilds {
for _, botGuild := range botGuilds {
if userGuilds[i].ID == botGuild.ID {
userGuilds[i].HasBot = true
break
}
}
}

// Render the servers view
c.HTML(http.StatusOK, "servers.html", gin.H{
"Title": "Servers",
"Servers": adminGuilds,
"UserServers": userGuilds,
})
}
4 changes: 2 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type DBConfig struct {
type DiscordConfig struct {
clientID string
clientSecret string
botToken string
BotToken string
redirectURL string
}

Expand Down Expand Up @@ -64,7 +64,7 @@ func LoadDiscordConfig() *DiscordConfig {
return &DiscordConfig{
clientID: getEnv("DISCORD_CLIENT_ID", ""),
clientSecret: getEnv("DISCORD_CLIENT_SECRET", ""),
botToken: getEnv("DISCORD_BOT_TOKEN", ""),
BotToken: getEnv("DISCORD_BOT_TOKEN", ""),
redirectURL: getEnv("DISCORD_REDIRECT_URL", ""),
}
}
Expand Down
3 changes: 2 additions & 1 deletion db/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ func Connect() {
// GetDB returns the database instance.
func GetDB() *gorm.DB {
if DB == nil {
log.Fatal("Database connection is not initialized. Call Connect() first.")
// Try to connect
Connect()
}
return DB
}
54 changes: 54 additions & 0 deletions db/guild.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Package db provides a database connection and helper functions to interact with the database.
// server.go has functions to interact with the server table in the database.
package db

import (
"errors"
"log"

"gorm.io/gorm"

"github.com/faulteh/nap-and-go/discordtypes"
"github.com/faulteh/nap-and-go/models"
)

// SyncGuilds synchronizes the servers in the database with the servers in the Discord API
func SyncGuilds(guilds []discordtypes.Guild) error {
// Sync the guilds with the database
conn := GetDB()
for _, guild := range guilds {
var server models.Server
if err := conn.Where("id = ?", guild.ID).First(&server).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Create a new server
server = models.Server{
ID: guild.ID,
Name: guild.Name,
Icon: guild.Icon,
Owner: guild.Owner,
Permissions: guild.Permissions,
PermissionsNew: guild.PermissionsNew,
}
if err := conn.Create(&server).Error; err != nil {
log.Printf("Failed to create server: %v\n", err)
return err
}
} else {
log.Printf("Failed to query server: %v\n", err)
return err
}
} else {
// Update the server
server.Name = guild.Name
server.Icon = guild.Icon
server.Owner = guild.Owner
server.Permissions = guild.Permissions
server.PermissionsNew = guild.PermissionsNew
if err := conn.Save(&server).Error; err != nil {
log.Printf("Failed to update server: %v\n", err)
return err
}
}
}
return nil
}
87 changes: 87 additions & 0 deletions discordapi/guilds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Package discordapi interacts with the Discord API to fetch server information and provide data types
// to represent the data.
package discordapi

import (
"context"
"encoding/json"
"fmt"
"net/http"

"golang.org/x/oauth2"

"github.com/faulteh/nap-and-go/config"
"github.com/faulteh/nap-and-go/db"
"github.com/faulteh/nap-and-go/discordtypes"
)

// UserAdminGuildList returns a list of servers the user has admin permissions in queried from the Discord API
func UserAdminGuildList(token *oauth2.Token) ([]discordtypes.Guild, error) {
// Retrieve list of servers for user from discord
oauth2Config := config.LoadDiscordConfig().OAuth2Config()
client := oauth2Config.Client(context.Background(), token)
resp, err := client.Get("https://discord.com/api/users/@me/guilds")
if err != nil {
return nil, err
}
defer resp.Body.Close() //nolint:errcheck

if resp.StatusCode != http.StatusOK {
return nil, err
}

var guilds []discordtypes.Guild
if err := json.NewDecoder(resp.Body).Decode(&guilds); err != nil {
return nil, err
}

// Filter guilds where the user has admin permissions
var adminGuilds []discordtypes.Guild
const ADMINISTRATOR = 0x00000008
for _, guild := range guilds {
if guild.Permissions&ADMINISTRATOR != 0 {
adminGuilds = append(adminGuilds, guild)
}
}

return adminGuilds, nil
}

// BotGuildList returns a list of servers the bot is in queried from the Discord API
// and syncs the database with the list of servers
func BotGuildList() ([]discordtypes.Guild, error) {
// Bot uses a simple Authorization: Bot <token> header
token := config.LoadDiscordConfig().BotToken
if token == "" {
return nil, fmt.Errorf("missing bot token")
}
// Use the bot authorization token to request the guild list
client := &http.Client{}
req, err := http.NewRequest("GET", "https://discord.com/api/users/@me/guilds", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bot "+token)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close() //nolint:errcheck

if resp.StatusCode != http.StatusOK {
return nil, err
}

var guilds []discordtypes.Guild
if err := json.NewDecoder(resp.Body).Decode(&guilds); err != nil {
return nil, err
}

// Sync the guilds with the database
err = db.SyncGuilds(guilds)
if err != nil {
return guilds, err
}

return guilds, nil
}
14 changes: 14 additions & 0 deletions discordtypes/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Package discordtypes provides the types used in the Discord API.
// Used by db and discordapi packages but kept separate to avoid circular dependencies.
package discordtypes

// Guild represents a Discord guild/server
type Guild struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
Owner bool `json:"owner"`
Permissions int64 `json:"permissions"`
PermissionsNew string `json:"permissions_new"`
HasBot bool
}
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,7 @@ require (
)

replace github.com/faulteh/nap-and-go/config => ./config

replace github.com/faulteh/nap-and-go/db => ./db
replace github.com/faulteh/nap-and-go/models => ./models
replace github.com/faulteh/nap-and-go/discordapi => ./discordapi
replace github.com/faulteh/nap-and-go/discordtypes => ./discordtypes
12 changes: 9 additions & 3 deletions models/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import (
type Server struct {
ID string `gorm:"primaryKey"` // Discord server ID (Snowflake)
Name string `gorm:"not null"` // Server name
OwnerID string `gorm:"not null"` // Discord user ID of the owner
Region string `gorm:"not null"` // Server region
MemberCount int `gorm:"not null"` // Current member count

Icon string `gorm:"type:text"` // Server icon URL
Banner string `gorm:"type:text"` // Server banner URL

Owner bool `gorm:"not null;default:false"` // Is the bot owner of the server
Permissions int64 `gorm:"not null"` // Bot permissions in the server
PermissionsNew string `gorm:"not null"` // New permissions in the server
Features []string `gorm:"type:text[]"` // Server features

CreatedAt time.Time `gorm:"autoCreateTime"` // Record creation time
UpdatedAt time.Time `gorm:"autoUpdateTime"` // Last update time
DeletedAt *time.Time `gorm:"index"` // Soft delete timestamp
Expand Down
5 changes: 3 additions & 2 deletions templates/dashboard.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{ template "header.html" . }}
<h1>Dashboard</h1>
{{ template "header.html" }}
<h1 class="title">Dashboard</h1>
<p>Welcome to your dashboard! This is where your main content will go.</p>
{{ template "footer.html" }}
22 changes: 18 additions & 4 deletions templates/footer.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
{{ define "footer.html"}}
</div>
<footer>
<p>&copy; 2024 Nap-and-Go</p>
</footer>
</main>
</div>
<script>
// Toggle navbar burger menu
document.addEventListener('DOMContentLoaded', () => {
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
if ($navbarBurgers.length > 0) {
$navbarBurgers.forEach(el => {
el.addEventListener('click', () => {
const target = el.dataset.target;
const $target = document.getElementById(target);
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
}
});
</script>
</body>
</html>
{{ end }}
Loading

0 comments on commit f8e1af2

Please sign in to comment.