hmn/src/website/discord.go

185 lines
5.5 KiB
Go

package website
import (
"errors"
"net/http"
"time"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/discord"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
)
func DiscordOAuthCallback(c *RequestContext) ResponseData {
query := c.Req.URL.Query()
// Check the state
state := query.Get("state")
if state != c.CurrentSession.CSRFToken {
// CSRF'd!!!!
c.Logger.Warn().Str("userId", c.CurrentUser.Username).Msg("user failed Discord OAuth state validation - potential attack?")
res := c.Redirect("/", http.StatusSeeOther)
logoutUser(c, &res)
return res
}
// Check for error values and redirect back to user settings
if errCode := query.Get("error"); errCode != "" {
if errCode == "access_denied" {
// This occurs when the user cancels. Just go back to the profile page.
return c.Redirect(hmnurl.BuildUserSettings("discord"), http.StatusSeeOther)
} else {
return RejectRequest(c, "Failed to authenticate with Discord.")
}
}
// Do the actual token exchange
code := query.Get("code")
res, err := discord.ExchangeOAuthCode(c.Context(), code, hmnurl.BuildDiscordOAuthCallback())
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to exchange Discord authorization code"))
}
expiry := time.Now().Add(time.Duration(res.ExpiresIn) * time.Second)
user, err := discord.GetCurrentUserAsOAuth(c.Context(), res.AccessToken)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch Discord user info"))
}
// Add the role on Discord
err = discord.AddGuildMemberRole(c.Context(), user.ID, config.Config.Discord.MemberRoleID)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to add member role"))
}
// Add the user to our database
_, err = c.Conn.Exec(c.Context(),
`
INSERT INTO handmade_discorduser (username, discriminator, access_token, refresh_token, avatar, locale, userid, expiry, hmn_user_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`,
user.Username,
user.Discriminator,
res.AccessToken,
res.RefreshToken,
user.Avatar,
user.Locale,
user.ID,
expiry,
c.CurrentUser.ID,
)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save new Discord user info"))
}
return c.Redirect(hmnurl.BuildUserSettings("discord"), http.StatusSeeOther)
}
func DiscordUnlink(c *RequestContext) ResponseData {
tx, err := c.Conn.Begin(c.Context())
if err != nil {
panic(err)
}
defer tx.Rollback(c.Context())
iDiscordUser, err := db.QueryOne(c.Context(), tx, models.DiscordUser{},
`
SELECT $columns
FROM handmade_discorduser
WHERE hmn_user_id = $1
`,
c.CurrentUser.ID,
)
if err != nil {
if errors.Is(err, db.NotFound) {
return c.Redirect(hmnurl.BuildUserSettings("discord"), http.StatusSeeOther)
} else {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get Discord user for unlink"))
}
}
discordUser := iDiscordUser.(*models.DiscordUser)
_, err = tx.Exec(c.Context(),
`
DELETE FROM handmade_discorduser
WHERE id = $1
`,
discordUser.ID,
)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete Discord user"))
}
err = tx.Commit(c.Context())
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit Discord user delete"))
}
err = discord.RemoveGuildMemberRole(c.Context(), discordUser.UserID, config.Config.Discord.MemberRoleID)
if err != nil {
c.Logger.Warn().Err(err).Msg("failed to remove member role on unlink")
}
return c.Redirect(hmnurl.BuildUserSettings("discord"), http.StatusSeeOther)
}
func DiscordShowcaseBacklog(c *RequestContext) ResponseData {
iduser, err := db.QueryOne(c.Context(), c.Conn, models.DiscordUser{},
`SELECT $columns FROM handmade_discorduser WHERE hmn_user_id = $1`,
c.CurrentUser.ID,
)
if errors.Is(err, db.NotFound) {
// Nothing to do
c.Logger.Warn().Msg("could not do showcase backlog because no discord user exists")
return c.Redirect(hmnurl.BuildUserProfile(c.CurrentUser.Username), http.StatusSeeOther)
} else if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get discord user"))
}
duser := iduser.(*models.DiscordUser)
ok, err := discord.AllowedToCreateMessageSnippet(c.Context(), c.Conn, duser.UserID)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
if !ok {
// Not allowed to do this, bail out
c.Logger.Warn().Msg("was not allowed to save user snippets")
return c.Redirect(hmnurl.BuildUserProfile(c.CurrentUser.Username), http.StatusSeeOther)
}
type messageIdQuery struct {
MessageID string `db:"msg.id"`
}
imsgIds, err := db.Query(c.Context(), c.Conn, messageIdQuery{},
`
SELECT $columns
FROM
handmade_discordmessage AS msg
WHERE
msg.user_id = $1
AND msg.channel_id = $2
`,
duser.UserID,
config.Config.Discord.ShowcaseChannelID,
)
msgIds := imsgIds.ToSlice()
for _, imsgId := range msgIds {
msgId := imsgId.(*messageIdQuery)
_, err := discord.CreateMessageSnippet(c.Context(), c.Conn, duser.UserID, msgId.MessageID)
if err != nil {
c.Logger.Warn().Err(err).Msg("failed to create snippet from showcase backlog")
continue
}
}
return c.Redirect(hmnurl.BuildUserProfile(c.CurrentUser.Username), http.StatusSeeOther)
}