Skip to content

Commit d1367cc

Browse files
committed
Add discord integration
1 parent eceabc6 commit d1367cc

File tree

16 files changed

+895
-147
lines changed

16 files changed

+895
-147
lines changed

api/pkg/di/container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,7 @@ func (container *Container) DiscordClient() (client *discord.Client) {
888888
container.logger.Debug(fmt.Sprintf("creating %T", client))
889889
return discord.New(
890890
discord.WithHTTPClient(container.HTTPClient("discord")),
891+
discord.WithApplicationID(os.Getenv("DISCORD_APPLICATION_ID")),
891892
discord.WithBotToken(os.Getenv("DISCORD_BOT_TOKEN")),
892893
)
893894
}

api/pkg/discord/application.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ package discord
22

33
// CommandCreateRequest is the request for creating a new command
44
type CommandCreateRequest struct {
5-
Name string `json:"name"`
6-
Type int `json:"type"`
7-
Description string `json:"description"`
8-
Options []CommandCreateRequestOptions `json:"options"`
5+
Name string `json:"name"`
6+
Type int `json:"type"`
7+
Description string `json:"description"`
8+
Options []CommandCreateRequestOption `json:"options"`
99
}
1010

11-
// CommandCreateRequestOptions are options for creating a command
12-
type CommandCreateRequestOptions struct {
11+
// CommandCreateRequestOption are options for creating a command
12+
type CommandCreateRequestOption struct {
1313
Name string `json:"name"`
1414
Description string `json:"description"`
1515
Type int `json:"type"`
@@ -18,22 +18,22 @@ type CommandCreateRequestOptions struct {
1818

1919
// CommandCreateResponse is the response after creating a command
2020
type CommandCreateResponse struct {
21-
ID string `json:"id"`
22-
ApplicationID string `json:"application_id"`
23-
Version string `json:"version"`
24-
DefaultMemberPermissions any `json:"default_member_permissions"`
25-
Type int `json:"type"`
26-
Name string `json:"name"`
27-
NameLocalizations any `json:"name_localizations"`
28-
Description string `json:"description"`
29-
DescriptionLocalizations any `json:"description_localizations"`
30-
GuildID string `json:"guild_id"`
31-
Options []CommandCreateResponseOptions `json:"options"`
32-
Nsfw bool `json:"nsfw"`
21+
ID string `json:"id"`
22+
ApplicationID string `json:"application_id"`
23+
Version string `json:"version"`
24+
DefaultMemberPermissions any `json:"default_member_permissions"`
25+
Type int `json:"type"`
26+
Name string `json:"name"`
27+
NameLocalizations any `json:"name_localizations"`
28+
Description string `json:"description"`
29+
DescriptionLocalizations any `json:"description_localizations"`
30+
GuildID string `json:"guild_id"`
31+
Options []CommandCreateResponseOption `json:"options"`
32+
Nsfw bool `json:"nsfw"`
3333
}
3434

35-
// CommandCreateResponseOptions are options after creating a command
36-
type CommandCreateResponseOptions struct {
35+
// CommandCreateResponseOption are options after creating a command
36+
type CommandCreateResponseOption struct {
3737
Type int `json:"type"`
3838
Name string `json:"name"`
3939
NameLocalizations any `json:"name_localizations"`

api/pkg/discord/application_service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ type ApplicationService service
1313
// CreateCommand creates a new guild command
1414
//
1515
// API Docs: https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command
16-
func (service *ApplicationService) CreateCommand(ctx context.Context, serverID string, channelID string, params *CommandCreateRequest) (*CommandCreateResponse, *Response, error) {
17-
url := fmt.Sprintf("/applications/%s/guilds/%s/commands", serverID, channelID)
16+
func (service *ApplicationService) CreateCommand(ctx context.Context, serverID string, params *CommandCreateRequest) (*CommandCreateResponse, *Response, error) {
17+
url := fmt.Sprintf("/applications/%s/guilds/%s/commands", service.client.applicationID, serverID)
1818
request, err := service.client.newRequest(ctx, http.MethodPost, url, params)
1919
if err != nil {
2020
return nil, nil, err

api/pkg/discord/client.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ type service struct {
1616
// Client is the campay API client.
1717
// Do not instantiate this client with Client{}. Use the New method instead.
1818
type Client struct {
19-
httpClient *http.Client
20-
common service
21-
baseURL string
22-
botToken string
19+
httpClient *http.Client
20+
common service
21+
baseURL string
22+
applicationID string
23+
botToken string
2324

2425
Channel *ChannelService
2526
Guild *GuildService
@@ -35,9 +36,10 @@ func New(options ...Option) *Client {
3536
}
3637

3738
client := &Client{
38-
httpClient: config.httpClient,
39-
botToken: config.botToken,
40-
baseURL: config.baseURL,
39+
httpClient: config.httpClient,
40+
botToken: config.botToken,
41+
baseURL: config.baseURL,
42+
applicationID: config.applicationID,
4143
}
4244

4345
client.common.client = client

api/pkg/discord/client_config.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ package discord
33
import "net/http"
44

55
type clientConfig struct {
6-
httpClient *http.Client
7-
botToken string
8-
baseURL string
6+
httpClient *http.Client
7+
botToken string
8+
applicationID string
9+
baseURL string
910
}
1011

1112
func defaultClientConfig() *clientConfig {
1213
return &clientConfig{
13-
httpClient: http.DefaultClient,
14-
botToken: "",
15-
baseURL: "https://discord.com/api",
14+
httpClient: http.DefaultClient,
15+
botToken: "",
16+
applicationID: "",
17+
baseURL: "https://discord.com/api",
1618
}
1719
}

api/pkg/discord/client_option.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ func WithBaseURL(baseURL string) Option {
3535
})
3636
}
3737

38+
// WithApplicationID sets the discord bot application ID
39+
func WithApplicationID(applicationID string) Option {
40+
return clientOptionFunc(func(config *clientConfig) {
41+
config.applicationID = applicationID
42+
})
43+
}
44+
3845
// WithBotToken sets the discord bot token
3946
func WithBotToken(botToken string) Option {
4047
return clientOptionFunc(func(config *clientConfig) {

api/pkg/entities/discord.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type Discord struct {
1111
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
1212
UserID UserID `json:"user_id" gorm:"index" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
1313
Name string `json:"name" example:"Game Server"`
14-
ServerID string `json:"server_id" gorm:"uniqueIndex" example:"1095780203256627291"`
14+
ServerID string `json:"server_id" gorm:"uniqueIndex" example:"1095778291488653372"`
1515
IncomingChannelID string `json:"incoming_channel_id" example:"1095780203256627291"`
1616
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
1717
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`

api/pkg/handlers/discord_handler.go

Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"fmt"
99
"os"
1010

11+
"github.com/google/uuid"
12+
1113
"github.com/NdoleStudio/httpsms/pkg/repositories"
1214
"github.com/NdoleStudio/httpsms/pkg/requests"
1315
"github.com/NdoleStudio/httpsms/pkg/services"
@@ -47,24 +49,148 @@ func (h *DiscordHandler) RegisterRoutes(app *fiber.App, authMiddleware fiber.Han
4749
router := app.Group("discord")
4850
router.Post("/event", h.computeRoute(middlewares, h.Event)...)
4951

50-
authRouter := app.Group("v1/discord")
51-
authRouter.Post("/", h.computeRoute(append(middlewares, authMiddleware), h.Event)...)
52+
authRouter := app.Group("v1/discord-integrations")
53+
authRouter.Post("/", h.computeRoute(append(middlewares, authMiddleware), h.Store)...)
54+
authRouter.Get("/", h.computeRoute(append(middlewares, authMiddleware), h.Index)...)
55+
authRouter.Delete("/:discordID", h.computeRoute(append(middlewares, authMiddleware), h.Delete)...)
56+
authRouter.Put("/:discordID", h.computeRoute(append(middlewares, authMiddleware), h.Update)...)
57+
}
58+
59+
// Index returns the discord integrations of a user
60+
// @Summary Get discord integrations of a user
61+
// @Description Get the discord integrations of a user
62+
// @Security ApiKeyAuth
63+
// @Tags DiscordIntegration
64+
// @Accept json
65+
// @Produce json
66+
// @Param skip query int false "number of discord integrations to skip" minimum(0)
67+
// @Param query query string false "filter discord integrations containing query"
68+
// @Param limit query int false "number of discord integrations to return" minimum(1) maximum(20)
69+
// @Success 200 {object} responses.DiscordsResponse
70+
// @Failure 400 {object} responses.BadRequest
71+
// @Failure 401 {object} responses.Unauthorized
72+
// @Failure 422 {object} responses.UnprocessableEntity
73+
// @Failure 500 {object} responses.InternalServerError
74+
// @Router /discord-integrations [get]
75+
func (h *DiscordHandler) Index(c *fiber.Ctx) error {
76+
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
77+
defer span.End()
78+
79+
var request requests.DiscordIndex
80+
if err := c.QueryParser(&request); err != nil {
81+
msg := fmt.Sprintf("cannot marshall URL [%s] into %T", c.OriginalURL(), request)
82+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
83+
return h.responseBadRequest(c, err)
84+
}
85+
86+
if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
87+
msg := fmt.Sprintf("validation errors [%s], while fetching discord integrations [%+#v]", spew.Sdump(errors), request)
88+
ctxLogger.Warn(stacktrace.NewError(msg))
89+
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching discord integrations")
90+
}
91+
92+
discordIntegrations, err := h.service.Index(ctx, h.userIDFomContext(c), request.ToIndexParams())
93+
if err != nil {
94+
msg := fmt.Sprintf("cannot get discord integrations with params [%+#v]", request)
95+
ctxLogger.Error(stacktrace.Propagate(err, msg))
96+
return h.responseInternalServerError(c)
97+
}
98+
99+
return h.responseOK(c, fmt.Sprintf("fetched %d discord %s", len(discordIntegrations), h.pluralize("integration", len(discordIntegrations))), discordIntegrations)
52100
}
53101

54-
// Store a webhook
55-
// @Summary Store a webhook
56-
// @Description Store a webhook for the authenticated user
102+
// Delete a discord integration
103+
// @Summary Delete discord integration
104+
// @Description Delete a discord integration for a user
57105
// @Security ApiKeyAuth
58106
// @Tags Webhooks
59107
// @Accept json
60108
// @Produce json
61-
// @Param payload body requests.WebhookStore true "Payload of the webhook request"
62-
// @Success 200 {object} responses.WebhookResponse
109+
// @Param discordID path string true "ID of the discord integration" default(32343a19-da5e-4b1b-a767-3298a73703ca)
110+
// @Success 204 {object} responses.NoContent
111+
// @Failure 400 {object} responses.BadRequest
112+
// @Failure 401 {object} responses.Unauthorized
113+
// @Failure 422 {object} responses.UnprocessableEntity
114+
// @Failure 500 {object} responses.InternalServerError
115+
// @Router /discord-integrations/{discordID} [delete]
116+
func (h *DiscordHandler) Delete(c *fiber.Ctx) error {
117+
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
118+
defer span.End()
119+
120+
discordID := c.Params("discordID")
121+
if errors := h.validator.ValidateUUID(ctx, discordID, "discordID"); len(errors) != 0 {
122+
msg := fmt.Sprintf("validation errors [%s], while deleting discord integration with ID [%s]", spew.Sdump(errors), discordID)
123+
ctxLogger.Warn(stacktrace.NewError(msg))
124+
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting discord integration")
125+
}
126+
127+
err := h.service.Delete(ctx, h.userIDFomContext(c), uuid.MustParse(discordID))
128+
if err != nil {
129+
msg := fmt.Sprintf("cannot delete discord integration with ID [%+#v]", discordID)
130+
ctxLogger.Error(stacktrace.Propagate(err, msg))
131+
return h.responseInternalServerError(c)
132+
}
133+
134+
return h.responseOK(c, "discord integration deleted successfully", nil)
135+
}
136+
137+
// Update an entities.Discord
138+
// @Summary Update a discord integration
139+
// @Description Update a discord integration for the currently authenticated user
140+
// @Security ApiKeyAuth
141+
// @Tags DiscordIntegration
142+
// @Accept json
143+
// @Produce json
144+
// @Param discordID path string true "ID of the discord integration" default(32343a19-da5e-4b1b-a767-3298a73703ca)
145+
// @Param payload body requests.DiscordUpdate true "Payload of discord integration to update"
146+
// @Success 200 {object} responses.DiscordResponse
147+
// @Failure 400 {object} responses.BadRequest
148+
// @Failure 401 {object} responses.Unauthorized
149+
// @Failure 422 {object} responses.UnprocessableEntity
150+
// @Failure 500 {object} responses.InternalServerError
151+
// @Router /discord-integrations/{discordID} [put]
152+
func (h *DiscordHandler) Update(c *fiber.Ctx) error {
153+
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
154+
defer span.End()
155+
156+
var request requests.DiscordUpdate
157+
if err := c.BodyParser(&request); err != nil {
158+
msg := fmt.Sprintf("cannot marshall params [%s] into [%T]", c.Body(), request)
159+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
160+
return h.responseBadRequest(c, err)
161+
}
162+
163+
request.DiscordID = c.Params("discordID")
164+
if errors := h.validator.ValidateUpdate(ctx, request.Sanitize()); len(errors) != 0 {
165+
msg := fmt.Sprintf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request)
166+
ctxLogger.Warn(stacktrace.NewError(msg))
167+
return h.responseUnprocessableEntity(c, errors, "validation errors while updating discord integration")
168+
}
169+
170+
user, err := h.service.Update(ctx, request.ToUpdateParams(h.userFromContext(c)))
171+
if err != nil {
172+
msg := fmt.Sprintf("cannot update discord integration with params [%+#v]", request)
173+
ctxLogger.Error(stacktrace.Propagate(err, msg))
174+
return h.responseInternalServerError(c)
175+
}
176+
177+
return h.responseOK(c, "discord integration updated successfully", user)
178+
}
179+
180+
// Store an entities.Discord
181+
// @Summary Store discord integration
182+
// @Description Store a discord integration for the authenticated user
183+
// @Security ApiKeyAuth
184+
// @Tags DiscordIntegration
185+
// @Accept json
186+
// @Produce json
187+
// @Param payload body requests.DiscordStore true "Payload of the discord integration request"
188+
// @Success 201 {object} responses.DiscordResponse
63189
// @Failure 400 {object} responses.BadRequest
64190
// @Failure 401 {object} responses.Unauthorized
65191
// @Failure 422 {object} responses.UnprocessableEntity
66192
// @Failure 500 {object} responses.InternalServerError
67-
// @Router /webhooks [post]
193+
// @Router /discord-integrations [post]
68194
func (h *DiscordHandler) Store(c *fiber.Ctx) error {
69195
ctx, span := h.tracer.StartFromFiberCtx(c)
70196
defer span.End()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package requests
2+
3+
import (
4+
"strings"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/repositories"
7+
)
8+
9+
// DiscordIndex is the payload for fetching entities.Discord of a user
10+
type DiscordIndex struct {
11+
request
12+
Skip string `json:"skip" query:"skip"`
13+
Query string `json:"query" query:"query"`
14+
Limit string `json:"limit" query:"limit"`
15+
}
16+
17+
// Sanitize sets defaults to MessageOutstanding
18+
func (input *DiscordIndex) Sanitize() DiscordIndex {
19+
if strings.TrimSpace(input.Limit) == "" {
20+
input.Limit = "1"
21+
}
22+
input.Query = strings.TrimSpace(input.Query)
23+
input.Skip = strings.TrimSpace(input.Skip)
24+
if input.Skip == "" {
25+
input.Skip = "0"
26+
}
27+
return *input
28+
}
29+
30+
// ToIndexParams converts HeartbeatIndex to repositories.IndexParams
31+
func (input *DiscordIndex) ToIndexParams() repositories.IndexParams {
32+
return repositories.IndexParams{
33+
Skip: input.getInt(input.Skip),
34+
Query: input.Query,
35+
Limit: input.getInt(input.Limit),
36+
}
37+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package requests
2+
3+
import (
4+
"github.com/NdoleStudio/httpsms/pkg/entities"
5+
"github.com/NdoleStudio/httpsms/pkg/services"
6+
"github.com/google/uuid"
7+
)
8+
9+
// DiscordUpdate is the payload for updating an entities.Webhook
10+
type DiscordUpdate struct {
11+
DiscordStore
12+
DiscordID string `json:"discordID" swaggerignore:"true"` // used internally for validation
13+
}
14+
15+
// Sanitize sets defaults to WebhookUpdate
16+
func (input *DiscordUpdate) Sanitize() DiscordUpdate {
17+
input.DiscordStore.Sanitize()
18+
return *input
19+
}
20+
21+
// ToUpdateParams converts DiscordUpdate to services.DiscordUpdateParams
22+
func (input *DiscordUpdate) ToUpdateParams(user entities.AuthUser) *services.DiscordUpdateParams {
23+
return &services.DiscordUpdateParams{
24+
UserID: user.ID,
25+
Name: input.Name,
26+
ServerID: input.ServerID,
27+
IncomingChannelID: input.IncomingChannelID,
28+
DiscordID: uuid.MustParse(input.DiscordID),
29+
}
30+
}

0 commit comments

Comments
 (0)